Agent 找到工具以后,为什么还是会用错
给 Agent 接入几十个工具以后,团队通常先观察一个指标:用户提问时,正确工具有没有出现在搜索结果里。
如果正确工具已经进入 Top 5,检索看起来就没有问题。可真实任务仍然可能失败:
- Agent 从五个候选里选了另一个名字相似的工具;
- 工具选对了,时间范围却填错;
- 参数格式合法,但证券代码、时区或计量单位不对;
- 调用返回 HTTP 200,正文里却是业务错误;
- 结果本来可用,Agent 误以为是空数据,又换工具重试;
- 查询工具完成了,Agent 却在没有授权的情况下继续调用写工具。
“找到工具”和“用好工具”中间,隔着选择、参数落地、执行语义和结果解释。只盯召回率,会把后面四段的问题都错算成模型不够聪明。
本文用一条完整调用链来拆这个问题。重点不是推荐哪一种向量数据库,也不是列出所有 Function Calling 框架,而是给出一套能定位、能测试、能逐步优化的工程方法。
正确工具在 Top 5,任务成功率可能仍然不到 70%
先做一个简化计算。
假设一条工具调用链有五道门,每道门的通过率如下:
| 环节 | 通过率 | 含义 |
|---|---|---|
| 正确工具进入候选集 | 95% | Recall@5 |
| 模型从候选中选对工具 | 90% | Selection Accuracy |
| 参数完整且语义正确 | 90% | Argument Accuracy |
| 工具真实执行成功 | 95% | Execution Success |
| 模型正确理解并使用结果 | 95% | Result Utilization |
端到端成功率不是这些数字的平均值,而是近似相乘:
[ 0.95 \times 0.90 \times 0.90 \times 0.95 \times 0.95 \approx 69.4% ]
召回率已经有 95%,最后仍有三成任务失败。任何一段出现短板,都会被乘法放大。

这也是工具平台最容易出现的指标错觉:搜索团队报告 Recall@5 很高,模型团队报告 Function Calling 格式正确率很高,供应商报告接口成功率很高,产品侧却仍然收到大量“它调用错了”的反馈。每个局部数字都可能是真的,只是没有人对乘起来的结果负责。
第一道门:召回的不是“文字相似”,而是可执行能力
工具检索常见的第一版,是把 name + description 做 Embedding,然后用用户问题搜 Top K。这能工作,但它把工具当成普通文档,忽略了工具还有输入、输出、时效、权限和副作用。
看一个很普通的用户问题:
查一下某只股票过去 20 个交易日的成交额,并和前 20 个交易日比较。
工具库里有四个候选:
market.quote_snapshot
market.daily_bars
market.money_flow
analytics.period_compare
quote_snapshot 的描述里有“最新成交额”,和问题文字很相似;money_flow 里也有“资金”和“交易日”。真正需要的却是 daily_bars 取得历史行情,再用 period_compare 或本地计算完成两个窗口的比较。
工具索引至少应包含四类信息
identity:
name: market.daily_bars
title: 日线行情
aliases: [历史行情, 日 K, daily prices]
capability:
does: 返回指定证券在日期区间内的日频 OHLCV 与成交额
does_not:
- 不返回实时盘口
- 不提供资金流向
- 不自动比较两个时间窗口
inputs:
required: [symbol, start_date, end_date]
semantics:
symbol: 市场限定的标准证券代码
start_date: 自然日,闭区间
end_date: 自然日,闭区间
operational:
freshness: 上一交易日收盘后更新
side_effect: read_only
typical_latency_ms: 600
result_scale: 每个证券每年约 250 行
does_not 往往比 does 更有区分度。当两个工具都写“查询股票数据”时,正向描述很难拉开距离;明确“不返回实时行情”“不支持多证券”“不适合复权价格”以后,检索和模型选择都会更稳定。
先硬过滤,再检索和排序
一条比较稳妥的发现链路是:
权限与环境硬过滤
→ 任务意图与副作用过滤
→ 关键词 + 向量混合召回
→ 结合参数可满足性的 rerank
→ 相似工具去重与结果多样化
→ 暴露少量候选给模型

为什么权限要放在检索前?因为 Agent 不应该先看到一个无权调用的高相似工具,再被迫“忍住不用”。为什么参数可满足性要进入 rerank?因为一个工具语义再匹配,如果必填参数无法从当前上下文获得,也不应排在可以立即执行的工具前面。
可以把最终分数写成一个可解释的组合:
[ score = 0.30 \cdot semantic
- 0.20 \cdot lexical
- 0.20 \cdot input_readiness
- 0.15 \cdot reliability
- 0.10 \cdot freshness
- 0.05 \cdot cost ]
权重不是行业标准。重要的是不要把相似度当作唯一事实,并且离线评测时能分别观察每一项是否真的改善任务结果。
第二道门:候选太像,模型选错并不奇怪
工具名称经常来自后端 API,而不是来自 Agent 的认知方式:
get_order
get_order_detail
query_order
search_orders
lookup_order_status
人类工程师看到代码和文档,知道它们分别属于精确查询、详情、条件搜索和状态查询;模型只看到几段高度重叠的描述,选错很正常。
按任务边界设计工具,不要机械包装接口
假设订单系统有十几个 REST endpoint。最省事的做法是一个 endpoint 包一个工具,结果会形成大量重叠能力。更适合 Agent 的设计可能只有三个:
orders.search
已知客户、时间、状态等条件,不知道订单 ID 时使用
orders.get
已知唯一订单 ID,需要订单事实和当前状态时使用
orders.request_refund
已完成退款资格判断并准备产生副作用时使用
orders.get 可以在服务端聚合基本信息、支付状态和履约摘要,不必强迫 Agent 连续调用三个底层 endpoint,再自己做 join。工具不是后端接口目录的镜像,而是给一个不确定决策者使用的操作界面。
Anthropic 在工具工程实践中提到一个很有代表性的例子:其 Web Search 工具上线时,模型会无意义地在查询参数后附加年份,导致搜索结果受偏。最后通过改进工具描述纠正了行为。工具描述不是静态说明书,它本身就是运行时控制面的一部分。
描述里要写“何时不用”
下面这段描述信息不少,但仍然难选:
Search orders in the order system. Supports customer, date and status filters.
更好的写法会明确边界:
Use this tool only when the exact order_id is unknown and you need to find
candidate orders by customer_id, creation time, or lifecycle status.
Do not use it when:
- an exact order_id is already available; use orders.get instead;
- the task is to create or submit a refund;
- the user asks for aggregate sales metrics.
The result is a paginated candidate list, not a full order record.
工具名、正向用途、禁用条件、结果粒度和相邻工具之间的关系,都要写出来。可以把它理解成给新同事做交接:不能只说“这是订单查询”,还要告诉他什么时候用、和另外两个查询有什么区别、拿到结果后下一步是什么。
候选集要小,但不能被裁得只剩一个
给模型一次塞 80 个工具,会消耗上下文,也增加相似工具之间的干扰。完全只给 Top 1 又会把检索错误变成不可恢复错误。
实践中可以按风险和复杂度设置候选数:
| 场景 | 建议候选 |
|---|---|
| 意图明确、工具差异大 | 3–5 |
| 需要组合多个只读工具 | 5–8 |
| 高风险写操作 | 先暴露只读确认工具,满足条件后再暴露 1–2 个写工具 |
| 候选高度相似 | 先让分类器确定子域,再在子域内选 3–5 个 |
动态暴露比永久提供完整工具列表更可靠。Agent 做研究时不需要看到退款工具;尚未确认订单时也不需要看到真正执行退款的工具。
第三道门:参数格式正确,不代表参数语义正确
Function Calling 很容易测“JSON 是否符合 Schema”,却不容易测“参数是不是用户真正想要的”。
仍以“过去 20 个交易日”为例:
{
"symbol": "600519",
"start_date": "2026-07-08",
"end_date": "2026-07-27"
}
这段 JSON 类型全对,却把 20 个交易日误写成了 20 个自然日。还可能有其他问题:
600519缺少市场信息,工具要求600519.SH;- 用户所在时区与交易所时区不同;
end_date当天尚未收盘,日线数据并不完整;- “成交额”被误写成“成交量”;
- 用户要求前复权价格,参数却使用不复权。
这些错误不会被 JSON Schema 发现。
参数校验要分三层
结构校验:类型、必填项、枚举、格式
语义校验:日期顺序、单位、代码体系、字段关系
业务校验:权限、市场状态、资源存在、任务前置条件
把三层错误混成 invalid_arguments,Agent 很难自我修正。错误响应应该告诉它哪一层失败、哪个字段错、哪些值可选、是否允许自动修复。
{
"status": "validation_error",
"field": "symbol",
"reason": "market suffix is required",
"received": "600519",
"accepted_examples": ["600519.SH", "000001.SZ"],
"repair": {
"safe_to_auto_apply": true,
"suggested_value": "600519.SH",
"evidence": "unique symbol match in Shanghai market"
}
}
参数修复要有红黄绿边界
不是所有缺失参数都应该让模型“聪明地补上”。

绿色:可以自动修复
- 去除字符串首尾空格;
- 唯一确定的代码格式转换;
- ISO 日期格式规范化;
- 大小写与已知枚举同义词转换;
- 用户已经明确给出的单位换算。
黄色:需要澄清或给出预览
- “最近”到底是 7 天还是 30 天;
- “苹果”是公司、股票还是水果;
- 同名客户匹配到多个实体;
- 时区会改变日期边界;
- 查询范围过大,费用和结果量明显增加。
红色:禁止猜测
- 收款账号、退款金额、目标邮箱;
- 生产环境资源 ID;
- 删除范围;
- 身份、权限和审批人;
- 任何不可逆动作的关键参数。
参数修复的原则是:修复表达形式,不替用户做风险决策。
第四道门:调用成功有好几种含义
一个第三方接口返回:
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": "AUTH_EXPIRED",
"message": "credential expired",
"data": null
}
如果 Tool Wrapper 只看 HTTP 状态,会把它记成成功;Agent 看到 data: null,可能判断“没有数据”,然后换一个查询条件继续调用。最终监控里既没有鉴权故障,用户也只看到“查不到”。
工具结果至少要分开:
| 层级 | 示例字段 | 关注的问题 |
|---|---|---|
| 传输 | transport_status | 网络是否连通、是否超时 |
| 协议 | protocol_status | HTTP/RPC 是否符合协议 |
| 业务 | business_status | 上游是否接受并完成业务请求 |
| 数据 | data_status | 结果是可用、合法空集、截断还是损坏 |
| 副作用 | side_effect_status | 写操作未开始、已提交、未知还是已回滚 |
MCP 的工具规范支持 outputSchema 和 structuredContent,并要求工具执行错误通过结果中的 isError 表达,而不是都升级成协议错误。MCP Tools Specification 这为结构化结果提供了基础,但具体业务错误分类、重试规则和副作用状态仍要由工具实现者定义。
空结果必须是一级语义
下面三种情况都可能表现为 data=[]:
- 查询合法,确实没有记录;
- 上游鉴权失败,Wrapper 错误地返回空数组;
- 解析器不认识新版响应,把数据丢了。
对 Agent 来说,它们的下一步完全不同:
- 合法空集:调整问题或直接告知用户;
- 鉴权失败:停止重试,报告服务不可用;
- 解析失败:切换版本、保留原始响应并报警。
因此结果里需要明确的 empty_reason、parser_version、raw_response_ref 和 retryable,不能让模型根据空数组猜发生了什么。
第五道门:工具给了答案,Agent 未必会使用
有些调用链每一步都成功,最终答案仍然错在结果解释。
例如一个分析工具返回:
{
"metric": "revenue",
"value": 1280,
"unit": "CNY_10K",
"period": "2026Q2",
"comparison": {
"type": "year_over_year",
"rate": -0.08
}
}
Agent 可能写成“收入 1280 元,同比下降 8 个百分点”。这里有两处错:
- 单位是万元,不是元;
- 同比增长率下降 8%,通常不是“下降 8 个百分点”。
工具输出不应只为程序可解析,还要让模型不容易误读。关键字段需要单位、口径、时间、精度和缺失语义;复杂结果最好同时给结构化内容和一段受控摘要。
大结果要返回导航,不要返回一堵墙
数据库工具一次返回 10 万行,Agent 不会因为上下文足够大就自动分析得更好。更合适的结果可以是:
{
"row_count": 103842,
"columns": [...],
"profile": {
"date_min": "2025-01-01",
"date_max": "2026-07-27",
"null_rates": {...},
"numeric_summary": {...}
},
"sample_rows": [...],
"artifact_ref": "artifact://query/result-01J...",
"next_actions": [
"use filter_rows for a narrower time range",
"use aggregate_rows before requesting full output"
]
}
Anthropic 的工具实践建议对大结果提供分页、范围选择、过滤和截断,并在截断或校验失败时返回能指导下一步的错误,而不是一段不透明 traceback。工具返回本身也在参与 Agent 的规划。
结果必须带证据边界
研究和分析类工具还要告诉 Agent:
- 哪些字段来自原始数据;
- 哪些字段是工具计算或推断的;
- 数据更新时间和覆盖范围;
- 是否存在截断、抽样或降级;
- 哪些结论不能由当前结果支持。
如果工具把原始事实和生成摘要混在一起,Agent 很容易把工具的猜测再包装成确定结论。
一个好用的 Tool Contract 应该长什么样
把前面五道门放在一起,一份工具契约不能只有函数名、描述和 JSON Schema。下面是一份更接近生产需要的最小版本:
identity:
name: orders.request_refund
version: 3
title: 创建退款申请
domain: order_after_sales
selection:
use_when:
- 已取得唯一 order_id
- 已读取订单当前状态
- 已完成退款资格判断
do_not_use_when:
- 仅需要查询退款政策
- 订单仍有多个候选
- 用户尚未确认退款金额
adjacent_tools:
orders.get: 已知 order_id 时读取订单
orders.search: 不知道 order_id 时查找候选
input:
schema: RefundRequestV3
semantic_rules:
- amount must not exceed refundable_amount
- currency must equal order currency
- reason_code must match policy version
clarification_fields:
- amount
- reason_code
forbidden_inference_fields:
- order_id
- payment_account
execution:
side_effect: irreversible
idempotency: required
timeout_ms: 10000
retry:
transport_not_sent: exponential_backoff
outcome_unknown: reconcile_before_retry
business_rejected: never
approval:
required_when: amount >= 200
output:
schema: RefundResultV3
success_evidence:
- refund_request_id
- committed_at
error_taxonomy:
- validation_error
- permission_denied
- policy_rejected
- provider_unavailable
- outcome_unknown
operational:
typical_latency_ms: 1200
cost_class: low
owner: order-platform
support_runbook: docs://orders/refund-tool
模型不会使用全部字段做自由推理。硬规则由 Gateway 执行,选择相关字段进入模型上下文,运行信息用于调度和监控。契约的价值,在于不同层读取同一份事实,而不是在 Prompt、代码和运维文档里各写一套。
工具选择和参数生成,最好分两次决策
在工具少、参数简单时,模型一次输出 tool_name + arguments 足够。工具多、参数复杂以后,可以拆成两个阶段。
第一阶段只选择能力:
{
"intent": "historical_market_comparison",
"selected_tools": ["market.daily_bars"],
"missing_information": [],
"selection_evidence": [
"requires daily historical amount",
"does not require real-time quote"
]
}
第二阶段读取选中工具的完整 Schema、用户上下文和实体解析结果,再生成参数:
{
"symbol": "600519.SH",
"start_date": "2026-05-29",
"end_date": "2026-07-27",
"fields": ["trade_date", "amount"],
"adjustment": "none"
}
这样做增加一次模型或规则处理,但带来几个好处:
- 选择评测和参数评测可以分开;
- 未选中的几十个 Schema 不占上下文;
- 实体解析、交易日换算等确定性逻辑可以插在中间;
- 高风险工具可以在第二阶段前增加权限和审批。
不必所有任务都拆两段。是否拆分,应看工具数量、重叠程度、参数复杂度和错误成本。
别用一个“调用成功率”评估整条链
Toolformer早在 2023 年就把工具使用拆成“何时调用、调用哪个 API、传什么参数、如何使用结果”。到了生产系统,这几个问题仍然应该分别测。
Berkeley Function Calling Leaderboard从 AST 和可执行调用开始,后来扩展到多轮、幻觉与更完整的 Agent 场景,也说明“输出了一段合法函数调用”只是工具能力的一部分。
一套分层指标
| 阶段 | 核心指标 | 典型失败 |
|---|---|---|
| 发现 | Recall@K、MRR、权限过滤准确率 | 正确工具没进候选、越权工具进入候选 |
| 选择 | Tool Accuracy、无工具判断准确率 | 选相似工具、该不用工具时强行调用 |
| 参数 | Schema Valid、Semantic Valid、Clarification Accuracy | 类型对但语义错、该追问时猜值 |
| 执行 | Provider Success、可重试错误率、重复副作用数 | 200 假成功、重试风暴、重复写 |
| 结果 | Result Usability、单位与口径正确率 | 空结果误判、单位误读、忽略截断 |
| 任务 | Task Completion、人工接管率、成本和延迟 | 局部都成功但目标没完成 |
指标必须能沿着同一个 trace_id 串起来。否则发现系统记录一份 query,执行系统记录另一个 call,最终任务又只有一段文本,很难知道一次失败究竟掉在哪道门。
测试集要故意放进难例
只用“北京今天天气怎样”测工具调用,结果通常很好看。真正有价值的测试集应该包含:
- 两个名字和用途都很像的工具;
- 正确答案是不调用任何工具;
- 缺失关键参数,必须向用户追问;
- 参数格式合法但业务语义冲突;
- HTTP 200 携带业务错误;
- 合法空结果、鉴权失败和解析失败三种空数据;
- 工具结果被截断,需要二次过滤;
- 写操作响应超时,但实际上已经提交;
- 多个工具顺序调用,后一步依赖前一步证据;
- 同一个任务存在两条都合理的工具路径。
最后一类很重要。评测不应强制 Agent 必须走唯一轨迹。Anthropic 也建议可以标注预期工具,但不要因为存在多条有效路径而过度限定策略。应优先验证任务结果、关键约束和禁止行为。
从日志里找优化点,比凭感觉改描述有效
工具上线后,可以定期把失败轨迹按模式聚类:
高召回 + 低选择准确率
→ 检查工具重叠、名称和禁用条件
选对工具 + 参数错误多
→ 检查字段命名、示例、实体解析和澄清策略
执行成功 + 结果利用率低
→ 检查单位、口径、截断说明和输出 Schema
重复调用多 + 没有新增证据
→ 检查错误是否可操作、是否缺少停止条件
某个工具成功率突然下降
→ 检查供应方、凭证、版本和解析器,不要先怪模型
工具改进也要使用留出测试集。针对最近二十条失败轨迹改完描述后,原样重测这二十条很可能提升;真正的问题是新描述会不会让其他任务退化。
比较稳妥的流程是:
- 从线上失败生成训练集,分析问题并修改;
- 用互不重叠的留出集验证泛化;
- 对高风险工具做人工审查;
- 小流量灰度,比较任务完成率、参数错误率和成本;
- 保留旧版本,异常时能回滚 Tool Contract。
工具描述、Schema、Wrapper 和错误分类都应有版本。否则模型版本没变,Agent 行为却因为某个工具悄悄改了字段而漂移,排障时连复现条件都找不到。
三个案例,分别应该先改哪里
历史行情比较总是少算交易日
不要先换模型。先看:
- 工具参数把自然日和交易日说清楚了吗?
- 是否应该提供
lookback_trading_days,由服务端计算日期? - 当前交易日未收盘时如何处理?
- 结果是否返回实际覆盖的首尾交易日和行数?
如果大量调用都在重复计算交易日窗口,这个能力应该下沉到工具,而不是让每个 Agent 各算一次。
退款工具偶尔重复创建申请
这不是工具选择问题。应先检查:
- 幂等键是否来自稳定业务语义;
- 超时后是否进入对账,而不是立即重试;
- 工具是否返回
side_effect_status=unknown; - 同一订单是否有业务互斥和唯一约束;
- Agent 是否在验证阶段查询退款申请。
换一个 Function Calling 准确率更高的模型,也不能补上服务端幂等。
数据分析结果经常把单位写错
重点检查结果契约:
- 单位是否是独立字段,而不是藏在列名或文档里?
- 金额是否说明含税、币种和缩放倍率?
- 比率是小数
0.08还是百分数8? - 同比、环比和百分点是否分别定义?
- 摘要是否继承了结构化字段,而不是重新猜测?
这类问题最好通过输出 Schema 和确定性格式化解决,不要依赖 Prompt 反复提醒“注意单位”。
真正值得优化的是整条链
工具调用能力经常被压缩成一句话:模型会不会 Function Calling。实际系统里,模型只负责其中一部分。
工具能否被找到,取决于索引和权限过滤;候选是否容易选择,取决于能力边界;参数是否正确,取决于 Schema、实体解析和澄清策略;执行是否可靠,取决于 Wrapper、幂等和错误分类;结果能否被正确使用,取决于输出契约和上下文设计。
因此,看到“正确工具已经搜出来了”,排障才刚刚开始。最有效的下一步不是笼统地说“模型又选错了”,而是沿着 Trace 逐道检查:候选是什么、为什么选它、参数从哪里来、工具真实发生了什么、结果怎样进入下一步。
把这五道门分开以后,工具调用不再是一团难以解释的概率问题。每一次失败都有归属,每一项优化都有指标,也更容易判断应该改检索、改工具、改运行时,还是确实应该换模型。
参考资料
- Toolformer: Language Models Can Teach Themselves to Use Tools
- Berkeley Function Calling Leaderboard
- Anthropic:Writing effective tools for AI agents
- Model Context Protocol:Tools Specification
- OpenAI:A practical guide to building agents
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。
X / Twitter
关注 @ax2_zicode
更即时的技术观察、新文章提醒和一些短想法会发在 X 上。