ほとんどの開発者はLLMを逆さまに使っている。5語ほどで欲しいものを説明し、200行のコードを受け取り、次の1時間はモデルが行った仮定をデバッグすることに費やす。
私は3ヶ月間、逆のワークフローを実行した。1行のコードを依頼する前に、完全な設計説明を書くというものだ。結果としてコードのバグは減ったが、本当の改善は自分自身の理解にあった。
LLMとのLiterate Programmingとは
Donald Knuthは1984年に「literate programming」という言葉を生み出した。アイデアは単純だった。プログラムを説明する散文を書き、その散文にコードを織り込む。ほとんどの開発者は2つの成果物を維持するのが面倒だと無視した。LLMはその計算を変える。散文はもはやドキュメントではない。それはpromptだ。
コードを依頼する前にLLMに自分のアプローチを説明するとき、それは標準的なprompt engineeringとは異なることを行っている。モデルのためのtokenを最適化しているわけではない。モデルが勝手にアーキテクチャを幻覚させる前に、自分でconstraint、edge case、依存関係を明確にすることを強いられているのだ。LLMは話しかけてくるrubber duckになる。ただし、実際の思考を自分で済ませた後に限る。
実験:30日間、先に説明する
バックログから12の実際のタスクを選んだ。CSVパーサーからWebSocket再接続戦略まで、様々だ。それぞれについて、2つのアプローチを使った。
- ショートカット:チケットの説明をLLMに貼り付けてコードを依頼する。
- 説明:問題、constraint、エラー処理戦略、test caseを説明する300〜500語の文章を書く。そしてLLMにその設計を実装させる。
出力を3つの基準で評価した。code reviewで見つかったバグ数、mergeまでの時間、問題修正に必要なfollow-up promptの数だ。
数値は圧倒的だった。説明優先のコードは平均1.2回のfollow-up promptで済み、ショートカットアプローチは4.7回だった。code reviewでは、説明優先の提出物1件あたり0.3個のバグが見つかり、ショートカットコードは2.1個だった。
問題は時間だった。説明を書くことで、各タスクの冒頭に15〜20分追加された。それが payoff するかどうかは、何を作っているかによる。
実際に機能する説明の構成法
良い説明は、単なる長いpromptではない。特定の構造を持つdesign documentだ。以下は1週目以降に磨き上げたテンプレートである。
## 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の署名を検証する関数が必要だったが、ひねりがあった。署名ヘッダーは、リクエストを送信したサービスによって2つの異なる形式を使う可能性がある。
Problem:受信したwebhookの署名を検証する。一部の送信者はX-Signature: sha256=<hex>を使い、他はプレフィックスなしのX-Signature: <hex>を使う。既存の統合を壊さずに両方をサポートする必要がある。
Constraints:Node.jsのcryptoモジュールを使用しなければならない。timing attackを防ぐためconstant-time比較を使用しなければならない。明確なbooleanを返し、malformed inputでは絶対にthrowしてはならない。
Edge Cases:ヘッダーが存在しない。ヘッダーが空文字列。ヘッダーにsha512=のような未知のプレフィックスがある。hex文字列の長さが奇数であるか、非hex文字を含む。
Error Handling Strategy:デバッグレベルで具体的な失敗理由をログに記録するが、すべての無効なケースで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に遭遇するまで簡単に見える。説明を省略して最も時間を失うのは、こういうタスクだ。
今日から真似できるワークフロー
プロセス全体を変える必要はない。今週の1つのタスクで試してみよう。
- LLMのチャットを開き、まず説明を書く。まだコードには言及しない。
- 自分の説明を読み返す。edge caseを明確に言語化できないなら、停止する。まだコードを書く準備ができていないし、LLMに書かせる準備もできていない。
- 同じメッセージに実装依頼を追加する。
- 出力を自分の直感ではなく、自分のtest caseに照らして確認する。
ほとんどの人が飛ばすのは2番目のステップだ。説明が曖昧なら、LLMはその曖昧さを巧妙なバグの形で反映して返してくる。説明は、自分自身の理解に対するsmoke testなのだ。
本当のメリットはコードではない
30日後、予期しないことに気づいた。コードは良くなったが、自分の設計スキルの方がより速く向上していた。説明を書くことは、自分の仮定を明示的に命名することを強いる。そうすると、しばしば1行のコードを書く前に、アプローチが間違っていることに気づく。
LLMは依然としてタイピングをしている。しかし、思考を行っているのはあなたであり、それが常にbottleneckだった。
自分で試してみたいなら、次のバグ修正から始めよう。LLMに助けを求める前に、何が起こっていると思うか、なぜそう思うかを説明する2段落を書いてみよう。書き終わる前に解決しているかもしれない。