ほとんどの開発者は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つのアプローチを使った。

  1. ショートカット:チケットの説明をLLMに貼り付けてコードを依頼する。
  2. 説明:問題、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

  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も低いため、ショートカットアプローチで十分うまくいく。

本当の罠は中間地帯にある。単純に感じるが、実際にはそうでないタスクだ。CSVパーサーは、カンマを含む引用フィールドやBOMマーカー、混在するline endingに遭遇するまで簡単に見える。説明を省略して最も時間を失うのは、こういうタスクだ。

今日から真似できるワークフロー

プロセス全体を変える必要はない。今週の1つのタスクで試してみよう。

  1. LLMのチャットを開き、まず説明を書く。まだコードには言及しない。
  2. 自分の説明を読み返す。edge caseを明確に言語化できないなら、停止する。まだコードを書く準備ができていないし、LLMに書かせる準備もできていない。
  3. 同じメッセージに実装依頼を追加する。
  4. 出力を自分の直感ではなく、自分のtest caseに照らして確認する。

ほとんどの人が飛ばすのは2番目のステップだ。説明が曖昧なら、LLMはその曖昧さを巧妙なバグの形で反映して返してくる。説明は、自分自身の理解に対するsmoke testなのだ。

本当のメリットはコードではない

30日後、予期しないことに気づいた。コードは良くなったが、自分の設計スキルの方がより速く向上していた。説明を書くことは、自分の仮定を明示的に命名することを強いる。そうすると、しばしば1行のコードを書く前に、アプローチが間違っていることに気づく。

LLMは依然としてタイピングをしている。しかし、思考を行っているのはあなたであり、それが常にbottleneckだった。

自分で試してみたいなら、次のバグ修正から始めよう。LLMに助けを求める前に、何が起こっていると思うか、なぜそう思うかを説明する2段落を書いてみよう。書き終わる前に解決しているかもしれない。