Claude Codeで新機能を追加したとする。PRはテストをパスし、コードは読みやすく、レビュアーは承認した。3週間後、あるエンジニアがReactコンポーネントからデータベースクライアントを直接呼び出していることに気づく。チームが数ヶ月かけて整備したサービス層を、エージェントは静かに飛び越えていた。
これがアーキテクチャの乖離(ドリフト)だ。コードはローカルでは正しく動作するが、チームが定めた構造的な制約に違反している。人間のエンジニアでも同様の問題は起きるが、AIコーディングエージェントはそれをより速く、より組織的に広げる。エージェントは目の前の問題——このコンポーネントに正しいデータを取得させること——を解決しながら、そのために越えた境界を追跡しない。
CLAUDE.mdにルールを追加しても、これは確実には防げない。なぜそうなのか、そして実際に機能するのは何かを整理する。
AIが生成するコードにおけるアーキテクチャの乖離
AIが生成するコードのアーキテクチャ的な問題は、一見して明らかでないことが多い。構文エラーは出ない。テストも通る。PRの説明書き通りに動く。しかし、テストがカバーしていない構造的な制約に違反している。
典型的なパターンを示す。エージェントがUIコンポーネントに新機能を追加するとき、最も単純な経路はデータベースクライアントを直接インポートすることだ。
// AIエージェントが追加したコード
import { db } from '@/lib/db';
export function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
db.user.findUnique({ where: { id: userId } }).then(setUser);
}, [userId]);
// ...
}
コードは動く。しかし、このチームのアーキテクチャはUIコンポーネントがサービス関数を呼び出し、サービスがリポジトリを呼び出し、リポジトリがデータベースを呼び出す構造を定めている。エージェントは2層を飛ばした。
複数のエージェントセッションが、それぞれ局所的な問題を解決しながら同じ近道を選ぶと、このパターンはコードベース全体に広がる。個々の変更はそれぞれ説明がつく。積み重なるとアーキテクチャが壊れる。
CLAUDE.mdがあっても乖離が起きる理由
次のようなCLAUDE.mdのルールはよく見かける。
アーキテクチャルール:
- UIコンポーネントから @/lib/db を直接インポートしてはならない
- データアクセスは @/services/ のサービス関数を経由すること
エージェントはこのルールを読む。場合によっては明示的に了解さえする。それでも違反するインポートを生成することがある。
これはモデルのバグではない。大規模言語モデルにおける指示への従い方の特性だ。CLAUDE.mdの指示はモデルがコンテキストとして受け取るテキストであり、モデルの出力の確率分布に影響を与えるが、出力を制約はしない。
信頼性を損なう要因がいくつかある。
コンテキストの競合。 コンテキストウィンドウ内のすべてのトークン——既存のコード、ユーザーのリクエスト、会話の履歴、CLAUDE.mdの全文——がモデルの注意を奪い合う。400行のCLAUDE.mdの200行目に書かれたルールは、目の前のタスクを解決するという直接的な圧力と同等には扱われない。
局所的な問題フレーミング。 エージェントの直近の問題は「このコンポーネントにユーザーデータを取得させること」だ。アーキテクチャルールはグローバルな制約だ。エージェントが局所的な問題を推論するとき、グローバルな制約は後退しやすい。
確率的な遵守。 エージェントがルールへの従い方を突然変えるような閾値は存在しない。遵守は徐々に、予測できない形で低下する。どのリクエストで違反が起きるかを事前に知る方法はない。
Claude Codeのユーザーはこの問題をプロジェクト自体のIssueトラッカーに記録している(issue #7777)。数回のプロンプトの後、CLAUDE.mdが存在してロードされていても、エージェントは指示を無視し始めると報告されている。正確な劣化メカニズムはモデルプロバイダーから公式には説明されていないが、複数の独立した報告で同じパターンが確認されている。
本質的な問題は指示の数ではない。指示ベースのアプローチはそもそも確率的な性質を持つという点にある。
「指示」と「チェック」の根本的な違い
指示はモデルへの入力として与えられるテキストだ。チェックはモデルの出力に対して実行されるスキャンだ。
指示は助言する。チェックは強制する。
マージ前に禁止インポートを検出するチェックは、エージェントの意図もCLAUDE.mdの内容も問わない。禁止されたインポートパスが存在するかどうかだけを見る。結果は決定論的だ——同じ入力に対して常に同じ結果が得られる。
2つのアプローチの違いを整理する。
CLAUDE.mdのルールがある場合:エージェントがコードを生成し、ルールが出力に影響するかもしれないし、しないかもしれない。違反が発生した場合、レビュアーがそれを見つけなければならない。
CIチェックがある場合:エージェントがコードを生成し、PRに対してチェックが実行され、エージェントの意図に関わらず違反はマージ前に検出される。
チェックはエージェントを賢くしない。エージェントが失敗したときに、その失敗をコードベースに入る前に可視化する。
マージ前のアーキテクチャチェックの実装
上記の例に対する禁止インポートチェックは、UIレイヤー配下のファイルに対してコマンドを実行するだけでも実現できる。
# src/components/ 配下に @/lib/db からのインポートがあればPRを失敗させる
grep -r "from '@/lib/db'" src/components/ && exit 1
これをCIでPRごとに実行する。エージェント——あるいは人間のエンジニア——がコンポーネントにデータベースの直接インポートを追加すれば、PRは失敗する。レビュアーが違反を見つけることに依存しない。
インポートチェーンや推移的な依存関係など、より複雑な制約にはモジュールグラフを理解するツールが適している。しかし原則は同じだ——チェックは決定論的に実行され、明確な合否を返す。
Tree-sitterクエリを使えば、言語の構文構造を理解した形でASTパターンを照合できる。
; コンポーネントファイルからのdbクライアントインポートを検出
(import_statement
source: (string) @import_path
(#match? @import_path "lib/db"))
このクエリはASTノードに一致するかしないかだけを判定する。文字列のフォーマットの違いに惑わされない。判断の余地はない。
重要なのは、このチェックがすべてのPRで実行されることだ。AIが生成するPRは人間が生成するより速く到着し、同じ違反を1セッションで複数ファイルに同時に広げることがある。レビュアーが注意深く見たPRだけをゲートとして使うのでは不十分だ。
CLAUDE.mdが本来得意とすること
CLAUDE.mdは例外なく遵守する必要がないものに向いている——命名規則、コードスタイル、ライブラリの選択指針、プロジェクトの文脈、テストの実行方法、PRの説明に書くべきこと。これらは多少の逸脱が許容できる、またはレビュアーの判断が最終ゲートとして適切な場合だ。
問題は、CLAUDE.mdを例外なく成立しなければならないアーキテクチャ制約を強制するメカニズムとして扱うことだ。CLAUDE.mdはそのために設計されていないし、その役割では確実には機能しない。
CLAUDE.mdはエージェントのデフォルト動作を形成する。チェックはデフォルトが失敗したときに違反を捕捉する——そしてデフォルトは必ず失敗する場面がある。
アーキテクチャ強制に必要な最小限のポイント
ほとんどのコードベースでは、AIが生成する典型的な問題に対処するために、複雑なアーキテクチャ強制は必要ない。2種類のルールが大半の重要なケースをカバーする。
禁止インポートルール。 どのモジュールがどの他のモジュールからインポートしてはならないか。UIコンポーネントはデータベースクライアントをインポートしてはならない。コントローラーはインフラストラクチャアダプターをインポートしてはならない。ルートハンドラーは外部HTTPクライアントを直接呼び出してはならない。これらはシンプルなパスパターンとして表現でき、CIで検査できる。
レイヤー境界ルール。 どのディレクトリがどの他のディレクトリを呼び出せるか。src/components/ → src/services/ → src/repositories/ → src/lib/ という構造を強制するチェックで、レイヤーをまたいだ依存関係の違反を自動的に検出できる。エージェントが追加したものか、急いでいた人間のエンジニアが追加したものかを問わず。
これらのチェックに知性は要らない。すべてのPRで実行され、決定論的な合否を返せばいい。
AIコーディングツールを使っていてレイヤー境界の違反が繰り返されているなら、Unbxがマージ前のアーキテクチャチェックを実装している——禁止インポートルールとレイヤー境界の強制をすべてのPRに対して適用する。