レガシー移行を「読み取り範囲を絞ったAIエージェント編成」で回す設計
概要
10年もののレガシーモノリス(旧世代の PHP / Laravel)を、複数のマイクロサービス(Laravel 12)+ Vue 3 フロントエンドへ移行する案件を担当している。移行対象は数百エンドポイント、共有モデルは600超。人間ひとりで「どこまで移ったか」「旧ロジックが正しく写っているか」を追い切るのは現実的ではない。
そこで、Claude Code のサブエージェント機能を使い、役割ごとに読み取り範囲を絞った複数のAIスタッフを編成して移行を回す仕組みを組んだ。本稿はその設計——特に「情報隔離」と「ファイル経由の受け渡し」——を再現可能な粒度でまとめたものだ。
環境
- モノレポ(親リポジトリ)+ Git サブモジュール構成
- 認証 / メイン業務 / ワークフロー / 外部連携(各 Laravel 12)
- 共有モデルパッケージ / 共有設定パッケージ
- フロントエンド(Vue 3 + Vite)は独立リポジトリ
- ローカルは Docker Compose、DB は MySQL 8 + Memcached
- 移行元の旧モノリスは参照専用(書き換え禁止)
- Claude Code のネイティブサブエージェント(
.claude/agents/)
発生した問題
AIに移行を丸投げすると、次の失敗が起きる。
- ハルシネーション:旧実装を読まずにレスポンス構造を捏造する
- 文脈の汚染:フロント・旧実装・新実装の都合を1コンテキストに載せると判断がブレる
- 見切り発車:仕様未確定のまま実装を始め、やり直しになる
- 依存順序の破壊:共有パッケージより先に個別サービスをいじって整合性を壊す
これらは「AIが賢くない」から起きるのではなく、役割と情報範囲を設計していないから起きる。人間のチームでも、全員に全情報を渡せば同じ事故が起きる。
原因
根本原因は、1つのコンテキストに責務と情報を詰め込みすぎていること。
- 旧実装の抽出役に新実装を見せると、新実装に引きずられる
- 契約設計役にコードを触らせると、事実照合ではなく「良かれと思った仕様変更」を始める
- 実装役に計画抜きで着手させると、既存パターンとの矛盾を裏取りせず突っ走る
つまり、「何を読めるか」「何を出せるか」を制約することが、そのままハルシネーション抑止になる。
対応方法
1. 役割ごとにエージェントを分け、読み取り範囲を物理的に絞る
各エージェントの定義で、使えるツールと読み取り許可範囲を宣言する。抽出役は編集ツールを持たない。契約設計役はコードを一切読まない。
| 役割 | 読める範囲 | 書けるもの |
|---|---|---|
| FE分析役 | フロントのコードだけ | FE契約ファイル |
| レガシー分析役 | 旧モノリスのコードだけ | 旧ロジック抽出ファイル |
| 契約設計役 | 抽出済みファイルだけ(コード禁止) | 確定契約ファイル |
| 配置判定役 | 確定契約 + 各サービスのルート定義 | 配置判定ファイル |
| 実装役 | 担当サービス + 共有パッケージ | 実装 + 実装ログ |
| 検証役 | 確定契約 + 実装コード | 差分検出レポート |
ポイントは、分析役はお互いのソースを見ないこと。両者の出力を突き合わせるのは、コードを読まない契約設計役の仕事にする。こうすると「片方に引きずられた解釈」が構造的に生まれない。
2. 受け渡しは会話ではなくファイルで行う
エージェント間の情報は、会話ではなく docs/ 配下のファイルで受け渡す。次工程は前工程の「発言」ではなく「ファイル」を読む。
FE分析 → docs/triage-inputs/{domain}/fe/_index.md
レガシー分析 → docs/triage-inputs/{domain}/storage-plus/_index.md
│ この2ファイルだけを入力に
契約設計 → docs/triage-inputs/{domain}/contract.md
│
実装 → docs/pipelines/{domain}/30-impl-*.md
│
検証 → docs/pipelines/{domain}/40-review-*.md
ファイルに落とせば、次工程はそのファイルだけで文脈を再現でき、サブエージェントを使い捨てにできる。未確認情報には ⚠️UNVERIFIED: を付け、事実と推測を機械的に区別させる。
3. 実装は「計画 → 承認 → 実装」の二段階でゲートする
コード変更は、必ず読み取り専用の planner が計画(20-plan.md)を書き、人間が承認してから executor が実装する。
planner(読み取り専用) → 20-plan.md を status: draft で出力 → 人間が中身を確認して承認 → status: approved に更新 → executor が実装(approved でなければ停止)
planner の仕事は、既存コードを深掘りして仕様と実装の矛盾・影響範囲を裏取りすること。疑義は ❓要ユーザー判断: として列挙し、勝手に埋めない。
4. 出力先をフックで機械的に強制する
docs/ 直下や未定義ディレクトリへの書き込みを PreToolUse フックでブロックする(サブエージェント内も対象)。規約は文章だけだとドリフトするので、機械で弾く。
実装例
レガシー分析役の定義の骨格。
--- name: legacy-analyst description: 旧モノリスのロジック抽出専任。旧コードのみを読む tools: Read, Grep, Glob, Write model: opus --- ## 絶対ルール - 旧モノリスのコードだけを読む。フロントや新サービスは一切見ない - ロジックを簡略化しない。条件分岐・エッジケースを全て記録する - 「多分こうだろう」で補完しない。事実だけを出力する ## 読み取り許可範囲 - 旧モノリスのリポジトリのみ
実装役にはゲートを明記する。
## 実装開始の前提条件(ゲート) - 承認済み計画(20-plan.md, status: approved)が必須 - 無い / approved でなければ実装せず停止する - 軽微な修正でも例外にしない
確認方法
- トリアージ入力が「完備」になるまで移行を始めない(manifest でゲート)
- 実装後は契約検証役(確定仕様 vs 実装)と結合検証役(フロントの実呼び出し vs 実装)の2系統で差分を出す
- 進捗はスケジュールファイルのチェックボックスを唯一の正本にする
注意点
- 1回で処理するEPは5〜10件に絞る(コンテキスト保全)
- 複数ドメインを1コンテキストで連続処理しない
- サブエージェントは入れ子起動不可。委譲はディスパッチ元を経由する
- 移行元は書き換え禁止。「無い」と結論する前に必ず
fetch
まとめ
AIエージェントを実務で回す肝は、賢いモデルを選ぶことではなく、役割・読み取り範囲・受け渡し方法・着手ゲートを設計することだった。
- 情報を絞ることがハルシネーション抑止になる
- 受け渡しは会話でなくファイルで(文脈を再現可能にする)
- コード変更は必ず計画→承認→実装でゲートする
- 規約はフックで機械的に強制する
人間のチーム運営で当たり前にやっている「担当分け」「引き継ぎ書」「レビューゲート」を、そのままエージェント編成に写しただけとも言える。組織設計の知見が、そのままAIオーケストレーションに効く——という体験だった。
