创见博客
什么是 Tool:为什么需要、怎么实现、如何调用
七崽爱吃小饼干2026/10/14阅读 0

大模型(LLM)很强大,但它本质上是一个「只会根据上下文预测下一个 token」的文本模型。它能对话、能写作、能推理,却有几件根本做不到的事:拿不到实时数据、访问不了你的私有系统、也无法真正产生副作用。Tool(工具) 就是用来补齐这些能力的关键机制。这篇文章从「是什么、为什么、怎么实现、怎么调用」四个角度,把 tool 讲透。

一、什么是 tool

一句话:tool 是交给模型调用的一个函数。

但这里有个必须澄清的认知误区:模型并不会亲自执行这个函数。

模型的角色只是「决策」——它根据用户的问题,判断该不该用工具、用哪个工具、参数填什么,然后把结论输出成一段结构化的调用请求(tool_calls)。真正干活的,是你自己的程序。程序拿到请求后执行对应函数,再把执行结果作为一条消息回传给模型,模型基于结果继续推理,直到给出最终答案。

所以一次工具调用的本质,是模型与程序之间的分工协作:

用户提问
  └─> 模型决策:我要调用 get_weather(city="北京")
        └─> 你的程序执行:得到「北京今天晴,25℃」
              └─> 结果回传给模型
                    └─> 模型给出最终答案

可以这样理解两个角色:

  • 模型负责「想」:理解意图、决定用不用工具、生成参数、结合结果作答;
  • 程序负责「做」:真正执行函数、访问外部世界、控制副作用。

这种分工带来一个很重要的好处:执行过程完全在程序这一侧,因此可测试、可审计、可拦截、可加权限校验——而不是把外部能力「烧」进模型里。

二、为什么需要 tool

模型自身有四个典型的短板,工具正是针对它们设计的:

1. 知识有截止日期,拿不到实时数据。 模型只知道训练时见过的内容,问它「现在北京几度」「今天美元汇率多少」,它要么不知道,要么一本正经地编造(幻觉)。接一个天气 API、汇率接口,就能给出真实答案。

2. 访问不了你的私有数据。 公司的订单数据库、内部工单系统、你本地的文件——这些模型从未见过。把它们包装成工具,模型才能在回答时引用。

3. 不能产生副作用。 模型只能「说」,不能「做」。下单、发邮件、写文件、调用支付接口这些真实动作,必须由程序在受控的前提下执行。

4. 不擅长精确计算与确定性逻辑。 大数乘法、日期推算、严格的格式转换,交给专门的函数比让模型「心算」可靠得多。

换一个角度看:模型擅长的是理解与推理(听懂你要什么、把结果组织成人话),而数据的获取、精确的执行交给工具。工具让模型从「一个知道很多但封闭的脑子」,变成一个「能感知外部世界、能动手做事的智能体(Agent)」。

三、怎么实现 tool

LangChain 提供了 tool(func, fields) 辅助函数来定义工具。它接收一个普通函数和一段元信息,自动把参数模式转成模型能读懂的 JSON Schema,返回一个工具对象。

3.1 一个工具由四部分组成

组成给谁看作用
name模型工具名,模型靠它指定「调用谁」,需唯一
description模型说明能做什么,模型靠它判断「什么时候用」
schema模型参数形状(类型、必填、含义),模型靠它生成合法参数
func程序真正的实现逻辑,普通 JS 函数

前三个决定模型「知不知道、会不会用」这个工具;最后一个由你的程序执行。

3.2 定义结构化工具(最常用)

js
import { tool } from "langchain";
import { z } from "zod";

const getWeather = tool(
  ({ city }) => `${city} 今天晴,气温 25℃`,   // func:真正的实现
  {
    name: "get_weather",                       // name
    description: "查询指定城市今天的天气",      // description
    schema: z.object({                          // schema
      city: z.string().describe("要查询的城市名,例如 北京"),
    }),
  }
);

z.object 定义参数结构,.describe() 写上字段含义。LangChain 会把它转成模型实际看到的 JSON Schema:

json
{
  "type": "object",
  "properties": {
    "city": { "type": "string", "description": "要查询的城市名,例如 北京" }
  },
  "required": ["city"]
}

这份 JSON Schema 才是模型真正看到的东西。模型靠字段名和 description 来生成参数——描述写得越清楚,模型传参就越准。

3.3 多参数工具

js
const getExchangeRate = tool(
  ({ from, to, amount }) =>
    `${amount} ${from} ≈ ${(amount * 7.2).toFixed(2)} ${to}`,
  {
    name: "get_exchange_rate",
    description: "查询两种货币之间的汇率,并把给定金额换算过去",
    schema: z.object({
      from: z.string().describe("原货币代码,例如 USD"),
      to: z.string().describe("目标货币代码,例如 CNY"),
      amount: z.number().describe("要换算的金额"),
    }),
  }
);

3.4 只吃一个字符串的工具

js
const echo = tool((input) => `你说的是:${input}`, {
  name: "echo",
  description: "把用户说的话原样回显,用于测试",
  schema: z.string().describe("任意一段文本"),
});

这类工具的参数会被包装成 { input: string } 暴露给模型。

3.5 工具可以脱离模型单独调用

工具对象本身是一个 Runnable,能像普通函数一样直接 invoke,非常适合写单元测试、调试实现:

js
await getWeather.invoke({ city: "北京" });
// "北京 今天晴,气温 25℃"

注意:传入的是普通对象({ city: "北京" }),不是 JSON 字符串;LangChain 会按 schema 校验并解析参数。

3.6 实现工具的注意事项

  • name 要自解释:get_weather 远好于 gw;
  • description 写清「做什么 + 何时用」:模型完全靠它做选择,含糊会导致该用时不用、不该用时乱用;
  • schema 用 .describe() 标注单位与格式:避免模型瞎猜;
  • func 要能容错:外部 API 可能失败,返回的错误信息最好也能被模型读懂,方便它换策略。

四、tool 的调用过程

一次完整的工具调用,是一个「模型决策 → 程序执行 → 结果回传 → 再决策」的循环。分三步。

第 1 步:把工具绑定给模型

模型事先并不知道有哪些工具可用,需要注册:

js
const model = await initChatModel("deepseek:deepseek-chat", { apiKey });
const modelWithTools = model.bindTools([getWeather, getExchangeRate]);

第 2 步:模型返回带 tool_calls 的 AIMessage

js
const messages = [
  new SystemMessage("你是助手,需要实时信息时必须调用工具,不要凭空编造。"),
  new HumanMessage("北京今天天气怎么样?"),
];

const aiResp = await modelWithTools.invoke(messages);
// aiResp.content    === ""   (这一轮通常没有正文)
// aiResp.tool_calls === [
//   { name: "get_weather", args: { city: "北京" }, id: "call_xxx", type: "tool_call" }
// ]

关键点:模型只是提出了请求,并没有执行。tool_calls 是一份「调用清单」,可能包含一个或多个工具(也可并行调用多个)。

第 3 步:程序执行工具,用 ToolMessage 回传

js
messages.push(aiResp);

for (const call of aiResp.tool_calls) {
  const result = await toolMap.get(call.name).invoke(call.args);
  messages.push(
    new ToolMessage({
      content: String(result),
      tool_call_id: call.id,   // 必须对应上具体是哪次调用
      name: call.name,
    })
  );
}

然后再次调用模型。这次历史里多了工具结果,模型会基于它给出最终答案:

js
const finalResp = await modelWithTools.invoke(messages);
console.log(finalResp.content);

如果模型还想调用别的工具,就继续循环;直到某次返回没有 tool_calls,说明它拿到了足够信息,循环结束。

消息历史的演化

一次调用结束后,消息类型序列是:

system -> human -> ai -> tool -> ai

如果一轮里并行调用了两个工具:

system -> human -> ai -> tool -> tool -> ai

也就是说,一次工具调用其实是三段式:AI 提出请求 → Tool 返回结果 → AI 给出答案。这也解释了为什么每个 ToolMessage 都必须带 tool_call_id:一次对话里可能有多个(甚至并行)调用,得靠 id 把「结果」和「请求」一一对上号。

自动化:createAgent

上面的 for 循环就是 Agent(智能体)的核心。LangChain 用 createAgent 把「绑定工具 → 执行工具 → 回传结果 → 再调用」整个循环封装了起来:

js
import { createAgent } from "langchain";

const agent = createAgent({
  model: "deepseek:deepseek-chat",
  tools: [getWeather, getExchangeRate],
  systemPrompt: "你是助手,需要实时信息时必须调用工具,不要凭空编造。",
});

const res = await agent.invoke({
  messages: [{ role: "user", content: "上海今天天气怎么样?" }],
});
console.log(res.messages.at(-1).content);

res.messages 的类型序列同样是 human -> ai -> tool -> ai。生产环境直接用 createAgent;而理解手动循环,是为了知道它内部到底发生了什么。

五、小结

tool 是连接「模型推理」和「外部能力」的桥梁,用来弥补模型在实时数据、私有数据、副作用操作、精确计算上的短板。

实现一个工具,就是写一个普通函数并配上一段元信息:name(叫什么)、description(能干什么、何时用)、schema(参数长什么样,用 zod 描述)、func(怎么实现)。

调用过程是一个分工明确的循环:模型决策(输出 tool_calls)→ 程序执行(运行对应函数)→ 结果回传(包成 ToolMessage,用 tool_call_id 关联)→ 模型再决策,直到不再请求工具、给出最终答案。

记住三点:name / description / schema 是写给模型看的,决定它会不会用、用得对不对;func 是给你执行的,可脱离模型单独测试;手动循环帮你理解原理,createAgent 用于生产。

评论
0/100