把 Markdown 放进 /src
Carson Gross 认为 Markdown 正在从文档变成源代码,应该提交进 /src、与它生成的代码放在一起,让代码和测试从 Markdown 派生,而不是从转瞬即逝的 prompt 里来。
中文
复制
TLDR
- Markdown 正在变成源代码,而不是文档
- 这样的 Markdown 应该提交进
/src,和它生成的代码放在一起 - 代码和测试应该从这份 Markdown 派生,而不是从转瞬即逝的 prompt 派生(或者至少,prompt 会话最终应该沉淀成持久化的 Markdown)
引言
为了补贴我在 Montana State University 当教授的薪水,我在外面做咨询。我喜欢咨询,也喜欢写代码、帮忙搭系统,既是为了这件事本身,也是因为它让我的技能保持不过时,让我能给学生讲软件开发的最新思路。
过去几年开发领域最大的变化显然是 agentic coding:用 LLM 生成代码,取代手写。我写过几篇相关的文章:
这篇文章里我想聊一个想法,它在我为那些把 agentic coding 放在首位的公司工作时变得越来越清晰:
Markdown 现在是源代码,不是文档。
这当然不是什么新鲜或特别聪明的想法。
在 Markdown is the new source code 一文中,Hartley Brody 写道:
软件的应用逻辑开始像是用 markdown 定义和编辑的,而 agent 实际生成的代码,某种程度上正在变成一个底层的实现细节。
正如上面那几篇文章所显示的,我对 AI 生成的代码态度矛盾。但我的咨询工作表明,各家组织正在朝这个方向走,而且往往速度惊人。
接下来我想讨论的是:当 Markdown 越来越成为软件系统的真相来源,这意味着什么。
缺失的源代码
有一种思路,上面那段引文就是它的写照:LLM 类似于编译器,把高层规格变成底层实现。按这种看法,我们不需要看 LLM 生成的代码,就像我们不看编译器生成的机器码一样。
正如我在 Code is Cheap(er) 中提到的,我并不完全认同这个类比,原因有几个,但与本文相关的是:编译器工作流会保留原始源代码,而 LLM 工作流通常不会。
如今,LLM 生成的代码往往是开发者构建功能时,通过一连串提示词喂给 agent 产生的。实践中,这意味着生成的代码就是该功能最接近“基准真相”的东西。功能文档可能存放在别处(比如 Linear、Slack 讨论串、wiki 等),但就代码库而言,生成的代码才是事实来源。
我的看法是,在专业的 agentic 编程环境中,我们必须接受一个现实:来自临时提示会话的 LLM 生成代码并不理想,并应开始转向在源码目录中把 Markdown 与生成的代码一起捕获并提交。
Markdown 即源码
Markdown 有许多优良特性,使它类似于传统源代码:
- 它是纯文本,因此可 diff、可 grep,也能在 pull request 中审阅
- LLM 能原生读写它
- 人类无需工具即可阅读和编辑
事实上,它已经在某种程度上充当源码了,比如 AGENTS.md、specs、plans、TASK.md 等等。我们只是还没有把这种源码的捕获标准化。
在 Markdown is the new source code 一文中,Brody 说他工作时会把 Markdown 文件放在 .scratch/research/ 和 .scratch/plan/ 里。我也沿用了类似惯例,为临时性需求创建 /tmp 目录。
我的提议是,把其中一些文件提升到一个新目录,与现有源代码并列:/src/md
这个拟议目录中捕获的 Markdown,会比传统设计文档更底层:
- 它包含架构决策
- 它包含源码级决策
- 它包含底层数据设计决策
它更接近规格说明(尽管并不是),而不是项目经理或设计师按传统方式管理的设计文档。
局部性
我是局部性的拥趸,我认为把 Markdown 放进 /src 有很强的局部性优势:
- 代码模块现在会包含解释代码意图的 Markdown
- 不会再有诡异的“远程规格说明”——为什么这样写的逻辑散落在 wiki/Notion/Confluence/Jira 的某处
/src里的 Markdown 可以同时被人和 agent 消费- agent 不再需要去别处寻找某个代码库的上下文
那 Linear/Wiki 之类的呢?
系统行为的其他事实来源仍然可以存在。这些来源提供更高层的、或者说“面向流程”的文档:高层设计文档、需要解决流程的 issue,等等。
但系统核心的、当前的、静态的预期行为,会越来越多地直接记录在源码目录里的 Markdown 中。
那测试呢?
我在网上看到很多人说,测试就是新的规格说明(或者说一直都是)。我觉得这话有一定道理。
但测试并不是人机交互的好机制:
- 它们涉及大量仪式性的东西,往往掩盖了真正在测什么
- 它们通常比大多数人愿意处理的层次更低,尤其是在理解一个系统的时候
- 像 Mermaid 图这样的高层解释没法自然地放进去
我认为下面这种分工是合理的:
- Markdown 放在
/src里,充当(大致上的)规格说明 - 测试放在
/test(或随便哪里),以这些 Markdown 为依据,提供自动化的正确性确认
再强调一次,这里的核心想法是:开发者不再是靠 prompt 生成代码和测试,而是在 /src 目录里写 Markdown,代码和测试由它派生出来。
/src/md 里的 Markdown 长什么样
/src/md 里的 Markdown 介于系统的形式化规格说明和高层设计文档之间。
和源码一样,这些 Markdown 也有对应的复杂度预算。要让这些文档保持干净、分解得当、处在合适的抽象层次上,需要有意识地去管理。
开发者应当预期自己会同时接触 Markdown 和由它派生出的代码,因此,在适当的时候让两者保持同步将成为一项重要技能。
例如,开发者常常会对生成的代码做减法式、约束性的修改,而这些改动可能需要回写到 Markdown 中。
我认为,/src/md 中的大部分内容不应由 agent 生成。这个目录主要应由人来撰写和维护。
一个提议的 /src/md 约定
这部分必然是本文最薄弱的地方,因为这是个新想法,我还没有大量使用过。这只是我在边想边说,抛出来供大家讨论。
尽管如此,下面是一个可能的 /src/md 标准:
src/
md/
README.md # index of all md, entry point for agents
TODO.md # a list of general TODOs open for this module
OVERVIEW.md # a technical overview of this module
features/FEATURE_1.md # a set of feature-specific documents
data/DATAMODEL_1.md # descriptions of data models in the module
api/API_1.md # descriptions of APIs the module provides
infrastructure/INFRASTRUCTURE_1.md # descriptions of infrastructure used by the module
其中 features、data、api 和 infrastructure 目录都是可选的;思路是按不同维度切分,以便在 /src/md 文件夹中直接给出对模块行为扎实、可用的描述。
结论
当代码的生成成本越来越低,真正有价值的只剩下代码背后的意图:它做什么、为什么这么做、以及它绝不能做什么。
今天,这种意图往往散落在一次性的提示会话里,或者零散地分布在 wiki、工单和 Slack 讨论串中。
我认为,出于局部性的考虑,我们应该考虑用 Markdown 记录这些意图,并把它和它生成的代码一起提交进 /src,让人和 agent 都能找到。
我还不知道 /src/md 这类东西的正确结构应该是什么样,而且随着我(和其他人)积累更多经验,我的想法大概也会变。
但我相当确信,Markdown 正在变成源代码,而我们也应该越来越把它当作源代码来对待。
(尽管,是的,LLM 并不是编译器 :))