上礼拜我在 Claude Code 里跑过一个实验,让模型先复述三条任务约束再动手改代码。复述零失误。动手后它碰了不该碰的文件,而且自己没发现。
上回我说跑通了复述、没跑通约束,还给自己留了个台阶——可能是我那个任务里文件耦合太紧,模型觉得不动那个文件就没法完成。今天读到 Yuiko Koyanagi 这篇,台阶可以拆了。这篇在讲一件事:编码规则不该只写在提示词里,因为提示词里的规则对模型本质上是「可拒绝的请求」。原作者的说法是,服从概率会随上下文长度增加和模型规模减小而下降。上下文一长,规则在 token 预算里的权重就被稀释了;模型越小,越容易在「完成动作」和「遵守规则」之间选前者。
这就解释了我那次实验。复述约束和遵守约束是两回事。复述只需要语言生成,不涉及执行时的优先级取舍。它答应你是一句生成的结果,不是个可靠承诺。
原作者的账算得很实:约一半的编码规则属于机械性规则,grep 能查出来的那种。禁用的 import、文件命名、目录结构、返回格式。这类规则不需要模型的判断力,只需要它「记住」。而「记住」恰恰是模型最不可靠的那一环。
我把自己的夹子翻出来清点了一遍,存过的 coding rules 里至少六成是这个性质。「别在 auth.py 之外改动权限相关代码」「测试文件命名必须是 test_ 开头」。全都是字符串匹配能查死的,我却一直指望模型自己记住。
作者给的方案是用 Claude Code 的 PostToolUse 钩子。每次模型写完或编辑文件之后,钩子自动跑一个检查脚本。退出码 0 表示没事,退出码 2 把 stderr 的内容返回给模型。模型在同一轮会话里看到这段审查反馈,当场修正,不需要人工介入,也不需要额外跑一个 review agent。
有个时机细节值得点出来:这是事后检查加反馈,不是事前拦截。文件已经写下去了,但违规信息被灌回给模型,让它自己改。这比在 prompt 里重复一百遍「不许这样写」有用,因为那条反馈带着具体的行号和违规内容,不是一条泛泛的规则。
它也不同于那种多 agent 的 review 流程。钩子的反馈是同步的,错误在写入那一刻就被标记出来。作者写得挺直白:确定性钩子不管上下文多长、模型多小,都以 100% 概率触发。违规时消耗 token,不违规就不消耗。而 prompt 里的规则每条每次都在消耗 token,换来的是一个随上下文衰减的服从概率。
这笔账我信。常在 Claude Code 里写 rules 的人,多半都经历过 CLAUDE.md 越写越长、规则越堆越多、模型却越来越不听话。不是模型变了,是每条规则的权重都被稀释了,最后它什么都在表面答应,执行时什么都是自己看着办。
作者已经把检查脚本做进了 ccteams v0.3.0,给 next-ts、go-api、python-fastapi、rails、django、react-native 这些技术栈都内置了常见失败模式构建的检查脚本。另加了一个模型档次配置——预算档里 builder 用 Haiku,reviewer 用 Sonnet。对「省 token」的执念在这个细节里又露了一手。
评论区还有一条值得记下来。有人提醒检查脚本假设的 payload 结构可能漂移,一旦漂移,脚本会静默失效——某字段改名后,脚本不再报错,是因为它把文件当成了不匹配类型而不是违规。作者回应说 v0.3.1 修了:载荷合同假设错误时响亮失败,而不是静默当成文件类型不匹配。
这个细节好,因为它是从真用钩子的人嘴里出来的。静默失效比直接报错更麻烦——你以为钩子在守门,其实它已经瞎了。作者能把「脚本瞎了」和「脚本在检查但没违规」这两种情况区分开来,这是他较真的地方。
不点链接也能带走的一句:想拦住模型,别把规则写进 prompt 里求它——能 grep 出来的规则,就该挂进 PostToolUse 钩子,让它在写坏的那一刻把错误喂回给它。