Skip to content

AI 编程代理为什么会陷入修复循环?一套可执行的诊断与收敛流程 ​

AI 编程代理很容易进入一种看似勤奋、实际没有进展的状态:改一处代码,跑测试,看到红字,再改一处,再跑同一条测试。文件一直在变,问题却没有变清楚。

判断循环是否有效,不看它跑了多少次测试,而看每一轮是否减少了一个不确定因素,或产生了新的可验证证据。如果错误输出完全相同、假设没有变化、改动范围却越来越大,这不是调试,是盲目试错。

下面给出一套适用于 Codex、Claude Code 等编程代理的工作方式:先分清问题属于哪一类,再按“复现—定位—修改—验证—总结”推进,并为每次改动设置硬边界。

一、动代码之前,先判断是哪类问题 ​

同一个红色测试结果,可能来自完全不同的原因。先报告“主因、证据、已排除项”;不要把“测试失败”直接翻译成“代码需要修改”。代理策略失控还可能叠加在其他类别上,并不总是单独的一类。

1. 需求不清:验收标准本身没有定下来 ​

典型信号:“符合预期”“处理好异常”“体验更好”等描述没有具体输入、输出或边界;产品说明、现有行为和测试断言彼此冲突;不同人对同一行为有不同解释。

**先检查:**把一个失败案例写成“给定什么输入,在什么条件下,预期观察到什么”;列出最少的一组边界条件。若“相同请求重试”是否应该返回原结果都尚未确定,就不能先写补丁。

**处理方式:**把未决行为列为问题,请需求方确认;不要让代理通过改测试断言替需求方做决定。

2. 测试失真:测试没有正确代表产品契约 ​

**典型信号:**同一代码、同一命令重复运行结果不同;测试依赖随机数、真实时间、网络或共享数据却没有固定输入与清理;mock 与真实实现不一致;断言检查了内部调用方式,而不是用户能观察到的行为。

**先检查:**测试是否稳定、fixture 是否可信、断言是否对应已确认的需求;必要时在隔离的基线版本上跑同一测试,区分既有问题与本次改动引入的回归。基线结果只能用于比较,不能单独证明需求或测试哪一方正确。

**处理方式:**先修正测试模型或明确标注不稳定性;不能为了“变绿”而删断言、放宽期望或刷新快照,除非已证明原测试错误且修正后的测试仍能拦住该缺陷。

3. 环境配置:代码还没进入有效验证 ​

**典型信号:**数据库、容器或服务未启动;环境变量、权限、端口、工作目录、运行时版本或依赖不匹配;下载、网络、证书或缓存故障;失败发生在测试断言之前。

**先检查:**完整命令、退出码、失败阶段、依赖服务健康状态、运行时和依赖版本、必要配置是否存在。分清“测试代码执行后断言失败”和“测试根本没有跑起来”。

**处理方式:**在已授权的本地/测试环境中修复或记录环境前置条件;环境未就绪时停止代码修改。不要为了绕过失败而关闭安全校验、跳过测试或改动生产配置。

4. 代码缺陷:稳定复现且测试契约可信 ​

**典型信号:**在声明的环境中可以稳定复现;失败断言准确描述了已经确认的需求;测试数据和依赖状态可信;问题能指向一个具体的执行路径、状态转换或数据约束。

**处理方式:**定位最小根因,补一个修复前会失败的回归测试,再改最少的共享实现。不要用增加重试、吞掉错误或更改期望来遮住症状。

5. 代理策略失控:每一轮都没有增加信息 ​

**典型信号:**没有先复现就编辑;一次改多个不相关文件;重复运行相同命令并得到相同输出;每次失败就猜一个新补丁;测试失败后改测试;失败日志、假设和变更记录在长对话里丢失。

**处理方式:**立即暂停写入,整理一份失败摘要,恢复到最后一个可信状态;下一轮只能验证一个能被证伪的假设。若没有新证据,就不要再改代码。

快速分诊顺序 ​

  1. 预期行为能否用一个具体例子说清?不能,先澄清需求。
  2. 精确命令能否复现?不能或结果飘忽,先查测试稳定性与环境。
  3. 断言是否表达已确认的业务契约?不是,先纠正测试模型。
  4. 在可信环境下是否稳定违反契约?是,进入代码根因定位。
  5. 最近两次改动是否带来新证据?没有,停止自动修复,转人工判断。

二、标准闭环:复现—定位—修改—验证—总结 ​

每轮开始前,先看 git status --short 和目标文件差异,保存当前基线;记录已有未提交改动,不覆盖、不清理用户工作。对已有复杂改动,可用独立分支或 worktree 隔离代理修改。

1. 复现:先拿到可信的红灯 ​

**输入:**预期与实际行为、触发步骤、完整测试命令、失败输出、运行环境,以及当前 Git 状态。

**动作:**从最窄的测试或命令开始,原样执行并记录退出码和输出。怀疑随机性或并发问题时,固定种子、时间、输入和依赖状态,并重复运行几次作为筛查;这只是排查手段,不是稳定性的数学证明。对本次改动引入的回归,可在隔离的基线工作树中比较。

**退出条件:**有可复述、对应验收标准的可信失败证据;或者明确归类为证据不足、测试失真、环境阻塞,并停止代码编辑。并发和时序缺陷不一定每次触发,应记录固定条件、重复次数、失败频率和相关轨迹,尽量构造受控复现;不能靠“多跑几次终于绿了”证明已修复。没有可信复现或明确根因证据时,不做猜测性修改。

2. 定位:一次验证一个可证伪的假设 ​

**输入:**最小复现、相关调用链、测试和 fixture、环境信息、最近变更。

**动作:**先检查失败发生在哪一层,再沿实际调用链阅读必要代码。提出一个可证伪假设,并同时写出它预测的现象,例如:“如果是数据库唯一约束缺失,那么两个并发请求使用同一幂等键时会创建两行;加入并发用例后应能复现。”优先做只读检查、缩小测试或临时诊断,不要同时尝试多个修复。

**退出条件:**找到有证据支持的根因,知道最小修改范围;或发现关键事实依赖需求方、数据所有者或环境负责人,暂停并提问。仅仅“这行代码看起来可疑”不算根因。

3. 修改:只修已验证的原因 ​

**输入:**根因证据、通过的验收标准、目标文件清单、回归测试。

**动作:**先让回归测试在修复前按预期失败,再做最小修复。一次只改一个根因;保留原有错误处理、权限检查和数据校验。超出预先约定的文件范围时,先解释为什么必须扩展;不要顺便重构、升级依赖或修改无关配置。

**退出条件:**差异只覆盖已批准范围,且每处改动都能映射到根因或测试。无法解释的改动先撤回或单独确认。

4. 验证:先针对性,再扩大范围 ​

**输入:**修复差异、回归用例、项目声明的测试命令和运行环境。

**动作:**按顺序运行:原始复现用例、相邻边界用例、相关模块测试、必要的构建/类型检查/lint;改动跨模块、数据库或用户链路时,再跑相关集成或端到端验证。记录实际命令、退出码和关键输出。对回归用例,还要确认它确实能区分“修好”和“缺陷重新出现”。

**退出条件:**验收标准被实际验证,或失败被归类并带着新证据回到“定位”。如果失败输出与上一轮完全相同、没有新证据,直接停止,不再叠加补丁。

5. 总结:交付证据,不交付感觉 ​

**输入:**根因、差异、测试结果、未验证事项。

**动作:**用简短清单说明:修了什么、为什么是根因、改了哪些文件、执行了哪些命令及结果、还有哪些限制。不要把“执行过命令”写成“通过”,也不要把局部测试通过写成整体验收通过。

**退出条件:**用户可以按摘要重现验证结果;未知项和下一步负责人明确;停止自动修改。

三、防止无限循环:让每轮都有预算和刹车 ​

建议把以下值当作可调整的团队默认值,而非普适定律:

  • **总预算:**每个缺陷默认最多 3 轮“最小补丁 + 验证”;同一根因假设最多 2 轮,第二轮必须说明新证据与上一补丁为何不足。换提示词、测试命令或会话不重置预算,不必为了获得下一次机会编造一个新根因。
  • **立即停止:**相同错误签名连续两次没有变化且没有新证据、无法建立可信复现或根因证据,或代理准备扩大需求/测试契约时,先停下来。
  • **范围边界:**开始前列出允许修改的文件、测试命令和非目标;读取范围可沿相关调用链扩展,写入范围不得静默扩大。发现必须修改范围外的共享模块时,先报告依赖关系和影响,不静默扩范围。
  • **命令预算:**给构建、测试和服务探测设置合理超时;命令卡住时先判断是否等待输入、网络或依赖服务,不重复启动相同任务。诊断性重跑也应有固定次数,不因没有编辑代码就无限执行。
  • **失败摘要:**每轮保留命令、退出码、错误签名、改动文件、验证结果、已排除的假设和下一条可证伪假设。长日志留在文件中,摘要只保留定位所需的片段。
  • **人工介入:**需求有歧义、两种行为都合理、需要真实数据或外部服务、涉及安全/权限/迁移/生产环境、预算耗尽,或修复需要更改公共契约时,停止并提出一个明确问题。
  • **回滚策略:**优先回到最后一个已验证的 Git 检查点;只撤销代理自己本轮产生的目标文件/代码块。不要用 git reset --hard、git checkout . 等命令覆盖工作树中来源不明的改动。Claude Code 的 /rewind 可以恢复它通过文件编辑工具做的变更,但官方文档明确指出它不覆盖 Bash 或外部进程带来的副作用,所以不能替代 Git 检查点。
  • **自动门禁:**提示词是行为约束,不等于强制执行。对无人值守任务,把测试和写入范围校验放进脚本、CI 或工具权限门禁;检查失败应停止并返回摘要,而不是无限重试。

Codex 与 Claude Code:工具门禁不等于修复预算 ​

  • Codex:迭代开发工作流说明 AGENTS.md 可选但会被 Codex 识别,计划、阶段文件和人工门禁是可按项目调整的约定。迭代修复示例把验证通过、达到最大尝试次数、剩余差异不再变化、需要人工审查列为停止条件,并保留每轮验证结果与剩余差异。示例中的三轮是演示配置,不是所有缺陷的最佳数字。提示指南也要求避免反复重读或重改同一批文件却没有进展。
  • Claude Code:Stop hook可在检查不通过时阻止结束;如果钩子不断返回阻止结果,可能形成自动续跑,因此应检查 stop_hook_active 并设计明确的结束条件。它适合做验证门禁,不能代替任务级修复轮次预算。检查点能撤回部分文件编辑,也不能替代对数据库、外部调用和脚本副作用的独立回滚。

四、让提示词、任务和测试更容易收敛 ​

提示词:从“帮我修好”改成一张任务卡 ​

一次只定义一个可独立验收的目标。把事实、假设和未知分开;给出精确命令、完整错误的关键部分、预期/实际行为、允许改动范围和验收标准。不要只写“不断尝试直到通过”,也不要要求长篇内部推理;要求代理提交结论、引用的代码/测试证据、实际命令和退出码。

任务拆分:先调查,再修复 ​

  • **调查任务:**只读复现、分类、定位,输出最小复现和根因假设,不写代码。
  • **修复任务:**只处理一个已确认根因,附回归测试和范围限制。
  • **验证任务:**检查回归用例、相关测试、差异和残余风险;审查者不默认相信实现者的结论。

小且清晰的缺陷不需要先写一份大计划,几个明确的检查点就够;跨模块、未知架构或多条验收路径时,再准备简短方案并分阶段实施。不要为了“防循环”把每个小改动都变成需要人工逐步确认的大流程。

上下文:保留事实,丢掉重复噪声 ​

每次验证失败后,保存一个短“失败摘要”,避免代理重新尝试已经证伪的路径:

text
症状/复现命令:
预期 / 实际:
退出码与稳定性:
运行环境:
失败发生阶段:
已检查文件与最近差异:
已证伪假设:
仍成立的假设及其预测:
已用修改轮次:1/3

把稳定的项目事实放进仓库级指导文件,把单次任务日志留在当前任务,不要把整段历史、全量构建日志和重复规范塞进每轮提示。Codex 可用 AGENTS.md;Claude Code 可用简洁的 CLAUDE.md。Codex 官方资料说明 AGENTS.md 可被识别,Claude Code 文档也建议用 CLAUDE.md 提供项目上下文;两者都适合写测试入口、项目约束和不明显的环境事实,而不是复制整份日志。互不相关的问题另开会话;循环已积累多轮失败路径时,带着失败摘要开干净会话,而不是把整段对话再贴一次。参见 Codex 提示指南与 Claude Code 最佳实践。

测试:让红灯足够可信 ​

  • **断言行为,不断言实现细节。**检查返回值、持久化状态、权限边界等外部可观察结果,而不是某个私有函数必须被调用几次。
  • **固定不确定输入。**注入时钟、随机种子、临时目录和可控依赖;避免测试暗中访问生产数据或不稳定网络。间歇问题预先设定有限的重复次数并记录失败率,不使用“重跑直到通过”的验收方式。
  • **隔离并清理。**每个测试不依赖上个测试留下的状态;集成环境单独标注前置条件。
  • **覆盖错误路径。**除成功路径外,至少测试一个关键边界或失败路径;对并发、重试、权限等缺陷,测试必须覆盖对应机制。
  • **验证测试本身。**修复前回归用例应按预期失败,修复后通过;如果删掉或逆转修复后用例仍通过,测试没有证明缺陷已被拦截。
  • **分类报告。**把失败分成代码断言失败、测试基础设施失败、未运行/被跳过、间歇失败;不要只报一个“测试失败”。

五、可直接使用的调试提示词 ​

下面的模板可以作为单次任务提示词,也可以拆成 Codex 的 AGENTS.md / Claude Code 的 CLAUDE.md 中的常驻工作规则。尖括号内容按项目填写。

text
你负责定位并修复一个缺陷。目标不是让命令表面变绿,而是用证据证明已确认的行为恢复正确。

## 问题
- 预期行为:<具体输入与预期输出>
- 实际行为:<具体输入与实际输出>
- 复现步骤/命令:<命令>
- 环境:<运行时、依赖服务、必要配置>
- 验收条件:<可观察、可重复的检查>

## 边界
- 本轮允许修改:<文件或目录>
- 非目标:<不做的重构、依赖升级、接口变化等>
- 不删除或弱化断言来迎合实现;若测试与已确认契约不符,先举证并修正测试模型;涉及契约歧义或契约变化时请求确认。
- 不覆盖已有未提交改动;不执行破坏性 Git 命令。
- 不访问生产数据、外部服务或执行不可逆操作,除非任务明确授权。

## 必须按顺序执行
1. 检查当前 Git 状态、相关差异、调用方和测试入口。
2. 先复现并判断是需求、测试、环境、代码还是代理流程问题。没有可信复现或明确根因证据时不改代码;间歇失败记录频率与验证局限。
3. 列出一个带证据、能被证伪的根因假设和最小验证方案。
4. 回归测试在修复前按预期失败后,只做一个最小根因修复。
5. 先跑原始复现和相关测试,再按需要扩大验证范围。
6. 每轮汇报:轮次 x/3、改动文件、命令/退出码、错误是否变化、下一条假设。
7. 总结根因、证据、差异、验证结果和未覆盖风险。

## 停止条件
- 最多 3 轮最小补丁与验证;同一根因假设最多 2 轮,第二轮必须有新证据;新会话不重置预算。
- 相同错误连续两轮无变化且没有新证据、缺少可信复现/根因证据、验收条件冲突、写入范围需要扩展或涉及高风险操作时,立即停下,不再猜补丁。
- 停止时给出失败摘要和需要我决定的唯一关键问题,不要声称已修复。

六、案例:重复提交订单时生成了两笔订单 ​

下面是一个假设的 Go 服务缺陷,用来演示闭环如何运作。本例只讨论本地数据库中创建订单,不包含支付、发货等外部副作用。

问题与验收标准 ​

用户报告:网络超时后重试 POST /v1/orders,相同 Idempotency-Key 会创建两笔订单。先把契约写清:在同一租户、用户和接口作用域内,相同键、相同请求体应返回同一订单;相同键、不同请求体应返回冲突;并发重试也只能落一笔订单。不同用户即使使用相同键,也不能读取彼此的订单。

按闭环执行 ​

  1. **复现:**在本地测试数据库可用、配置已核对的情况下,增加一个最小集成用例,例如 TestCreateOrderIdempotency:并发发送两个相同键、相同请求体的请求,检查订单行数和两个响应的订单 ID。假设用例位于 internal/orders,最小命令可以是 go test ./internal/orders -run '^TestCreateOrderIdempotency$' -count=1;路径与用例名按实际项目替换。保存完整命令与退出码。若命令实际失败在“无法连接数据库”,则先归为环境阻塞,不改业务代码。
  2. **定位:**阅读 handler 与 repository 的真实调用链。调查发现实现先查询幂等键,再插入订单;并发时两个请求都可能在查询阶段看到“尚不存在”。并发用例稳定复现,而测试使用真实本地数据库,所以不是单纯的 mock 假象。
  3. **修改:**在存储层按业务作用域建立唯一约束,例如 (tenant_id, user_id, idempotency_key),原子地记录请求指纹与订单;冲突时只读取当前授权作用域内的结果,同键不同请求体按契约返回冲突。具体事务和 SQL 写法按项目使用的数据库适配;有存量数据时先检查重复记录,迁移与清理不能未经授权执行。不要靠客户端重试次数或进程内锁掩盖问题,因为它们不能保证多个服务实例共用同一份状态时仍然只有一笔订单。
  4. **验证:**依次跑并发回归、同键重复提交、同键不同请求体、不同用户同键隔离用例,再跑订单模块集成测试。若并发用例仍失败,记录实际写入行数和数据库约束状态,进入下一轮定位;不能只改预期结果。
  5. **总结:**报告数据库读后写竞态是根因、唯一约束与冲突处理是修复、列出真实执行的命令和结果,并说明尚未覆盖的跨数据库或多版本迁移风险。

这个过程可能只需要一次代码修改,也可能触发“环境没启动”或“测试只用了 mock”的提前停止。关键是每一步都有退出条件,代理没有权限把未知状态无限解释成“再试一遍”。

结语 ​

AI 编程代理的循环并非越短越好;没有验证的快改仍然是猜测。真正有效的调试循环应该是先定义可信的失败,再提出可证伪假设,只改一个根因,用同一条证据链验证,并在预算耗尽时主动停下。

把“最大尝试次数、写入范围、失败摘要、人工介入条件和回滚边界”写进任务协议。Codex 和 Claude Code 的仓库说明、钩子与检查机制可以帮助固化部分流程,但真正的验收仍要以项目测试和可观察结果为准。

如果你刚开始用 AI 结对编程,可以先看提问、执行、验证、复盘的入门流程;本文重点解决的是“测试反复失败后怎样诊断和刹车”。

参考资料 ​

Claude Code 的命令和 Codex 工作流功能会随版本演进;涉及具体工具行为时,请以对应版本的官方文档为准。

最近更新