AI 笔记 · 2026 年 11 月 11 日

Agent 的工具调用为什么总失败:五个工程细节

演示里能查单、能下单的 Agent,接进真实系统后成功率不到七成。问题多半不在模型,而在参数 schema、错误信息、幂等、工具数量和终止条件这五处,每一处都能单独修。

演示里的 Agent 能查天气、查订单、发邮件,一路顺滑。接进真实系统之后,成功率掉到七成以下,而且每次失败的形态还不一样:有时参数缺一个字段,有时同一个工具被连调三次,有时它干脆停在半路不返回。

把失败日志按原因归类,会发现模型只是背锅的那个。真正的问题分布在五个工程细节上,每一个都能单独修。

一、参数 schema 太宽,模型就开始猜#

最常见的写法是把参数全定义成字符串,再在描述里补一句「格式见接口文档」。模型看不到文档,只能猜。

几条实用的收紧方式:

  • 能用枚举就别用字符串,状态、类型、动作都列出来;
  • 必填项写进 required,可选参数越少越好;
  • 关掉 additionalProperties,别让模型自由加字段;
  • 嵌套超过两层就拍平,{"a": {"b": {"c": 1}}} 的出错率明显高于 {"a_b_c": 1};
  • 日期统一用 ISO 格式,金额统一到分并注明单位。

参数描述要写「什么时候该调用」,而不只是「这个参数是什么」。模型选错工具,很多时候是因为描述里没写适用条件。

二、错误信息是写给模型看的#

工具抛异常,你把 stack trace 或一句 500 塞回上下文,模型只能盲试:换个参数再调一遍,再失败,再换。三次之后步数耗尽。

有效的错误返回至少包含三样东西:错在哪、正确的形状长什么样、一个可照抄的例子。

提示

还有一类更隐蔽的「假成功」:查不到数据就返回空数组,HTTP 状态还是 200。模型会认为「查过了,没有」,然后自信地编一个答案。查无结果应该显式返回 {"ok": false, "reason": "not_found"}。

错误还要分级。参数格式错误、权限不足这类问题,重试一百次结果一样,应该直接把原因告诉用户;超时和限流才值得重试。在工具契约里统一带上 retryable 字段,模型看到 false 就知道该换路子,而不是再试一次。

三、重试把「查一次」变成「扣两次」#

工具调用失败后自动重试,是很多框架的默认行为。读操作重试无害,写操作重试就是事故:用户被扣两次款,工单被创建两条。

三个防线,从便宜到贵:

  1. 写操作必须带幂等键,键由业务参数派生,服务端做去重;
  2. Agent 层记录已执行的调用指纹(工具名加参数哈希),重复的直接拦掉;
  3. 高风险写操作不自动执行,先返回「待确认」,由用户点一下。

第三条看起来降低了自动化程度,但它把「Agent 自作主张」变成「Agent 提议」,出事时的责任边界清楚得多。

四、工具超过十个,选择就开始崩#

工具越多,模型选错的概率越高,这和上下文里塞太多文档是同一个问题。十几二十个工具全量塞进请求,除了贵,还会让名字相近的工具互相干扰,比如 get_user、fetch_user_info、query_user_detail 同时存在。

处理办法是分层:先按业务域选一个分组,再在组内选具体工具。或者干脆按当前会话状态动态挂载,只有相关工具出现在请求里。

工具命名也值得花十分钟:动词加名词,别用缩写,描述里写清「不适用于什么」。删掉半年没被调用过的工具,收益比优化 prompt 大。

五、没有预算,Agent 会自己绕圈#

没有步数上限的循环,遇到一个永远失败的工具就会无限重试。加上预算之后,还要定义「什么算完成」——是拿到最终答案,还是连续两步没有新信息。

后者更实用:连续两次调用的返回值高度相似,基本可以判定在原地打转,此时应该停下来,让模型基于已有信息作答,或者转人工。还有一种更隐蔽的失控:模型不断把大段工具输出重新塞进上下文,token 涨到几十万,账单比错误答案更早到。给每轮记录累计 token,超阈值就截断历史,只保留最近一次工具结果和最初指令。

python
"""agent_loop.py —— 带步数预算、调用去重与参数校验的最小循环
依赖:pip install openai;把 execute 换成你的真实工具实现即可"""
import hashlib, json, os
from openai import OpenAI

client = OpenAI(base_url=os.getenv("OPENAI_BASE_URL"), api_key=os.getenv("OPENAI_API_KEY"))

TOOL_SPEC = {"type": "function", "function": {
    "name": "get_order",
    "description": "按订单号查状态。仅在用户给出订单号时调用。",
    "parameters": {"type": "object",
                   "properties": {"order_id": {"type": "string", "description": "形如 SO-123456"}},
                   "required": ["order_id"], "additionalProperties": False}}}

def execute(name, args):
    if not args.get("order_id", "").startswith("SO-"):
        return {"ok": False, "error": "order_id 必须以 SO- 开头,如 SO-123456"}  # 指导修正
    return {"ok": True, "status": "shipped", "amount": 128.5}

def run(user_input, max_steps=6):
    messages = [{"role": "user", "content": user_input}]
    seen = set()
    for _ in range(max_steps):
        resp = client.chat.completions.create(model="your-model", messages=messages, tools=[TOOL_SPEC])
        msg = resp.choices[0].message
        messages.append(msg.model_dump(exclude_none=True))
        if not msg.tool_calls:
            return msg.content or ""
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments or "{}")
            key = hashlib.sha1(f"{call.function.name}{sorted(args.items())}".encode()).hexdigest()
            if key in seen:  # 同一调用重复出现,不再执行
                result = {"ok": False, "error": "该调用已执行,请换参数或直接总结"}
            else:
                seen.add(key)
                result = execute(call.function.name, args)
            messages.append({"role": "tool", "tool_call_id": call.id,
                             "content": json.dumps(result, ensure_ascii=False)[:2000]})
    return "已达步数上限,基于已知信息作答"

注意最后那行截断:工具结果动辄几千字,全塞回上下文既贵又会把早期指令挤掉。保留关键字段,把长列表转成计数加前三条。

失败现象 根因 修法
参数缺字段、类型错 schema 太宽或没有枚举 收紧 schema,关掉额外字段
反复重试同一工具 错误信息无法指导修正 返回错因、正确形状和示例
写操作重复执行 无幂等键、自动重试 调用指纹去重,写操作加幂等键
选错工具 工具过多、命名相近 分组挂载,命名动词加名词
停在半路不返回 缺步数预算与完成定义 设 max_steps 与终止条件

结论#

  • 工具失败先看 schema:能枚举就不留自由字符串,嵌套不超过两层。
  • 错误信息要能指导下一步,包含错因、正确格式和例子;查无结果要显式失败。
  • 写操作从设计上就该幂等,Agent 层的调用指纹只能当第二道防线。
  • 工具数量控制在十个以内,超出就按业务域分组或按会话动态挂载。
  • 每个 Agent 都要有步数、时间和 token 三种预算与明确的完成条件,并留下失败调用的工具名、参数和错误返回——一个月后你会发现失败集中在两三个工具上。

把 Agent 当成一个需要清晰接口、明确报错和硬性预算的调用方来设计,成功率会上来。指望换一个更强的模型解决这些,通常只是把失败推迟到下一个接口。

完

#Agent#工具调用#LLM#工程实践
回到顶部 ↑