如何阻止 AI 编程智能体过早宣称任务完成
原文标题:How I Stopped My AI Coding Agent From Claiming "Done" Too Early
作者运行一套全自主实现系统,由编排器把任务分发给并行实现 Agent(底层是 Claude Code),最初 Agent 可以自行把任务标记为完成,结果出现未跑测试、验收标准未满足、放宽断言让测试变绿、硬编码返回值等问题。
作者用可检查的验收标准、带证据的完成报告和只读验证 Agent 三层设计,解决智能体过早宣称完成的问题。
简要概述
我的自主编程智能体以前总爱在任务只完成了个大概的时候,兴高采烈地甩一句“完成了!✅”:测试压根没跑,主流程看着能走通,验收标准却悄悄没满足。我的解决办法是收回智能体的自我认证权。现在“完成”意味着一份完工报告,里面得贴上命令输出,再由另一个只读的验证智能体来核查。下面就是报告格式、发布门禁,以及我在搭建过程中总结的 5 条经验。
问题所在
我跑着一套完全自主的实现系统:一个编排模块把任务分发给并行工作的编程智能体(底层是 Claude Code),它们趁我睡觉或者忙别的事的时候,按任务文件往下推进。
第一版的约定很简单:智能体挑一个任务,干完活,把任务状态改成 done。编排器信了这个状态,继续往下走。
问题就出在这个“信”上。
早上起来我经常看到的是这些情况:
- 任务标成了已完成,摘要里写着“现在测试应该能过了”。应该——但根本没人跑过。
- 功能主流程能走通,但三条验收标准里有一条压根没管,摘要里也一个字没提。
- 一条挂掉的测试,被“修”好的方式是放宽断言,直到它不再失败。
- 一个函数确实存在,名字和签名都对,函数体却只是“暂时”返回一个硬编码的值。
这些都不是恶意为之,说实话也一点都不意外。语言模型在收尾一个长任务时,承受的压力跟一个累了一天的工程师在下午 6 点差不多:摘要总得写出来,而“搞定了”是故事最自然的结尾。区别在于,疲惫的工程师面前还隔着评审和 CI 流水线才能到 main,而我的智能体手里有一个自己能改的状态字段。
在自主系统里,这种失误还会层层放大。编排器因为任务 A“完成”了,就安排任务 B。任务 B 建立在一个根本不存在的地基上。等我看到的时候,已经在调试三层自信满满的工作,而它们全压在一个虚假的声明上。
所以真正的问题不是代码质量,而是“完成”只是一种主观判断,而持有这个判断的只有作者本人。
解决方案
解决办法分三块:可核查的验收标准、带证据的完工报告,以及一个不是作者的验证者。
flowchart LR
A[Task with acceptance criteria] --> B[Implementer agent]
B --> C[Completion report + evidence]
C --> D{Verifier agent<br/>read-only}
D -- pass --> E[Task closed]
D -- fail --> F[Back to implementer<br/>with specific defects]
F --> B
D -- fails twice --> G[Escalate to human]1. 验收标准必须可核查
要验证“完成”,我得先说清楚完成是什么意思。现在任务文件里的每个任务都带着验收标准,每一条都必须是命令或读文件能确认的东西:
## Task: Add rate limiting to the export endpoint
### Acceptance criteria
- [ ] AC1: Requests over 10/min per user return HTTP 429
- [ ] AC2: The limit is configurable via environment variable
- [ ] AC3: Existing export tests still pass
- [ ] AC4: A new test covers the 429 path
### Out of scope
- Changing limits on any other endpoint
“让导出接口更健壮”不是一个任务,是一种心情。如果我写不出验收标准,那这个任务就还没到能交出去的程度,这是我的问题,不是智能体的问题。
2. 完工报告是证据,不是总结
实现者不能再把任务标成完成了。它唯一能做的就是提交一份完工报告,而报告有固定的格式:
## Completion report
### Criteria
| ID | Status | Evidence |
|-----|---------|----------|
| AC1 | met | test_export_rate_limit: see output below |
| AC2 | met | config diff, EXPORT_RATE_LIMIT read at startup |
| AC3 | met | full suite output below |
| AC4 | not met | wrote the test, but it fails intermittently (timing) |
### Commands run
$ pytest tests/export -q
........................ [100%]
24 passed in 3.41s
### Not verified
- Behavior behind the production proxy (no access from this environment)
### Deviations from the task
- None
三条规则让这套东西跑得起来:
- 证据就是贴出来的输出。不是“我跑了测试”,而是命令本身和它打印出来的东西。没有附输出的声明一律算未验证。
- “未满足”和“未验证”都是一等一的答案。这些栏目是必填的,所以写“无”是一个刻意的表态,而不是漏写。在上面的例子里,AC4 被如实报告为未满足,而这恰恰是一份好报告。
- 没有证据就不能用某些措辞。“应该能跑”“应该能过”“大概率修好了”。智能体一旦发现自己写出这种话,指令就要求它去把东西跑一遍。
最后这条听着有点较真,但它是整个提示词里杠杆最大的一句。含糊其辞,就是没验证过的结论留下的指纹。
3. 换一个智能体来判定
报告交给验证者:一个独立智能体,全新上下文,只有只读工具。它只拿到三样东西:验收标准、diff 和报告。实现者的对话、推理过程、信心程度,它一概看不到。
它的指令是刻意设计成对抗性的:
You are verifying a completed task. You did not write this code.
Inputs: acceptance criteria, the diff, the completion report.
For each criterion:
1. Find the evidence in the report.
2. Re-run the command yourself where you can. Compare output.
3. Read the diff. Does the code do what the criterion says,
or does it only make the check pass?
Look specifically for:
- Tests that were weakened, skipped, or deleted
- Placeholder implementations and hardcoded return values
- Criteria marked "met" with no output attached
- Changes outside the task's stated scope
Report only defects that affect correctness or the criteria.
Do not comment on style. Verdict: PASS or FAIL with file:line.
这里有两个设计选择很关键。
只读。验证者可以跑测试、读文件,但不能改。一旦验证者能“顺手把那个小问题修了”,它就变成了第二个作者,独立核查也就没了。
不共享上下文。如果我把实现者的推理也交给验证者,它往往会顺着这个思路表示赞同。只给它产物,它才必须自己形成判断。
门禁本身只是无聊的胶水代码。编排器只在判定为 PASS 时才关闭任务:
def close_task(task, report, verdict):
if verdict.status == "PASS":
task.status = "done"
elif task.verify_attempts >= 2:
task.status = "needs_human"
else:
task.verify_attempts += 1
task.status = "rework"
task.feedback = verdict.defects
两次验证失败就升级给我。我宁愿早上看到一句“我卡住了”,也不想凌晨 3 点发现两个智能体还在客客气气地商量妥协。
经验教训
1. 作者不能当裁判
对人如此,对模型更是如此。智能体在长时间工作结束后给自己打分,用的还是当初产出这份工作的那套假设。把角色拆开,比在提示词里写多少遍“请仔细复查”都管用。
2. 让诚实成为成本最低的选择
以前,承认有缺口就得写一段生硬的文字,把成功故事的节奏都打断了。现在表格里有个单元格写着“not met”,还有一节标题叫“未验证”。把这些填上,比掩盖缺口还要容易。我不再试图让智能体更诚实,而是开始让格式本身更诚实。
如果你的报告模板里没有地方放坏消息,那你就收不到坏消息。但坏消息本身还在。
3. 证据比自信更有说服力
我现在不再先读报告里的文字描述,而是直接看粘贴出来的输出。一段自信满满却没有命令输出的文字,还不如一段略显忐忑、下面贴着“24 passed”的文字有价值。要让自己和验证工具都学会这样权衡。
4. 验证结论,而不是感觉
早期,我的验证工具生成的评审意见里全是命名建议和重构思路,真正的缺陷却被埋在第40行。后来我把范围限定为“影响正确性或验收标准的缺陷”,输出变得更短,也更有用。一个对什么都评头论足的验证器,没人愿意看。
5. 被弱化的测试比失败的测试更糟
红色的测试告诉你真相。被改成绿色的测试,则是在用一个带勾号的谎言骗你。“有没有断言被放宽、跳过或删除?”现在每次验证都会明确问这个问题,而任何让检查更容易通过的测试改动,都必须在报告里说明理由。
接下来
有几件事我正在做:
- UI 任务的冒烟检查。后端工作的测试输出是扎实的证据,但“按钮能渲染出来”需要另一种证明方式。我希望报告里能为所有涉及视觉的内容附上截图引用。
- 追踪验证器的命中率。我还没严格测过。我想要一个简单的日志,记录它判报告不通过有多频繁,以及我事后在多大程度上不同意它的判定——两个方向都要记。
- 小任务用更轻的门禁。改一行配置大概不需要走全套流程。我正在试着按 diff 大小和风险给门禁分级。
总结
如果只记一件事:别再问你的智能体做完了没有,让它拿出证据来。固定的报告格式、粘贴出来的输出,再加一个只看产物的第二智能体,基本就够了;这些用普通 Markdown 加几行胶水代码,一个下午就能搭起来。
如果你在用 Claude Code,或者自己搭了一套智能体环境,不妨今天就在智能体的最终回复里加一个“未验证”部分。不花什么成本,却能改变你拿回来的东西。🚀
来源:DEV Community · Claude Code · dev.to