你在 CLAUDE.md 里加过“不要用 any”吗?在 code review 里说过“这个函数别超过 80 行”吗?在 system prompt 里提醒过“改完代码要跑 lint”吗?
如果你发现自己第二次写下同一条指令,或者第三次纠正同一个错误,问题就不在那条指令写得够不够醒目。问题在于:你一直在用文本解决一个本该用结构解决的问题。
principle-encode-lessons-in-structure 把这件事说得很直白:
Encode recurring fixes in mechanisms (tools, code, metadata, automation) instead of textual instructions.
把反复出现的修复固化成机制,而不是更多文本。文本指令要求读者“注意到、记住、并遵守”;结构化机制不需要配合就能强制执行。
触发点:第二次写下同一条指令的时刻
这条原则的触发条件极其具体:当你发现自己第二次写下同一条指令,或者注意到一个反复出现的纠正时,停下来,问一个问题:
这条规则能不能变成 lint 规则、元数据标志、运行时检查,或者脚本?
如果能,编码它,删掉那条指令。如果不能——因为它 genuinely 需要判断力——才退回到文本:把指令写得更醒目,并附上一个失败模式的示例。
这个判断顺序是关键。大多数工程团队的做法恰好反过来:先写文本,反复写,直到有一天有人受不了了才考虑自动化。而这条原则要求的是:在你第二次写同一句话的瞬间,就把“文本是否足够”判定为已经失败。
为什么“第二次”是阈值?因为第一次写下指令时,你尚不清楚它是一次性告知,还是一个会反复回归的模式。第二次出现,就是模式的确证。从这一刻起,每多写一遍文本,都是在为一条从未被编译的规则支付利息。
机制强度阶梯:为什么“够用就行”是错的
一旦决定编码,下一个问题是:编码成什么?
原则给出了一个从强到弱的明确排序:
| 强度 | 机制 | 强制方式 | 示例 |
|---|---|---|---|
| 最强 | 不可表示的非法状态 | 编译不通过 | 联合类型 / ADT 让非法组合在类型层面不存在 |
| 2 | lint 规则 / 禁用 API | CI 失败 | ESLint rule、gitleaks、禁止直接调用某 API |
| 3 | 规范化辅助函数 | 使用侧唯一入口 | 唯一正确的工具函数,绕开它需要写更多代码 |
| 最弱 | 运行时检查 | 运行时才报错 | 断言、边界校验 |
规则说得很清楚:选择情境允许的最强机制,而不是“够用就好”的那一个。
理由不是教条。原则给出的解释非常尖锐:Agent 会复制周围代码已有的做法。 如果你的代码库里“正确路径”和“侥幸路径”并存,一个更弱的防护就会成为下一个模板——新代码会照着弱防护的样子写。弱防护不只是没拦住这一次违规,它还会自我复制,把坏习惯扩散成代码库的常态。
这就是为什么“先加个 runtime check 凑合”经常是一个陷阱。runtime check 只在你记得它存在的时候有效;而 lint 规则、类型约束、CI gate 不依赖任何人的记忆。一个 runtime assert 被复制一百次,代码库里就多了一百个“本来可以早点拦住”的地方。
反馈闭环:不是“记下来”,而是“路由到正确的层”
原则附带一个三阶段的反馈回路:捕获 → 路由 → 闭环。
捕获每一次纠正不难理解。容易做错的是路由。
pstack 的实践给出了一个清晰的分层:
- 一次性事件 → 大脑笔记,不值得固化;
- 反复出现的修复 → skill 或 lint 规则;
- 系统性问题 → 升格为原则。
关键在于:不要把一个本该成为 lint 规则的东西,留在一份 skill 文档或 CLAUDE.md 里。 pstack 的 reflect 技能做了一个很硬的设计:当它从对话中提炼出“已接受”的经验列表后,任何本可以用 lint、脚本、元数据标志或运行时检查强制的条目,必须从 Accepted 降级为 Backlog——用文字去承载本可机制化的规则,被判定为错误的分层。
“记录而不路由,等于没有捕获。” 一条关于 lint 规则的笔记,如果 lint 规则本身没有被实现,它就只是一条更长的待办。
2025–2026:这条原则正在从理念变成工具链
这条原则之所以现在值得认真对待,是因为它不再停留在方法论层面。2025 年到 2026 年出现的一批工具,正在把“把教训编码进结构”变成可执行的流水线。
prompt-to-code-skill 的定位很准确:它是一个编译器,把 CLAUDE.md 中的声明性规则,以及对话历史中反复出现的纠正,编译成确定性的强制措施——lint 规则、pre-commit 条目、带 native test 的检查,写进项目自己的工具链。它的 README 里有一句话可以直接作为这条原则的题眼:
A remembered rule is a suggestion; a compiled check is a wall.
记住的规则是建议,编译的检查是墙。
它的分类逻辑也值得注意:Tier A(原生 linter 能查的)→ lint 规则;Tier B(确定性工具能提取的)→ ast-grep / semgrep / 脚本;Tier C(不可约的判断力)→ 诚实地留在 prompt 里,不伪装成脆弱的正则代理。
SOTA-skills 则从另一个方向做同一件事:它的 init-gates.sh 会根据仓库语言自动生成对齐的 .pre-commit-config.yaml,把 lint、format、secret 扫描挂在 commit 上,把 type-check 和测试挂在 push 上;而且它附带 verify-setup.sh,专门检查“配置了”和“真的生效了”之间的差距。
这些工具的共同逻辑是:规则不应该活在某个会漂移的上下文窗口里,而应该活在版本控制之下、由 git 事件触发的确定性检查中。
反模式:三种“假闭环”
原则还列出了三个需要警惕的反模式,它们看起来在做正确的事,实际上没有改变任何结构:
“知道了,我会注意的。” 这句话不持久。人类和 Agent 一样,在上下文压缩或会话切换之后,一条没有写入结构的承诺会最先消失。
“记下来了,但只是一个笔记。” 一条关于“应该有个 lint 规则”的笔记,如果没有被实现成 lint 规则,它只是在用另一种格式重复那条文本指令。
“修了这一次,但没有泛化。” 修好眼前这一个实例,而反复出现的模式原封不动。下一次同样的错误换一个函数名、换一个文件,再次出现。
这三种反模式的共同点是:动作发生了,但结构没有改变。 而这条原则的判据只有一个:下一次同类错误再发生时,有没有一个不需要任何人记住规则的机制,自动拦住它。
最后的边界:判断力不能伪造
这条原则最诚实的地方,在于它承认自己的极限。
“如果修复是结构性的,就只用结构性修复。指令只是症状”——这句话很锋利,但它有一个前置条件:修复确实是结构性的。 对于设计取舍、产品语感、命名品味这类真正需要判断力的问题,强行编码成机械规则反而有害。此时正确的做法是让指令更醒目,并给出一个失败模式的示例。
prompt-to-code 把这一点做成了原则:Tier C 的规则不会被伪装成脆弱的 regex proxy,它们诚实地留在 prompt 里。
所以这条原则要求开发者做的,不是把所有东西都自动化,而是每一次反复纠正发生时,先做一次诚实的判断:这到底是一个可以被结构消灭的问题,还是一个必须由判断力来承载的问题?
如果是前者,删掉你正在写的第二遍指令,去写那条检查。