LLM时代的文学编程
1984年,Donald Knuth发表了一篇论文,标题叫《Literate Programming》1,核心观点只有一句话:程序首先是写给人看的,其次才是给机器执行的。 四十年后的今天,这个观点在LLM Agent的浪潮中以一种Knuth本人可能都没想到的方式复活了。
什么是文学编程
传统编程的叙事角度是面向机器的——你得按编译器的规则,把逻辑拆成函数、模块、类,按语法树的层级组织代码。Knuth觉得这搞反了。他认为程序应该像写一篇文章一样,按照人类思考问题的顺序来组织,代码只是嵌在叙事中的片段2。
文学编程的技术实现依赖一个元语言系统和两个工具3:
- Weave(编织):把源文件编译成一份排版精美的文档(比如PDF),供人阅读
- Tangle(缠绕):从同一份源文件中提取出可执行代码,供机器编译
也就是说,一份源文件同时产出两样东西:给人看的文档和给机器跑的代码。源文件本身既不是纯文档也不是纯代码,而是两者的交织。
wc:一个经典的例子
Knuth用文学编程重写过Unix的wc(word count)程序4,这个例子是理解文学编程最好的入口。
在传统C语言中,wc的实现大约是这样的——一个main函数从头写到尾,变量在顶部声明,逻辑按编译器的要求线性排列:
#include <stdio.h>
int main(int argc, char *argv[]) {
int c, nl, nw, nc, inword;
FILE *fp;
fp = fopen(argv[1], "r");
nl = nw = nc = inword = 0;
while ((c = fgetc(fp)) != EOF) {
nc++;
if (c == '\n') nl++;
if (c == ' ' || c == '\n' || c == '\t') {
inword = 0;
} else if (!inword) {
inword = 1; nw++;
}
}
printf("%d %d %d\n", nl, nw, nc);
fclose(fp);
return 0;
}
而在Knuth的文学编程版本中,同样的程序被组织成一篇「小论文」。开头先解释这个程序要解决什么问题,然后用命名片段(Named Sections)按人类理解的顺序展开:
@* Introduction. This program counts lines, words,
and characters in a text file...
@ The main variables. We keep track of the current state
with the following variables:
@<Global variables@>=
long line_count, word_count, char_count;
int in_word;
@ Counting logic. A "word" is a maximal sequence of
non-whitespace characters...
@<Scan the file@>=
while ((c = fgetc(fp)) != EOF) {
char_count++;
@<Check for newline@>
@<Update word count@>
}
注意区别:文学编程版本按「这个程序要干什么→需要哪些变量→计数逻辑怎么工作」的思路展开,每个片段先用自然语言解释,再附上代码。Tangle工具会把这些片段重新拼回编译器需要的顺序,Weave工具则生成一份带交叉引用的漂亮文档。
这种写法的好处显而易见:六个月后回来看这段代码,你读的是一篇文章而不是在做逆向工程。但坏处也很明显——维护成本是双倍的。每次改代码都要同步更新叙述,稍不注意文档和代码就会脱节。这是文学编程在过去四十年一直没能成为主流的核心原因5。
文学编程与LLM Agent Skill
有意思的是,当Anthropic发布Agent Skills框架6时,很多人发现这东西和四十年前的文学编程形神皆似。
Agent Skill的核心是一个SKILL.md文件7,用YAML前置内容描述元数据,用Markdown写执行指令,绑定可执行脚本。看看这个结构:
---
name: data-analysis
description: "Analyze CSV data and generate summary reports"
---
## Instructions
1. Read the input CSV file
2. Identify column types and data distribution
3. Generate summary statistics
4. Output a formatted report
## Scripts
Use `scripts/analyze.py` for heavy computation.
一份文件,同时包含「给Agent理解的叙述」和「给机器执行的脚本引用」——这不就是Knuth的Weave + Tangle吗?
对比
| 维度 | 文学编程 (WEB/CWEB) | Agent Skill (SKILL.md) |
|---|---|---|
| 源文件 | 混合文件(散文 + 代码片段)3 | SKILL.md(指令 + 元数据)7 |
| 元数据 | 宏与命名片段 | YAML前置内容(description字段触发匹配) |
| Weave产物 | 排版文档(TeX/PDF) | Agent的推理日志与自然语言输出 |
| Tangle产物 | 可编译源代码(C/Pascal) | 绑定的可执行脚本(Python/Bash) |
| 读者 | 人类程序员 | LLM Agent |
| 维护方式 | 手动同步散文与代码 | Agent自动化的「代码-执行-迭代」循环 |
| 失败原因/成功条件 | 双倍维护成本拖垮了人类 | LLM天然擅长处理自然语言与代码的双重语义 |
关键的转变在最后一行。文学编程失败的原因恰恰是Agent Skill成功的前提:LLM本身就是一个超级Weaver和Tangler。它读自然语言叙述,理解意图,调用脚本执行,遇到错误再回到叙述中寻找修正线索——整个过程不需要人类手动维护叙述与代码的同步5。
更妙的是Agent Skill的「渐进式披露」(Progressive Disclosure)设计8:
- 第一层:只加载YAML元数据(约100 Token),作为技能索引
- 第二层:匹配到相关性后加载Markdown指令(< 5,000 Token)
- 第三层:执行时才访问脚本文件(不计入上下文窗口)
这像极了文学编程中「先读摘要,感兴趣再展开章节,需要细节再看代码」的阅读体验。只不过读者从人类变成了LLM,「翻页」的动作从手动变成了自动。
新时代:自然语言作为源代码
回到之前那篇关于未来的编程语言的讨论,编程语言的演进路线是:机器码 → 汇编 → 高级语言 → 解释型语言 → ?。每一步都在用性能换可读性。到了LLM时代,这条路的终点可能就是Knuth四十年前指的方向:自然语言。
区别在于,Knuth设想的读者是人类同行,而现在的读者是LLM。 这种转变带来几个有趣的推论:
-
文档即程序将变成现实。 当Agent能够理解一份写得足够好的技术文档并据此执行任务时,「文档」和「程序」之间的界限就会模糊到消失。SKILL.md已经是这种趋势的雏形。学术研究也表明,万亿参数规模的模型能够有效对齐自然语言描述与代码语义9,随着模型能力的继续增强,这种对齐只会越来越精确。
-
「写清楚」将比「写正确」更重要。 传统编程中,编译器要求你的代码语法正确;未来的「编程」中,LLM要求你的意图表达清晰。一段模糊的自然语言指令产生的Bug,本质上和一行语法错误的代码没什么区别。Knuth说程序员应该把自己当作文学家1,这话在LLM时代不再是比喻,而是字面意义上的职业要求。
-
维护负担将从人类转移到机器。 文学编程最大的痛点——手动保持叙述与代码同步——必将被LLM解决。Agent可以在修改代码后自动更新文档,或者在文档变更后自动调整执行逻辑。SyntAGM10等系统已经在用编译器反馈自动化Weave和Tangle的角色。
四十年前,Knuth的文学编程因为太理想主义而被束之高阁。四十年后,LLM Agent让这种「理想主义」变成了最实用的工程范式。程序即文学,不是修辞,是现实。
-
Knuth, D. E. “Literate Programming.” The Computer Journal, 1984 以及 literateprogramming ↩ ↩2
-
Literate Programming Resurges as AI Agents Solve 40-Year Problem ↩ ↩2
-
Renaissance of Literate Programming in the Era of LLMs - arXiv ↩
-
Grammar-Aware Literate Generative Mathematical Programming with Compiler-in-the-Loop ↩