《AI Agent 核心机制》第二篇:Agent 怎么“动手“:工具调用与 Function Calling 的工程真相
引子:第二轮面试,这次聊"动手"
上次那场关于记忆的拷问(《Agent 记忆与 RAG 怎么做》)之后,对方又约了第二次。寒暄不到一分钟,问题就来了:
“你们的 Agent 能查库存、能发工单,说说看,模型是怎么调用这些工具的?”
我心想这题我熟:“Function Calling 啊,把工具定义往请求里一传,模型就会自己调用了。”
对方停顿了两秒:“你这句话里,有一个词是错的。”
然后,又是熟悉的连环追问:
“模型真的’调用’了那个函数吗?执行到底发生在哪?”
“模型吐出来的参数,为什么恰好是合法 JSON?它怎么知道你的函数长什么样?”
“要同时查三个城市的天气,你的 Agent 得跑三个来回?”
“工具报错了怎么办,Agent 当场趴窝?”
“公司要接五个内部系统,每家接口都不一样,你打算写几套对接代码?”
“五百条数据的批量任务,每一条都过一遍模型?token 的钱谁出?”
“真上了线,模型把同一笔订单提交两次,你怎么兜底?”
七个问题,还是那个套路:从"会说术语"一路钻到"真会实现"。而那个错的词,就是**“调用”**——这个误会几乎人人都有,它恰好也是理解整套机制的最佳入口。这篇文章照旧把七问补完,作为全文的路线图。
第一章:先拆穿那个错词——模型从头到尾没执行任何东西
面试官: 模型真的"调用"了那个函数吗?执行到底发生在哪?
误会之所以普遍,是因为 “Function Calling” 这个名字起得实在有误导性。听起来像是模型伸手执行了你的函数——但大模型本质上是一个纯文本进、纯文本出的函数:没有网络访问,没有运行时,没有手。它"调用工具"的全部能力,其实只是输出一段格式规范的文本,表达"我想让你帮我执行某个函数"的意图。
真正的执行,从头到尾发生在你的代码里。完整闭环长这样:

把六步拆开,有三个容易被忽略的关键点:
第一,②和③之间有一条清晰的产权分界线。 模型在第②步输出的是一个结构化的"调用意图"(在 Claude API 里是一个 tool_use 内容块,带着工具名和参数 JSON),然后就停下来了(stop_reason 变为 tool_use),把球踢给你。要不要执行、怎么执行、执行前要不要校验参数和权限——全由你的代码说了算。模型是决策器,不是执行器。
第二,第④步不是"补发一条消息",而是一次全新的完整请求。 大模型 API 是无状态的:它不记得上一轮说过什么,所谓"多轮对话"是客户端每次把完整历史全量重发。所以工具结果(tool_result)要连同之前的全部消息一起再发一遍——这也顺带解释了上一篇讲的上下文管理为什么重要:工具调用越多,历史里堆积的调用轨迹越厚,后面每一轮都在为它们付 token 钱。
第三,⑤→②那条回边,才是 “Agent” 的本体。 单次工具调用只是"带工具的问答";让模型拿到结果后自己判断"信息够不够、要不要再调下一个",循环直到能回答为止——这个循环跑起来,才叫 Agent。循环怎么控制、怎么防止空转,是下一篇(规划与 ReAct)的主题,这里先把结构立住。
为什么要设计成这么"绕"的一来一回?因为它把能力和权力分开了:模型贡献判断力,但每一次触碰真实世界的动作都必须经过你的代码——这里就是天然的检查点,鉴权、审计、限流、沙箱全都挂在这一层(护栏怎么设计,04 篇细讲)。
💡 开发者视角:第一次手写这个闭环,几乎人人踩过三个坑:(1)只把模型回复的文本部分存进历史,丢了
tool_use块——下一轮直接报错,因为tool_result找不到能配对的 id;(2)tool_result没有放进紧随其后的那条 user 消息,或者并行调用只回传了部分结果——同样报错或卡住;(3)忘了第⑤步,把工具的原始返回直接甩给用户——用户看到一坨 JSON。记一个心智模型:tool_use 和 tool_result 是一对括号,必须成对、紧邻、id 对齐。
第二章:Schema 是写给模型的说明书,描述质量就是调用质量
面试官: 模型吐出来的参数,为什么恰好是合法 JSON?它怎么知道你的函数长什么样?
这个问题拆开是两问:语法为什么合法,语义为什么(经常)正确。答案完全不同。
语法合法,靠的是"训练 + 约束"双保险。 后训练阶段(《暴力美学》第六章讲过的三部曲),模型被喂过海量"看到工具清单 → 输出调用块"的示范轨迹,格式早已刻进权重;推理阶段,厂商还可以再加一层约束解码(Constrained Decoding)——生成参数时只允许采样出符合 JSON Schema 的 token,语法上想错都错不了。两层保险的配比各家不同,所以你会看到:主流模型输出格式坏掉的概率已经很低,但在没开严格模式的场景下,偶尔仍会出现编造参数名、枚举值拼错这类翻车——格式对了,内容错了。
而语义正确,几乎完全取决于你的 Schema 写得好不好。 这是全篇最值得划线的一句:工具描述是最被低估的 Prompt。 它每次请求都原样进入上下文,是模型判断"这工具是干嘛的、什么时候该用我、参数怎么填"的唯一依据。模型没读过你的代码,没见过你的数据库,它对你系统的全部认知,就是那几行 description。
同一个查订单工具,两种写法:
// 坏:模型只能靠猜
name: search_orders
description: 查询订单
// 好:模型照着做就行
name: search_orders
description: 按条件检索订单列表。适用于用户询问"我的订单在哪/什么状态"
这类问题;只做查询,不能修改订单(修改请用 update_order)。
- status 只接受枚举值:pending / shipped / delivered
- date_range 格式为 YYYY-MM-DD,默认最近 30 天,时区 UTC+8
- 返回最多 20 条,每条含订单号、状态、金额(单位:分,不是元)
- 查无结果返回空数组,不报错
差别在哪?好的版本回答了模型决策时真正要问的五件事:这工具做什么、什么时候该用、什么时候不该用、参数的确切格式、返回什么(含单位和边界情况)。"金额单位是分不是元"这种细节尤其致命——漏写一句,模型就可能把 128000 分理解成十二万八千元,一路错到底,而且这种错不抛异常,最难排查。
描述之外,还有几条同样便宜但有效的杠杆:
- 命名自解释:用"动词_宾语"式命名(
get_weather、create_ticket),并避免两个工具语义重叠——search_web和browse_web并存,模型选错的概率会陡增; - 能用枚举就不用自由文本:参数值域收得越窄,模型的发挥空间越小,错误率越低;
- 必填参数越少越好:每多一个必填项,就多一个被编造的机会;有合理默认值的一律做成可选;
- 工具数量本身就是成本:每个 Schema 都占上下文,几十个工具同时挂载,既烧 token 又让模型陷入选择困难。按任务动态挂载,或做两级选择(先选类别再选具体工具),都是成熟做法。
这一章面试官的标准追问是:"描述写得模糊和精确,效果能差多少?"我现在的答案是:很多"Agent 不聪明"的问题,真相是说明书烂,不是模型笨。 Anthropic 的官方文档把"详尽的描述"列为影响工具调用准确率的头号因素,建议每个工具的说明至少三四句起步。把描述改精确,往往比换一个大一号的模型见效更快,还便宜得多。
💡 开发者视角:一个零成本自测法——把工具定义单独发给模型,问它"这个工具什么时候该用、参数怎么填、和 XX 工具怎么区分"。它复述得含糊,就说明你写得含糊。模型是一个只能读文档、不能翻代码、也没法跑来问你的新同事,用这个标准审自己的 Schema,大部分问题当场现形。
第三章:来回次数是第一成本——并行与串行怎么选
面试官: 要同时查三个城市的天气,你的 Agent 得跑三个来回?
不用。模型可以在一条响应里同时输出多个 tool_use 块——这就是并行工具调用。你的代码把三个调用并发执行,把三份结果按 tool_use_id 一一对应,一次性喂回去。一个来回,办完三件事。
但前提是:这几个调用互相不依赖。 一旦后一步要用前一步的结果——先查到订单号,才能拿订单号查物流——就只能老老实实串行,一步一个来回:

图 2:左边三个调用互不依赖,一个来回打包完成;右边后一步依赖前一步的输出,两个来回一步不能省。
为什么要这么在意来回次数?算两笔账:
延迟账:每个来回 = 一次完整的模型推理 + 最慢那个工具的执行时间。模型推理动辄几秒,是大头;串行 N 步,延迟就是 N 倍叠加。用户感知到的"这 Agent 怎么这么慢",十有八九是来回次数堆出来的。
成本账:API 无状态,每个来回都要全量重发历史——第 10 轮请求里,前 9 轮的调用轨迹全都在,input token 随轮数滚雪球(prompt 缓存能显著缓解,但不是免费的)。
所以这条工程铁律值得贴墙上:round-trip 数量是 Agent 延迟和成本的第一大头:能并行就并行,能合并就合并。 甚至工具设计本身也该为此服务——与其提供三个必须串行的细粒度工具,不如提供一个一步到位的粗粒度工具(比如把"查订单号 + 查物流"合并成一个 track_order),直接把一个来回省掉。
💡 开发者视角:并行调用的三个坑:(1)结果必须回齐——三个
tool_use只回传两个tool_result,请求直接报错,漏一个都不行;(2)有的模型/SDK 对并行有开关(比如disable_parallel_tool_use),行为不符合预期时先查配置,别急着怪模型;(3)并行只适合读操作——两个写操作并发打到同一资源,竞态问题和普通后端并发一模一样,模型可不懂加锁;写操作要么串行,要么在工具实现层自己保证安全。
第四章:失败是常态——把错误喂回去,模型会自己纠
面试官: 工具报错了怎么办,Agent 当场趴窝?
恰恰相反——失败处理是工具调用最能体现 "Agent 味"的地方:传统流水线遇到异常就终止,而 Agent 可以把错误当成新信息,自己纠偏。
但"自己纠偏"不等于把所有错误一股脑丢给模型。生产系统里,错误要按性质分三层,处理位置完全不同:
第一层:瞬时错误,代码层消化,模型无感。 网络抖动、429 限流、偶发超时——这类错误直接在代码里重试(配指数退避)就好,犯不着浪费一个来回让模型说"那再试一次吧"。模型每思考一次都是真金白银,别拿它当重试调度器。
第二层:语义错误,喂回模型,让它自愈。 参数格式不对、查无结果、业务规则拒绝——这类错误代码修不了,因为"怎么改"需要理解语义。做法是把错误信息标记后(Claude API 里是 tool_result 带上 is_error: true)当成一条正常的工具结果传回去:

图 3:错误不是终点,而是喂给模型的新信息 —— 前提是错误信息写得足够"可操作"。
这里藏着一个几乎所有团队都会跳过、但回报极高的设计点:错误信息是写给模型看的 UX。 同一个失败,返回 “Error 400” 和返回 “date 参数格式应为 YYYY-MM-DD,收到的是’明天’”,自愈率天差地别——前者模型只能瞎猜,后者一次改对。原则就一句:说清哪里错了、怎么才算对,最好再带个正确示例。
第三层:系统性错误,明确告知此路不通。 没权限、服务下线、配置缺失——这类错误重试一万次也不会好。喂回去时要把话说死(“该工具当前不可用,请改用其他方式或如实告知用户”),否则模型会锲而不舍地反复撞墙,烧完你的预算。
最后是兜底:防死循环。同一工具、同一参数连续失败 N 次就熔断;给整个任务设总步数或总 token 预算;连续几轮没有任何新进展就强制收敛,如实告诉用户"卡住了"。另外别忘了上一篇的上下文视角——失败重试的垃圾轨迹会塞满历史,该剪除的及时剪除(context editing 干的就是这个活),别让模型在一堆错误日志里越看越糊涂。
💡 开发者视角:两个反面教材。一是把原始 stack trace 整段塞回去——又长又带偏,模型会热心地开始"修"你的框架代码;二是只说"执行失败"四个字——模型只能盲改参数碰运气。正确姿势是给每个工具设计错误信息模板:一句话说明原因 + 一句话说明正确格式。这活半天就能干完,效果立竿见影。
第五章:MCP——把"接工具"从 M×N 变成 M+N
面试官: 公司要接五个内部系统,每家接口都不一样,你打算写几套对接代码?
在 2024 年底之前,这个问题的诚实答案是:接口数乘以客户端数那么多套。数据库、GitHub、Slack、内部 CRM……每家的接口格式、鉴权方式、错误码都不一样;而要用工具的"宿主"也不止一个——你的 Agent 服务、Claude 桌面端、IDE 插件。M 个宿主 × N 个工具 = M×N 套一次性胶水代码,并且每接一个新工具,每个宿主都得再写一遍。
MCP(Model Context Protocol,模型上下文协议)解决的就是这个 M×N 问题:把"工具怎么被发现、怎么被描述、怎么被调用"标准化成一个开放协议——宿主实现一次协议,就能用所有符合协议的工具;工具按协议包一次,就能被所有宿主使用。M×N 塌缩成 M+N:

图 4:左边每条线都是一套要单独开发维护的对接代码;右边接口收敛到协议层,新工具接一次,所有客户端复用。
概念上记三组词就够了:
- 角色:Host(宿主应用)通过内置的 Client(协议客户端)连接一个或多个 Server(工具提供方,比如 “GitHub Server”、“内部订单系统 Server”);
- 原语:Server 可以提供 tools(可执行的动作)、resources(可读取的数据)、prompts(预设模板)三类东西;
- 传输:本地进程走 stdio,远程服务走 HTTP,协议本身不关心传输层。
这里要做一个关键的认知纠偏:MCP 没有让模型变得更会用工具。 到了模型那一端,MCP 工具照样被翻译成前几章那种普通的工具清单,走一模一样的 Function Calling 闭环。它是 USB-C 式的标准化——统一的是接口,不是能力;省的是集成工程量,换来的是生态:装一个现成的第三方 Server,就像给 Agent 装一个 App,而不是启动一个为期两周的对接项目。它由 Anthropic 在 2024 年 11 月开源,如今主流厂商和工具链都已跟进,基本成了事实标准。
💡 开发者视角:便利的另一面是新的攻击面——接入第三方 MCP Server,等于把别人写的工具描述和工具返回值直接放进你的上下文。描述里可以藏指令(所谓 tool poisoning),返回内容更是间接提示注入的主通道。上一篇说过"检索内容是数据,不是指令",这里性质相同但后果更重,因为工具是能动手的。给团队立一条纪律:装 MCP Server 像装浏览器插件,来源不明的不装,权限一律最小化。(攻防细节留给 04 篇安全专篇。)
第六章:编程式工具调用——让模型写代码,而不是一次次伸手
面试官: 五百条数据的批量任务,每一条都过一遍模型?token 的钱谁出?
这一问打在了经典 Function Calling 模式的软肋上。回看图 1 的闭环:每一次工具调用,中间结果都要进上下文,而且随后每一轮都会被全量重发。 处理 500 条订单,逐条"模型想一下 → 调一次工具"就是 500 多个来回;哪怕一次读回 500 行再让模型加工,这 500 行也会全程赖在上下文里,后续每一轮都在为它们付费。延迟、成本、上下文污染,三头吃亏。
破局思路是换一种"动手"的方式——编程式工具调用:不再让模型一次次伸手,而是把工具暴露成代码 API,让模型写一段程序,丢进沙箱一口气跑完;循环、筛选、聚合、重试全部发生在代码里,中间数据从头到尾不经过模型上下文,只有最终结果(或摘要)回传:

图 5:上面每个中间结果都要在模型里过一遍;下面循环和中间数据全留在沙箱,上下文只接住最终答案。
这不是纸面优化。Anthropic 工程博客给过一个真实量级:同一个跨系统搬运会议记录的任务,逐次调用消耗约 15 万 token,改成代码编排后约 2 千 token,降幅 98.7%。Cloudflare 等厂商也在推同方向的 “Code Mode”——把 MCP 工具转成 TypeScript API,让模型写代码去调。行业共识正在形成:模型更擅长"写调用工具的代码",而不是"一次次直接调用工具"——毕竟训练语料里,真实世界的代码比工具调用轨迹多出几个数量级。
什么时候该切到这种模式?一条实用的判断线:
- 适合:批量数据处理、步骤确定的流水线、中间数据巨大但最终结论很小的任务(“统计这批日志的错误分布”);
- 不适合:每一步都需要模型做语义判断的任务(逐封读邮件决定怎么回),以及高风险写操作——代码一口气跑完意味着中间没有检查点,该有人审批的动作,不能塞进一段自动执行的脚本。
和第三章连起来看,这是一套组合拳:并行调用做横向压缩(一轮多办几件事),编程式调用做纵向压缩(多轮塌缩成一轮),目标是同一个——把昂贵的模型推理,只花在真正需要判断的地方。
💡 开发者视角:这套玩法的前提是沙箱——文件系统、网络、资源配额都要隔离,跑完即弃。让模型写的代码直接在生产环境执行,相当于把 shell 交给一个热情过头的实习生:大概率没事,出事就是大事。沙箱怎么搭、权限怎么划,04 篇细讲。
第七章:上线后的踩坑清单——每一条背后都是真实事故
面试官: 真上了线,模型把同一笔订单提交两次,你怎么兜底?
最后一问,问的已经不是"会不会用工具",而是"敢不敢把工具交给它"。以下七条,每一条都值得在评审会上单独过一遍。
幂等性:重复调用是常态,不是异常。 超时后的重试——不管是你代码发起的,还是模型自己决定再试一次——随时可能让同一个写操作执行两遍。转账、下单这类工具必须幂等:调用方生成唯一的 request_id,服务端凭它去重。和分布式系统里的做法一模一样,只是这次的"不可靠调用方"换成了模型。
破坏性操作要分级。 读操作放行,写操作留痕,删除/转账/群发这类不可逆操作必须过人工确认。别指望在 Prompt 里写"危险操作请三思"就完事——权限控制要做在代码层的检查点上(还记得图 1 里②③之间那条分界线吗?它就是为这个准备的)。
大结果要治理。 工具一口气返回 10MB 的 JSON,上下文直接爆炸。返回层就要做截断和分页;更优雅的做法是"落盘 + 引用":完整结果写进文件,只返回摘要和路径,配合读取工具按需取用——这正是上一篇"记忆外置"思想在工具层的翻版。
工具结果是头号注入通道。 会读网页、读邮件、读工单的 Agent,等于把外部世界的任意文本直接接进上下文。恶意内容可以伪装成正常返回,诱导模型执行额外动作——间接提示注入在工具时代的威力远大于 RAG 时代,因为工具能动手。(04 篇的主菜,这里先立牌子。)
Schema 变更是破坏性变更。 改个参数名、加个枚举值,旧的评估集会失效、缓存的 prompt 前缀会失效、模型对老格式的"手感"也会失效。把工具 Schema 当对外 API 管理:有版本、有变更记录、变更后跑回归评估。
评估要拆成三类错误分开看。 “工具调用准不准"太笼统,拆开才能定位:漏调(该用工具时空手作答,常见病因是描述没写清"什么时候用我”)、乱调(不该调时乱调,甚至编造不存在的工具或参数)、参数错(调对了工具,但格式、单位、枚举值错了)。三类的病因和药方完全不同,混在一个指标里等于没测。(评估体系是 06 篇的主题。)
长任务走异步。 有的工具一跑十分钟,同步等待会拖死整个循环。成熟做法是任务模式:submit_job 立即返回 job_id,再配一个 check_status 工具让模型轮询;或者干脆挂回调,任务完成再唤醒 Agent。
结语:"调用"是个错词,但它是个好错词
回到开头那句被纠正的话。“把工具传给模型,它就会调用了”——现在可以说清错在哪:模型从头到尾没有调用任何东西,它只是在合适的时机,说出了一句格式严谨的"请你帮我调一下 X"。 决策与执行分离,判断力归模型,执行权归代码——这条分界线是工具调用全部工程设计的地基:Schema 是给决策器看的说明书,闭环是两者之间的通信协议,错误喂回是容错机制,MCP 是接口标准化,编程式调用是批处理优化。
按系列惯例,把玄学拆成老熟人:工具 Schema 是接口契约(只是这次的读者是一个格外较真、又完全没背景知识的调用方),调用闭环是事件循环,错误喂回是重试与补偿,MCP 是 USB 和驱动程序的关系,编程式工具调用则相当于把逐条解释执行换成了整段编译执行。没有魔法,全是被验证过几十年的工程套路,换了个新的调用方而已。
但工具会用了,更难的问题立刻浮出来:一个模糊的大目标,怎么拆成一串正确的调用顺序?做到一半发现路走错了,怎么办?什么时候该停?Agent 卡在循环里空转,怎么发现、怎么兜底?——这是下一篇的主题:规划与 ReAct 循环。
这个系列会持续更新,也欢迎把你被问住的问题丢过来,说不定就是下一篇的选题。
本文是《AI Agent 核心机制》系列第二篇。第一篇《Agent 记忆与 RAG 怎么做:一场把我问到哑口无言的技术面试》讲"怎么记得住事";想从头搞懂大模型本身怎么"思考",请看《别再把 AI 当黑盒:万字拆解大语言模型的底层"暴力美学"》与《自注意力中的 QKV 详解》系列。
更多推荐


所有评论(0)