Anthropic 工程团队发了一篇关于长任务 Harness 设计的文章。有一句话让我停下来:
"Generators self-assess poorly — confident praise even for mediocre output."(生成器自我评估很差——即使是平庸的输出,也会自信地称赞自己。)
我一直在 OpenSpec 的 apply 阶段内置了代码审查技能。它在和实现相同的 agent context 里运行。审查者和实现者是同一个 session。看到这句话我立刻明白:这就是我的问题。审查者在刚写完代码的 context 里运行,充满了自信,看不到偏差。
问题找到了,以下是我的增强。
那个早就存在的缺口
上一篇文章里,我修了五个问题。其中第三个增强把 superpowers:requesting-code-review 直接内置到 /opsx:apply 里每个任务组的执行流程中。在那之前,代码审查是我得记得主动发起的事;增强3之后,它会自动触发。
这个改动是对的。但它没有解决根本问题。
审查在 apply agent 的 context 里运行。那个 agent 刚刚实现了功能,它推理过每一个设计决策,写了每一个测试,亲眼看着它们通过。它审查自己的代码时,带着对「这段代码是怎么写的」的完整认知。Anthropic 的发现说得很准:一个自我评估的生成器,只会确认自己的意图,不会发现自己的错误。
实际症状:审查结果回来是「没有 critical 或 high 问题」,然后我 merge。有两次,审查在技术上是正确的,但还是漏掉了东西——因为审查者和实现者共享了同一个视角。我需要的不是一个更聪明的审查者,而是一个根本不知道实现者意图的审查者。
Harness 的设计
我做了结构改变:把评估完全移出实现者的 context。
新设计在 apply 里加入了每组一次的 harness 循环:
Group N:
[N.0 CONTRACT]
apply agent writes contracts/group-N.md
content: spec SHALL statements, runtime test commands, code review points, threshold
[N.1 RED → N.X GREEN]
TDD as before
[N.E EVAL] ← replaces the old requesting-code-review step
spawn evaluator subagent — fresh context, haiku model
context: contract file + spec + design + git diff (group only)
evaluator:
1. invoke superpowers:requesting-code-review
2. run runtime test commands from contract
3. diff implementation vs spec SHALL statements
4. aggregate score: Spec (40%) + Runtime (40%) + Code (20%)
5. write to eval-log.md
6. return BLOCK | PASS | RETRY
on RETRY: append FIX tasks, re-run N.E (up to 3 attempts)
on BLOCK: pause immediately, report to human
和旧的审查步骤相比,有三个关键差异。
全新的 context。 evaluator subagent 启动时对代码是怎么写的一无所知。它读 contract 文件,读 spec,读 diff,仅此而已。它不会被实现者的意图影响,因为它从没看过实现者的推理过程。
显式 contract。 实现开始之前,apply agent 先写好 contracts/group-N.md——这是一个扁平的清单,定义这组任务的「完成」是什么:哪些 spec SHALL 语句适用、要运行哪些测试命令、要验证哪些设计决策。evaluator 读的是这份 contract,而不是实现者脑子里对 contract 的记忆。
结构化评分。 PASS/FAIL 不够用。evaluator 在三个维度上给出带权重的评分。只要出现任何 CRITICAL 或 HIGH 的代码问题,无论总分多少,立即触发 BLOCK。总分低于阈值则触发 RETRY。分数 78 的 RETRY 和分数 55 的 RETRY 信号是不一样的——信号的强度和问题的严重程度成正比。
实际发生了什么
我在 python-agent 的 multi-user-auth-admin-ui 变更上跑了这套流程:一个管理用户的 Admin UI,支持邀请、停用、重新启用、删除,带二次确认输入门。八组任务,七十二个任务,八次 evaluator 运行。
平均分 98.3。六组一次通过,两组各重试了一次。
最关键的重试是 Group 3。
Group 3 覆盖 Pinia store——调用后端 API 端点的前端状态层。evaluator 返回分数 78、状态 RETRY,附带这条发现:
"HIGH SEVERITY — all 5 actions call endpoints without /api prefix (e.g.
/admin/usersinstead of/api/admin/users). Backend route is/api/admin/users. Mocked tests pass but real API calls will 404."
七个测试全部通过了。因为 Axios 被 mock 了,mock 测试不验证实际的请求路径。实现者的审查确认了测试套件是绿的。evaluator——读着 contract,读着 spec,对照实际的后端路由——发现了实现者从内部看不到的东西。
没有 harness,这个 store 就会被 merge。Admin UI 的每一个 API 调用都会在生产环境默默 404。
另一个发现来自 Group 2 的代码审查维度。evaluator 发现 invite 在用 request.host_url 拼接邀请链接的 URL——这是 API 服务器的 host。我的 NAS 环境里,API 和 Web UI 跑在不同的端口上,每一条发给用户的邀请链接都会带错误的端口。解法是用 APP_BASE_URL 环境变量。自我审查会看到这段代码,看到它在测试里能跑通,然后继续往下走。evaluator 只读 contract 和 spec,看出这个实现方式对部署目标来说是错的。
八组任务,新增九十九个测试,两个 HIGH 问题被一个根本不知道「我以为代码已经做完了」的 evaluator 捕获。
完整 eval log 在这里。
成本模型
加一个 evaluator 意味着多烧 token。这次跑的实际数字:
- 总 token:约 58M
- 缓存命中:约 56M(96%)
- evaluator 模型:haiku
evaluator 的 context 刻意设计得很小。它看的是 contract 文件、相关 spec 章节、设计文档、以及只属于那一组的 git diff——不包括 apply agent 的对话历史。小 context + haiku 模型,每次跑的成本很低。单组最坏情况是三次 evaluator 运行,实际上大多数组第一次就过了。
96% 的缓存命中率是控制整体成本的关键。apply agent 跨八组积累 context,但大部分 context 是稳定的——spec 文件、设计文档、前几组已完成的工作。haiku evaluator 每次评估组任务都命中同一份缓存的 spec。结构化文档加低变动率,正是 prompt caching 设计的使用场景。
不需要我做的事
这个功能的 explore 是十六天前做的——读代码库、起草需求、画 mock。之后搁在那里没动。今晚跑了 /opsx:propose 和 /opsx:apply,一气呵成。八组任务,七十二个任务执行完毕,九十九个测试写好并通过,两个 HIGH 问题被发现、修复、重新评估。最后冒烟测试:邀请流程通了,二次确认流程通了,Admin UI 和 mock 对得上。
我定义了需求。我做了冒烟测试。中间什么都没做。
这不是一个「AI 什么都做了」的故事,而是一个「工作流知道完成意味着什么」的故事——因为 contract 定义了它,evaluator 执行了它,而两者都不需要我盯着才能工作。
这套工具链的早期版本,在审查步骤上需要我的判断。我得读 diff,决定什么重要,决定 spec 是否被满足。其中一部分是自律,大部分是在完善一个不靠我就说不清楚「完成」是什么的工作流。harness 没有替代那个判断——实现开始之前我写的 contract 本身就是判断。但一旦 contract 存在,验证循环就不再需要我。
人在开发Loop中变成了:提前定义完成是什么,最后验证它发生了。只有头和尾
Agile,更深一层
上一篇文章把四个命令映射到了 Agile 原则——explore 是规划,propose 是「可工作的软件作为进展的度量」,apply 是测试驱动,archive 是回顾。这些映射依然成立。harness 的演进没有改变命令,它改变的是 apply 内部发生的事情。
三个 Agile 原则变得更完善了。
Definition of Done。 Agile 团队在 sprint 开始前写验收标准。现实是,验收标准在 ticket 里,ticket 在 Jira 里,Jira 不是 Claude 读的东西。N.0 CONTRACT 在实现开始前写的 contract 文件,是 evaluator 实际能用的验收标准形式。「完成」不再是「测试是绿的」,而是「evaluator 对这份具体的 contract 打分超过阈值」。这是一个实质性不同的标准。
小迭代。 原来的 apply 流程按任务跑 TDD,但反馈循环在最后关闭:整组一次代码审查,一次人工决策。harness 循环按组自动关闭。每组是一个微型 sprint:定义、实现、评估、需要的话重试、推进。迭代之所以小,不是因为我计划了,而是因为 evaluator 在组边界触发,不让组向前推进直到分数通过。约束是结构性的。
回顾。 eval-log 是回顾记录。八组跑完,我可以读到哪些组需要重试、evaluator 发现了什么、生成了哪些 FIX 任务、最终哪个分数通过了。archive 阶段读 eval-log 来识别 CLAUDE.md 的潜在陷阱——重试超过一次的组是自动候选。这正是 Agile 回顾的模式:哪里出了问题,带什么往前走。区别在于 eval-log 在工作进行时自己写好了,而不是 sprint 结束后凭记忆整理。
上一篇的结论是:约束内置到工作流本身,不管我记不记得触发它,它都会运行。harness 更进一步:约束在运行,运行的证据也被保留下来。不在对话记忆里,在文件里。
当前状态与下一步
harness 验证的变更在 opsx-superpowers 的 feat/harness-validation 分支上,我现在正在用它跑 apply。
如果这个模式在接下来几次变更中都没问题,我会开 PR 合并到 main。
你可以在你的项目上试用:
# 直接安装 harness 分支
claude --plugin-url https://github.com/austinxyz/opsx-superpowers/tree/feat/harness-validation
# 提升 schema——在项目根目录执行一次,每次安装/升级后执行
opsx-install
# Windows (PowerShell): 用 ! 前缀
# ! bash /c/Users/<你的用户名>/projects/opsx-superpowers/bin/opsx-install
然后按原来的方式运行:/opsx:explore → /opsx:propose → /opsx:apply → /opsx:archive。contract 步骤在每组开始时触发,evaluator 在每组结束时触发。需要重试的组会自动重试,触发 BLOCK 的组会暂停等待。eval log 写入 openspec/changes/<topic>/eval-log.md,/opsx:archive 运行时随其他内容一起归档。
参考链接
系列文章:
- Part 1 — 第七章:从 Vibe Coding 到 Spec-Driven Development——OpenSpec 实战
- Part 2 — OpenSpec × Superpowers:3 小时跑完一次 SDD 实战
- Part 3 — 三周之后:OpenSpec + Superpowers 的五个问题和一个 Plugin
- Part 4(本文英文版)— Stacking OpenSpec and Superpowers, Then I Added a Harness
本文引用:
- Anthropic Engineering — Harness Design for Long-Running Apps
- Harness 验证设计文档 — 2026-05-25-harness-validation-design.md
- 本次跑的 eval log — multi-user-auth-admin-ui eval-log.md
- Plugin 分支 — feat/harness-validation
- python-agent 项目 — github.com/austinxyz/p…
方法论没有完成。也不会完成。这依然是重点。