跳到正文
原文
Google AI:DEV 作者专属(RSS)· yureki_lab·· 2 小时前精选AI 评分67

如何为 AI 编码智能体搭建复盘循环,让它不再重复犯错

How I Built a Post-Mortem Loop So My AI Coding Agent Stops Repeating Mistakes

AI 导读

作者为基于 Claude Code 的全自动开发系统搭建了 capture、distill、admit 三阶段复盘循环:失败运行生成结构化事故文件,独立 agent 提炼规则,每条规则须通过 replay 回归检查才能进入 playbook。三个月内重复失败占比从 38% 降至 6%,playbook 硬上限 60 条规则,约三分之一被采纳的规则实际修补的是作者自己任务描述中的歧义。

推荐理由

作者给出可复用的复盘循环设计,用 believed 与 actual 差值和 replay 准入门槛把重复失败从 38% 降到 6%。

正文 · AI 翻译

简而言之

我的全自主实现系统每周都会犯同样的几个错误,而我就是那个在晚上11点修补它指令的人。所以我构建了一个事后复盘循环:每次失败的运行都会生成一个结构化的事件文件,一个独立的智能体将这些事件提炼成一行规则,每条规则都必须通过重放回归检查,才能被允许进入智能体的操作手册。三个月内,重复失败从所有失败的38%下降到6%。以下是设计、关键代码,以及我会做出哪些不同的改变。🚀

问题所在

先交代一些背景。我运行着一个基于Claude Code(截至本文写作时为2.x系列)的24/7自主开发系统。一个编排模块挑选任务,并行的实现智能体在隔离的工作树中完成工作,一个自愈智能体重试损坏的构建,我在早上审查输出。

它确实有效。大多数日子我醒来时都会看到几个可合并的PR。但在生产环境中运行大约两个月后,我做了一件本该在第一天就做的事:我对前十二周的每一次失败进行了分类。

失败类别 占比
真正的新问题 41%
不稳定的基础设施 / API中断 21%
我们以前已经见过的问题 38%

那38%的类别很扎心。同样的错误,不同的星期二:

  • ❌ 当包位于packages/api时却从仓库根目录运行测试套件,然后“修复”由此产生的导入错误
  • ❌ 删除一个它认为是“过时”的文件,而该文件实际上在运行时被动态加载
  • ❌ 在一次运行中重试不稳定的网络调用40次,而不是快速失败
  • ❌ 通过禁用规则来解决lint错误

每次重复大约花费我25分钟的审查和清理时间,外加几美元的token费用。更糟的是,它侵蚀了信任。一个犯新错误的智能体是在学习。一个犯同样错误的智能体是个负担。

而尴尬的部分是:我确实有一份经验教训文档。它有900行。智能体要么从未读过它,要么读了却把它当作背景噪音。我在为一个不存在的读者写教训。

让这件事变得有趣的约束是:我不想在循环中加入人类。这个系统的全部意义就在于它无需我就能运行。所以修复必须是一个机制,而不是一个习惯。

我如何解决它

这个循环有三个阶段:捕获、提炼、准入。每个阶段由不同的智能体拥有,使用不同的提示词,并且每个阶段都会生成一个供下一阶段读取的文件。

flowchart LR
    A[Failed run] --> B[Capture: incident file]
    B --> C[Distill: proposed rule + replay scenario]
    C --> D{Admit gate: replay passes?}
    D -- yes --> E[Playbook]
    D -- no --> F[Rejected, logged]
    E --> G[Next run reads playbook]
    G --> A

阶段1:捕获——结构化的事件,而不是日志转储

当一次运行因任何原因失败时(测试失败、看门狗终止、审查中被人拒绝),编排器会在上下文被丢弃之前,要求刚刚失败的智能体填写一个固定模板。这个模板故意做得很小:

# incidents/2026-07-14-0932.yaml
task: "Add rate limiting to /v1/upload"
failure_class: wrong_assumption   # wrong_assumption | bad_tool_use | scope_creep | env | unknown
trigger: "CI failed: 14 import errors after agent moved tests"
believed: "Tests are run from repo root with `pytest`"
actual: "Each package has its own pytest.ini; must cd into packages/api first"
cost_minutes: 22
resolved_by: human

最重要的两个字段是believed和actual。堆栈跟踪告诉你什么坏了。智能体所相信的与真实情况之间的差异告诉你为什么,而这个差异是唯一值得转化为规则的东西。

我最初尝试了自由形式的事后复盘。它们冗长、充满歉意,而且毫无用处。强制要求一行believed和一行actual,让智能体真正做出诊断。

阶段2:提炼——一个独立的智能体编写规则

每十次事件(或每周,以先到者为准),一个提炼代理会读取所有未解决的事件文件并提出规则。它不是失败的那个代理。这种分离后来证明非常重要(见第 4 课)。

提炼器的提示词有三条硬性约束:

  1. 一条规则最多两句话,并且必须说明它何时适用以及做什么。
  2. 一条规则必须至少引用两个事件。只引用一个事件的规则会被标记为 provisional。
  3. 每条规则都附带一个重放场景:一个从原始事件中派生出的最小化、可复现的任务。

以下是提炼器输出的一条建议规则的样子:

# proposed/rule-0041.yaml
rule: >
  Before running any test command, check for a package-local test config
  (pytest.ini, jest.config.*, vitest.config.*) and run from that directory.
cites: [2026-07-14-0932, 2026-07-21-1105, 2026-08-02-0847]
status: candidate
replay:
  repo: fixtures/monorepo-two-packages
  task: "Fix the failing test in packages/api/tests/test_limits.py"
  pass_if: "agent runs pytest with cwd == packages/api"

注意这条规则很无聊。这正是目标。聪明的规则会被忽视;无聊、具体、可核查的规则会被遵守。

阶段 3:准入——没有重放,就没有规则

这是带来差异的阶段。一条规则进入操作手册,不是因为它听起来合理,而是因为它可证明地修复了重放并且没有破坏其他任何东西。

准入关卡会在重放场景上运行代理两次:一次使用当前操作手册,一次使用当前操作手册加上候选规则。然后它会运行包含该候选规则的完整重放套件(到第三个月时约有 30 个场景)。

# admit_gate.py (Python 3.13) — the part that matters
from dataclasses import dataclass

@dataclass
class ReplayResult:
    scenario_id: str
    passed: bool
    cost_usd: float

def admit(candidate: Rule, playbook: Playbook, suite: list[Scenario]) -> bool:
    # 1. Does the rule actually fix the thing it claims to fix?
    before = run_replay(candidate.replay, playbook)
    after  = run_replay(candidate.replay, playbook.with_rule(candidate))
    if before.passed or not after.passed:
        reject(candidate, reason="replay not discriminative")
        return False

    # 2. Does it break anything we already protect against?
    regressions = [
        r for r in run_suite(suite, playbook.with_rule(candidate))
        if not r.passed
    ]
    if regressions:
        reject(candidate, reason=f"regressed {[r.scenario_id for r in regressions]}")
        return False

    # 3. Playbook cap: 60 rules. Adding one past the cap retires the coldest one.
    if len(playbook) >= 60:
        playbook.retire(playbook.coldest())
    playbook.add(candidate)
    return True

有两个细节值得指出:

  • “之前”的运行必须失败。如果代理在没有该规则的情况下已经通过了重放,那么这条规则就没有教会任何东西。大约四分之一的候选规则在这里被拒绝,这告诉我提炼器过于急切了。
  • 操作手册有 60 条规则的硬性上限。每条规则都会跟踪一个 last_hit 时间戳(每当代理在一次运行中明确引用它时就会更新)。当达到上限时,最冷的规则会被退役到归档中。正是这一条约束让操作手册保持可读,而不是变成又一份 900 行的文档。

三个月后这个循环的样子

指标 第 1 个月 第 3 个月
总失败运行数 71 64
重复失败(占比) 38% 6%
已准入规则 9 44(累计)
被关卡拒绝的规则 4 19(累计)
已退役规则(冷) 0 7
每次准入的平均重放套件成本 $1.10 $3.40

总失败数几乎没有变化,起初这让我很惊讶。这个循环并不能阻止代理遇到新问题。它阻止的是代理两次遇到同一个问题。这正是我想要的:新失败是信息,重复失败是浪费。

每次准入的重放套件成本增加了两倍,因为套件变大了。我对此没意见。花三美元永久消除一个每 25 分钟重复出现的错误,是整个系统里最划算的交易。

经验教训

1. 教训在差异中,而不在堆栈跟踪中

believed 与 actual 是单个最高杠杆的设计决策。日志告诉你发生了什么。差异告诉你需要改变什么。如果你的事后复盘模板没有强制代理说明它假设了什么,那你收集到的就是噪音。

2. 无法重放的规则是一种感觉,而不是规则

在准入关卡之前,我有一些像“小心 monorepo”这样的规则。完全无法证伪。要求每条规则都附带一个重放场景,过滤掉了所有含糊的东西,而幸存下来的规则足够具体,代理实际上可以据此行动。

3. 给操作手册设上限,否则它又会变成那份 900 行的文档

AI 智能体的指令文件会像 wiki 页面一样腐化。没人去修剪它们,于是它们不断膨胀,直到被彻底无视。一个硬性上限加上一条“淘汰最冷门”的规则,能让这份操作手册始终保持在智能体真正能放进注意力里的范围内。对我的系统来说,六十条感觉刚刚好。你的可能是四十条。

4. 别让失败的智能体自己来写规则

早期,我让失败的智能体在同一个上下文里自己提出规则。那些规则既防御性十足又过度具体(“永远不要移动测试文件”)。而一个全新的提炼智能体一次性阅读三个事件后,写出的规则更好、更通用,因为它不需要为自己辩解。关注点分离同样适用于智能体。

5. 很多“AI 的错误”其实是我的错误,只是延迟显现

大约三分之一被采纳的规则,最终发现是在修补我自己任务规格里的歧义。“修复失败的测试”,却没说清是哪个包。这个循环让我的马虎变得可见、可度量,这让人不舒服,但也极其有用。

接下来

  • 规则评分。 目前淘汰完全依据最后命中日期。我想按原始事件的代价来加权,这样一条能防止 2 小时灾难的规则,就能比一条只防止 5 分钟烦扰的规则活得更久。
  • 规格反馈,而不只是规则。 当提炼智能体发现某个事件可追溯到一条含糊的任务规格时,它应该提议修改规格模板,而不是加一条操作手册规则。治本,而非治标。
  • 跨项目规则。 我在好几个代码库上运行这套东西。有些规则(比如 monorepo 测试配置那条)显然是通用的。我想要一个共享层级的规则,配一套自己的、更严格的准入关卡。
  • 更便宜的回放。 回放套件是整个循环里最慢的部分。先在一个更小的模型上跑判别性检查,只有通过时才升级到完整运行,能大幅降低成本。

总结

如果你的编程智能体把同一个错误犯了两次,那不是模型的问题。那是反馈循环的问题。捕捉信念差量,让规则可回放,把关准入,给操作手册设上限。这就是全部诀窍。

我很想听听你的智能体最常重复的错误是什么。写在评论里吧,如果你找到了比“运行前必须失败”更好的准入标准,我真的想偷师一下。💡

在 Dev.to 上关注我,获取更多在生产环境中运行全自主实现系统的构建日志。下一篇:回放套件本身如何维护,而不至于变成第二个代码库。✅

来源:Google AI:DEV 作者专属(RSS) · dev.to