给 Agent 搭个笼子:能写成 assert 的规则,就别写进文档
前两篇我聊了怎么减少人在 vibe coding 里的干预,还有怎么用 multi-agent 把搬砖的活外包出去省 token。这两件事解决之后,我的效率确实上来了,但有个问题一直没解决,而且它比 token 贵得多——agent 写出来的代码,语法全对,业务全错。
测试全绿,lint 全过,PR 看着漂亮,合进去之后发现它理解的业务和 PM 想的根本不是一回事。这种返工不是改几行代码的事,是整个 ticket 白做。
后来我看到 Martin Fowler 那篇 Harness Engineering,才知道这个东西有名字。他的说法是:你得给 agent 搭一个外挂的笼子,让它在轨道上高速试错,而不是指望它自己长出判断力。我读完对着自己的流程一格一格比,发现我前半段做得比大多数人都好,后半段基本是空的。
Harness 到底是个啥
两个轴,四个格子。横轴是时间——写代码之前(前馈)和写代码之后(反馈);纵轴是形态——给 AI 读的文档(guide)和能直接吐结果的工具(sensor)。
| Guide(给 AI 读的) | Sensor(能直接吐反馈的) | |
|---|---|---|
| 写之前 | AGENTS.md、business rules、skill、术语表 | 脚手架、模板、codemod、契约生成 |
| 写之后 | AI reviewer(拿 business rules 当裁判手册) | 类型检查、lint、单测、架构约束测 |
这里最关键的一个区别,我一开始完全没意识到:guide 是概率性生效,sensor 是确定性生效。你的 skill 写到 46K 了,AI 读到第几条会漂谁也说不清;但一个 assert 红了就是红了,没有第二种可能。同一条规则你放在哪一格,效果差一个数量级。
外面还套着两层循环:
1 | ┌─────────── 外层 STEERING LOOP(团队,周频)───────────┐ |
内层 loop 就是 agent 写代码、跑测试、改,分钟级全自动。外层 loop 是我观察到它反复犯同一个错,然后决定加一条 guide 还是加一个 sensor,周级,人主导。Fowler 那篇里最有价值的一句话是:同一个错第三次出现,就必须动 harness,而不是第三次继续在 review 里手工纠正。
拿我自己的流程对一遍
我现在是四个 skill 顺序跑:

对着四个格子比下来,结果挺尴尬的:
前馈 guide 这一格,我塞得满满的。 .ai/*.md 全项目自动加载,Obsidian 里十几份需求分析文档,还有一份 1213 行的领域技术设计——里面甚至写了权威仲裁规则,说清楚某个范围内”代码正确、需求已经 stale”。这一块我觉得比大多数团队都强。
前馈 sensor 这一格,完全是空的。 没有脚手架,没有模板,新 model 新 endpoint 全是从零手写。
反馈 sensor 这一格,只有最右边一小块。 pre-commit 里躺着 black、isort、flake8,加上单元测试。听起来不少,但它们全部只管代码风格,一条业务规则都不管。
外层循环倒是在跑——我的 skill 里那些”血泪例子”、memory 里那十几条,都是它的产物。但我盯着看了半天才发现问题:它只往一个方向出货。
最扎的一刀:我把该是 assert 的东西全写进了文档
我每次发现 agent 犯错,就往 skill 的 Pitfalls 里加一条。加了很久了,implement/SKILL.md 现在 46K。
问题是这些条目里混着两类完全不同的东西。一类是确定性的结构约定——表前缀、路由形状、异常怎么抛,这些能写成 assert。另一类是需要语义判断的——“别无凭据把字段收紧成 NOT NULL”、”别把 out-of-scope 的东西塞进来”,这些只能靠读。
我全都塞进了 Pitfalls。原因也不难猜:写进 Pitfalls 一分钟,写个 test 半小时,每次都选便宜的那个。
代价在后面。第一类每多一条,第二类就被稀释一分。更糟的是guide 会写错——我有一条 pitfall 断言”表前缀跟着 app 走”,我一直以为这是铁律。这次真去跑了一遍全量 model,250 个里 42 个不满足,17%。而且那条 pitfall 举的例子是”app/billing_v2/models/ 用 billing_v2_*“,我去看了一眼,那个目录是空包,一个 model 都没有。
这才是最扎的地方:不准确的 guide 比没有 guide 更糟。 它会让 agent 拿着一条假规则,去改本来正确的东西。
所以我给自己定了一条判定,现在每次要往 Pitfalls 加东西之前先过一遍:
这条规则能不能写成
assert?
能——落成 sensor 脚本,Pitfalls 里只留一行”约束由某个脚本强制”。
不能——才留在 Pitfalls。
然后我在仓库根开了个 .harness_engineering/,专门放这些 sensor。第一个脚本盯的就是表前缀,写完立刻验证它会不会红——清空 allowlist 之后准确报出 9 条历史遗留。只会 pass 的传感器等于没验证,这一步不能省。
同时干的另一件事更重要:把那条 6 行的 pitfall 删到 2 行。这是我的 guide 第一次变短而不是变长。
第二刀:我的质量左移是反的
1 | guide ←──────────→ sensor ← 第一刀:纵轴(层级错配) |
我的传感器全部堆在最右边。前面两个阶段产出的是 md 文档,一个自动检查都没有——而它们恰恰是最贵的阶段。需求理解错,后面全错。
这里我犯了个反面教材式的错误,值得单独说。我想给 refine 阶段加个检查器,验证 plan 里那些 file.py:123 引用有没有随代码漂移。写之前先测了一下:**445 条引用,真实漂移只有 1 条,0.2%**。而且结果会随分支变化产生大量假阳性——同一份 plan 换个分支跑,一堆文件”不存在”,其实只是代码在别的分支上。
测量的结论是不该建这个闸门。 sensor 也要过成本判定,不是越多越好。要是我先写完再测,那就是白干半天。
不过换了个方向测就有收获了。同样扫那些 md,去查 URL 前缀有没有写反——我的 skill 里明明白白写着 billing 的接口是 /billing/<scope>/<name>/,结果 md 里有写成 /client/billing/... 的。我直接把 Django 的路由表打出来对,真实路径就是 api/v2/billing/client/return-accounts/,所以那些是真错。而且它们落在「API 契约 · 给前端」那张表里——前端照着调就是 404。
这条我写成了 sensor,一扫30 处,其中一份文档整篇 28 处全写反了。规则在 skill 里白纸黑字写着,还解释了拼接顺序,照样漂——而且一漂就是整份文档。同一条规则,写在 guide 里就漂了;写成 assert,根本漂不了。
那 30 处我没改,代码早就按正确路径上线了,文档错着也不影响谁。直接冻进 baseline,只对新出现的报警。
第三刀:裁判手册没人用
我的 implement skill 里有一层独立复核,会重新跑测试、核对改动范围、抓弱断言。但它只在把活外包给 copilot 的时候才跑,理由写在 skill 里:代码是 claude 自己刚写的,不存在信不过汇报这个问题。
这个理由我当时觉得挺有道理,现在看是错的。它对虚假汇报成立,对业务规则读错完全不成立——claude 把一条业务规则读错的概率和 copilot 一样高,而且测试全绿也照样错,因为测的就是被误解的那条规则。
更浪费的是,我手上有一份 1213 行的技术设计文档,写得非常细,连锁顺序和状态机的每条边都有。它一直只被”写代码的 agent”读,从来没被”审代码的 agent”读过。同一份文档,两个消费者我只接了一个。
所以我把这一层单独拆出来,改成每次都跑,不管代码是谁写的:找到本次改动对应领域的技术设计文档,按章节比对状态流转、校验规则、锁顺序。加一句提示词的事,白捡一个传感器。
顺着这个往前推,我发现更靠前的地方漏得更狠:refine 从来不读原始 spec。 它只 fetch Jira ticket,业务上下文全靠 analyze 那份二手摘要。于是整条链变成 PRD → 分析文档 → 方案 → 代码,每一跳都是有损压缩,没有任何一跳回头对源头。现在改成每张卡都必须回去读一次 PRD 或我直接给的文档,找不到就停下来问我,不许默认往下走。
回读的方向也有讲究,和覆盖矩阵是一模一样的道理:从 spec 遍历方案,不是从方案遍历 spec。拿着方案去 spec 里找依据,永远看起来是全的——因为你只会去找你已经写了的那些。反过来才看得见漏。重点抓三类:漏、冲突,还有越权决定——spec 明明标着待确认,方案里却悄悄给了个实现。第三类最危险,它不报错,只会在上线半年后变成”当初谁定的”。
顺带说个细节——这类文档常常自己写明了权威顺序,比如”某范围内代码正确、需求已 stale”。发现不一致的时候先看这个声明,落在该范围内的冲突代码赢,范围外的需求赢。没有这条声明,冲突就只能靠人猜。
回到标题:scope 怎么才能清晰正确
分析阶段理解不了业务,这是我最开始最想解决的事。折腾一圈之后我的答案是三层,缺一层就漏:
第一层,状态机图,定义全集。 一共有几条边。碰状态字段的卡必须配图,散文描述状态机是最难读的形式,读者要读三遍才拼得出来的东西,一张图三十秒。
第二层,覆盖矩阵,定义分配。 每条边归谁,哪条无主,哪条撞车。这一层是我原来完全没有的,而且我发现我原来的映射方向是反的,结构上不可能发现遗漏——我的表是「ticket → 干了什么」,按 ticket 遍历。这种表永远看起来是满的,因为行本身就是从 ticket 生成的,漏掉的那条业务边根本不会出现在表里。
要发现漏,必须按业务流程遍历。三条硬规则:行从状态图的边抄,不从 ticket 抄(行数应该等于业务边数,如果恰好等于 ticket 数,基本可以确定是抄错方向了);每行必须有归属,或者显式写”不在本 epic → 去哪了”,留空就是漏;一条边落了两个 ticket,那就是矛盾点,两张卡对同一条边给不同规则,是我后期返工的主要来源。
这张表还有个意外的好处:它天然可检查。”每行非空”就是个 assert。所以它顺便就是分析阶段的第一个传感器。
第三层,AC 加测试,定义边界。 这张卡做到哪算完。这一层我本来就做得挺硬,所以单卡范围一直很清晰——问题恰恰是前两层没有,epic 级别的漏和矛盾只能等到实现阶段才浮出来。
还有一点我很喜欢:图和测试是同一份业务规则的两种投影。 图给人看,测试给机器看。状态图上每条边就是一个测试名。补上之后,新人读测试文件就等于读业务规则清单,而且永不腐烂——因为它会红。
现在长什么样
1 | ┌── 外层 STEERING LOOP ──┐ |
★ 三处新接的裁判手册:analyze 和 refine 回读 PRD / 我给的文档,implement 对照技术设计文档。都不看代码是谁写的,每次都跑。
比最初多了四样东西:转向循环有了两个出口(以前所有教训都流向 guide);guide 开始缩短而不是只增长(第一次闭环就删掉了 4 行,还顺手改掉一条 17% 代码不满足的假规则);三个阶段都接上了裁判手册;refine 从零 sensor 变成有 sensor——它产出的 DDL、API 契约、落点是下游照抄的东西,错在这儿没人拦,测试还全绿。
还没做完的
- 前馈 sensor 依然全空。 脚手架、模板、契约生成,一个都没有。新 model 新 endpoint 还是从零手写,只靠 guide 描述形状。
- 业务规则本身还没有计算性传感器。 恒等式、状态机的边,现在只有”图 + guide”,没有对应的一致性测试。图和测试那两种投影,我只做了一半。
- sensor 是”自觉型”的。 已经挂进 pre-commit 了,但只在本地生效——托管 CI 的容器里没有项目环境,跑不了 Django,只能显式 skip 掉。谁没装 hook、或者
--no-verify一下就绕过去了。要真正强制得让 CI 也装完整环境,代价不小,我现在这条链上就我一个人跑 agent,本地生效够用。 - 必填模板项该不该强制,我还没想清楚。我的 refine 模板要求四个必填项,结果扫了 49 个已定稿的块,「落点」这一项一次都没出现过。这时候不该急着加 sensor 逼自己遵守——更可能是这一项本身没价值。规则被长期无视,先怀疑规则。
写到这儿我发现,这一整轮折腾下来,真正的收获不是多了几个脚本,而是那条判定:每次想往文档里加规则的时候,先问一句这条能不能写成 assert。 能,就别写文档。
- Title: 给 Agent 搭个笼子:能写成 assert 的规则,就别写进文档
- Author: Xiao Qiang
- Created at : 2026-09-06 15:46:07
- Updated at : 2026-09-07 08:52:30
- Link: http://fdslk.github.io/LLM/agent/harness/2026/09/06/harness-engineering-1/
- License: This work is licensed under CC BY-NC-SA 4.0.