AIエージェントに機能追加や改修を任せていると、ある日突然コードベースが窒息し始める現象に遭遇します。
新しいユースケースを頼んだはずなのに、既存の巨大なサービスクラスやメインコントローラーの末尾に、なぜか数百行のプライベートメソッドと状態フラグが生えている。ファイルを新しく作って責務を分ける気配は一切なく、すべての道がひとつの「神クラス」へと収束していく光景です。
画面の前で「どうして綺麗にファイルを分けてくれないんだ」と頭を抱える前に、この行動の力学を正確に捉える必要があります。
これはAIの知能が低いから起きる事故ではありません。「倉庫に荷物をしまってくれ」と頼まれた新人が、一番手前の入口付近の床にすべてのダンボールを積み上げて「完了しました!」と報告してくるあの現象と、まったく同じ構造です。
- 現象:なぜ新人は「倉庫の入口」に荷物を積み上げるのか?
新人に「この荷物を倉庫に格納しておいて」と指示を出したときのことを想像してみてください。
監督者(人間)の頭の中には、「工具は奥のA棚、塗料は危険物用のBラック、書類は事務所側のキャビネット」という分類の青写真があります。しかし、具体的な荷解きのルールを与えずに新人に任せると、高確率で倉庫のドアを開けてすぐの広いスペースにドサッと荷物を置いて作業を終了します。
AIが既存の巨大クラス(神クラス)にコードを追記し続けるのは、まさにこれと同じです。AIの立場から見ると、この行動には極めて強烈な「合理性(局所最適)」が存在します。
- 「指示通り倉庫の中に入れました」
機能要件は満たしており、コンパイルも通り、テストもグリーンになる。 - 「奥の棚を動かして壊すリスクを避けました」
既存のクラス定義、import、DIの設定、周辺の配線を触らずに済むため、変更に伴う構文エラーや型不整合を起こすリスクが最小で済む。 - 「ここなら次に取り出すときもすぐ見つかります」
すでにコンテキスト内に展開されている「見慣れた神クラス」に生やすのが、一番トークン消費が少なく、スコープ解決も容易である。
結果として何が起きるか。入口の床面(神クラスのファイル行数と責務)だけが天井まで圧迫され、人が通れなくなり、倉庫全体が機能不全に陥ります。
さらに厄介なのは、AIはこの荷物の山を見て「ここは荷物を置く場所なんだ(前例がある)」と判断し、次のタスクでも迷わず入口に荷物を積み増すという悪循環です。
- 失敗の本質:「綺麗にして」と一撃で頼むから破綻する
この肥大化した神クラスを前にして、多くの開発者がやってしまいがちな悪手がこれです。
「この神クラスを責務ごとに綺麗に分割してリファクタリングして」
一撃で奇跡を起こそうとするこのプロンプトは、99%失敗します。
AIに大掛かりな解体を一発で丸投げすると、高確率で暗黙の依存関係を破壊して実行時エラーを多発させるか、あるいは「クラス名やファイル名だけを細かく分けたが、中身は相互参照でガチガチに結合したままの偽装分割」を生み出します。
プロの現場監督は、新人に「この散らかった倉庫を一晩でピカピカにしておけ」とは言いません。プロの仕事が一見シンプルに見えるのは、魔法の呪文を使っているからではなく、「絶対に事故が起きない段取り」を敷いているからです。
必要なのは、以下の泥臭く当たり前の4ステップだけです。
- 現状の調査(コードベースを調べさせる)
- 境界違反の棚卸し(神クラスが抱えている責務と依存のリストを作らせる)
- 計画の固定(リストを元にIssue群を起票させる)
- 順次実装(1Issueずつ、物理的な逃げ道を塞ぎながら解体させる)
- 核心:AIの逃げ道を塞ぐ「減算ラチェット」の仕組み
この段取りを自律的に回す上で、最も重要な安全装置が「減算ラチェット(許可リストの単調減少)」です。
AIに「綺麗にしろ」と定性的なお願いをしても意味がありません。必要なのは、「許可リストが減らないとCIが落ちる」「リスト外の変更が入ると即座に死ぬ」という客観的な物理法則です。
[現状の神クラス]
│ メンバー/責務のベースライン抽出
▼
[許可リスト(Allowlist)]
├── ① 許可リスト外の追加 ───▶ CIで即座にエラー(入口への新規搬入を禁止)
├── ② 分解後のリスト未減 ───▶ CIで即座にエラー(仕事したフリ・名前のロンダリングを禁止)
└── ③ テスト通過 ───────────▶ 振る舞いの破壊を禁止
│
▼ 単調減少(Monotonic Decrease)
[許可リスト最小化 / 消滅]
3つの不変条件
このラチェットは、CIまたはローカルの検証スクリプトで以下の3つのルールを強制します。
- 許可リスト外の追加は即エラー(入口への新規搬入を禁止)
神クラスに存在するメソッド名やプロパティ一覧を、あらかじめ「許可リスト(ベースライン)」として固定します。リストにない新しいメソッドやフィールドが生えた瞬間、リントやCIでビルドを落とします。AIは「既存ファイルに足す」という逃げ道を物理的に塞がれ、新しいモジュールを作らざるを得なくなります。 - 分解タスクでリストが減っていなければ即エラー(仕事したフリを禁止)
「今回は〇〇の責務を別クラスへ切り出す」というIssueを実行した際、PRの差分で許可リストから該当項目が削除されていなければCIを落とします。「内部を少し弄っただけで、公開インターフェースはそのまま残した」というAI特有の空振りを機械的に検知します。 - 全テストのパス(振る舞いの破壊を禁止)
当然ながら、既存の振る舞いを固定するテストが通っていなければマージを許しません。
この仕組みを敷くことで、AIに対する要求は「綺麗に設計して」という曖昧な主観から、「テストを通したまま、許可リストの要素数を N から N-1 に減らせ」という厳格な制約付き最適化問題へと変貌します。
数学的に要素が単調減少(Monotonic Decrease)していくため、作業が堂々巡りになったり、無限ループに陥ったりすることなく、確実に神クラスの解体(最小化)へ収束していきます。
-
自律解体プロトコルの実装手順
実際にAIエージェントへ指示を出し、解体を完了させるまでの具体的な流れは以下の通りです。このプロセス自体も、AIに計画Issueを立てさせ、自律的に回させます。
Step 1: 境界違反の棚卸しと許可リストの作成
まず、AIに「コードを直すな」と明示し、神クラスの調査とベースラインの抽出だけを命じます。
【指示】
TargetClass.ts を分析し、現在このクラスが抱えている責務とメンバーを棚卸ししてください。
まだコードは修正しないでください。 -
TargetClass が依存している外部リソース・状態のリストアップ
-
本来別モジュールにあるべき境界違反メソッドの一覧抽出
-
現在の全パブリックメソッドを allowlist.json として出力
出力される allowlist.json は、そのままラチェットの初期ベースラインになります。
{
"target": "TargetClass",
"allowed_methods": [
"calculateTax",
"exportToPdf",
"sendNotificationEmail",
"formatInvoiceSummary",
"fetchBillingAddress"
]
}
Step 2: 解体計画のIssue群をAI自身に起票させる
抽出したリストを元に、どの順番で荷物を棚へ移すかの計画(Issue)をAIに切らせます。
【指示】
allowlist.json の項目を責務ごとにグループ化し、
段階的に解体するための GitHub Issue 計画を作成してください。
各Issueには「今回の対象メソッド」「切り出し先の新設クラス」「更新後の allowlist 差分」を明記すること。
AIは以下のような、極めて作業単位の小さいIssue群を自律的に生成します。
- Issue #1: 通知処理(sendNotificationEmail)を NotificationService へ委譲し、allowlistから削除
- Issue #2: PDF生成(exportToPdf)を InvoicePdfExporter へ抽出し、allowlistから削除
- Issue #3: 課税計算(calculateTax)を純粋関数へ切り出し、allowlistから削除
Step 3: ラチェット検査スクリプトを仕込む
CIやpre-commitで、コミットごとにリストの減少をチェックする簡易チェッカーを動かします(以下は概念例)。
#!/bin/bash
現在のクラスからメソッド一覧を再抽出し、allowlist.json と比較
CURRENT_METHODS=$(extract_methods src/TargetClass.ts)
ALLOWLIST=$(cat allowlist.json | jq -r '.allowed_methods[]')
1. 許可リストにない新しいメソッドが存在しないか?
for m in $CURRENT_METHODS; do
if ! echo "$ALLOWLIST" | grep -q "^$m$"; then
echo "Error: 許可リストにないメソッド '$m' が TargetClass に追加されています。"
exit 1
fi
done
2. 分解タスクPRにおいて、allowlist が前回コミットより減っているか?
(CI環境で BASE ブランチとの diff 行数を検証)
Step 4: 1Issueずつ自律ループを回す
あとはAIに1つのIssueだけを渡し、「テストを作成 ➔ 切り出し ➔ 本体の委譲 ➔ allowlist.json から対象行を削除 ➔ テストとラチェットCIの通過確認」を順に実行させます。
1つのIssueが終わるたびに神クラスの行数は物理的に削られ、許可リストの行数も減り、神クラスはただの「薄いファサード(配線役)」へと縮退していきます。
5. 解体時の落とし穴:カバレッジブースターを呼び戻さないために
この解体プロセスで絶対に避けて通れないのが、「テスト先行(振る舞いの固定)」です。
しかし、ここで注意しなければならない罠があります。神クラスのように副作用と計算ロジックが泥臭く結合した状態のコードに対して、そのままAIに「テストを書け」と命じてはいけません。
境界が溶けたままのコードにテストを無理やり書かせると、AIは以前書いた記事の通り、ただ実行行を踏むためだけの空虚なダミーテスト(_coverage_boost.test)を乱造する数値ハックに走ります。
参考: AIに「カバレッジを上げろ」と命じると、なぜ coverage_boost.test が錬成されるのか?――境界分離としてのテスタビリティ強制論
解体時のテストは、神クラス全体を一撃で網羅しようとするのではなく、「今回新設する別クラス(棚)に移管するロジック」から順に、純粋な入力と出力の境界を型で縛ってテストを書かせるのが鉄則です。
副作用から切り離された新しいモジュールであれば、モックを何重にも重ねる必要も、空虚なアサーションを捏造する必要もなく、AIは極めて自然で堅牢なテストを自律生成できます。
プロが現場で行っている仕事は、外から見ていると驚くほどシンプルに見えます。「コードベースを調べて、境界違反をリストにし、Issueを立てて、順番に実装しただけ」です。そこには奇をてらった黒魔術も、門外不出のプロンプトエンジニアリングも存在しません。
AIという新人は、放っておけばどこまでも楽な方へ流れ、手元の入口にすべての荷物を積み上げます。
必要なのは、言葉で「綺麗にしろ」と諭すことではなく、入口に荷物を置いたら即座にベルが鳴り、1箱片付けるごとに検問のゲートが狭まっていく「減算ラチェット」という現場の段取りです。逃げ道を物理的に塞ぐ仕組みさえ組めば、AIは自律的に、驚くほど正確な仕事をし始めます。