Skip to content
工具调用与智能体
模型本身只能生成文本。它没有网络、没有时钟、不会做算术,也不能查数据库。要让模型“做事”,就得告诉它:哪些动作可以执行,以及每种动作需要什么参数。模型的任务是产出一段结构化的指令,描述它想调用什么功能、传什么参数——这就是工具调用(Tool Calling)。智能体(Agent)在这个基础上多了一层循环:执行完工具拿到结果后,把结果再喂给模型,让模型决定下一步做什么,直到产生最终答案。
Tool:将函数声明为可调用工具
名称与描述:模型选择工具的依据
LangChain 中的工具本质是一个对象,包裹一个函数,同时携带三项元信息:
- name:工具的唯一标识,模型用它决定“我要调哪个”。
- description:工具的功能说明和调用时机,写在自然语言里。
- schema:参数的 JSON Schema(通常用 Zod 定义),约束模型生成的参数。
这三项信息会作为系统提示的一部分传给模型。模型不是通过函数签名匹配,而是通过名称和描述来理解工具。所以名称是否明确、描述是否包含典型使用场景,直接影响调用准确率。经验规律是:如果人类能通过描述理解工具该怎么用,LLM 大概率也能。
定义一个乘法工具:
typescript
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
const multiply = new DynamicStructuredTool({
name: "multiply",
description: "Multiply two numbers together.",
schema: z.object({
a: z.number().describe("The first number"),
b: z.number().describe("The second number"),
}),
func: async ({ a, b }) => (a * b).toString(),
});name 和 description 会直接出现在模型的上下文里。如果把名称写成 foo,描述写成“does something”,模型就不知道该不该调它,甚至可能调用不到。
参数 Schema:约束工具输入
schema 用 Zod 声明后就自动转成 JSON Schema。模型在决定调用某个工具时,必须产出符合这个 schema 的参数 JSON。如果产出不匹配,运行时解析会失败,形成“工具调用错误”。
对参数加上 .describe() 能进一步提升模型生成正确参数的几率。上面的 a 和 b 都有明确描述。对于复杂的类型(枚举、嵌套对象),Zod 也支持:
typescript
const getWeather = new DynamicStructuredTool({
name: "get_weather",
description: "Get the current weather for a given city.",
schema: z.object({
city: z.string().describe("City name, e.g. 'Beijing'"),
unit: z.enum(["celsius", "fahrenheit"]).optional()
.describe("Temperature unit, default is celsius"),
}),
func: async ({ city, unit }) => {
// 模拟返回
return JSON.stringify({ city, temperature: 22, condition: "sunny" });
},
});模型在调用 get_weather 时会生成类似 { "city": "Tokyo", "unit": "celsius" } 的参数。schema 在这里承担了输入校验的角色——不仅是给模型看,运行时也会对参数做验证。
将工具绑定到模型:bindTools
定义好工具后,需要把它们“告诉”模型。这个操作叫工具绑定。LangChain 提供 model.bindTools(tools) 方法,返回一个新的 Runnable,其底层会在请求里带上工具定义。
typescript
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const modelWithTools = model.bindTools([multiply, getWeather]);绑定之后,modelWithTools 还是一个 Runnable,接口不变,可以直接 invoke:
typescript
const res1 = await modelWithTools.invoke("Hello!");
// res1 是一个 AIMessage,content 包含普通文本,没有 tool_calls
const res2 = await modelWithTools.invoke("What is 23 times 17?");
// res2.content 可能为空,res2.tool_calls 包含一个调用:
// { name: "multiply", args: { a: 23, b: 17 }, id: "call_abc" }关键点:模型并不执行工具,它只是返回“我想调 multiply,参数是这些”。真正执行工具是后边 AgentExecutor 的事。
bindTools 与 bindFunctions 的选择
早期 OpenAI 的函数调用声明用的是 functions 参数,LangChain 对应暴露了 bindFunctions。但 OpenAI 已经明确声明 functions 是遗留方式,tools 是替代方案。在 LangChain.js 里,工具绑定应该优选 bindTools。bindFunctions 仅在与某些旧版模型或特殊场景下才会用到,行为上它只是 tools 的一种退化形式。
创建 Agent:提示词、模型与工具的装配
工具绑定让模型产生了“调用意图”,但要真正执行工具并把结果反馈回去,需要 Agent 和 AgentExecutor 配合。
createToolCallingAgent 的组装方式
createToolCallingAgent 把一个模型、一组工具和一个提示模板组装成一个可运行的 Agent Runnable。Agent 内部会做三件事:
- 按模板格式化提示词(含工具描述、系统指令、对话历史、中间消息等)。
- 把提示词交给模型生成响应。
- 从响应里解析出“下一步动作”——可能是一个或多个工具调用(AgentAction),也可能是最终答案(AgentFinish)。
typescript
import { createToolCallingAgent } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant. Use tools when needed."],
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"), // 关键占位符
]);
const agent = createToolCallingAgent({
llm: model,
tools: [multiply, getWeather],
prompt,
});提示词里必须包含一个名为 agent_scratchpad 的 MessagesPlaceholder。执行循环时,历史工具调用消息会通过这个占位符注入上下文。缺失它会导致 Agent 无法记住前一步做过什么。
createOpenAIFunctionsAgent 的适用场景
createOpenAIFunctionsAgent 是早期的实现,专为 OpenAI 的 function calling 设计。内部逻辑上,它和 createToolCallingAgent 收敛到了同一条路径上。现在除非要兼容旧代码,否则直接用 createToolCallingAgent 即可。
AgentExecutor:执行循环的内部机制
Agent 本身只产生“意图”,不执行任何函数。AgentExecutor 才是负责真正运行循环的组件。它接收一个 Agent 和工具列表,接管“思考—行动—观察”的迭代。
循环流程:模型输出 → 工具调用 → 结果回传
一次典型的执行过程:
- AgentExecutor 调用 Agent,Agent 返回一个
AgentAction(含工具名和参数)或AgentFinish(含最终输出)。 - 如果是 AgentAction,Executor 用工具名找到对应工具,执行
tool.invoke(args),得到结果字符串(观察值)。 - 将这次“调用”和“观察”记录到中间消息里。
- 再次调用 Agent,让模型看到新消息后决定下一步。
- 重复直到 Agent 返回
AgentFinish或达到最大步数。
下面伪代码表达了这层逻辑:
text
while (step < maxIterations) {
output = await agent(input, intermediateMessages);
if (output.isFinal) return output;
observation = await tools[output.tool].invoke(output.args);
intermediateMessages.push(output.actionMessage, observationToToolMessage(observation));
}agent_scratchpad:中间消息的传递通道
agent_scratchpad 就是循环中积累的那些中间消息。具体说,它是一组 AIMessage(含 tool_calls)加上对应的 ToolMessage 组成的列表。每次迭代,AgentExecutor 把这些消息填入提示模板里的 agent_scratchpad 占位符,让模型拥有完整的“记忆”——刚才调了什么工具、收到什么结果。
没有这个通道,模型每轮都像第一次看到问题,会不断调用同一个工具或者直接输出错误答案。
模型如何决定调用哪个工具
绑定了多个工具后,模型根据提示词中的工具描述、当前对话上下文以及可能的“思考链”来决定调用哪个。不同模型提供商对工具选择控制的实现方式不尽相同。以 OpenAI 为例,在 API 层面可通过 tool_choice 参数干涉选择行为,常见取值包括:
"auto"(默认):模型自行判断是否需要工具、调用哪个、调用几个。"any"或"required":要求至少调用一个工具。"tool":强制调用某个指定工具。
在 LangChain.js 中,可以在 bindTools 的第二个参数里传入相应选项,格式取决于所用模型:
typescript
// OpenAI 的写法
model.bindTools(tools, { tool_choice: "any" });其他模型提供商(如 Anthropic、开源模型)可能不支持这一 API 参数,或者通过提示词工程模拟类似效果。日常开发中保持默认的自动选择即可,除非一个步骤必须执行特定工具。
循环终止:最大步数与 AgentFinish
循环不会无限执行。两个终止条件:
- AgentFinish:模型判断已经拿到足够信息可以回答用户,返回最终文本。AgentExecutor 收到后立即结束循环,返回最终输出。
- 最大步数:通过
maxIterations设置。到达上限时,AgentExecutor 会抛出错误(例如Agent stopped due to max iterations),也可以根据配置强行返回当前累积的结果。
typescript
const executor = new AgentExecutor({
agent,
tools: [multiply, getWeather],
maxIterations: 5, // 最多执行 5 次工具调用
});如果模型在 5 次内没有产出最终答案,就会触发错误。可以捕获该错误并给出降级回复。
工具调用失败的恢复策略
工具执行时可能抛错——参数不合法、外部 API 超时、工具不存在(幻觉)。默认情况下,AgentExecutor 会让异常直接传播,中断整个循环。
一种常见的恢复方法是:在工具的 func 里自己捕获异常,返回错误信息字符串。这样工具调用不会抛异常,模型在下一轮里看到这段错误消息,可能调整参数重试,或选择其他工具尝试。
typescript
const safeMultiply = new DynamicStructuredTool({
name: "multiply",
description: "Multiply two numbers.",
schema: z.object({ a: z.number(), b: z.number() }),
func: async ({ a, b }) => {
if (isNaN(a) || isNaN(b)) {
return "Error: both arguments must be numeric.";
}
return (a * b).toString();
},
});更系统化的做法是利用 LangChain 的解析错误处理回调(handleParsingErrors),但这主要针对模型输出无法解析的情况。工具执行错误通常推荐在工具级别处理——把错误变成模型可阅读的反馈。
完整示例:多工具智能体
以下是一个可运行的例子,组合了乘法和天气两个工具。天气工具返回模拟数据。
定义天气与计算工具
typescript
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
const multiply = new DynamicStructuredTool({
name: "multiply",
description: "Multiply two numbers. Use when user asks to calculate a product.",
schema: z.object({
a: z.number(),
b: z.number(),
}),
func: async ({ a, b }) => (a * b).toString(),
});
const getWeather = new DynamicStructuredTool({
name: "get_weather",
description: "Get current weather for a city. Use when user asks about weather.",
schema: z.object({
city: z.string().describe("City name, e.g. Tokyo"),
}),
func: async ({ city }) =>
JSON.stringify({ city, temperature: 18, condition: "cloudy" }),
});创建 AgentExecutor 并运行
typescript
import { ChatOpenAI } from "@langchain/openai";
import { createToolCallingAgent, AgentExecutor } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant. Use tools when needed."],
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const agent = createToolCallingAgent({
llm: model,
tools: [multiply, getWeather],
prompt,
});
const executor = new AgentExecutor({
agent,
tools: [multiply, getWeather],
maxIterations: 4,
// verbose: true, // 调试时可打开
});
const result = await executor.invoke({
input: "What is the weather in Tokyo? Also compute 23 × 17.",
});
console.log(result.output);
// 预期输出类似:
// The weather in Tokyo is 18°C and cloudy. 23 × 17 is 391.观察中间步骤
executor.invoke 的返回对象除了 output,还包含 intermediateSteps 数组,每个元素记录一次工具调用的完整信息(动作、参数、观察值)。可以把它打印出来理解模型的决策顺序:
typescript
result.intermediateSteps.forEach((step, i) => {
console.log(`Step ${i + 1}:`);
console.log(` Tool: ${step.action.tool}`);
console.log(` Args: ${JSON.stringify(step.action.toolInput)}`);
console.log(` Result: ${step.observation}`);
});对于上面的复合问题,中间步骤可能是:
Step 1:
Tool: get_weather
Args: {"city":"Tokyo"}
Result: {"city":"Tokyo","temperature":18,"condition":"cloudy"}
Step 2:
Tool: multiply
Args: {"a":23,"b":17}
Result: 391这反映了模型是如何分步完成任务:先查天气,再算乘法,最后汇总。
注意点
- 模型对工具调用的支持程度不一致。不是所有模型都支持原生工具调用接口(如 GPT‑4o、Claude 等支持,而一些开源模型需要额外配置或通过提示词引导 JSON 输出)。绑定工具前务必确认所用模型的能力。
- 工具调用 ≠ JSON 模式。JSON 模式只强制模型输出合法 JSON,但不保证内容和工具意图匹配。工具调用是在 API 层面把工具定义和模型响应耦合在一起,参数 schema 是事先声明而非事后校验。
- 结构化返回值。工具的输出不一定要是自然语言字符串,也可以是 JSON 字符串,方便后续查找或展示。模型能理解 JSON,也能在最终回答里引用其中的字段。
- 幻觉与工具选择。模型可能请求调用不存在的工具(特别是工具名拼写模糊时)。参数可能用错类型。合理的名称、描述和参数注解能降低这类情况,但不能完全消除。需要借助错误恢复或提示词约束来兜底。
- 最大步数设置。
maxIterations过大会导致消耗过多 token 和时间;过小会导致复杂任务未完成就被截断。对大多数对话,3-5 次通常够用,但真正复杂推理可能需要更多,需要根据场景调优。 - 工具选择选项的提供商差异。
tool_choice这类选项属于各模型提供商的扩展参数,并非 LangChain.js 统一定义的行为。使用时请查阅所用模型的具体文档,避免将 OpenAI 的行为误认为是所有模型的通用行为。
参考链接
- [1] https://python.langchain.ac.cn/docs/concepts/tool_calling
- [5] https://docs.langchain.com/oss/python/langchain/frontend/tool-calling
- [8] https://docs.langchain4j.info/tutorials/tools
- [12] https://docs.aws.amazon.com/zh_cn/nova/latest/userguide/tool-choice.html
- [14] https://developer.cloud.tencent.com/article/2718636
- [15] https://github.com/QingyaFan/blog/issues/96
- [17] https://cloud.tencent.com/developer/article/2627113
