当编码对象变成文档
这个博客仓库最近一次改动最大的 commit,只改了一个 Markdown 文件——就是这篇。上线后它会变成 HTML、RSS、sitemap,一堆我永远不会手写的产物。
不是我偷懒。是我编码的对象换了。
人在往后退
计算机史可以按"人被迫描述到哪一层"重新分期。每一层都比上一层退得更远。
机器码时代,人写 10110000 01100001,意思是"把 97 放进 AL 寄存器"。你得知道 AL 是哪个物理寄存器、它只有 8 位、装不下 1000。
汇编时代,同一件事写成 mov al, 97。助记符替掉了二进制,但你仍在关心寄存器宽度——这是 1:1 映射,你在描述"怎么做",只是换了个好记的写法。
C 语言时代,写成 int a = 97。你不再管 a 落在哪个寄存器,甚至不知道它有没有进寄存器。寄存器分配、指令选择、栈帧布局,全部交给编译器。
这一跳的分量常被低估。它不是语法糖,而是人主动放弃了对"怎么做"的完全控制,换来表达力的数量级提升。代价是真实的:C 的产物可能比你手写的汇编慢,而且你不知道为什么。但没人愿意回去。
按这条线推,下一个被外包的就是"把意图翻译成高级语言"本身。人只说"把温度读数存下来,精度到小数点后一位",剩下的交给模型。编码对象于是从代码上移到文档。
把类比推到底
如果这个类比成立,src/ 该消失。docs/ 成为源文件,代码降级成构建产物,和 dist/、node_modules/ 一样不该进版本库。README 会写:本项目源码见 specs/,构建需模型 X 版本加上下文 Y。
这个推论很顺。但顺得太快的地方,通常藏着被类比吃掉的差别。
三处断裂
一、这不是编译,是合成。 汇编到机器码是确定性映射:同一份输入,逐字节可复现,还能用 objdump 反查每一行是怎么来的。文档到代码不是。
同一份需求文档写"登录失败三次锁定账号",生成三次,可能得到三个实现:用 Redis 计数、用数据库字段、用进程内 Map。三个都"符合文档",但第三个重启就丢,只有前两个能上生产。汇编不会这样——mov al, 97 编译一万次都是同一串字节。
把 LLM 叫"编译器"是修辞上的偷懒,代价是让你误以为编译器的核心承诺还成立:源正确,产物就正确。这个承诺一破,“源码"这个词就失去意义。
二、完备性不对称。 汇编语义完备,因为它是机器的直接表达——机器能做的,它都能说。自然语言不完备。
文档里写"按用户权限过滤”,这六个字至少藏着四个没答案的问题:在数据库层过滤还是应用层过滤?管理员能看到全部还是也受限?软删除的记录算不算?没有权限时返回空列表还是 403?这些不写清楚,代码就无从生成;而写清楚它们的成本,往往比直接写代码更高。
所以文档无法独立成为唯一源。它只能是主干,代码和测试必须留在源里补完语义。
三、可复现性崩塌,这条最致命。 编译器的价值有一半在 deterministic build:给源码和工具链版本,就能得到同一份二进制。LLM 不给这个承诺。
同一份 spec,模型从 A 换成 B,生成的项目结构可能全变;在 prompt 里多加一句"注意性能",产物可能从同步调用变成异步。也就是说,你真正的"源"散落在模型版本、prompt 措辞、检索到的文档快照、工具链配置里。你以为自己版本化了源,其实只版本化了一个指向源的指针。
历史撞过两次墙
这三处断裂不是推演,是有人已经撞过的墙。
第一次是 MDA。 2000 年代,UML 建模,代码自动生成,口号和今天一模一样。它死于形式化成本——画一张能生成代码的 UML 图,比直接写代码还累。
第二次是低代码。 拖拽出界面,逻辑靠配置。它死于表达力天花板——一旦业务长出平台没预设的枝节,人就卡死,最后靠写插件逃生,绕回了原点。
两次死因不同,共同点是:让文档当源,代价超过了它带来的收益。模型把这两条死因都摊薄了,于是有了第三次尝试。
我自己撞了三次
第一次:OpenSpec。 第三次尝试的形态是 spec-driven 开发,最有代表性的就是它:specs/ 是真相源,changes/ 只写增量(delta spec),每完成一个改动就 archive,把增量合并回 specs/,代码从规范生成。这是"文档即源码"最忠实的工程实现,比 MDA 务实得多——它不要求你画图,只要求你写清楚要什么。
我用了它一段时间,然后放弃了。原因不是形式化成本,而是第三条断裂。
specs/ 描述的不是"当前系统是什么",而是"历次 change 累加的结果"。这两者只在增量链完整时才等价。而增量链很脆:
- 某个已完成的 change 目录被清理掉了,因为"反正做完了"
- 某次 archive 忘了提交
- 某个 change 出自已经离职的人,没人记得它为什么这么写
- 新人接手,看见
specs/里两条互相矛盾的规则,不知道哪条还有效
任何一处断掉,specs/ 就从"真相"退化成一份看起来像真相的文档,而且它不会告诉你它错了。你按它改代码,改出来的东西自然也是错的。
第二次:AI 自主维护 docs/。 我在 AGENTS.md 里引用它,每次任务顺手更新。这比 OpenSpec 省事,但病得更重。
OpenSpec 至少把每次 change 的痕迹留在了 git 里,你能倒查。docs/ 方案连痕迹都没有——它是历次会话的累加,而 AI 只知道自己刚碰过什么。三个月后,某个 AI 任务读到一行过期描述"用户服务同步调用订单服务"(架构其实早改成事件驱动了),它会基于这个错误前提写代码,然后把新的错误继续增量下去。错误复利,而且不报警。
第三次:OpenWiki。 理由很简单:它把真相源还给了代码。wiki 是代码的投影,每个论断(Claim)都绑着指向源码的证据。代码一变,工具比对证据、定位受影响的论断、只重写那几页;没被影响的部分原样保留。
检验方法很粗暴:把整个 wiki 删掉,重跑一次。 得到的仍然是当前系统。OpenSpec 的 specs/ 删掉就永久没了,docs/ 删掉则要靠回忆重建。
同一个目标,两种架构:一个把文档当孤本维护,一个把文档当缓存重建。前者省了生成成本,付了维护成本;后者反过来。而当模型足够强、生成足够便宜时,维护成本才是决定生死的那一项。
这也解释了为什么模型变强之后,重流程反而不划算。OpenSpec 那套 proposal → specs → design → tasks → implement 五步,在模型只能写十行代码的时代是必要脚手架;在模型能一次读完整个仓库的时代,其中相当一部分工作它自己就做了。流程成本没降,收益被模型吃掉了。
一条判据
所以"文档即源码"到底对不对?取决于文档能不能被重建。
一份文档有资格当真相源,当且仅当它能从某个更低层、可验证的东西重建出来。
代码之所以是真相源,是因为它能被编译、执行、测试——能被现实检验。文档之所以通常不是,是因为它的正确性只能靠人声明。
于是正确的分层不是"文档在上、代码在下",而是三段:
- 代码是可验证的真相
- 文档是可重建的投影
- 测试是判定投影是否失真的标尺
你确实在"编码文档",在写意图层。但意图层不是新的汇编,它是新的声明式配置:声明要什么,工具负责达成,验证仍发生在底层。
落到工程上,四条判据:
- 文档存证据指针,不存结论——结论可重建,证据才需要维护
- 每个论断能追溯到源(文件、行、版本)
- 允许全量重建:删掉重跑仍得当前状态,否则它就是孤本
- 把漂移检测放进 CI——文档的"编译"就是漂移检查
回头再看 MDA 和低代码:一个死于形式化成本,一个死于表达力天花板,这两条都被模型摊薄了。但漂移没被解决,只是从"人忘记更新文档"变成了"工具能不能检测到漂移"。所以真正该问的不是"要不要让文档当源码",而是"你的文档漂移能不能被自动发现"。
代价
一个反直觉的推论:读代码的人会变少,但读代码的能力会变贵。
今天几乎没人手写汇编,但性能工程师必须读——编译器优化的关键路径上,只有那一层能解释问题。同理,代码变成产物后,多数人不需要读它,但负责判断"这个产物对不对"的人必须能读。
Code review 不会消失,它从"审查源"变成"审查证据":你不再逐行看实现,而是确认产物满足约束、没有引入未声明的行为。
收束
回到那个类比。它有一半是对的:编码对象确实在上移,人确实在从"怎么做"退到"要什么"。
但"文档之于代码"不等于"汇编之于机器码"。汇编和机器码之间是确定性映射;文档和代码之间是有损、概率、需要验证的映射。承认这一点不是唱衰,而是让这个转变真正可用。
意图第一次有机会成为可版本化、可评审的一等公民。但它只有在你保留了验证能力时才算数。
否则你不是在写源码,你只是在写一份很长的、没人会读的注释。