工具调用是 Agent 最基础也最容易翻车的一层:Schema 怎么设计、工具多了怎么选、参数错了怎么定位,这三件事我在线上都踩过。
Q1: 一次 Tool Calling 的完整链路和不变量是什么?
一次真实的请求长这样(OpenAI Chat Completions 风格):
finish_reason。 它是 tool_calls 还是 stop,决定了你后面查”选择”还是查”生成”。
Q2: 工具 Schema 应该怎么设计?
Schema 不是类型声明,是给模型看的接口契约。分四层设计:第一层:命名
第二层:描述
描述要回答五个问题,而不是一句”查询订单”:第三层:结构
- 扁平优先。 两层以上的嵌套对象会显著提高参数生成错误率;能用
userId+orderId就别传一个filter对象。 - 枚举优于自由字符串。
status给["paid","shipped","refunded"],不要让模型猜。 - 数量有边界。
limit写明默认值和最大值,避免一次拉十万条。 - 单位写进字段名或描述。
amountCents比amount少一类事故。 - 时间用 ISO 8601 并给例子。 时区必须明确。
- 避免
anyOf/oneOf/ 深层$ref。 各家对 JSON Schema 子集支持不同,复杂模式在 strict 模式下可能直接编译失败。
第四层:负面约束
同一个问题有两个工具都能答,是最常见的选错原因:Schema 演进规则
Schema 的目标是”让模型在正确的时机、用正确的参数、只调用一次”,“能通过校验”只是最低要求。
Q3: strict 模式、JSON Mode、结构化输出有什么区别?
三者经常被混为一谈,但约束点完全不同:
OpenAI 的函数工具支持
strict,Anthropic 的工具定义支持 strict: true,其机制是语法约束采样(grammar-constrained sampling):把 Schema 编译成语法,采样时只允许生成合法 token 序列。Anthropic 的文档明确要求所有对象加 additionalProperties: false,并只支持 JSON Schema 的一个子集。
三层防线,不要只靠一层
passengers 是整数,但保证不了这个用户有没有权限订 4 张票;它保证 orderId 是字符串,但保证不了这个订单属于当前用户。
常见坑
- Schema 太复杂导致被拒。 深层嵌套、
oneOf、递归$ref在部分 Provider 上不支持 strict。 - strict 打开后模型”变笨”。 约束越强,模型越难表达”我需要更多信息”,可能直接编一个合法但错误的值。此时应该在工具之外提供澄清路径(Elicitation 或普通提问)。
- 把 JSON Mode 当 Schema。
{"type":"json_object"}只保证是 JSON,不保证字段。 - 忽略
additionalProperties。 不写false时模型可能塞额外字段,执行器若做宽松解析,会把错误藏起来。
Q4: 工具太多怎么办?动态工具与 tool search 怎么落地?
工具数量与选择准确率并非线性关系:过了某个点会开始明显退化。Anthropic 在 Tool Search 文档里给的经验是:可用工具超过 30–50 个后选择能力下降;一个典型的多 Server 配置(GitHub、Slack、Sentry、Grafana、Splunk)光工具定义就能占约 55k tokens。 三种解法,成本依次上升:Tool Search 的工作方式
以 Anthropic 的实现为例(字段名以官方文档为准):defer_loading: true的工具不进入初始上下文;- 模型先调用检索工具,服务端返回
tool_reference块(默认最多 5 个),再自动展开成完整定义; - 至少保留一个非延迟工具(通常就是检索工具本身),否则模型没有入口。
deferLoading 被检索发现,并可用 allowedCallers 区分 DIRECT 与 PROGRAMMATIC 调用,用 namespace 做分组。
落地要点
- 描述就是索引。 检索质量取决于工具名、描述、参数描述,不是取决于向量库多高级。写描述时把用户可能的说法放进去。
- 保留一个 always-on 工具。 检索工具本身、或一个
ask_user/list_capabilities。 - 检索结果要可解释。 记录命中了哪些工具、为什么,否则线上只能看到”模型调了个奇怪的函数”。
- 工具定义变化会破坏 prompt cache。 动态挂载要按会话稳定,不要每轮都改工具列表。
- 评测要覆盖”该检索却没检索”和”检索到错的工具”两类错误。
与 MCP 的关系
MCP 的tools/list 解决”有哪些工具”,Tool Search 解决”这次该让模型看到哪些工具”。二者可以叠加:网关先按租户裁剪 tools/list,Harness 再按任务做一次检索。不要把上千个 MCP 工具直接全量塞进模型上下文,这是线上最常见的上下文浪费。
Q5: 并行调用什么时候安全?结果怎么配对?
模型在一轮里返回多个tool_calls 就是并行调用。OpenAI 用 parallel_tool_calls 控制,Anthropic 在同一轮返回多个 tool_use 块。
可以并行的条件
结果配对
- 必须用
tool_call_id配对,不能靠顺序。 并行完成顺序和返回顺序不一定一致。 Promise.allSettled而不是Promise.all。 一个失败不应该丢掉其他结果;模型需要看到”哪些成功、哪些失败”。- 并行度要有上限。 例如
p-limit(5),否则 20 个调用会同时打爆下游。
Provider 差异
Q6: 工具结果回灌时,怎么防止上下文被撑爆?
工具结果是上下文膨胀的第一大来源:一次cat 可能返回几十万字符,一次搜索可能返回 200 条结果。
预算视角
给每类工具设一个结果预算,而不是等上下文满了再救:截断策略
回灌的五个原则
- 统一外壳:
{ ok, data, meta },模型不用猜结构。 - 只给决策字段:模型不需要
createdAt的内部主键,就别给。 - 大对象给引用:返回 URI / 文件路径 / 查询游标,需要时再取。
- 敏感字段脱敏:手机号、地址、令牌在回灌前处理。
- 标明截断:
truncated: true和nextCursor,否则模型会以为看到的是全量。
Q7: 工具报错时应该返回什么?协议错误和业务错误怎么分?
错误信息本身就是给模型的重试指令。好的错误要回答三件事:发生了什么、能不能重试、下一步怎么做。协议错误 vs 工具执行错误
MCP 规范把错误分成两类,这个分类对任何 Harness 都适用:
关键判断:错误里有没有”模型可以据此改变行为”的信息。 参数格式错、值越界、资源不存在,都属于可行动错误,必须让模型看到;鉴权失败、代码 bug、数据库宕机,模型看了也只能瞎猜,应该由 Harness 处理并返回简短说明。
错误码分类
实现
不要做的事
- 不要把堆栈直接给模型。 既浪费 token,又可能泄露内部路径。
- 不要返回
{"error": "操作失败"}。 模型只能原样重试。 - 不要把所有错误都标成 retryable。 会造成重试风暴。
- 不要把业务错误抛成 500。 Harness 会当成基础设施故障,触发错误的重试策略。
Q8: 重试为什么要分层?幂等怎么保证?
三层职责
双重重试是隐形放大器。 工具层重试 3 次 × 模型层重试 3 次 = 9 次真实调用。写操作必须让其中一层”不重试”,通常是把工具层重试限制在幂等请求上。
幂等键设计
- 稳定:同一逻辑操作在重试中必须生成同一个键;不能用
Date.now()或随机数。 - 可追溯:键里带
taskId/stepId,出问题能反查。 - 服务端去重:键存在则返回第一次的结果,而不是再执行一次。
- 有 TTL:幂等记录不能永久增长,通常 24 小时到 7 天。
写操作的三种安全模式
卡死检测
最大步数只能防”无限”,防不了”无效重复”。还要检测相同调用与相同结果:"检测到重复调用,请基于已有信息总结,或说明缺少什么。"
Q9: 不同 Provider 的 Tool Calling 有哪些实质差异?
迁移策略
- 内部维护一份 canonical 工具定义(name / description / JSON Schema / 副作用等级 / 超时 / 幂等策略),再由 adapter 转成各家格式。
- 不要依赖某家的错误文案,把错误归一化成自己的
ToolError。 - 保存 golden trace:同一组输入下的工具调用序列,换 Provider 后逐条比对,而不是只看最终回答。
- 注意消息顺序约束:Anthropic 的
tool_result必须紧跟对应的tool_use,且同轮结果放在一条消息里;OpenAI 的 tool 消息按tool_call_id配对。
Q10: 怎么评测工具调用质量?
不要只测最终回答。按层拆指标:Fixture 设计
评测的四个陷阱
- 用 LLM 当裁判评”工具选得对不对”。 选择对错是可判定的,用规则比对,别引入随机性。
- 只测单轮。 真实故障多发生在第 3–5 轮,必须有多步任务集。
- 改了 Prompt 不重跑基线。 工具描述变化会改变选择分布,必须回归。
- 只看平均值。 某个高频工具 99% 正确、低频高危工具 60% 正确,平均值会掩盖灾难。
Q11: 线上四类症状怎么排?给出可执行的决策树
先统一日志字段,否则后面全是猜:症状 A:完全不触发 tool_calls
tool_choice),观察 tool_calls 是否出现。不要凭感觉说”模型变笨了”。
症状 B:调用了错误的工具
症状 C:工具对了,参数错
症状 D:循环停不下来或漏步
五分钟排查顺序
- 看
finish_reason,确认是”没选”还是”选了没执行”。 - 看
tools_hash与tool_count,确认模型看到的工具是否和你以为的一致。 - 看
tool_calls[].args原文,不要看 Harness 解析后的对象。 - 看
validation_error与retry_count,判断是契约问题还是运行时问题。 - 用单变量 fixture 复现,再决定改描述、改 Schema 还是改执行层。
Q12: 深度追问(Tool Calling)
strict 模式能保证工具参数一定正确吗?
strict 模式能保证工具参数一定正确吗?
及格回答: 能,开了 strict 就不会传错类型。满分回答: 只能保证”形状正确”,保证不了”语义正确”。strict 通过语法约束采样,让输出符合 Schema:类型对、必填字段在、枚举值合法。但它不知道这个
orderId 是否属于当前用户,也不知道 passengers: 4 是否超过该航班的余票。所以生产上要三层:strict 保证形状,运行时 Schema 校验保证版本和子集一致,业务规则校验保证权限与状态正确。另外 strict 对 JSON Schema 子集有限制,复杂 Schema 可能编译失败,需要降级到普通模式并加强事后校验。工具数量多了准确率为什么下降?怎么量化?
工具数量多了准确率为什么下降?怎么量化?
及格回答: 因为模型要选的东西太多,容易选错。满分回答: 两个机制叠加:一是选择空间变大,相似描述之间的区分难度上升;二是所有工具定义都占上下文,真正与任务相关的信息被稀释,而且工具定义通常在前部,会挤占指令和示例的位置。Anthropic 的公开文档给的经验是超过 30–50 个工具后选择能力明显下降,多 Server 场景光定义就可能占约 55k tokens。量化方法是构造”意图 × 工具”的混淆矩阵,统计误选率和漏调率;再把工具描述做 embedding 算相似度,相似度高的工具对通常就是误选高发区。解法优先级是静态裁剪 → 分组/子 Agent → Tool Search。
并行工具调用有哪些坑?
并行工具调用有哪些坑?
及格回答: 写操作不能并行,其他都可以。满分回答: 四类坑。第一是依赖:后一个调用需要前一个的结果,必须分轮。第二是副作用与竞态:两个写操作并行可能互相覆盖,需要串行、加锁或幂等键。第三是结果配对:必须用
tool_call_id 关联,不能靠返回顺序;一个失败不能让其他结果丢失,所以用 allSettled 而不是 all。第四是资源:并行度要有上限,否则会同时打爆下游限流。协议层还有格式差异:OpenAI 每个结果一条 tool 消息,Anthropic 要求同一轮所有 tool_result 放在同一条 user 消息里且排在文本之前。工具返回超长结果,你会怎么处理?
工具返回超长结果,你会怎么处理?
及格回答: 截断,保留前面一部分。满分回答: 先看结果类型。结构化结果应该先摘要再序列化,只保留决策字段,并带上
total / returned / truncated / nextCursor;纯文本可以保留头尾、截断中段,因为结构化信息在头、错误在尾。大文件不要内联,返回路径或 resource link,让模型按需分段读。二进制和图片转成引用或缩略描述。最关键的是截断必须显式:静默截断会让模型以为看到了全量,基于不完整信息下结论,比直接报错更危险。另外截断 JSON 字符串会破坏可解析性,所以顺序应该是”先结构化摘要,再考虑截断”。模型一直重复调用同一个工具,你怎么修?
模型一直重复调用同一个工具,你怎么修?
及格回答: 加最大步数,超了就停。满分回答: 最大步数只是兜底,不是修复。先看它为什么重复:如果错误信息只有”操作失败”,模型没有新信息,只能原样重试;如果结果为空但没说明原因,模型会以为再试一次就有了。正确做法是给结构化错误(code、retryable、hint),明确告诉它”下一步该查什么”或”不要再试”。同时加卡死检测:连续两次相同工具、相同参数、相同结果就判定卡死,注入一条收尾指令,让它基于已有信息总结或说明缺什么。最后从工具设计上解决:补前置查询工具、把多步任务拆开、给缺失状态一个明确的 NOT_FOUND 分支。
换模型后工具调用准确率下降,怎么定位?
换模型后工具调用准确率下降,怎么定位?
及格回答: 换个 Prompt 试试,不行就回滚模型。满分回答: 用同一套 fixture 做分层对比,不要只看最终回答。分别统计:工具选择准确率、参数合法率、多步任务完成率、平均步数、错误恢复率。如果只有选择下降,可能是新模型对描述更敏感或对工具数量更敏感;如果参数错误多,可能是 strict 支持差异或 Schema 子集差异;如果多步任务下降,可能是并行调用策略或结果消息格式不同(比如
tool_result 的位置);如果错误恢复率下降,可能是新模型对 hint 的利用方式不同。定位到具体层再改,不要整体回滚。同时检查 tools_hash 和消息顺序,迁移时常在这两处出错。工具结果里包含恶意指令怎么办?
工具结果里包含恶意指令怎么办?
及格回答: 在 Prompt 里告诉模型不要听工具结果里的指令。满分回答: 不能只靠 Prompt。工具结果是第三方内容,必须按不可信数据处理:指令只从 system/user 消息进入,工具结果只作为数据;对结果做来源标记和可疑片段过滤;高风险动作(转账、删除、发邮件)强制人工确认,并在确认 UI 展示真实参数;网络出口做限制,凭据不进入上下文,降低”被说服后能造成的破坏”。这就是间接提示注入的防御思路:不追求 100% 识别,而是让被攻陷后的影响面可控。MCP 规范也明确说工具描述和注解应视为不可信,除非来自可信 Server。
请设计一个工具调用的可观测性方案。
请设计一个工具调用的可观测性方案。
及格回答: 记录每次调用的工具名、参数、耗时和是否成功。满分回答: 至少要能回答四个问题:模型看到了什么、决定了什么、执行发生了什么、最后为什么停。所以一条 trace 要包含:
trace_id、step、provider/model、tools_hash 与 tool_count、tool_choice、finish_reason、tool_calls[].name/args(参数存摘要或脱敏)、validation_error、retry_count、latency_ms、prompt/completion tokens、result_bytes 与 truncated。指标上按层拆分:选择准确率、参数合法率、执行成功率、P95 延迟、错误码分布、平均步数、单任务成本。再加两类专项:安全审计(越权、注入命中)和回归(fixture 通过率)。最后把这些字段和用户反馈关联起来,才能从”用户说不好用”定位到”第 3 步选错了工具”。延伸阅读与引用参考
- OpenAI Function Calling:工具定义、
strict、parallel_tool_calls、工具结果消息格式。 - OpenAI Responses API 工具参考:
deferLoading、allowedCallers、namespace、strict。 - Anthropic Tool Use:
tool_use/tool_result、并行调用与消息顺序。 - Anthropic Strict Tool Use:语法约束采样、
additionalProperties: false。 - Anthropic Structured Outputs:
output_config.format与约束解码。 - Anthropic Tool Search Tool:
defer_loading、tool_reference、30–50 工具退化经验值。 - Anthropic Troubleshooting Tool Use:按症状分类的排障表。
- MCP Tools 规范:协议错误与工具执行错误的区分、
outputSchema、安全要求。 - Gemini Function Calling:
functionDeclarations与functionCallingConfig。
