文章目录
先立两条定义,后面所有论证都挂在上面:
- Spec 的合格线是”能完整重新生成代码、复现整个系统”。 达不到这条的,都不是 Spec。
- Spec 相对代码的层级,等于 OOP 相对汇编。 同样一份逻辑,Spec 用少得多的字符说完;字符少了,理解成本、token 消耗、上下文占用就都下来了。
一、先立标准:什么样的东西才配叫 Spec
在讨论”要不要基于 Spec”之前,得先说什么才算 Spec。标准只有一条:
把代码全删掉,只留下它,能不能重新生成等价的系统。
这条标准很残酷,它把很多东西直接排除掉了。项目里的 README、架构图、设计文档、注释——如果删掉全部代码后它们生成不出系统来,那它们就不是 Spec,只是”关于代码的描述”。它们和代码之间没有”源 → 产物”的关系。
真正的 Spec 与代码的关系是方向性的:
- 一份 Spec 可以对应多份正确实现。 同一份逻辑,数据结构、算法、代码组织都可以不同。
- 一份代码不能唯一确定一份 Spec。 同一段代码,可以被多种意图产生出来。
两条合起来:代码既不是唯一的,也不是完备的。所以它当不了源。
这件事在编译里早就解决过了。同一个 C 文件,-O0 和 -O3 生成两份完全不同的汇编,但它们等价。你不会问”这两份汇编哪份才是对的”——因为两者都是产物,源是那段 C。
Source of truth 只能属于规范性(normative)的表示,也就是描述”应该怎样”的那一份。
这里有个精确的区分:代码是描述性的,它只会是它自己,它没有”不符合预期”这个状态。 只有 Spec 能和现实不一致——而这种不一致,正是工程活动本身。
二、面向对象 vs 汇编:抽象的本质是压缩
现在算收益。
同一个循环,汇编里是”寄存器怎么搬、栈怎么压、跳转怎么加”,OOP 里是 for item in items。字符数差一到两个数量级。
这不是代码风格差异,是认知容量的差异。
人的工作记忆能同时容纳的概念块(chunk)是有限的,这是心理学里被反复验证的老结论。所以理解一个系统的上限,不取决于系统多大,取决于每层能压成多少个 chunk:
- 汇编里一个循环占掉 5 个 chunk。十个循环,工作记忆就满了,剩下的代码你只能”逐行追踪”,不能”理解”。
- OOP 里一个循环占 1 个 chunk。同样的注意力预算,能装下的系统大一个数量级。
每一次抽象跃迁,本质都是同一件事:把细节打包成概念,让有限的心智(或有限的上下文)容纳更大的东西。
而 OOP 那次跃迁的关键机制是——编译器替你选表示。你只说”这里有个对象、它有什么行为”,寄存器分配、内存布局、调用约定由编译器负责。你从”逻辑 + 表示”里剥离掉了表示。
Spec 与代码的关系正是这个剥离:
| 层 | 你描述什么 | 谁决定表示 |
|---|---|---|
| 汇编 | 逻辑 + 表示(寄存器、栈) | 你 |
| C / OOP | 逻辑 + 少量表示 | 编译器 |
| Spec | 只有逻辑 | 模型 / 实现者 |
所以”Spec 之于代码,如 OOP 之于汇编”不是修辞。两者的机制完全一样:语言表达力的跃升,来自把一层细节交给下游去自动决定。
三、更少的字符,换来三件事
1. 更低的理解成本
上一节已经说明:字符数直接对应认知容量。少一个数量级,意味着同样的注意力能覆盖大一个数量级的系统。
2. 更少的 token 消耗
上下文是 LLM 的硬资源,也是真金白银。同一个系统,Spec 可能是几千 token,代码是十几万 token——前提是抽象找对了(见第七节)。而且这个差距在每一轮对话里都要重复支付。
3. 更短的上下文占用 —— 这条是分界线,不是优化
前两条是省钱,第三条决定你还能不能做这件事。
同样一份上下文预算:
- 给代码:只看得了局部。于是所有改动都只能是局部改动,跨模块的影响你看不见。
- 给 Spec:你看得见整个系统。因为整个系统的逻辑本来就没多长。
这解释了为什么很多代码库会不可避免地烂掉——它不是被改坏的,是被”看不见”改坏的。
当没有任何人、也没有任何模型能在一次上下文里装下整个系统,所有的修改就退化成局部补丁,系统进入”只能打补丁、不能重构”的死锁。这不是 AI 带来的新问题,是技术债的老问题;AI 只是把它加速了:模型产出代码的速度远超任何人消化理解的速度。
代码的增长速度 > 理解的增长速度,这个剪刀差就是熵增速率。把理解速度提上来,是唯一的解法。
四、最有力的旁证:人本来就是这么干的
这个论证其实不需要新理论支持,因为工程界已经这么干了五十年。
- 新人入职第一周给你什么?README、架构文档、设计文档。没人让你先 grep 二十万行代码。
- 你要理解 Linux 的调度器,先读什么?文档、论文、
Documentation/目录。然后才去kernel/sched/做验证。
人在理解一个系统时,默认动作是读文档,不是读代码。 只有在文档缺失或可疑时,才退回去读代码。
所以”让 AI 直接基于代码工作”,等价于”让新人直接读两百万行 kernel 源码来理解 Linux”。这不叫理解,叫勉强追踪。
五、那过去为什么文档一定会腐化?
这里必须回答一个问题:道理这么朴素,为什么几十年来大家都知道”文档会腐化”,却还是照腐不误?
因为文档不在构建路径上。
编译器只编译 *.c,不检查你的 design.md。测试只跑 *.py,不跑你的架构图。文档写得再离谱,也不会有任何机制报错。
一个不产生反馈的东西,必然腐化。 这不是纪律问题,是系统结构问题。
然后 AI 改变了这个结构。
当”Spec → 代码”这一跳第一次变得可行,Spec 就第一次进入了构建路径,也第一次有了唯一且最强的验证机制:
删掉代码,从 Spec 重新生成,看能不能等价地跑起来。
做不到,说明 Spec 不完备;做得到,说明这份 Spec 站得住。
这是文档第一次有了被强制执行、被验证、因而不腐化的结构性理由。
我认为这是 AI 对软件工程最实质的改变——不是”写代码变快了”,而是”高层表示第一次可以参与构建了”。 写代码变快只是这件事的副产品。
六、”基于代码”的两个隐含假设
主张”直接基于代码”的人,通常默认了两件事,而两件都不成立。
假设一:代码是完整的知识载体。
代码承载 how,几乎不承载 what 和 why。而且它是高冗余的低层表示——同样一份逻辑,代码里大部分字符在描述表示,不在描述逻辑。
假设二:能读懂代码 = 理解系统。
“读懂”和”理解”是两种活动。逐行追踪代码,能让你说出每一行在做什么;但不保证你能说出这个系统在解决什么问题、边界在哪、什么情况下会失效。后者才叫理解,而它需要更高层的表示。
七、怎么检验一份 Spec 合不合格
标准只有一条,也很残酷:
把代码全删了,只给 Spec,看能不能重新生成等价的系统。
好处是它不可糊弄。任何”看着挺全”的文档,一试就知道。
那么内容上,Spec 和代码的边界该怎么划?
原则:凡是有多个等价实现的地方,Spec 都不该管。
那些地方是”编译自由度”,交给下游决定。反过来,凡是换一种实现就会改变系统语义的地方,Spec 必须写死。
按这个原则过一遍,Spec 应该覆盖:
- 领域模型:有哪些实体、状态、关系
- 行为:每个操作的输入、输出、状态转移
- 不变量:任何时刻都必须成立的约束
- 对外契约:接口、错误、边界条件
- 非功能约束:性能、安全、兼容性
Spec 不该覆盖:数据结构怎么选、算法怎么写、代码怎么组织、变量怎么命名。这些都有多个正确答案,写进去等于把压缩比白白扔掉。
但要诚实:压缩收益不是均匀的。
- 业务逻辑、数据流、状态机、权限规则 —— 压缩比极高,几十倍。
- 底层算法的核心逻辑 —— 压缩比接近 1。快排的实现,Spec 写出来跟代码一样长。
所以实践中不是”全部改用 Spec 描述”,而是:能压缩的地方写 Spec,压不动的地方直接引用。
比如写一行”此处按 RFC 4180 转义”,就覆盖掉几十行手写代码——因为它引用的是一个已经存在的高层概念。这才是抽象的正确用法。
八、由此推出的几条工程纪律
一旦承认”Spec 是源、代码是产物”,下面几条就是自然推论,不需要额外论证。
1. 你不维护代码,你维护 Spec。 就像 C 项目里没人去改 .o 文件。代码是可再生资源。
2. 手改代码必须回灌 Spec。 线上热修可以,改完要把变更反映回 Spec,否则两边分叉(drift)。这和”不要手改生成代码”是同一条纪律,只是生成器从编译器换成了模型。
3. 分块生成 = 分模块编译。 系统越大,单次生成的可靠性越低——这是当前模型的真实限制。所以按子系统切分 Spec、分块生成,再靠接口契约把它们合起来。这和大型 C 项目分模块编译、靠头文件对齐完全同构。
4. 同步问题在这个框架下不再是个问题。 改 Spec,重新生成。需要操心的是生成物的验收,不是人工对齐。
九、几条诚实的边界
- 模型能力是硬约束。 “Spec 能重新生成系统”的前提是模型生成得对。系统越大,一次生成越不可靠,所以必须分块。这条限制会随模型变强而放松,但不会消失。
- 抽象层次找错,就压不动。 如果你的 Spec 写出来跟代码差不多长,通常不是”这个系统没法抽象”,而是你还在描述表示,没找到逻辑。但确实存在压不动的地方(见第七节)。
- 有些知识天然不属于 Spec。 线上实测的性能数字、某个版本的坑、运维口径上的行为——这些是经验性事实,正确载体是数据、测试和运行记录,不是 Spec。
收尾
回到最初那句话。
代码只是 Spec 的一个实例。
发表回复