大多數開發者把LLM用反了。我們用五個詞描述需求,拿回200行程式碼,然後花下一個小時除錯模型做出的假設。

我花了三個月時間執行相反的流程:在索取哪怕一行程式碼之前,先寫一份完整的設計說明。結果程式碼缺陷更少了,但真正的進步在於我自己的理解。

什麼是與LLM的Literate Programming?

Donald Knuth在1984年提出了「literate programming」這個概念。思路很簡單:先寫解釋程式的散文,再把程式碼編織進這些散文裡。大多數開發者無視它,因為維護兩份產物太繁瑣。LLM改變了這筆帳。散文不再是文件,它就是prompt。

當你在索取程式碼之前向LLM解釋自己的思路時,你做的不是標準的prompt engineering。你不是在為模型最佳化token。你是在強迫自己在模型替你幻覺出一套架構之前,先把constraint、edge case和依賴關係講清楚。LLM變成了一隻會回話的rubber duck,但前提是你自己已經完成了真正的思考。

實驗:30天先解釋再編碼

我從待辦清單裡挑了12個真實任務,從CSVparser到WebSocket重連策略都有。每個任務我都用了兩種方法:

  1. 捷徑:把工單描述貼進LLM,直接要程式碼。
  2. 說明:寫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

  1. Valid sha256=abc123... matches computed signature.
  2. Valid bare abc123... matches computed signature.
  3. Missing header returns false.
  4. Wrong signature returns false.
  5. 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很低。

真正的陷阱在中間地帶:那些看起來簡單實則不然的任務。CSVparser看起來直截了當,直到你遇到帶逗號的引用欄位、BOM標記或混合的line ending。跳過說明在這些任務上會讓你事後付出最大的時間代價。

今天就能照搬的工作流

你不需要改變整個流程。本週選一個任務試試:

  1. 打開LLM對話,先寫說明。先別提程式碼。
  2. 重讀自己的說明。如果你講不清edge case,停下來。你還沒準備好寫程式碼,更沒準備好讓LLM替你寫。
  3. 在同一則訊息裡附上實作請求。
  4. 對照你自己的test case來審核輸出,而不是憑直覺。

大多數人跳過的是第二步。如果你的說明含糊,LLM會把這種含糊以微妙缺陷的形式反射回來。說明是對你自己理解的smoke test。

真正的收益不在程式碼本身

30天後,我注意到一件意外的事。程式碼確實更好了,但我自己的設計能力提升得更快。寫說明迫使你顯式地命名自己的假設。這樣做時,你常常會在寫下第一行程式碼之前就意識到自己的思路是錯的。

LLM還在負責打字。但思考的是你,而思考從來都是瓶頸。

如果你想親自嘗試,從下一個缺陷修復開始。在向LLM求助之前,先寫兩段話解釋你認為發生了什麼、為什麼。你可能還沒寫完就已經解決了。