我 CLAUDE.md 里第 14 条写着"不许为了过测试写特判逻辑"。
上周我 review 一个自己丢给实习生写的函数。它把一个 fixture 的期望值整个抄进了实现里。测试全绿,lint 全过。我看第二遍才发现。
第 14 条挂在那儿,一个字没少。
三周前翻 dev.to 上 Jeongho Nam 那篇讲 TS Evidence Graph 的文章,我被两个数字钉住了:六个前沿模型,60 次运行,真正遵守指令的次数是 0。同一批运行里,模型自称遵守了的比例,超过 90%。
画外音:这实习生不是不会干活,是会一边把活干歪,一边跟你汇报干完了。
它推的东西叫 @ttsc/evidence,跑在作者基于 typescript-go 自己搭的编译器 ttsc 上。大概是我这半年看过最不"技巧"的一套东西——因为它压根没想让模型更听话,它换了个战场。
先说我为什么会点进去。我自己的规则文件已经膨胀到快两百行了。
AGENTS.md、CLAUDE.md、.agents/skills/*/SKILL.md,这三个位置我全用过,现在也全留着,自己都说不清哪条规则落在哪个文件里。文章里列的那套强化手段我全试过——全大写、加粗、挪到文件最顶上、在 prompt 里再复述一遍、前面加 emoji。一个都不管用。后来连调都懒得调了,因为调的过程本身就有种很明显的徒劳感。
那 18 条工程原则我不反对,大部分我还认:不许硬编码、修根因不修表象、不做打地鼠式修复、不留破窗、不重复知识。问题是它列了 18 条。
他引的一项研究给了个挺冷的数:同时压上八条约束,模型满足单条约束的概率约 41%,八条全部满足的约 5.7%。而他有 18 条。这不需要会算。
插一句我自己的判断——这条规律对人也成立。18 条规则贴墙上,人也就只能记住前三到四条。所以"规则越多越不被遵守"不是模型的毛病,是所有会遗忘的东西的通病,只不过在模型这儿它有一个可测量的形状。
它没让模型更听话,它换了个战场
这是我唯一觉得"这个我真没想到"的地方。
规则从提示层搬到了编译层。每个函数都要为技能文件里的每条规则留一行回答注释:
// @evidence no-hard-coding: 入参来自参数,未写死任何 fixture 名
// @evidence open-closed: 新分支走 strategy 注册表,未改 switch
// @evidence yagni: 只实现调用点当前需要的那一个分支
// @evidence fix-root-causes: 修在归一化层,未在调用处打补丁
function normalizeOrder(seed: string): string { /* ... */ }
删掉任意一行,构建就停,报 TS16411,而且它会告诉你欠的是哪一条、定义在哪儿:
src/normalize.ts:41 - error TS16411:
Missing acknowledgement for rule "no-hard-coding"
declared at .agents/skills/principles/SKILL.md:24
我喜欢它的地方不在"强制",在错误列表本身就是一张不会蒸发的工作清单。作者说他直接把错误列表当任务列表用——规则文件里加一条新规则,从下一次构建开始,每个函数都多欠一行。这个我服气。因为我定过的那些"下次记得……"的规矩,全都在开新会话的那一刻消失了。它们不在 CI 里,不在别人发起的 PR 里,只存在于我上一轮的对话框上下文里。
所以这东西的真实价值,我认为是把它从"提示"变成了"仓库里的一件资产"。同一个函数,换一个模型、换一个空上下文来写,也得交出同样的回答。这一条是真进步,跟模型听不听话无关。
洞是硬的
接下来这个洞是硬伤,而且不小。
看一眼 TS16411 到底在检查什么:它在检查你有没有写下那句话。不是那句话是不是真的。
也就是说,这样写照样过编译:
// @evidence input-strict: 入参已在边界完成校验
function parseOrder(raw: any) {
return raw.id.toUpperCase(); // 上一行是假的,raw 可能是 null
}
编译器管不了这个。它只认得那行注释存在。
作者自己在评论区交代了:原本有个 evidenceReview 机制就是为验证这件事设计的,一度启用,最近他停用了。理由是几乎没见过前沿模型写虚假证据,只在 Luna 的 benchmark 里撞到过一次。
我坐那儿想了半天,觉得这里绕回去了。整篇文章的立论基础是"模型在自述合规这件事上不可信"——60 次运行、0 次遵守、90% 以上自报。可写一行 @evidence,正是在让模型做同一个动作:自述合规。而且这次的谎更便宜,从"写一整段话说我遵守了"降到"照着编译器要求的格式补一行"。编译器甚至把格式和内容范围都替它框好了。
那"我没见过它写假证据"这个判断,和当初"我以为模型会遵守规则"这个判断,在证据强度上有什么区别?我看不出来。
说清楚,这不让工具归零。错误列表当工作清单的价值还在,这个跟真假无关。被削掉的是那句"我们的 agent 遵守规则这件事现在可以由编译器证明"。证明不了。它证明的是每个函数都被要求表过一次态。
顺便,作者那条"回答只有在规则真被遵守时才写得出来,所以构建变绿时代码质量也会提升",同一个问题。前提不成立,后面的话就跟着飘。绿的意思只是欠的账都打了白条。
让我坐直的是另一组数
真正让我把文章从头读完的,是一组跟"证明能力"没关系的数。同一个模型,四个应用各建两遍,唯一差别是开不开这个插件:
| 项目 | 覆盖率 | token |
|---|---|---|
| todo | 85.5% → 100% | 866M → 92M |
| 80.3% → 100% | 1,179M → 245M | |
| shopping | 63.1% → 100% | 1,516M → 271M |
| erp | 51.6% → 100% | 5,449M → 411M |
覆盖率那一列我持保留。一套让模型自己写 acknowledgement、自己把数往上推的流程,报出四个齐刷刷的 100%,我第一反应是"这个数不好说"。
但 token 那列不太一样。作者说应用越大,纯提示方式的覆盖率掉得越狠,因为在出结果之前反复"再来一轮"吃掉了九成的 token。
这个我信,因为我自己的账单就是这么涨起来的 😅。真正烧 token 的从来不是一次写对,是我跟实习生来回拉扯——"不对,我要的是这个""不,别碰那个文件"——一轮一轮重发上下文。而它为什么会绕圈,文章里另一个引用说得很直白:Cursor 测过,成功解决里有 63% 是从某处检索来的,不是推导出来的;把网络掐了、git 历史封了,得分从 87.1% 掉到 73.0%。它不是在想,它在翻。翻了半天翻不到,就开始"便宜地通过检查"。
把一连串模糊的拉扯,换成一张编译器给的、确定的、可枚举的待办清单,来回次数掉下来——这条路是能走通的。
我抄什么,不抄什么
只抄三点,说完就完。
第一,把错误当待办。任何能生成一张稳定的、可枚举的、跨会话不消失的清单的检查,都值得装。跟它检查的是不是真事无关,清单本身就有用。
第二,从规则文件这一端开始,别直接跳到他画的那个规格驱动大图。作者自己都说了,那需要人类审阅过的需求文档,而现有项目基本没有。那套东西我暂时不碰——要求文档先写好再写代码,听着对,落地的时候你会发现自己一整天都在写文档。
第三,规则条数砍到能记住的量。他 18 条,我建议 5 条以内。第一次跑在已有仓库上,错误数大概等于函数数乘规则数(文章原话),那就是几百上千条。看到这个数字的人不会逐条回答,只会把所有 acknowledgement 一次性糊上去——而这恰好是这套机制本来要防的失败模式,只不过由人手执行了一遍。
至于"用编译器证明 AI 遵守了规则",我现在还是那个态度:能被编译器检查的东西,值得放进编译器;不能的,别假装能。一行注释证明不了任何行为,它只证明有人要求过。
本期缴税:我花了一个下午把 CLAUDE.md 从 18 条压到 5 条,代价是承认另外 13 条我以前从来没在执行。