这篇学完你能回答什么
- Function Calling 的完整机制是什么?模型真的「调用」函数了吗?
- 工具的 schema 和描述怎么设计,模型才能选得准、填得对?
- 并行工具调用什么时候用?工具结果太长塞爆上下文怎么办?
从一个真实故障讲起
一个电商客服 Agent,上线三个月,工具从 6 个长到 47 个——每来一个新需求就加一个工具,很自然。然后出现了两类越来越频繁的事故:
第一类:选错工具。 用户说"这单我不想要了",模型调用了 refund_order(直接退款)而不是 cancel_order(取消未发货订单)。两个工具的描述分别是"退款"和"取消订单"——在人看来区别很大,但它们的语义邻域高度重叠,而模型只能靠这两行字做判断。更糟的是还有 cancel_refund、order_cancel_apply 这类历史遗留命名。抽样统计,工具选择准确率只有 71%。
第二类:上下文被工具返回值撑爆。 get_order_detail 直接把订单的完整 JSON 返回给模型,包含 60 多个字段、物流轨迹全量节点、优惠券快照。单次返回 8000+ token。一个需要查三笔订单的会话,光工具返回就吃掉 25000 token,模型开始遗忘对话早期的用户诉求,答非所问。
修复动作没有换模型,只做了三件事:
- 工具收敛到 9 个,语义重叠的合并成一个带
action枚举参数的工具; - 把 list 型工具改成 search 型:
get_order_detail增加fields参数,默认只返回 8 个核心字段; - 给每个工具的描述加上"什么时候不要用我":
cancel_order的描述里明写"仅适用于未发货订单;已发货请使用 return_request"。
工具选择准确率回到 94%,平均会话 token 降了 62%。
结论:Agent 的效果,一半在模型,另一半在工具设计。而工具设计的本质是——你在给模型写一份它只能读一遍、且没法提问的说明书。
工具三个月长到 47 个
每来一个新需求就加一个,很自然
语义邻域高度重叠
还有
cancel_refund这类遗留命名模型只有两行字可读断点
「退款」和「取消订单」,人看区别很大
该取消的单被直接退款
抽样统计,工具选择准确率只有 71%
get_order_detail 返回60 多个字段全量 JSON加 fields 参数,默认 8 个核心字段核心概念:先打比方,再给定义
比喻:模型是点菜的客人,不是厨师。
你给它一份菜单(工具 schema),它说"我要一份宫保鸡丁,不要花生"(输出一个结构化的调用意向)。然后厨房是你的代码——真正去炒菜、去查数据库、去调第三方 API 的,从头到尾都是你。菜做好了端上桌(把结果塞回上下文),客人才知道这道菜长什么样。
所以那句面试高频反问的答案是:模型没有调用任何函数。它只是生成了一段符合你给定 schema 的结构化文本。所谓 Function Calling,全称应该念成"function call request generation"。
定义与术语对照:
- Function Calling / Tool Use(函数调用 / 工具使用):模型根据工具定义,输出结构化的调用请求(工具名 + 参数),由宿主程序执行并把结果回填到上下文的机制。
- Tool schema(工具契约):通常是 JSON Schema,描述工具名、用途、参数类型与约束。它会被拼进模型的上下文,是要花 token 的。
- tool_call_id(调用标识):一次请求与其结果的配对凭证。并行调用时靠它对齐,错配就会张冠李戴。
- Constrained decoding / structured outputs(约束解码 / 结构化输出):在解码阶段用语法约束保证输出一定符合 schema 的技术,是"参数格式错误"这类问题的工程解法(见 T1-2)。
说一句「宫保鸡丁,不要花生」
从头到尾没碰过锅,菜端上桌才知道长什么样
生成工具名 + 参数的结构化请求
到这一步为止,什么都还没有真正发生
查库、调 API、跑脚本,全在这儿
菜没端上桌之前,客人对这道菜一无所知
解析、校验、执行、回填全归它
结果回填进上下文,模型才第一次「看见」这道菜
原理拆解
stop_reason=tool_usecall_a1| 占位项 | 算式 | 结果 | 读法 |
|---|---|---|---|
| 工具描述常驻 | 20 个工具 × 平均 150 token | 3000 token | 每一轮都要重付一次 |
| 第 N 轮输入 | ≈ 前 N-1 轮的全部内容 | 超线性增长 | Agent 账单的真正根因 |
| 单次工具返回 | get_order_detail 全量 JSON | 8000+ token | 查三笔就吃掉 25000 |
第 ⑤ 步的措辞决定模型能不能自愈:工具报错返回 Error: 500 毫无用处;返回「订单号格式错误,应为 ORD- 开头的 16 位字符串,你传入的是 123」,模型下一轮就能自己改对。
并行工具调用省的是墙钟时间,不省 token。一次回复输出多个 tool_call,你的代码用 asyncio 或线程池并发执行,三个 tool_result 一起回填。只适合无依赖、无副作用的只读查询;模型有时会「乐观并行」,猜一个 id 就发出去,得靠参数校验拦住。
完整六步,缺一不可
① 你发请求:messages + tools 定义(工具定义被拼进上下文,占 token)
│
▼
② 模型输出:stop_reason=tool_use
{ "id":"call_a1", "name":"search_order",
"arguments":{"user_id":"U123","status":"unshipped"} }
│ ← 到这一步为止,什么都没有真正发生
▼
③ 你的代码解析 + 校验参数(类型、越权、注入)
│
▼
④ 你的代码真正执行:查库 / 调 API / 跑脚本
│
▼
⑤ 结果作为 tool_result 消息回填,携带同一个 call_a1
│
▼
⑥ 带着完整历史再请求模型 → 它可能给最终答案,也可能再调工具(回到②)三个常被忽略但面试爱问的细节:
- 第 ③ 步是安全边界。模型输出的参数是不可信输入,等同于用户输入。
delete_file(path)的 path 必须校验,不能因为"是模型生成的"就信任。提示词注入(T1-5)的实际落点就在这里。 - 第 ⑤ 步的结果是纯文本(或结构化片段),模型对它的信任度完全取决于你怎么写。工具报错时返回
Error: 500毫无用处,返回订单号格式错误,应为 ORD- 开头的 16 位字符串,你传入的是 "123"能让模型下一轮自我修正。 - 每一轮都要重发全部历史。这是 Agent 成本随步数超线性增长的根因:第 N 轮的输入 ≈ 前 N-1 轮的所有内容。prompt 缓存(T1-4)之所以对 Agent 特别关键,原因就在这。
并行工具调用
模型可以在一次回复里输出多个 tool_call:
一次回复 → [call_1: 查天气(北京), call_2: 查天气(上海), call_3: 查汇率]
↓ 并发执行(你的代码用 asyncio/线程池)
三个 tool_result 一起回填 → 下一轮适用:彼此无依赖、无副作用、可并发的只读查询。省的是墙钟时间(三次串行往返变一次),不省 token。
不适用:有依赖关系(必须先查到 order_id 才能查物流)——模型有时会"乐观并行",猜一个 id 就并发发出去,这类幻觉参数在生产里很常见,要靠参数校验拦住;也不适用于写操作,并发写更难保证幂等。
工具返回值:Agent 上下文的头号杀手
工程上最容易被低估的一点:上下文很贵而磁盘很便宜,但很多工具是照着"给人看"的 API 设计的。传统程序可以流式扫描一万条记录逐条判断,模型必须把这一万条全部读进上下文——相当于为了找一个联系人,从头到尾读完整本通讯录。
Anthropic 在工具设计的工程实践中给出的默认约束值得记住:Claude Code 默认把单次工具返回截断在 25,000 token 上限。这不是模型限制,是产品侧主动设的护栏。你自己的 Agent 也应该有一个这样的数字。
工程实践(截至 2026-08)
工具设计七条硬规则
| 规则 | 反例 | 正例 |
|---|---|---|
| 搜索优于罗列 | list_all_contacts() 返回全部 |
search_contacts(query, limit=10) |
| 参数名消歧 | user |
user_id(明确是 id 不是姓名) |
| 命名加前缀分组 | search、create |
crm_search_contact、jira_create_issue |
| 功能合并去重叠 | 5 个近义的取消/退款工具 | 1 个 order_action(action: enum) |
| 返回值默认精简 | 整个订单 JSON | 默认 8 字段,fields 参数按需展开 |
| 描述写清边界 | "取消订单" | "取消未发货订单;已发货请用 return_request" |
| 错误信息可操作 | Error 400 |
参数 date 需为 YYYY-MM-DD,你传入 "上周三" |
参数经验值
- 工具数量:单个 Agent 挂载 ≤ 15~20 个工具时选择准确率通常可接受;超过之后,改用分组 + 二级检索(先让模型选"域",再列出该域的工具)或渐进式披露(T3-12 的 Agent Skills 思路)。这是经验区间不是定律,务必用自己的评测集验证。
- 单次工具返回:设一个硬上限(数千至 25,000 token 量级),超出必须分页或截断,且截断时要在返回里明写"结果被截断,请用 filter/offset 缩小范围"——引导模型改用更精确的检索,而不是让它以为数据就这么多。
- 描述长度:工具描述每个字都占常驻上下文。20 个工具 × 平均 150 token 描述 = 3000 token 的固定开销,每一轮都要付。
迭代方法:先评测,再优化描述
工具优化不是拍脑袋,成熟做法是三段式循环:
① 原型:先写最小可用的 3-5 个工具
↓
② 评测:造 20-50 条真实任务,跑起来,记录每次"选了哪个工具/参数对不对"
↓
③ 优化:只改指标最差的那个工具的描述与 schema,重跑对比
↑____________________________________________|真正有效的杠杆常常是改描述而不是改代码——因为描述就是喂给模型的 prompt,它属于提示词工程的范畴。这也是 2026 年很多团队把工具描述纳入版本管理和回归测试的原因。
进阶技术三段式:并行工具调用
- 解决什么:多个独立只读查询的墙钟延迟,从 N 次串行往返压缩到 1 次。
- 代价是什么:不省 token;并发放大下游压力(限流风险);模型可能凭空猜参数发起"乐观并行";错误处理复杂化——部分成功部分失败时,回填的上下文会让模型困惑,你得明确标注哪个失败了、为什么。
- 什么时候不该用:工具间有数据依赖;任何带副作用的写操作;下游 API 有严格 QPS 限制;以及——单次查询就够的时候,别为了并行而并行。
避坑清单
- 别把内部 API 直接 1:1 包成工具。内部 API 是给程序调的,工具是给模型读的,受众不同。
- 别在工具描述里写"如果用户很生气就调用这个"这类主观条件,模型判断不稳。写可验证的客观条件。
- 工具返回里别夹带原始 HTML/日志噪音,它们会污染注意力(context rot,见 T1-12)。
- 高危工具单独走确认流程,不要靠在描述里写"请谨慎使用"来防(T3-10)。
- 工具多了之后,先做减法再做检索。很多团队直接上"工具检索",其实合并同类项就能解决大半。
| 规则 | 反例 | 正例 |
|---|---|---|
| 搜索优于罗列 | list_all_contacts() 返回全部 | search_contacts(query, limit=10) |
| 命名消歧并分组 | user、search | user_id、crm_search_contact、jira_create_issue |
| 功能合并去重叠先做减法 | 5 个近义的取消 / 退款工具 | 1 个 order_action(action: enum) |
| 返回值默认精简 | 整个订单 JSON | 默认 8 字段,fields 参数按需展开 |
| 描述写清边界 | 「取消订单」 | 「取消未发货订单;已发货请用 return_request」 |
| 错误信息可操作 | Error 400 | 「参数 date 需为 YYYY-MM-DD,你传入 上周三」 |
- 工具数量
- 单个 Agent 挂载 ≤ 15~20 个时准确率通常可接受;超过就分组 + 二级检索,或渐进式披露(T3-12)
- 单次返回上限
- Claude Code 默认截断在 25,000 token;截断时要明写「结果被截断,请用 filter/offset 缩小范围」
- 描述的常驻成本
- 20 个工具 × 平均 150 token 描述 = 3000 token 固定开销,每一轮都要付一次
- 迭代三段式
- ① 原型:3-5 个最小可用工具 → ② 评测:20-50 条真实任务,记录选错 / 填错 → ③ 优化:只改最差那个工具的描述,重跑对比
- 最有效的杠杆
- 改描述往往比改代码有效 —— 描述就是喂给模型的 prompt,所以要纳入版本管理与回归测试
- 别踩这三条
- 内部 API 别 1:1 包成工具;描述里别写「用户很生气就调用」这类主观条件;返回里别夹带 HTML / 日志噪音(T1-12)
面试视角
- 先破一个常见误解「模型没调用任何函数,执行方是宿主程序」
- 完整讲六步点名 ③ 是安全边界、⑤ 的错误信息要可操作
- 把话题引向工具设计「坑不在机制,在工具设计」+ 47 砍到 9 的故事
- 给数字准确率 71% → 94%,会话 token 降 62%
- 说「模型调用了函数并拿到了结果」—— 机制根本没讲清
- 只会说「工具描述要写清楚」,给不出任何一条具体规则
- 认为工具越多能力越强,加工具是唯一的迭代方式
- 谈并行调用只说「更快」,说不出不省 token,也说不出失败处理
- 从没提过工具定义本身要占常驻上下文
- 说得出执行方是宿主程序,还顺手带上 tool_call_id 配对
- 给得出「搜索优于罗列」「返回默认裁剪」这类规则,并配自己的数字
- 先做减法再做检索 —— 合并同类项就能解决大半
- 并行省墙钟不省 token,部分失败要在回填里标清是哪一个失败了
- 算得出 20 个工具 × 150 token = 3000 的每轮固定开销
面试官怎么问
答题结构建议
- 先破一个常见误解:"严格说模型没有调用任何函数,它只输出了一个结构化的调用意向,执行方是宿主程序。"——这句话能立刻建立可信度。
- 完整讲六步,并点名第 ③ 步是安全边界、第 ⑤ 步的错误信息要可操作。
- 主动把话题引向工具设计:"我们踩过的坑其实不在机制,而在工具设计"——然后给你那个"从 47 个砍到 9 个"级别的具体故事。
- 给数字:准确率从多少到多少,token 降了多少。
分水岭信号
只读过资料的回答:
- 说"模型调用了函数并拿到了结果"——机制理解不清。
- 只会说"工具描述要写清楚",给不出任何具体规则。
- 认为工具越多能力越强。
- 谈并行调用只说"更快",说不出不省 token、也说不出失败处理。
- 从没提过工具定义本身要占常驻上下文。
真做过的回答:
- 提到 tool_call_id 配对、部分失败的回填处理。
- 说得出搜索优于罗列、返回值默认字段裁剪这类具体设计原则,并给了自己的数字。
- 把模型输出的参数当作不可信输入来校验——这是安全意识的直接体现。
- 提到幂等与重试(模型重试是常态)。
- 说得出自己是怎么评测工具的:多少条任务、看哪些指标、改了描述后回归对比。
- 意识到工具描述属于 prompt,会被纳入回归测试。
小结与延伸
- Function Calling 的本质是"模型生成结构化调用意向,宿主程序执行",模型从不真的调用函数。
- 六步循环里,参数校验是安全边界,错误信息是模型的自我修正燃料。
- 工具设计是 Agent 效果的一半:搜索优于罗列、合并语义重叠、默认裁剪返回、描述写清边界。
- 并行调用省墙钟时间不省 token,只适合无依赖的只读操作。
- 工具优化靠评测驱动,改描述往往比改代码更有效。
延伸:把这套工具机制放进循环里,就是下一篇 T3-3 的 ReAct。工具太多导致的上下文压力,最终的系统性解法是渐进式披露(T3-12)。工具的来源标准化——从"你自己写"变成"接别人的 server"——见 T3-4 的 MCP。
继续深入
本篇归属第 3 章「Agent 开发」,去做这一章的题。