MISSION 02 · 手与眼TASK 05

Tool Use 原理

工具是我的手,但这只手的工作方式可能和你想的不一样:我从不亲自执行任何工具。我只会产出一段结构化的「我想调用 X,参数是 Y」,真正去跑的是你的代码。这一章把这一来一回的报文格式讲透——它就是所谓 function calling 的全部,拆开看简单得让人意外。

JSON Schematool_usetool_resultfunction calling互动 Demo

一次工具调用,其实是一次 JSON 往返

先破除一个常见误解。很多人以为「Agent 调用了天气 API」意味着我内部藏着一个 HTTP 客户端,自己发了请求。不是的。真实发生的是:

  1. 你在调用我时,连同问题一起告诉我「你有这些工具可用」,每个工具附带名字、说明、参数格式。
  2. 我判断需要用某个工具,于是不回复文本,而是回复一段结构化数据:「type 是 tool_use,name 是 get_weather,input 是 {city: '上海'}」。然后我这一轮就停了,把球踢给你。
  3. 你的代码看到这段 tool_use,真正去执行 get_weather("上海")——查数据库、发 HTTP、读文件,随便你怎么实现。
  4. 你把结果包成一段 tool_result 塞回对话,再发给我。
  5. 我读到结果,用它组织出给你的最终答复(或者决定再调一个工具)。

看清楚这个分工:我负责「决定调什么、传什么」,你负责「真正执行、返回结果」。模型和真实世界之间那道墙(第一章讲过的),就是靠这一来一回打通的。而且这道墙是安全边界——因为工具由你执行,你可以在执行前检查、拦截、要求确认。这一点在卷四讲安全时至关重要。

Android 类比

这几乎就是 startActivityForResult 的老模式,或者更现代的 ActivityResultLauncher:我(调用方)不亲自打开相机,我发出一个「我要拍照」的 Intent(= tool_use),系统去执行,拍完把结果通过 onActivityResult 回调给我(= tool_result)。我声明意图,宿主执行,结果回传。工具调用就是我和你之间的一套 Intent 协议。

工具定义:一张 JSON Schema 名片

我怎么知道有哪些工具、每个怎么用?靠你给我的工具定义。每个工具是一张名片,三个字段:

一个工具的定义
{
  "name": "get_weather",
  "description": "查询某个城市的当前天气。当用户问到天气、气温、"
               "是否下雨时调用。",
  "input_schema": {
    "type": "object",
    "properties": {
      "city": { "type": "string", "description": "城市名" },
      "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
    },
    "required": ["city"]
  }
}

name 是标识,input_schema 是一段标准的 JSON Schema——定义参数有哪些、什么类型、哪些必填、枚举取值。而 description 是最被低估的字段:它是我决定「什么时候用这个工具」的唯一依据。写得含糊,我就会该用的时候不用、不该用的时候乱用。这条我在下一章「工具设计」会展开讲——好的 description 里要说清楚「什么情况下调我」,而不只是「我是干嘛的」。

亲手构造一次调用

下面的构造器让你扮演一次完整往返:你填参数,它生成两段报文——第一段是我该吐出的 tool_use(模型 → 你),第二段是你执行后回填的 tool_result(你 → 模型)。填不同的城市和单位,看报文怎么变。这两段 JSON 就是 function calling 在网线上真实传输的东西,没有别的魔法。

工具调用构造器DEMO

注意第一段报文里,那两个参数值(城市、单位)不是你填的——在真实场景里,是从你的自然语言问题里理解、抽取、填进去的。你问「上海今天热不热」,我理解成 city:"上海"。这个「把自然语言翻译成结构化参数」的能力,是工具使用的核心价值:用户说人话,我转成 API 调用。

严格模式与并行调用

两个你迟早会用到的进阶点:

严格模式(strict)。默认情况下,我填的参数通常符合 schema,但不 100% 保证。如果你把工具标记为 strict: true(且 schema 写规范),API 会保证我吐出的 input 严格符合你的 schema——省掉你手写一堆校验代码。参数格式要紧的场景值得开。

并行调用。一轮里我可以同时要求调多个工具——比如同时查上海和北京的天气。这时你要并发执行它们,然后把所有结果放进同一条消息里一次性回给我。有个坑:如果你把多个结果拆成好几条消息分开发,会悄悄「教」我以后别再并行了,拖慢后续所有任务。记住:一批 tool_use 对应一批 tool_result,打包一起回。

⚠ 坑

解析我吐出的 input 时,永远用 JSON 解析器(JSON.parse / json.loads),别用字符串匹配或正则去抠字段。不同模型对 JSON 里的转义(Unicode、斜杠)处理略有差异,你手撸的解析迟早翻车。SDK 一般已经帮你把 input 解析成对象了,直接用。另外:工具执行失败时,别把结果丢掉——回一个带 is_error: true 的 tool_result,把错误信息告诉我,我能据此换个方法或向你澄清。

◉ Agent 自白

我想让你体会「我不执行工具」这件事有多解放。它意味着我可以拥有任何能力——只要你愿意把它包成一个工具给我。我不需要内置数据库驱动、不需要网络栈、不需要知道你的鉴权怎么做;我只需要知道「有个叫 get_weather 的东西,给它城市名,它会还我天气」。这种关注点分离是我能力的上限来源:我的能力边界,基本等于你给我的工具的边界。所以下一章特别重要——我们来聊怎么设计工具。因为一句在圈里流传的话是真的:好工具比好提示词更能决定一个 Agent 靠不靠谱