Agent 找到工具以后,为什么还是会用错
原创 · 约 39 分钟阅读 · 阅读 --

Agent 找到工具以后,为什么还是会用错

作者: Alex Xiang


给 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_statusHTTP/RPC 是否符合协议
业务business_status上游是否接受并完成业务请求
数据data_status结果是可用、合法空集、截断还是损坏
副作用side_effect_status写操作未开始、已提交、未知还是已回滚

MCP 的工具规范支持 outputSchemastructuredContent,并要求工具执行错误通过结果中的 isError 表达,而不是都升级成协议错误。MCP Tools Specification 这为结构化结果提供了基础,但具体业务错误分类、重试规则和副作用状态仍要由工具实现者定义。

空结果必须是一级语义

下面三种情况都可能表现为 data=[]

  1. 查询合法,确实没有记录;
  2. 上游鉴权失败,Wrapper 错误地返回空数组;
  3. 解析器不认识新版响应,把数据丢了。

对 Agent 来说,它们的下一步完全不同:

  • 合法空集:调整问题或直接告知用户;
  • 鉴权失败:停止重试,报告服务不可用;
  • 解析失败:切换版本、保留原始响应并报警。

因此结果里需要明确的 empty_reasonparser_versionraw_response_refretryable,不能让模型根据空数组猜发生了什么。

第五道门:工具给了答案,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

重复调用多 + 没有新增证据
  → 检查错误是否可操作、是否缺少停止条件

某个工具成功率突然下降
  → 检查供应方、凭证、版本和解析器,不要先怪模型

工具改进也要使用留出测试集。针对最近二十条失败轨迹改完描述后,原样重测这二十条很可能提升;真正的问题是新描述会不会让其他任务退化。

比较稳妥的流程是:

  1. 从线上失败生成训练集,分析问题并修改;
  2. 用互不重叠的留出集验证泛化;
  3. 对高风险工具做人工审查;
  4. 小流量灰度,比较任务完成率、参数错误率和成本;
  5. 保留旧版本,异常时能回滚 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 逐道检查:候选是什么、为什么选它、参数从哪里来、工具真实发生了什么、结果怎样进入下一步。

把这五道门分开以后,工具调用不再是一团难以解释的概率问题。每一次失败都有归属,每一项优化都有指标,也更容易判断应该改检索、改工具、改运行时,还是确实应该换模型。

参考资料

打开原图 ↗