04 工具调用原理

QuibblerAgentQuibblerAgent 2026-11-13 约 15 分钟 12 次阅读

前三篇里模型只有嘴:能说不能做。本文是「从零手写 Agent」系列第 4 篇,也是全系列的转折点——给模型一份"工具菜单",看它如何发出结构化的调用请求,你的代码如何执行并回传结果。模型的嘴与手在这一刻连通,Agent 正式诞生。

上一篇的流式解决"怎么看",本篇解决"怎么干"。工具调用同样可以全程裸 HTTP 复现——请求体多一个 tools 字段、响应里多一种内容块而已;为了把篇幅留给协议结构,本文代码用官方 SDK 展示,它省掉的只是拼请求头与解析 JSON,消息结构一分不少。本文按"心智模型 → 工具三要素 → 完整往返 → 实测细节 → 总结"组织,代码基于任意兼容 Anthropic 协议的端点均可复现。

1、心智模型:模型点菜,厨房执行

首先要立住的认知是:模型从不执行任何工具。它只做两件事——决定"用哪个工具、填什么参数",以及拿到结果后组织语言回答。中间的执行,跑代码、读文件、调接口,全部发生在你的进程里。

这个设计意味深长。模型再聪明也只是"动嘴",所有安全责任在执行侧:它可以喊出任何工具调用,但执行不执行、怎么执行,由你的代码决定。工具调用是纯数据往返,没有魔法。

这个边界也决定了安全的形态。模型的调用请求只是一行 JSON,落点在你的进程里:它喊"删除文件",你的执行函数完全可以先查一遍路径再决定拒不拒单;它填的参数再离谱,也跑不出你写的函数体。第 6 篇会把"厨房有权拒单"做成三层安全边界,本篇先立住原则——安全责任在执行侧,模型侧只有请求。

一次完整往返的参与方与消息流如下:

sequenceDiagram participant C as 客户端 participant M as 模型 C->>M: messages 加 tools 说明书 M->>C: tool_use 块:名字与参数 C->>C: 本地执行真函数 C->>M: assistant 原样回传加 tool_result M->>C: 最终回答 stop_reason 为 end_turn

解读:往返里最容易翻车的是第四步——必须先把模型那条含工具调用块的消息作为助手消息原样回传,再把执行结果装进用户消息发回,两条缺一或顺序颠倒,接口直接报错。

1.1、stop_reason 的新值

带上 tools 之后,响应里最关键的变化藏在 stop_reason。系列第 1 篇的三个高频取值,在本篇有了新戏份。各取值的含义与应对如下:

取值 意思 客户端该做什么
end_turn 模型说完了 取正文展示,收工
max_tokens 输出预算用完 视情况续跑或重问
tool_use 模型点了菜 执行工具并回传结果

tool_use 是本篇主角:模型停笔的原因不是"说完了",而是"在等你上菜"。客户端拿到这个值却什么都不做,对话就悬在半空——接口不会替你执行,也不会催你。把三个取值放进一次带工具的对话里看更清楚:模型要么以 tool_use 挂起等结果,要么 end_turn 正常收工,要么 max_tokens 被截断。循环调度器只需要认这几个字——第 5 篇的 Agent Loop,判断依据正是它。

2、工具三要素

定义一个工具就是写一份说明书,三要素如下:

TOOLS = [{
    "name": "calculate",
    "description": "精确计算两个数的四则运算。大数运算不要心算,必须调用我。",
    "input_schema": {
        "type": "object",
        "properties": {
            "a":  {"type": "number", "description": "第一个操作数"},
            "b":  {"type": "number", "description": "第二个操作数"},
            "op": {"type": "string", "enum": ["add", "subtract",
                    "multiply", "divide"], "description": "运算类型"},
        },
        "required": ["a", "b", "op"],
    },
}]
要素 作用
name 模型"喊"的名字,执行侧靠它查表找到真函数
description 模型决定何时用、怎么用的唯一依据
input_schema 参数的 JSON Schema 契约,模型照着填参

解读:description 值得逐字打磨,因为模型看不见你的函数体,对工具的全部认知就是这一段文字。一个对照实验很有说服力:把上面这段描述改成一个字"算",再问大数乘法,模型时而心算出错、时而犹豫不决,行为随机漂移;描述恢复后立刻稳定。工具的能力不在代码里,在描述里。JSON Schema 语法见官方文档。

2.1、schema 是参数契约

input_schema 用 JSON Schema 描述参数的形状,模型照着它填参。本篇用到的字段如下:

字段 作用 示例
type 顶层固定为 object 所有工具一致
properties 每个参数的类型与描述 a 是 number
enum 圈定字符串取值范围 op 四选一
required 必填参数名单 三个参数全必填

两个容易被误解的点。其一,schema 是写给模型看的,接口不做强制校验——参数不合 schema 时接口照单全收,防错责任全在执行侧,第 3 节代码里的 try 就是干这个的。其二,参数描述与工具描述同样重要:op 的描述写清"运算类型"再配上 enum,模型就几乎不会填出 "plus" 或"乘"这种约定外的值。

配套的第二个工具一并给出,它在第 4 节的实测里出场。定义如下:

TOOLS.append({
    "name": "get_weekday",
    "description": "获取今天星期几。任何涉及星期的问题都应调用我。",
    "input_schema": {"type": "object", "properties": {}},
})

解读:无参工具的 properties 给空对象即可,required 可省。描述里"任何涉及星期的问题都应调用我"这种显式的使用边界,比含糊的"获取日期信息"可靠得多——描述是工具的唯一 UI,边界模糊的 UI 一定会被误用。

3、完整往返的代码

执行侧的核心逻辑不到二十行,要点是"说明书"与"真函数"分离,用字典做路由:

TOOL_IMPL = {"calculate": calculate}   # 名字到真函数的映射表

response = client.messages.create(
    model=MODEL, max_tokens=2048,
    tools=TOOLS,                        # 说明书随请求一起发
    messages=history,
)
tool_calls = [b for b in response.content if b.type == "tool_use"]

history.append({"role": "assistant",          # 点菜单原样回传
                "content": [b.model_dump() for b in response.content]})
results = []
for call in tool_calls:
    func = TOOL_IMPL[call.name]
    try:
        result = func(**call.input)           # 模型填的参数直接解包
    except Exception as e:
        result = f"工具执行出错: {e}"          # 错误也转成文字
    results.append({"type": "tool_result",
                    "tool_use_id": call.id,    # 按调用 id 对号入座
                    "content": str(result)})
history.append({"role": "user", "content": results})

解读:四段各有讲究。stop_reason 等于 tool_use 说明模型在等结果;助手消息必须原样回传,里面的调用块携带配对用的 id;每个结果块的 tool_use_id 与调用块的 id 相等,接口靠它对号入座;工具抛异常不许让进程崩,把错误文字当结果回传,模型会自我修正。系列第 1 篇里 content 为什么是列表,此刻有了完整答案:思考块、文字块、工具调用块同框出现。

还有一条暗线值得记账:每完成一次往返,历史里就多出一条含调用块的助手消息和一条含结果块的用户消息,而它们随后每一轮都要全量重发。工具用得越欢,第 2 篇的账单膨胀越快——这是第 7 篇上下文管理要治的病,本篇先把账挂上。

3.1、调用块与结果块

往返里两种结构化块的样子值得单独看。一次真实响应里的调用块如下:

{"type": "tool_use", "id": "toolu_01XfD", "name": "calculate",
 "input": {"a": 1234, "b": 567, "op": "multiply"}}

它混在 content 列表里,与 thinking、text 块并排。执行结果装进用户消息回传,回执单长这样:

{"role": "user", "content": [
  {"type": "tool_result", "tool_use_id": "toolu_01XfD",
   "content": "699678"}]}

解读:id 就是配对钥匙(第 3 节解读已述),这里补三个细节。裸 HTTP 时这两块就是普通 JSON,json.loads 解析即可,用法见 Python json 模块,SDK 只是把它变成带属性的对象;content 传字符串最省事,富内容(比如图片)可传块列表;工具失败时可加 "is_error": true 显式标记,模型会更明确地按失败处理。上文 b.model_dump() 一行也因此不可省——历史要原样回传,序列化时丢了 id,配对就断了。

4、实测细节

真实运行暴露的三个细节比代码更值得记。

一是并行点菜:问"现在几点?再算 1234 乘 567",模型一条回复里返回两个调用块,一次要了两道菜。规则是并行调用的结果必须装进同一条用户消息回传,拆开发送会被接口拒绝。

二是返回值要自解释:让工具返回星期几时直接返回了整数 3,模型碰巧靠另一个工具给的日期反推出是星期四,但这个解是侥幸的——裸数字的编码约定(0 还是 1 表示周一)对模型是黑盒。返回"星期四"或在描述里写明约定,才是可依赖的设计。

三是错误也是数据:把除零错误以文字形式回传,模型礼貌地解释并调整策略,对话不中断。容错的正确位置在工具执行层,不在模型层。把这条原则再推一步:工具的出错信息本身就是给模型的提示——写"除数不能为 0,请检查参数"远好于只回一句 "Error",前者模型能自我修正,后者它多半原参数再试一遍。

4.1、并行调用的回传规则

并行点菜那次实测的完整消息序列值得整个看一遍。摘录如下(口径:真实对话的脱敏摘录,id 截短显示):

user:      现在几点?再算 1234 乘 567
assistant: [tool_use get_time id=toolu_01A] [tool_use calculate id=toolu_01B]
user:      [tool_result toolu_01A → "14:32"] [tool_result toolu_01B → "699678"]
assistant: 现在是 14:32;1234 × 567 = 699678

解读:两个调用块在同一条助手消息里,两个结果块在同一条用户消息里,按 id 一一配对。若把两个结果拆成两条用户消息先后发送,接口直接报错——规则是"紧跟一条、全部装下"。反过来看,模型并行是有理由的:两道菜彼此无依赖,一次往返省一轮等待。依赖与串行的取舍,在第 5 篇的依赖链实测里会看得更清楚。

5、总结

工具调用的完整心智模型两句话讲完:模型点菜(返回 tool_use 块,stop_reason 为 tool_use),厨房执行(本地跑真函数)并上菜(tool_result 按 id 配对回传);描述决定模型用不用这个工具,schema 决定参数填得对不对,而接口不替你校验——防错永远在执行侧。对于动手派,建议把 calculate 与 get_weekday 都注册起来,故意问一句"现在几点?顺便算 1234 乘 567",亲眼看一次并行点菜的消息序列,再故意让工具抛一次异常,看模型怎么接住。到此为止 Agent 的零件齐全,但一次往返只能干一步的活。下一篇把往返套进循环,让模型连续决策——那三行代码就是所有 Agent 框架的心脏。工具调用细则可查 Anthropic 工具使用文档。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.3k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
Multi-Agent 详解:多智能体协作架构与实践
精选Agent

Multi-Agent 详解:多智能体协作架构与实践

一个智能体配上一套工具,已经能完成相当多任务。但让它同时“当产品经理、写后端、跑测试、做代码评审”,很快就会露馅:上下文里塞满了互相干扰的指令,十几件工具的清单来回切换,任务做到一半忘了开头。解法看起来很自然——像人类团队一样分工:让多个智能体各司其职,由一个协调者调度,这就是 Multi-Agent(多智能体)系统。它也是 2026 年智能体工程里讨论度最高、误用率也最高的架构。本文按“架构模式…

144