LLMは「怠惰」になれない
目次
Bryan Cantrill(Oxide Computer共同創業者)が4月12日に「The Peril of Laziness Lost」を公開した。
Larry Wallが定義した「プログラマの三大美徳」のひとつである「怠惰」が、LLMの台頭によって失われつつある、という話。読んでみたらかなり刺さる内容だったのでまとめる。
Larry Wallの「怠惰」
Perlの生みの親Larry Wallは、著書「Programming Perl」(通称Camel Book)でプログラマの三大美徳を挙げた。
- 怠惰 (Laziness): 将来の自分のために今ちゃんと設計する動機
- 短気 (Impatience): 遅いシステムに耐えられない感覚
- 傲慢 (Hubris): 人に見せて恥ずかしくないコードへの執着
Cantrillはこの中で「怠惰」がいちばん深い美徳だと言う。
「怠惰」は単なるサボりじゃない。
同じことを二度やりたくない。後で面倒になるのが嫌だ。
だから今コストを払って抽象化する。将来の自分のために、そして後からそのコードを使う全員のために。
皮肉なのは、怠惰であるためには膨大な努力が必要だということ。
Rich Hickeyが提唱した「ハンモック駆動開発」のように、最適な抽象を見つけるために考え抜く行為は知的にはかなりハードだ。
LLMには「面倒くさい」がない
Cantrillの主張の核心がここ。
LLMには時間的制約がない。
コードを書くコストがゼロだから、「面倒だからまとめよう」という動機が生まれない。
結果として、何層にも積み上がったレイヤーケーキ状のコードを際限なく生成する。
記事ではGarry Tan(Y Combinator社長)がLLMで「1日37,000行のコードを書いた」と豪語した件を引き合いに出している。
参考値として、DTraceプロジェクト全体が約60,000行。
行数でソフトウェアを評価するのは、ページ数で小説を評価するようなものだ、とCantrillは皮肉る。
Tanのプロジェクトを実際に分析すると、典型的な症状が出ていたという。
- 重複するテストフレームワーク
- 不要なスターターテンプレート
- なぜか組み込まれたテキストエディタ
- 8種類のロゴバリエーション
個々の問題は修正可能だが、根はもっと深い。
LLMは「増やす」ことしかしない。削ることも、統合することも、最小化することも自発的にはやらない。
この傾向は日常的なツールでも出る。Claude Codeの品質劣化を17,871件のThinkingブロックで立証したIssueでは、思考深度が下がるとRead:Edit比率が崩壊し、ファイル全体の書き直し(Write操作)が増えることがデータで示されていた。「読んで直す」より「全部書き直す」を選びがち、というのもコード膨張の一因だろう。
「書くエンジン」と「削るエンジン」
ここからは自分の解釈も混ぜた話。
Cantrillの論を整理すると、こういう構図が浮かび上がる。
| 主体 | 特徴 | 得意なこと |
|---|---|---|
| LLM | 時間コストゼロ、無限に生成可能 | 書く、展開する、量を増やす |
| 人間 | 時間が有限、認知負荷に上限がある | 削る、統合する、抽象化する |
LLMは「書くエンジン」。人間は「削るエンジン」。
重要なのは、人間の時間的制約こそが良い設計の源泉だという点。
認知負荷に上限があるからこそ、冗長なインターフェースを拒否してシンプルな抽象を追求する。
制約が質を生む。LLMにはこの制約がないから、自発的にそこへ到達できない。
じゃあ実際にLLM + 人間で開発するとき、フローはどうなるか。
flowchart TD
A[人間: 構造を設計] --> B[LLM: 実装を生成]
B --> C[人間: 削る・統合する]
C --> D{重複・肥大化<br/>はないか?}
D -->|ある| E[人間: 抽象を更新]
E --> B
D -->|ない| F[完成]
style A fill:#4a9eff,color:#fff
style C fill:#4a9eff,color:#fff
style E fill:#4a9eff,color:#fff
style B fill:#ff6b6b,color:#fff
このフローで最も重要なのはステップCの「削る・統合する」。
ここを省略してLLMの出力をそのまま積み上げると、Cantrillが警告するレイヤーケーキが完成する。
「LLMを道具として使って、人間の怠惰を守れ」と一見読めるが、もう少し正確に言うと「LLMに面倒な作業をやらせて、人間は怠惰(=設計・削減・抽象化)に集中しろ」になる。
人間は楽をしていいのではなく、楽をするために必要な知的作業をこそ担え、ということ。
雑に線を引くとこうなる。
| 人間が担うこと | LLMに任せること |
|---|---|
| 境界の定義(責務分割、IF設計) | ボイラープレート生成 |
| 重複の検出と削除 | 同型パターンの展開(CRUD、DTO、テスト等) |
| 「書かない」という判断 | プロトタイピング、叩き台の作成 |
| システム全体の単純化 | 既知パターンの適用 |
判断の目安は3つ。
- 同じ処理が2回出たら立ち止まる
- 3回出たら抽象化する
- LLMのコードは「一度壊す前提」で読む
3つ目が地味に重要。
LLMの出力を「完成品」として扱った瞬間、怠惰は失われる。
出力は叩き台であって最終形ではない、という前提を保ち続けることが、Cantrillの言う「怠惰であり続ける」ということだと思う。
LLMのセルフレビューは使えるのか
「じゃあLLMに自分のコードをレビューさせれば?」という発想は自然に出てくる。
一時期かなり流行ったワークフローで、実際に組んでいる人も多い。
Googleが公開したLinuxカーネル向けAIレビューシステムSashikoは既知バグの53.6%を検出できた。一方でAIコーディングエージェントを本番に載せる設計原則でまとめたCodeRabbitの調査では、AIが生成するコードは人間の1.7倍バグが多いというデータもある。レビューの需要は確実にある。問題はレビューの方向性だ。
バグ検出や型チェックには効く。「正しさ」のチェックはLLMの得意分野だ。
ただし「単純さ」のチェック、つまり肥大化防止の目的だと、そのままでは弱い。
人間なら「同じこと2回書いてるの嫌だな、共通化しないと後で死ぬ」と感じる。その「嫌だ」が抽象化の起点になる。LLMにはこの感覚がない。
なぜ止まるか
コードベースが大きくなるにつれてセルフレビューが効かなくなる。
まず、自分の出力に対するバイアス。
生成した流れをそのまま前提にしてしまう。「なぜこの構造にしたのか」「そもそもこのファイル分割は正しいのか」「この抽象は不要ではないか」のような前提を壊す問いかけが弱い。
バグの有無は見るが、「これ全部いらなくないか」とは言わない。
次に、コンテキスト溢れ。
コードが肥大化すると、全体像・依存関係・設計意図・同名だが微妙に違う処理を一気に保持できなくなる。
レビューが表層的なlint(命名、nullチェック、例外処理の薄さ)に落ちて、一番見てほしい「設計上の重複」「責務崩壊」「層の漏れ」が抜ける。
そして、「削る」評価軸の欠如。
放っておくとレビュー自体が「改善案の追加」になる。パターンを足す、安全策を増やす、汎用化を提案する。
レビューなのにさらに肥大化案を出すという本末転倒が起きる。
レビューではなく「削減監査」にする
普通のコードレビューではなく、目的を「削減」に絞ると精度がかなり上がる。
| 効かない投げ方 | 効く投げ方 |
|---|---|
| このコードレビューして | 重複箇所を列挙しろ |
| 問題点を指摘して | 不要な抽象を指摘しろ |
| 改善案を出して | 統合できるクラスを挙げろ |
| 仕様維持で削除できるコード量を見積もれ | |
| 「追加禁止」でリファクタ案を出せ |
「追加禁止」の制約がかなり重要。これを入れるだけで無駄な抽象化提案がかなり減る。
もう一つ大事なのは、生成役とレビュー役を分けること。 同じモデル・同じ会話で自己レビューさせると、「さっきの自分」を擁護し始める。別スレッドの別インスタンスに差分だけ渡すほうが、前提バイアスを崩しやすい。
レビュー対象の切り方
巨大コードベースを丸ごと投げると死ぬ。レビュー対象を論点単位で切る必要がある。
| 切り方 | 見るもの |
|---|---|
| 差分レビュー | PR diffだけ。一番現実的 |
| 関心事レビュー | 認証、DI、ルーティング、DBアクセスなど、責務単位で切る |
| 依存境界レビュー | ControllerがServiceを飛び越えてRepository触ってないか、DomainがFrameworkに汚染されてないか |
| メトリクスレビュー | 行数上位ファイル、メソッド長上位、import依存数上位、重複率上位を先に機械で出す |
一度に全観点を見させないのもコツ。重複 → 責務崩壊 → 例外処理 → テスト抜け → 削除候補、のように段階を分けたほうが精度が上がるし、コンテキストも溢れにくい。
メトリクスは特にLLMとの相性がいい。行数上位10ファイルやimport依存数上位を静的解析で出して、「このファイルの責務が適切か」「分割すべきか」をLLMに聞く、という使い方ができる。
1から作るときの共通化問題
Cantrillの話を受けて、じゃあ実際にLLMと1からプロジェクトを作るとき、どう共通化を管理するか。
人間なら、実装中に「あれ、これ前にも似たのやったな」と自然に思い出す。
書きながら同時に、設計・違和感検出・共通化判断・削減判断を並列で走らせている。
この「あ、また出た」「このままだとダルい」という感覚が、前の話の「怠惰」そのものだ。
LLMはこれが弱い。似ていてもそのまま続ける。将来の面倒を本気では嫌がらない。
だからといって最初から完璧な抽象化を目指すのも危険。
早すぎる共通化の罠
まだ要件差が見えていない段階で無理に一つの抽象に押し込むと、後で分岐だらけになる。
BaseProcessor ← 何を処理するのか不明
CommonService ← 何が共通なのか不明
AbstractHandler ← 何をハンドルするのか不明
UtilManager ← もう何もわからない
こういう名前しかつけられない時点で、共通化が早すぎる。
逆に、責務名が自然につくなら筋がいい。
TokenResolver ← 認証トークンの解決
RequestSigner ← リクエスト署名
ErrorResponseFormatter ← エラーレスポンス整形
名前が自然に出てこないなら、まだ共通化するタイミングじゃない。
「2回まで許容、3回目で共通化」
有名な経験則だが、LLM運用だとこれを意識的にルール化する必要がある。
人間は無意識に「あ、また出た」と気づく。LLMは気づかない。
だから「共通化候補ログ」を外部に持つ。
## 共通化候補
- auth token 取得処理が Controller A / B に散っている(2回目)
- API エラー整形が 3 箇所でほぼ同じ(→ 共通化検討)
- DB保存前の timestamp 正規化が複数箇所にある(2回目)
- external API retry が service ごとに重複(2回目)
その場で直さなくてもいい。検出だけ残しておけば、3回目のタイミングで判断できる。
たとえばlogin APIとprofile APIを作って、両方でtokenを読んでいるとする。
// LoginController
const token = req.headers.authorization;
// ProfileController
const token = req.headers.authorization;
この段階ではまだ2回。即共通化しなくてもいい。ただしCOMMON_CANDIDATES.mdに「Authorization header取得が複数Controllerに出ている」と書いておく。
3つ目のControllerが出たら止める。ここで初めて共通化を検討する。
良い共通化はこうなる。
class TokenResolver {
resolve(req: Request): string | null {
return req.headers.authorization ?? null;
}
}
責務名が自然につく。何をする部品なのか名前だけで伝わる。
悪い共通化はこうなる。
abstract class BaseAuthenticatedController {
protected getToken(req: Request): string | null {
return req.headers.authorization ?? null;
}
abstract handle(req: Request): Response;
}
LLMは「共通処理があるなら基底クラスに」と考えがちだが、これだと全Controllerが継承を強制される。token取得のためだけに継承ツリーを作るのは過剰で、後から外すコストが高い。
人間なら「継承するほどのことか?」と感じて小さい関数に切る。LLMはこの「過剰さへの嫌悪」がないから、平気で基底クラスを生やす。
共通化するかどうかの判断基準もシンプルでいい。
共通化してよい場合は、3回以上出現していて、差分の理由が説明でき、自然な責務名がつき、分岐を増やしすぎないとき。
逆に見送るべきなのは、2回以下の出現、要件差が固まっていない、名前が雑にしかつかない、基底クラスでしかまとめられない、といったケース。
責務単位で見る
コードの文字面だけでなく、処理カテゴリで重複を追うと精度が上がる。
入力検証、認証認可、例外整形、DTO変換、永続化前処理、外部API呼び出し、ログ出力、リトライ、キャッシュ、レスポンス整形。
この棚を持っておくと、ファイル名ベースでは見えない責務の重複が拾いやすくなる。 「前にもあったっけ」をファイル名ではなく処理カテゴリで記憶する。
定期的な「削減回」
書き進めるターンと、減らすターンを意識的に分ける。
- 機能2つ作ったら1回整理
- 500行増えたら1回整理
- 新しいServiceが2つ増えたら境界見直し
削減回では新機能を足さず、重複・責務漏れ・似たインターフェース・共通化候補ログの消化だけを見る。
これをやらないと「全部似てるけど全部違う」地獄が待っている。
最小運用フロー
ここまでの話を、LLMエージェントで回すための最小セットに落とす。
以前tmuxでClaude CodeとCodexを連携させた実験でManager/Worker/Reviewerの分離を試した。改善編ではコンテキスト管理やメタレイヤ設計まで踏み込んでいる。あの実験をCantrillの「怠惰」の観点で整理し直すと、足りなかったのは「削減を強制する仕組み」だった。
先に持つものは3つだけ
まずこの3ファイルを作る。
| ファイル | 役割 |
|---|---|
| ARCHITECTURE.md | 今ある責務の一覧。レイヤーごとに「やってよいこと」「やってはいけないこと」を書く |
| COMMON_CANDIDATES.md | 共通化候補のログ。出現箇所、回数、今すぐ共通化するかしないか、理由を記録 |
| RULES.md | LLMに毎回見せるルール集。レイヤー違反禁止、Base/Common/Utility禁止、2回まで許容/3回目で共通化検討 |
この3ファイルが、人間の設計勘の外部化になる。
- ARCHITECTURE.md → 責務の記憶(「何がどこにあるか」)
- COMMON_CANDIDATES.md → 違和感の記憶(「これ前にもなかったか」)
- RULES.md → 判断の柵(「やってはいけないこと」)
役割分担
flowchart LR
M[Manager<br/>設計・採否・削減判断] --> W[Worker<br/>機能実装]
W --> R[Reviewer<br/>削減監査]
R --> M
style M fill:#4a9eff,color:#fff
style W fill:#ff6b6b,color:#fff
style R fill:#ffa94d,color:#fff
Workerは1ユースケースずつ実装する。設計判断まで背負わせない。
実装後に「既存責務に寄せられそうな点」「共通化候補になりそうな点」を列挙させるだけ。
Reviewerは削減監査専門。新機能提案禁止、新しい大きな抽象化の提案禁止。
重複・不要構造・寄せ直し候補だけを指摘させる。
Managerが採否判定、ARCHITECTURE.md更新、共通化候補ログ更新を担う。 ここは人間がやってもいいし、LLMに任せる場合でもWorker/Reviewerとは別スレッドにする。マルチエージェントPRレビューの記事で分析したサブエージェントとオーケストレーションの違いがそのまま当てはまる。同一コンテキストで全部やらせるより、役割ごとにスレッドを分けたほうが前提バイアスを崩せる。
1ターンの流れ
- ManagerがWorkerにARCHITECTURE.md + RULES.md + ユースケース要件を渡す
- Workerが実装案を返す。ファイルごとの責務説明、共通化候補、不明点を添えて
- Managerはそのまま採用せず、Reviewerに実装案 + 3ファイルを渡す
- Reviewerが削減監査結果を返す。重複、不要構造、寄せ直し候補、候補ログ更新案
- Managerが最終判定
判定基準もシンプルでいい。
| 判定 | 条件 |
|---|---|
| Accept | 責務逸脱がない、重複が2回以下、新規構造が最小限 |
| Revise | レイヤー配置が少し悪い、既存責務に寄せられる処理がある、不要な薄いラッパーがある |
| Reject | レイヤー違反が多い、責務不明のクラスが増殖している、共通化すべき3回目が放置されている |
プロンプト例
tmuxでCodex(Manager)とClaude Code(Worker/Reviewer)を回す場合、タスクファイルに何を書くかで出力がかなり変わる。 以下は最小限の例。
Workerへの実装指示:
## タスク: POST /api/auth/login の実装
### 要件
- email, password を受け取る
- 認証成功で token を返す、失敗で 401
### 参照
- ARCHITECTURE.md(責務定義)
- RULES.md(制約)
### 制約
- 既存責務に寄せられるなら寄せる
- 新規クラスは必要最小限
- 将来の汎用化を先取りしない
- Base/Common/Abstract/Utility を安易に作らない
### 実装後に出力すること
1. ファイルごとの責務説明(1行ずつ)
2. 既存責務に寄せられそうだが今回は分離した点
3. 共通化候補になりそうな点
ポイントは最後の「実装後に出力すること」。これがないとWorkerは黙って書いて終わる。「似てるのなかったっけ」を強制的に言語化させる。
削減監査の指示:
## タスク: 削減監査
### 対象
Worker が出した login API の実装案
### 参照
- ARCHITECTURE.md
- RULES.md
- COMMON_CANDIDATES.md
### 制約(重要)
- 新機能の提案は禁止
- 新しい抽象化の提案は禁止
- 削除案・統合案のみ出すこと
- 「追加禁止。既存構造の削減・統合のみで提案せよ」
### 観点
1. 同じ責務の重複はあるか
2. 不要な新規クラスはないか
3. 既存レイヤに寄せ直せる処理はないか
4. COMMON_CANDIDATES.md に追加すべき項目はあるか
5. あえて今は残すべき箇所はあるか(理由付き)
### 出力形式
箇所、問題、削除/統合案の3点セットで。
「追加禁止」を2回書いているのは意図的。LLMは一度言っただけだと制約を破りがちなので、制約セクションと観点セクションの両方で念押しする。
この2つのプロンプトの差が、普通のエージェント運用と「怠惰を守る」運用の分かれ目になる。Workerに「共通化候補を列挙しろ」と言い、Reviewerに「追加禁止で削減案だけ出せ」と言う。どちらか片方だけだと、片方が積んだものをもう片方が放置して終わる。
よくある失敗パターン
Baseクラスが生える
前述のTokenResolver vs BaseAuthenticatedControllerの例がまさにこれ。
ルールで明示的に禁止して、本当に必要なら個別許可にするくらいがちょうどいい。
Utility地獄
AuthHelper
CommonUtils
AppHelper
責務名が言えない部品は作るべきではない。 TokenResolver、ApiErrorFormatterのように、何をする部品なのか名前だけで伝わる粒度で切る。
Workerが先回り汎用化する
「将来も使えるように」と気を利かせて、頼んでもいない設定可能性やプラグイン機構を足してくる。 LLMはまだ存在しない将来の共通化を先読みするのが苦手なくせに、「汎用っぽいもの」を足すのは大好き。 Workerには毎回「将来の汎用化を先取りしない。今回のユースケースに必要な最小限に留める」と明示する。
レビューが逆に増やす
「ここにバリデーションを追加すべき」「エラーハンドリングが足りない」「ログ出力を入れたほうがいい」。 削減監査のはずが改善提案になっている。 Reviewerのプロンプトに「新機能提案禁止。削除・統合案のみ」を強く指定する。
身近な例だと、このサイトのCLAUDE.mdを半分以下に削減したときも同じ構造だった。情報を足すのは簡単だが、削って整理するのは人間の判断がいる。LLMに「CLAUDE.mdを改善して」と言うと項目が増えるだけで、273行を121行に落とす意思決定はLLMからは出てこなかった。
Cantrillの結論はこの一文に集約される。
LLMs will certainly prove valuable, but as tools only.
LLMは道具として価値がある。ただし、人間が怠惰であることをやめた瞬間、その道具はシステムを改善するのではなく肥大化させるだけになる。
セルフレビューで品質を担保しようとするのも、フレームワークを先に作って安心しようとするのも、根っこは同じで「人間が削る責任から逃げている」構造だ。
LLMは無限に書ける。だからこそ、人間が「書かない」「削る」「統合する」を意思決定し続ける必要がある。