#skills#engineering#error-handling

Agent 错误处理:5 轮自动修复 + 兜底转人工的工程模式

Agent 跑在生产环境会出错。LLM 输出格式不合规、tool call 参数对不上、工具 timeout、第三方 API 503——任何一环都会让任务失败。这篇写我们在 SCL 项目里沉淀的一套模式:把校验报告塞回 prompt 让 LLM 自己改,最多跑 5 轮,超过就转人工。一次通过率 60% 做到 5 轮内 92%,还能反过来喂改进知识库。

AutorYGG 智能体周刊Veröffentlicht8 Min. Lesezeit

Agent 在生产为什么会出错

把一个 demo 跑通,和把它放到生产里一天处理几千个请求,是两件事。我们这一年踩过的坑大概可以归四类:

  • LLM 输出格式不合规 — 你要 JSON,它给你 JSON 包在 ```json 围栏里,或者多塞了一句"以下是您要的结果:"
  • tool call 参数错 — 类型对了但值不在 enum 里、必填漏了、嵌套对象扁平化了
  • 工具调用 timeout 或 503 — 下游不稳定,重试一次就好但没人写重试
  • 业务校验失败 — 格式都对,但生成的 SQL 引用了不存在的列、生成的代码引了不存在的函数

第一反应往往是在每个出错的地方包 try/except,加日志,然后把异常吞掉返回兜底答案。这样做的代价是用户体验崩了——本来 Agent 该直接给结果,现在退化成"抱歉我没听懂"。

5 轮自动修复模式

先说结论,再说为什么。

原始调用 → 校验 → 通过?返回 : 把校验报告塞 prompt → 重调 → 校验 → ... 最多 5 轮 → 超限转人工

核心是:每一次失败都不是终点,而是下一次 prompt 的输入。不要让 LLM 重新猜,而是告诉它"你上次错在哪、错的具体行号、期望是什么"。

async function runWithRepair<T>(
  task: Task,
  validate: (out: string) => ValidationResult<T>,
  opts = { maxRounds: 5 },
): Promise<RepairResult<T>> {
  const history: Round[] = []
  let prompt = buildInitialPrompt(task)

  for (let round = 1; round <= opts.maxRounds; round++) {
    const output = await llm.call(prompt)
    const result = validate(output)
    history.push({ round, output, result })

    if (result.ok) {
      return { ok: true, value: result.value, rounds: round, history }
    }
    // 关键:把结构化校验报告塞回去,不是把异常 message 拼接
    prompt = buildRepairPrompt(task, output, result.report)
  }

  // 超 5 轮兜底:完整 history 落库 + 转人工
  await escalate(task, history)
  return { ok: false, history }
}

注意 result.report 是结构化的——错误类型、出错位置(行号、字段路径)、期望值、当前值。不是 "validation failed: invalid input" 这种字符串。

为什么是 5 轮

经验值。从我们的数据看:

| 轮次 | 累计通过率(SCL agent,约 4000 个生产请求) | |------|-------------------------| | 1 | 60% | | 2 | 78% | | 3 | 86% | | 4 | 90% | | 5 | 92% | | 6+ | 92.5%(边际收益已经塌了) |

第 5 轮之后再修,token 和延迟成本曲线陡升,通过率几乎不动。这不是普适规律,业务复杂度高的可以放到 7 轮,简单 JSON 抽取 3 轮就够。但 "5" 是我们目前默认的起点。

更重要的一点——超过 5 轮的样本不是垃圾,是金子。这些 case 大概率是:prompt 描述模糊、工具 schema 设计有歧义、训练数据里没见过的边界。我们每周固定 review 这批样本,半年下来 prompt 改了 11 版、tool schema 加了 3 个 enum、知识库补了 27 条规则。

校验报告的格式很关键

同样是"SQL 校验失败",下面两种喂给 LLM,效果差一个数量级。

反例:

Validation failed: column not found

正例:

errors:
  - type: COLUMN_NOT_FOUND
    location: "line 3, col 18"
    snippet: "select user_age from users"
    issue: "users 表没有 user_age 列"
    candidates: ["age", "birth_year"]
    hint: "你可能想要 age"

第二种里面有三个 LLM 决策必需的信号:精确位置、可选项、人写的 hint。少了 hint,LLM 在多个 candidates 之间会瞎猜;少了位置,LLM 会重写整段而不是定点改。

每轮要清空 history 吗

看场景。

  • 生成类任务(写 SQL、写代码、写 JSON):保留 history。LLM 看到自己之前错在哪,第二次会绕开同一个坑。
  • 抽取/分类任务(从一段话里抽实体):清空 history,只保留校验报告。否则 LLM 会被自己上次的错误判断锚定。

我们 SCL 里两种都用——生成类节点保留,抽取类节点清空。这点从框架默认配置看不出来,需要业务自己定。

兜底转人工不是失败

很多团队把"转人工"当成 Agent 没用了的标志。我反着看:有兜底的 Agent 才能放生产。没有兜底就只剩两条路——要么把请求直接返回错误(用户跑路),要么死循环烧 token(账单跑路)。

兜底要做三件事:

  1. 完整 trace 入库 — 输入、5 轮的每次输出、每次校验报告、最终错误类型。少一项,事后都没法分析
  2. 人工有清晰的 UI — 不要让人去翻 JSON 日志。我们做了个简单的 web 页面:左侧原始请求,右侧 5 轮 diff,下面一个"采纳第 N 轮 / 手动修改 / 标记为不可解"的按钮
  3. 人工修过的 case 反向回流 — 不是简单加进 few-shot,而是分析错误模式,决定是改 prompt、加 tool 参数校验、还是补知识库

这套模式还能套到哪

任何"输出有可校验格式"的 Agent 都能套:

  • NL2SQL — 用 EXPLAIN 校验
  • 代码生成 — 用 lint + 单测校验
  • API 调用 — 用 schema validator 校验参数
  • JSON 抽取 — 用 JSON Schema 校验
  • markdown 文档生成 — 用 marked + 自定义规则校验

不能套的场景是"输出本身就是开放性文本",比如写邮件、写文案。这些场景的"校验"是主观的,套这个模式会过度修改、越改越平庸。

最后一句

我们一开始把 5 轮上限放到 10,跑了一周发现 token 账单飙到原来的 3 倍。然后退到 3 轮,通过率掉到 86%。最后停在 5。这种数字没有银弹,先上线,再用真实生产数据调。我现在写下的 5 也许过半年就被打脸——但这个判断你自己跑数据,比任何博客都靠谱。

Kuratiert vom YGG-Team aus ArXiv, HackerNews und öffentlichen Anbieter-Blogs. Menschlich geprüft.