LLMの制約を味方にする開発術
AIコーディングツールを使っているのに、なぜ期待通りの結果が得られないのか
「AIにコード生成を頼んだら、動くは動くけど、プロジェクトの規約に全然従っていない…」
「複雑な機能を実装させようとすると、途中で迷走してしまう…」
「リファクタリングを頼んだら、余計に複雑になってしまった…」
こんな経験はありませんか?
2025年6月現在、AI Agentic Codingは飛躍的に進化し、単純なタスクなら人間のプログラマーに匹敵する成果を出せるようになりました。しかし、実際のプロジェクトで使ってみると、思うような結果が得られないことも多いですよね。
この記事では、なぜAIコーディングツールが複雑なタスクで失敗するのか、そしてどうすれば効果的に使えるのかについて解説します。Chain-of-Thought promptingによる段階的な思考の誘導、CLAUDE.mdを活用したプロジェクト固有のコンテキスト管理、そして最新の学術研究(KITAB、Lost in the Middle現象など)に基づいた複数制約処理の最適化手法まで、実践的なアプローチを体系的にお伝えします。
さらに、Claude CodeのTodoWrite機能のように、これらのテクニックを自動化するツールの活用方法も紹介します。
すぐに試せる実践テクニック:段階的プロンプトの威力
Before/After:プロンプトの書き方で結果が劇的に変わる
まず、具体例を見てみましょう。
❌ 悪い例:曖昧で一括的な指示
「ユーザー認証機能を実装して」
このような指示では、AIは以下のような問題を起こしがちです:
- プロジェクトの既存の認証パターンを無視する
- セキュリティベストプラクティスが不十分
- エラーハンドリングが雑
- テストコードがない
✅ 良い例:段階的で明確な指示
前提条件:
- プロジェクトのCLAUDE.mdの認証規約(セクション4)を厳守
- TypeScript: strict: true
- 既存のsrc/auth/配下のパターンに準拠
- テストカバレッジ: 90%以上
タスク:新規ユーザー登録APIの実装
ステップ1: 既存の認証実装パターンを調査
- src/auth/配下のファイルを確認
- 検証:命名規則とフォルダ構造の一貫性
ステップ2: インターフェース設計
- DTOの定義(types/auth/register.dto.ts)
- 検証:既存の型定義との整合性
ステップ3: バリデーション実装
- メールアドレス、パスワード強度の検証
- 検証:OWASP推奨基準との適合
ステップ4: ビジネスロジック実装
- 重複チェック、ハッシュ化、DB保存
- 検証:トランザクション処理の適切性
ステップ5: テスト作成
- 正常系、異常系、境界値
- 検証:カバレッジ90%達成
この違いは何でしょうか?良い例では:
- 前提条件を明確化:プロジェクト固有のルールを最初に伝える
- 段階的な指示:複雑なタスクを小さなステップに分解
- 検証ポイント:各ステップで確認すべき事項を明示
今すぐ使える3つのテクニック
この記事では、CLAUDE.mdによってプロジェクト固有のコード例(few-shot prompting)が既に提供されていることを前提としています。そのため、ここではfew-shot promptingではなく、より高度なテクニックに焦点を当てます。
1. Chain-of-Thought(思考の連鎖)プロンプティング
複雑な問題を解く時、人間も段階的に考えますよね。AIも同じです。
タスク:パフォーマンスが悪いReactコンポーネントの最適化
ステップ1: 現状分析
- React DevTools Profilerで再レンダリング箇所を特定
- 検証:不要な再レンダリングの有無
ステップ2: 原因の特定
- props、state、contextの変更頻度を調査
- 検証:依存関係の適切性
ステップ3: 最適化戦略の選択
- React.memo、useMemo、useCallbackの適用判断
- 検証:過度な最適化になっていないか
ステップ4: 実装と効果測定
- 選択した最適化の実装
- 検証:Profilerで改善効果を確認
2. プロジェクトコンテキストの明示的な提供
AIはあなたのプロジェクトの慣習を知りません。明示的に伝える必要があります。
前提:このプロジェクトでは
- エラーは全てCustomErrorクラスを継承
- ログはstructured loggingでJSON形式
- 外部APIコールは必ずretry機構を実装
- 環境変数はzodでバリデーション
3. 検証ステップの組み込み
各段階で「何を確認すべきか」を明確にすることで、AIの暴走を防げます。
ステップ完了時の確認事項:
- [ ] TypeScriptのコンパイルエラーなし
- [ ] ESLintの警告なし
- [ ] 既存のテストが全てパス
- [ ] 新規追加コードにテストあり
なぜこれが効果的なのか:LLMの仕組みと制約
LLMが苦手なこと:複数の制約を同時に処理
最新の研究によると、LLMには以下のような根本的な制約があることが分かっています。
1. コンテキスト処理の限界
Transformer architectureの自己注意機構はO(n²)の計算量を持ちます。これは理論的に回避不可能な制約です。さらに、"Lost in the Middle"現象により、長いコンテキストの中間部分では20〜45%の性能低下が観測されています(Liu et al., 2024)。
つまり、長い指示や大量のコードを一度に渡しても、AIは全てを同等に処理できないのです。
2. 複数制約の同時処理は苦手
KITAB研究(2023)によると:
- 1つの制約から2つの制約で10-15%の性能低下
- GPT-4でも二重制約では充足率が41%に低下
例えば、「TypeScriptで型安全に、かつパフォーマンスを考慮して、かつアクセシビリティも確保して」という複数の要求を同時に満たすのは、AIにとって非常に困難なのです。
段階的アプローチが効く理由
LLMの処理特性を考慮すると、段階的アプローチが効果的である理由が明確になります。
- トークン処理の線形性:LLMは入力を先頭から順次処理するため、構造化された段階的な指示の方が処理しやすい
- 注意機構の制約:Transformerの注意機構は全てのトークンを同等に扱えないため、小さな単位に分割することで各部分により焦点を当てられる
- 中間状態の明示化:各ステップの出力を次の入力として明示することで、暗黙的な推論の必要性を減らせる
これらの特性により、複雑なタスクを段階的に分解することで、LLMはより確実に各ステップを処理し、全体として高品質な結果を生成できるようになります。
小規模プロジェクトでも起きる問題の具体例
「うちは小さなプロジェクトだから関係ない」と思うかもしれません。しかし、以下のような状況は身近ではないでしょうか?
例1:Todoアプリ(約5,000行)でも複雑
制約の組み合わせ:
- TypeScript strict mode
- React Hooksのルール
- アクセシビリティ基準(WCAG 2.1)
- レスポンシブデザイン
AIが苦戦するポイント:
- useEffectの依存配列と型安全性の両立
- 動的なaria-labelと静的型定義の整合性
例2:REST API(約10,000行)の落とし穴
制約の組み合わせ:
- OpenAPI 3.0仕様準拠
- JWT認証とRBAC
- 統一的エラーハンドリング
- テストカバレッジ95%以上
AIが苦戦するポイント:
- OpenAPIスキーマとTypeScript型の同期
- ミドルウェアチェーンでの型の一貫性
Chain-of-Thoughtの実践:手動から自動化へ
手動でのChain-of-Thought構造化
ここまで紹介してきたChain-of-Thoughtプロンプティングは、複雑なタスクを段階的に分解することで、LLMの性能を大幅に向上させます。しかし、毎回手動でタスクを構造化するのは時間がかかります。
LLMを使ったプロンプト生成
実は、Chain-of-Thoughtプロンプトの作成自体をLLMに任せることができます。
例:プロンプト生成の依頼
「ユーザー認証機能を実装する」というタスクを、
Chain-of-Thoughtアプローチで段階的に分解したプロンプトを生成してください。
各ステップには検証項目も含めてください。
LLMは以下のような構造化されたプロンプトを生成します:
ステップ1: 要件分析と既存実装の調査
- 認証方式の決定(JWT/Session/OAuth)
- 検証:セキュリティ要件との適合性
ステップ2: データモデル設計
- ユーザーテーブルの設計
- 検証:正規化とインデックス戦略
[以下続く...]
この方法により、プロンプト設計の負担を軽減できますが、生成されたプロンプトの品質確認は必要です。
専用ツールの活用:Claude CodeのTodoWrite機能
大規模なプロジェクトでChain-of-Thoughtを毎回手動で構造化するのは大変です。Claude Codeは、この問題をTodoWrite機能で解決します(詳細は公式ドキュメントを参照)。
動作の仕組み:
- 自動分解: 複雑なタスクを検出すると、自動的にサブタスクに分解
- 進捗管理: 各タスクの状態(pending/in_progress/completed)を追跡
- 文脈保持: 前のタスクの結果を次のタスクの入力として活用
実例:マイクロサービスの実装
ユーザー: 「商品検索APIをマイクロサービスとして実装して」
Claude Code(自動生成):
TodoList:
1. [pending] APIの要件定義とスキーマ設計
2. [pending] データベーススキーマの設計
3. [pending] 基本的なCRUD実装
4. [pending] 検索ロジックの実装
5. [pending] キャッシュ層の追加
6. [pending] テストとドキュメント作成
各タスクを順次実行し、前のタスクの成果物を活用...
メリット:
- プロンプト設計の負担軽減
- 一貫性のあるタスク分解
- 進捗の可視化
- コンテキストの継続性
このような自動化ツールを活用することで、Chain-of-Thoughtの恩恵を受けながら、開発効率を大幅に向上させることができます。
プロジェクトでの実装方法:CLAUDE.mdの活用
CLAUDE.mdとは
プロジェクトのルートに配置されるCLAUDE.md
は、AI(特にClaude)があなたのプロジェクトを理解するためのガイドラインファイルです。これは自動的に参照されますが、効果的に使うにはコツがあります。
効果的なCLAUDE.mdの構成例
CLAUDE.mdの重要な役割の1つは、プロジェクト固有のコード例を提供することで、AIに対して暗黙的にfew-shot promptingを行うことです。以下の構成例では、具体的なコード例を含めることで、AIが自然にプロジェクトのパターンを学習できるようになっています。
# CLAUDE.md
### プロジェクト概要
このプロジェクトは[簡潔な説明]です。
### 開発の原則
1. 型安全性を最優先
2. 早期リターンでネストを避ける
3. 単一責任の原則を守る
### コーディング規約
### 命名規則
- コンポーネント: PascalCase(例:UserProfile)
- 関数: camelCase(例:getUserById)
- 定数: UPPER_SNAKE_CASE(例:MAX_RETRY_COUNT)
### ファイル構成
src/
├── components/ # UIコンポーネント
├── hooks/ # カスタムフック
├── services/ # ビジネスロジック
├── types/ # 型定義
└── utils/ # ユーティリティ関数
### エラーハンドリング
- 全てのエラーはCustomErrorクラスを継承
- ユーザー向けメッセージとログを分離
- 例:
```typescript
class ValidationError extends CustomError {
constructor(field: string, message: string) {
super(`Validation failed for ${field}`, {
userMessage: message,
code: 'VALIDATION_ERROR'
});
}
}
CLAUDE.mdを最大限活用するコツ
-
具体例を豊富に:抽象的なルールより、実際のコード例が効果的
- これにより、明示的なfew-shot promptingを毎回行う必要がなくなります
- AIは提供された例から暗黙的にパターンを学習します
-
アンチパターンも記載:「こうしないで」という例も重要
- 良い例と悪い例の両方を示すことで、より正確な理解を促進
-
更新を怠らない:プロジェクトの成長に合わせて更新
- 新しいパターンや規約が生まれたら、すぐに反映
-
チームで合意:AIだけでなく、人間の開発者にも有用
- ドキュメントとしての価値も高い
プロンプトでの参照方法
CLAUDE.mdは自動参照されますが、重要な部分は明示的に指示するとより確実です。これは以下の理由によります:
-
コンテキストウィンドウの優先順位:LLMは長いコンテキストの中で、最初と最後の部分により注意を払う傾向があります("Lost in the Middle"現象)。明示的な指示は通常プロンプトの最後に配置されるため、より高い優先度で処理されます。
-
注意機構の限界:CLAUDE.mdが大きくなると、AIが全ての規約を同等に考慮することが困難になります。特定のセクションへの明示的な言及により、その部分への注意が強化されます。
-
タスク固有の文脈強調:あるタスクでは命名規則が重要で、別のタスクではエラーハンドリングが重要かもしれません。明示的な指示により、タスクに最も関連する規約を強調できます。
このタスクはCLAUDE.mdに従います。
特に以下のセクションを重視:
- エラーハンドリング(4.1節)
- 命名規則(3.1節)
このような明示的な参照により、AIはCLAUDE.md全体を考慮しつつ、特に重要な部分により確実に従うようになります。
実践的な事例集:よくある課題と解決策
事例1:エラーハンドリングの統一
問題:AIが生成するエラー処理がプロジェクトの規約に従わない
LLMの特徴による原因:
- 暗黙的な規約の理解困難:LLMは明示されていない慣習やパターンを推測することが苦手
- 一貫性の維持の限界:複数ファイルにまたがる実装で、同じパターンを維持することが困難
使用テクニック:制約の型システムへの埋め込み(Type-Constrained Generation)
解決策:型システムを使った制約の強制
// CLAUDE.mdに記載する型定義
type AppError =
| { type: 'NETWORK'; retryable: true; userMessage: string }
| { type: 'VALIDATION'; retryable: false; fields: Record<string, string> }
| { type: 'SYSTEM'; retryable: false; internalMessage: string };
// プロンプト:「AppError型に従ってエラーを処理してください」
// → AIは型に従わざるを得ない
このテクニックの重要な効果は、LLMが考慮すべき制約の数を劇的に削減することです。型定義により、以下の制約が自動的に保証されます:
- エラーの種類は3つのみ(それ以外は型エラー)
- 各エラータイプに必要なプロパティが明確
- retryableの値は型によって自動決定
これにより、LLMが解くべき問題が「どんなエラー処理を書くか」という創造的なタスクから「型に従って実装する」という確定的なタスクに変換されます。
結果:型チェッカーが自動的に規約違反を検出、一貫性が保証される
事例2:React状態管理の最適化
問題:最適な状態管理方法が不明確
LLMの特徴による原因:
- トレードオフの同時評価が苦手:複数の選択肢の長所短所を同時に比較することが困難
- コンテキスト依存の判断の限界:プロジェクト固有の制約を考慮した最適解の選択が難しい
使用テクニック:Tree-of-Thoughts(並行探索と比較評価)
解決策:複数のアプローチを並行検討
プロンプト:
「以下の3つのアプローチを比較検討してください:
案A: useContextでグローバル状態管理
- 実装の概要
- メリット/デメリット
- パフォーマンス影響
案B: Zustandによる状態管理
- 実装の概要
- メリット/デメリット
- バンドルサイズ影響
案C: ローカル状態 + props drilling
- 実装の概要
- メリット/デメリット
- 保守性
最終判断:[具体的な判断基準に基づいて選択]」
このテクニックが効果的な理由は、LLMに複数案を同時に処理させるのではなく、各案を独立して評価させることです。単一のプロンプトで「最適な状態管理方法を選んでください」と指示すると、LLMは一般的な知識に基づいた画一的な回答をしがちで、プロジェクト固有の制約を十分に考慮できません。
また、複数の選択肢を提示しても、それをステップに分解せずに「3つの方法を比較して選んでください」とだけ指示すると、LLMは表面的な比較に終始し、各案の深い分析を省略してしまいます。結果として、特定の案に偏った評価や、重要な制約の見落としが発生しやすくなります。
これに対して、選択肢を明示的に並べ、各案を独立したセクションで評価させることで、LLMは各案の特徴を混同することなく明確に把握できます。人間が意思決定する際と同様に、選択肢を並べて比較することで、各案の具体的なトレードオフが可視化されます。また、最終判断を独立したステップとすることで、トレードオフの評価に集中できるようになります。
結果:トレードオフを明確にした上での最適解選択、判断根拠の透明性
事例3:API認証システムの実装
問題:セキュリティ要件の見落とし
LLMの特徴による原因:
- 長期的な影響の予測困難:実装の決定が将来どのような影響を及ぼすかの予測が苦手
- 非機能要件の考慮不足:コードの動作以外の要件(セキュリティ、拡張性等)への配慮が弱い
使用テクニック:アーキテクチャ決定記録(ADR)駆動開発
解決策:ADRフォーマットでの要件整理
プロンプト:
「以下のADRに基づいて実装してください:
# ADR-001: 認証方式の選定
## Status
Accepted
## Context
- 要件:マイクロサービス間通信のセキュア化
- 制約:既存システムとの互換性必要
## Decision
JWT + mTLSのハイブリッド方式を採用
## Consequences
- Good: 高いセキュリティレベル
- Bad: 実装の複雑性増加
実装時は、このADRのConsequencesを考慮したエラーハンドリングを含めてください」
このテクニックが効果的な理由は、LLMに実装の背景と制約を構造化して提供することです。ADRフォーマットは以下の点でLLMの処理を助けます:
- Context(背景):なぜこの実装が必要かを明確にし、LLMが適切な判断基準を持てる
- Decision(決定事項):具体的な実装方針が明示され、曖昧さが排除される
- Consequences(影響):良い面と悪い面の両方を示すことで、LLMがトレードオフを意識した実装ができる
特に「Consequences」セクションは、LLMが見落としがちな非機能要件(将来の拡張性、運用の複雑性など)を明示的に扱うよう促します。
結果:設計意図が明確で、将来の変更にも対応しやすい実装
事例4:レガシーコードのリファクタリング
問題:大規模なリファクタリングでAIが迷走
LLMの特徴による原因:
- 大規模変更での文脈喪失:変更が大きくなるほど、元の意図や制約を忘れやすい
- 依存関係の追跡困難:複雑な依存関係を持つコードの全体像を把握することが苦手
使用テクニック:Strangler Figパターン + 段階的変換
解決策:小さな独立した変更の積み重ね
プロンプト:
「Strangler Figパターンで段階的にリファクタリング:
Phase 1: 新しいインターフェースの追加(既存コード変更なし)
- 新インターフェース定義のみ
- 既存実装はそのまま
Phase 2: アダプター実装
- 既存実装を新インターフェースでラップ
- テストは既存のものを流用
Phase 3: 段階的な内部実装の置き換え
- 1メソッドずつ新実装に置き換え
- 各置き換え後にテスト実行」
Strangler Figパターンは、レガシーシステムを段階的に置き換えるためのパターンです。このアプローチがLLMの問題を解決する理由:
- 文脈の局所化:各Phaseで扱う範囲を限定することで、LLMが追跡すべき依存関係を最小化
- 独立性の確保:新旧の実装が並存できるため、LLMは既存コードを壊すリスクなく新実装に集中できる
- 段階的検証:各Phaseごとにテストを実行することで、問題を早期発見し、大規模な手戻りを防ぐ
特に重要なのは、既存コードに触れずに新しいインターフェースを追加するPhase 1です。これにより、LLMは「既存の動作を維持しながら変更する」という複雑な制約から解放され、新しい設計に集中できます。
結果:リスクを最小化しながら確実なリファクタリング
事例5:パフォーマンス最適化
問題:推測に基づく最適化で効果が出ない
LLMの特徴による原因:
- 定量的判断の弱さ:LLMは主に言語パターンの学習に基づいており、数値の大小関係や比率の意味を本質的に理解していません。例えば「340ms」と「280ms」の差が実際のユーザー体験にどう影響するかの判断が困難です
- 一般論への偏り:プロジェクト固有の状況より、一般的な最適化手法を提案しがち
使用テクニック:データ駆動プロンプティング(実測値の活用)
解決策:実測データをプロンプトに含める
プロンプト:
「以下のプロファイリング結果に基づいて最適化:
Chrome DevTools Performance結果:
- Total Blocking Time: 850ms
- 主な原因:
1. ProductList.render: 340ms (40%)
2. calculateTotalPrice: 280ms (33%)
3. formatCurrency: 230ms (27%)
最も効果的な最適化から順に実装してください。
各最適化後の予想改善率も示してください。」
データ駆動プロンプティングがこの問題を解決する理由:
- 優先順位の明確化:パーセンテージ表示(40%, 33%, 27%)により、LLMは言語的に「最も大きな割合」を占める要素を識別できる
- 具体的な目標設定:「Total Blocking Time: 850ms」という全体値を示すことで、各最適化の影響度を相対的に評価可能
- 文脈の具体化:抽象的な「パフォーマンス改善」ではなく、人間が計測した具体的なメソッド名と実行時間を提供することで、LLMはどの部分を最適化すべきか明確に理解
特に重要なのは、数値を相対的な割合として示すことです。LLMは「340msは大きい」という絶対的判断は苦手ですが、「全体の40%を占める」という相対的な表現は理解しやすく、適切な優先順位付けができます。
結果:実測に基づく効果的な最適化、ROIの高い改善
まとめ:LLMの制約を理解した効果的な活用へ
本記事では、AI Agentic Codingツールが複雑なタスクで失敗する理由と、その解決策を紹介しました。
LLMの根本的な制約:
- コンテキスト処理の計算量制限(O(n²))と"Lost in the Middle"現象
- 複数制約の同時処理における性能低下(KITAB研究)
- 暗黙的な規約の理解と一貫性維持の困難さ
効果的な活用のための実践的アプローチ:
- Chain-of-Thought prompting:複雑なタスクを段階的に分解し、LLMの処理可能な単位に
- 制約の型システムへの埋め込み:考慮すべき制約の数を削減し、確定的なタスクに変換
- Tree-of-Thoughts:複数案の独立評価によるトレードオフの明確化
- CLAUDE.mdによるコンテキスト管理:プロジェクト固有の知識を効率的に提供
- 自動化ツールの活用:TodoWrite機能などによる負担軽減
これらのテクニックは、LLMを「万能のコード生成器」として扱うのではなく、その特性を理解した上で「制約の中で最大の価値を引き出す」アプローチです。
技術は急速に進化していますが、基本原理を理解していれば、新しいツールやモデルが登場しても適応できるでしょう。
参考情報
学術的根拠
-
Transformer Architecture: "On The Computational Complexity of Self-Attention" (2022) - 計算量の理論的限界
-
Lost in the Middle: Liu et al. (2024) - 長文コンテキストでの性能低下
-
GPT-3.5-Turboの結果:
- 56.1%のクローズドブック性能:支援文書なしでの質問応答精度
- 中間位置での性能:関連情報が中間に配置されると、この56.1%を下回る性能に低下
- 20%以上の性能低下:最悪のケースでは、関連情報が端にある場合と比較して20パーセントポイント以上の性能低下
-
Key-Value検索タスク:
- GPT-3.5-Turbo (16K):最悪の配置で45.6%の精度(クエリ認識コンテキスト化なし)
- 同モデルがクエリ認識コンテキスト化ありでは**ほぼ100%**の精度を達成
-
-
KITAB: Patel et al. (2023) - 複数制約処理の困難さ
-
Chain-of-Thought: "What Makes Chain-of-Thought Prompting Effective?" (2023) - 段階的思考の有効性
Discussion