大多数开发者把LLM用反了。我们用五个词描述需求,拿回200行代码,然后花下一个小时调试模型做出的假设。
我花了三个月时间运行相反的流程:在索要哪怕一行代码之前,先写一份完整的设计说明。结果代码缺陷更少了,但真正的进步在于我自己的理解。
什么是与LLM的Literate Programming?
Donald Knuth在1984年提出了”literate programming”这个概念。思路很简单:先写解释程序的散文,再把代码编织进这些散文里。大多数开发者无视它,因为维护两份产物太繁琐。LLM改变了这笔账。散文不再是文档,它就是prompt。
当你在索要代码之前向LLM解释自己的思路时,你做的不是标准的prompt engineering。你不是在为模型优化token。你是在强迫自己在模型替你幻觉出一套架构之前,先把constraint、edge case和依赖关系讲清楚。LLM变成了一只会回话的rubber duck,但前提是你自己已经完成了真正的思考。
实验:30天先解释再编码
我从待办清单里挑了12个真实任务,从CSV解析器到WebSocket重连策略都有。每个任务我都用了两种方法:
- 捷径:把工单描述贴进LLM,直接要代码。
- 说明:写300到500字描述问题、constraint、错误处理策略和test case,然后让LLM按这份设计实现。
我从三个维度评估输出:code review发现的缺陷数、合并耗时、修复问题所需的follow-up prompt数量。
数据非常悬殊。说明优先的代码平均只需1.2个follow-up prompt,而捷径法需要4.7个。code review在说明优先的提交中平均每份发现0.3个缺陷,捷径代码则是2.1个。
代价是时间。写说明让每个任务开头多了15到20分钟。值不值取决于你在做什么。
如何组织一份真正有效的说明
好的说明不只是更长的prompt。它是一份有固定结构的design document。以下是我第一周之后精炼出的模板:
## Problem
[What are we solving and why?]
## Constraints
[What can't we do? What formats, protocols, or dependencies must we respect?]
## Edge Cases
[What happens when input is null, empty, malformed, or hostile?]
## Error Handling Strategy
[Fail fast or fail graceful? What do we log? What do we return?]
## Test Cases
[The specific inputs and expected outputs that prove this works.]
这里有一个真实例子。我需要用HMAC-SHA256验证webhook签名的函数,但有个特殊情况:签名头根据发送请求的服务不同,可能采用两种格式。
Problem:验证收到的webhook签名。有些发送方使用X-Signature: sha256=<hex>,另一些使用不带前缀的X-Signature: <hex>。需要同时支持两者,且不破坏现有集成。
Constraints:必须使用Node.js的crypto模块。必须使用constant-time比较以防止timing attack。必须返回明确的boolean,遇到malformed input时绝不能抛出异常。
Edge Cases:头缺失。头为空字符串。头含有未知前缀如sha512=。hex字符串长度为奇数或包含非hex字符。
Error Handling Strategy:在debug级别记录具体失败原因,但所有无效情况都返回false。绝不在错误消息中泄露签名细节。
Test Cases:
- Valid
sha256=abc123...matches computed signature. - Valid bare
abc123...matches computed signature. - Missing header returns
false. - Wrong signature returns
false. - Non-hex characters in signature return
false.
然后我补了一句:“Implement this function in TypeScript.”
模型返回了以下代码:
import { createHmac, timingSafeEqual } from 'crypto';
export function validateWebhookSignature(
payload: string,
header: string | undefined,
secret: string
): boolean {
if (!header || header.length === 0) {
return false;
}
const hexPattern = /^[0-9a-fA-F]+$/;
let signatureHex: string;
const prefixMatch = header.match(/^sha256=(.+)$/);
if (prefixMatch) {
signatureHex = prefixMatch[1];
} else {
signatureHex = header;
}
if (!hexPattern.test(signatureHex) || signatureHex.length % 2 !== 0) {
return false;
}
const expected = createHmac('sha256', secret).update(payload).digest();
const actual = Buffer.from(signatureHex, 'hex');
if (actual.length !== expected.length) {
return false;
}
return timingSafeEqual(actual, expected);
}
这段代码一次编译通过。更重要的是,它处理了我指定的每一个edge case,因为我已经提前想清楚了。模型不需要猜测”validate”到底是什么意思。
什么时候有用,什么时候浪费时间
这种方法在任何涉及分支逻辑、外部依赖或安全影响的地方都大放异彩。上面的webhook验证器就是完美例子。模型不可能知道你的threat model,除非你告诉它。
但对于样板代码,这就小题大做了。我不会为了生成一个React表单组件或标准的数据库迁移而去写500字。这些任务用捷径法就行,因为模式在training data里已经充分体现,而且stakes很低。
真正的陷阱在中间地带:那些看起来简单实则不然的任务。CSV解析器看起来直截了当,直到你遇到带逗号的引用字段、BOM标记或混合的line ending。跳过说明在这些任务上会让你事后付出最大的时间代价。
今天就能照搬的工作流
你不需要改变整个流程。本周选一个任务试试:
- 打开LLM对话,先写说明。先别提代码。
- 重读自己的说明。如果你讲不清edge case,停下来。你还没准备好写代码,更没准备好让LLM替你写。
- 在同一条消息里附上实现请求。
- 对照你自己的test case来审核输出,而不是凭直觉。
大多数人跳过的是第二步。如果你的说明含糊,LLM会把这种含糊以微妙缺陷的形式反射回来。说明是对你自己理解的smoke test。
真正的收益不在代码本身
30天后,我注意到一件意外的事。代码确实更好了,但我自己的设计能力提升得更快。写说明迫使你显式地命名自己的假设。这样做时,你常常会在写下第一行代码之前就意识到自己的思路是错的。
LLM还在负责打字。但思考的是你,而思考从来都是瓶颈。
如果你想亲自尝试,从下一个缺陷修复开始。在向LLM求助之前,先写两段话解释你认为发生了什么、为什么。你可能还没写完就已经解决了。