1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIエージェントが「知っているはず」を間違える理由 — コンテキスト設計の実務

1
Posted at

この記事は Zenn に投稿したもの の再掲です。内容は同じで、更新も両方に反映します。

対象: Claude Code / Cursor / Copilot Agent などを業務で毎日使っているエンジニア
前提知識: エージェント型のAIコーディングツールを数ヶ月以上使っていること


この記事で解けること

  • CLAUDE.md に書いたはずのルールが守られない理由が、確率の問題ではなく構造の問題だと分かる
  • コンテキストを3層に分けて設計する具体的な方法
  • リファクタリングでコンテキストが壊れる問題を、構成管理の問題として潰す仕組み
  • 「劣化しているのに気づかない」状態を検知する測定方法

逆に、この記事はプロンプトの書き方のコツを扱いません。それは効果が出ても再現しないからです。


1. 「昨日はできたのに」が起きる構造

エージェント運用を数ヶ月続けたチームが必ず言う台詞があります。

同じことを頼んでいるのに、日によって結果が違う。

そして大抵、対策としてプロンプトに念押しを足します。「必ずテストを書いてください」「既存の規約に従ってください」「勝手にファイルを作らないでください」。CLAUDE.md はこうして肥大化していきます。半年後には500行になり、そして守られる率は下がっています

これは直感に反しますが、理由があります。

指示は増えるほど薄まる

エージェントに渡される情報は、システムプロンプト、プロジェクト指示、ツール定義、会話履歴、読み込んだファイル、すべてが1つの入力列に平坦化されます。その中で各トークンが受け取る「注意」の総量は有限です。

500行の指示書に書かれた1行は、50行の指示書に書かれた1行より確実に弱い。念押しを足すという行為は、その念押し以外のすべてのルールを薄める行為でもあります。

これは体感ではなく、公式ドキュメントにも明記されています。

Size: target under 200 lines per CLAUDE.md file. Longer files consume more context and reduce adherence.

Claude Code Docs / Memory

200行が目安。それを超えた分は、書いた本人が思っているほど効いていません。

ここに最初の原則が出ます。

コンテキストは足すものではなく、奪い合うもの。

新しいルールを1行足すときは、既存の1行を削れないか先に考える。これを守らない限り、指示書は必ず腐ります。

「書いてある」と「効いている」は別物

もう一段厄介なのは、指示書に書いてあるルールの一部が、セッションの途中で静かに消えることです。

ここは正確に押さえる価値があります。公式ドキュメントの記述はこうです。

Project-root CLAUDE.md survives compaction: after /compact, Claude re-reads it from disk and re-injects it into the session. Nested CLAUDE.md files in subdirectories and rules with paths: frontmatter are not re-injected automatically; they reload the next time Claude reads a file in that subdirectory or a file matching the rule's patterns.

Claude Code Docs / Memory

つまり、コンパクション後の挙動は場所によって違います

置き場所 /compact
ルートの CLAUDE.md ディスクから再読込され、再注入される
サブディレクトリの CLAUDE.md 再注入されない。そのディレクトリのファイルを再度読むまで戻らない
paths: 付きの .claude/rules/ 再注入されない。パターンに一致するファイルを再度読むまで戻らない
会話の中だけで伝えた指示 戻らない

ここが実務上の落とし穴です。ルートに書いたルールほど頑丈で、ディレクトリごとに置いた局所規約ほど脆い。
そして局所規約は、細かく具体的で、だからこそ守ってほしい類のルールです。

長いセッションの終盤で「なぜかこのディレクトリだけ規約が守られない」が起きるのは、多くの場合これです。
モデルが気まぐれなのではなく、そのルールが今この瞬間コンテキストに載っていない

この非対称性は、後述する分散コンテキスト(§3)の設計に直結します。
局所規約は、消えることを前提に設計する必要があります。


2. コンテキストを3層に分ける

対策は、すべての情報を同じ扱いにするのをやめることです。エージェントに与える情報は、変化の速度で3つに分かれます。

内容 変化の頻度 置き場所
不変層 設計思想、絶対に破ってはいけない制約 年単位 プロジェクト指示(CLAUDE.md
構造層 ディレクトリの責務、依存の方向、命名規約 月単位 自動生成される配下ファイル
作業層 今の課題、直前の決定、対象ファイル 分単位 プロンプト / セッション

設計を壊す典型は、構造層と作業層を不変層に書いてしまうことです。

<!-- 悪い CLAUDE.md -->
## 現在の作業
認証周りのリファクタリング中。`src/auth/legacy.ts` は削除予定なので触らないこと。

これは3日後に嘘になります。そして誰も消しません。嘘を含む指示書は、指示書全体の信頼度を下げます。エージェントは「ここに書いてあることは古いかもしれない」とは判断できないので、古い情報を現在の事実として扱います

不変層に書いてよいもの

判定基準は1つです。

半年後も同じ文言のまま正しいか?

正しくないなら不変層に置いてはいけません。

<!-- 良い CLAUDE.md -->
## 絶対制約
- 外部APIへのリクエストは `src/infra/` 以外から発行しない
- ドメイン層は外部ライブラリを import しない
- 破壊的なDB操作を含む変更は、必ず人間の確認を挟む

## 判断に迷ったとき
- 抽象化より重複を選ぶ。3回目の重複で初めて抽象化を検討する
- 既存の書き方と違う書き方を思いついたら、既存に合わせる

これらは半年後も正しい。だから書く価値があります。


3. 分散コンテキストという罠

規模が大きくなると、ルートの CLAUDE.md だけでは足りなくなります。レイヤーごとに規約が違うからです。

Claude Code にはこれに対する公式の答えがあります。.claude/rules/ です。

---
paths:
  - "src/infra/**/*"
---

# infra 層の規約
- ドメイン型への変換をこの層の出口で完結させる
- 接続情報は環境変数から読む。ハードコードしない

paths: に一致するファイルを読んだときだけ規約が載るので、コンテキストを食い潰しません。
Claude Code だけを使っているなら、これで話は終わりです。 自前の仕組みを作る必要はありません。

問題は2つ残ります。

問題1: 構造を変えると、規約が静かに壊れる

モノリシックな構成からレイヤードアーキテクチャへ移行する、といった大きなリファクタリングを考えます。ディレクトリが動けば、paths: に書いたグロブも全部書き換えが要ります。

このとき、次のことが起きます。

  1. 一致しなくなったグロブは、エラーにならないsrc/service/**/*src/usecase/ に変わっても、規約ファイルは残ったまま、ただ一度も載らなくなるだけ。CIは緑、テストも通る
  2. 共通ルールの更新漏れ。エラーハンドリングの方針を変えたとき、20個の規約ファイルを手で直す。必ず数個は忘れる

2番目が最も静かに効きます。20個のうち3個が古いルールのままなら、そのディレクトリで作業したエージェントは古いルールに従います。人間から見れば「なぜかここだけ書き方が違う」コードが生えてくる。原因が規約ファイルの更新漏れだと気づくまでに、数週間かかることがあります。

問題2: ツールが増えると、規約が分裂する

こちらの方が実務では重い。チームで使うAIコーディングツールは、たいてい1つではありません。

ツール 置き場所 スコープの指定
Claude Code .claude/rules/*.md paths: フロントマター
Cursor .cursor/rules/*.mdc globs: フロントマター
GitHub Copilot .github/instructions/*.instructions.md applyTo: フロントマター

3ツールともディレクトリ単位のスコープを持っています。持っているのに、置き場所も拡張子もキー名も全部違います。
(Copilot だけは全体に効く .github/copilot-instructions.md も併存していて、こちらにはスコープがありません。)

同じ規約を3箇所に手で書けば、必ずズレます。 そしてズレても誰も気づきません。
「Aさんの環境では規約通りのコードが出るのに、Bさんの環境では出ない」。原因がツールごとの規約の乖離だと特定するのは、経験上かなり難しい部類です。

解決: 規約を成果物として扱う

根本原因は、両方とも同じです。AI用の規約を「手で書いて手で保守するドキュメント」として扱っていること。

これをやめます。規約はマスタから生成されるビルド成果物にします。

先に実装の話を片付けておきます。このアプローチのツールは既にあります。
intellectronica/ruler(★2.8k)は20以上のツールに配布でき、
dyoshikawa/rulesync(★1.3k)は rules だけでなく
mcp・commands・subagents・skills・hooks・permissions まで生成します。
単に生成したいだけなら、このどちらかを入れるのが最短です。 以下の自前実装を写経する必要はありません。

それでもここに実装を載せるのは、仕組みの中身を知らないと、効かなかったときに原因が特定できないからです。
この記事の後半(4節以降)で扱うのは「生成したのに守られない」ケースで、それはツール選定では解けません。

# ai-rules/rules.yml
common:
  - 例外は握りつぶさない。上位に伝播させるか、明示的にログを残す
  - 外部入力は境界で検証する。内部関数では信頼する

layers:
  domain:
    path: src/domain
    description: ビジネスルールを表現する層
    rules:
      - 外部ライブラリを import しない
      - I/O を行わない
  infra:
    path: src/infra
    description: 外部システムとの接続を担う層
    rules:
      - ドメイン型への変換をこの層の出口で完結させる
      - 接続情報は環境変数から読む。ハードコードしない

生成側は、ツールごとに「置き場所とフロントマターを変えるだけ」です。本文は共通で構いません。

# scripts/build_context.py
import sys
from pathlib import Path
import yaml

MARKER = "GENERATED from ai-rules/rules.yml — DO NOT EDIT"

def body(name: str, layer: dict, common: list[str]) -> str:
    """ツール共通の本文。フロントマターは各エミッタが付ける。"""
    lines = [f"# {name}", "", layer["description"], "", "## この層の規約"]
    lines += [f"- {r}" for r in layer["rules"]]
    lines += ["", "## 全体共通"]
    lines += [f"- {r}" for r in common]
    return "\n".join(lines)

def emit_claude(config):
    for name, layer in config["layers"].items():
        yield (
            Path(".claude/rules") / f"{name}.md",
            f'---\npaths:\n  - "{layer["path"]}/**/*"\n---\n\n'
            f"<!-- {MARKER} -->\n\n{body(name, layer, config['common'])}\n",
        )

def emit_cursor(config):
    for name, layer in config["layers"].items():
        yield (
            Path(".cursor/rules") / f"{name}.mdc",
            f"---\ndescription: {layer['description']}\n"
            f"globs: {layer['path']}/**/*\nalwaysApply: false\n---\n\n"
            f"<!-- {MARKER} -->\n\n{body(name, layer, config['common'])}\n",
        )

def main(check_only: bool = False) -> int:
    config = yaml.safe_load(Path("ai-rules/rules.yml").read_text())
    stale = []

    for target, content in [*emit_claude(config), *emit_cursor(config)]:
        if check_only:
            current = target.read_text() if target.exists() else ""
            if current != content:
                stale.append(str(target))
            continue

        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(content)

    if stale:
        print("生成物が古い、または欠落しています:", file=sys.stderr)
        for path in stale:
            print(f"  {path}", file=sys.stderr)
        print("`python scripts/build_context.py` を実行してください", file=sys.stderr)
        return 1
    return 0

if __name__ == "__main__":
    sys.exit(main(check_only="--check" in sys.argv))

重要なのは生成そのものではなく --check モードです。これを CI に入れます。

- name: AIコンテキストの整合性を検証
  run: python scripts/build_context.py --check

これで、指示書の更新漏れがCIの赤になります。人間の注意力に依存していた整合性が、機械が保証する不変条件に変わる。ディレクトリを動かしても、rules.ymlpath を直して再生成するだけで済みます。

繰り返しますが、生成と配布が目的なら rulerrulesync を入れてください。 対応ツールも生成対象もこちらが広く、実績もあります。

上記をそのまま CLI にしたものも MIT で置いてありますが、これは
Node を持ち込まずPythonだけで完結させたい場合の最小版という位置づけです。

pip install git+https://github.com/ben-saito/ai-rules
ai-rules init     # ai-rules/rules.yml の雛形
ai-rules build    # 各ツールの形式に生成
ai-rules check    # 古かったら exit 1(CI用)

ここでの一般化された教訓はこうです。

AIに読ませるMarkdownは、ドキュメントではなくコードである。
手で書いて手で保守してよいのはマスタだけ。配布物は生成し、CIで検証する。


4. 会話履歴という見えない汚染源

ここまではファイルの話でした。実務でより強く効くのに、ほとんど誰も管理していない要素があります。直前の会話です。

エージェントは対話コンテキストを保持しています。レビューを頼む直前まで「リファクタリングでコードを整理する」話をしていたなら、エージェントの注意はすでに「綺麗さ」に引き寄せられています。その状態でレビューを頼むと、ロジックの不備ではなく変数名の指摘が返ってきます。

これは温度パラメータの問題ではありません。入力が違うのです。同じ差分を渡していても、その前に積み上がった履歴が違えば、それは別の入力です。

さらに厄介なのが「自分のコードへの甘さ」です。エージェントに修正させたコードを、同じセッションでそのままレビューさせると、エージェントはそれを「既知の、修正済みのもの」として扱います。修正過程で入り込んだデグレを見逃す。人間のセルフレビューが機能しないのと同じ構造が、そのまま再現します。

対策: 判断を要する仕事はセッションを切る

原則は単純です。

品質の判断を伴うタスクは、履歴を持たないクリーンなセッションで実行する。

Claude Code なら headless モード(claude -p)でサブプロセスとして起動すれば、履歴のない状態を強制できます。

# 対話セッションとは別プロセスで、履歴ゼロからレビューさせる
claude -p "$(cat prompts/review-system.md)

## 対象差分
$(git diff main...HEAD)
"

これを運用ルールとして「意識してください」と依頼しても、絶対に守られません。ツールとして固める必要があります。私は revi という CLI としてこれを実装しています(https://github.com/ben-saito/revi)。設計として強制しているのは次の5点です。

  1. セッション分離 — 毎回クリーンなヘッドレスセッションで起動する
  2. 観点の固定 — Correctness / Security / Performance / Maintainability をシステムプロンプトに焼き込む
  3. 入力スコープの限定 — 全ファイルではなく差分を主軸とし、依存解決に必要なファイルだけを明示的に読ませる
  4. 出力の構造化 — 自然文を禁止し、file / line / severity / category / confidence を持つJSONで返させる
  5. 段階分割 — 「差分解析 → 背景理解 → レビュー → 集約」を単一プロンプトに詰め込まず、ステージとして分ける

4番目の confidence は地味ですが効きます。確信度を自己申告させると、閾値で足切りができるようになり、低確信の指摘がノイズとして混ざるのを防げます。


5. 劣化を検知する

ここまでの対策を入れても、効いているかどうか分からないなら運用できません。指示書は静かに腐るので、腐敗を測る手段が要ります。

対称差分テスト

同じ入力に対してエージェントを3回走らせ、出力集合の対称差分を取ります。

実行1: {A, B, C}
実行2: {A, B, D}
実行3: {A, B, C}

Severity: High の指摘だけに絞れば、この集合は安定して一致するべきです。一致しないなら、それは入力の制御に失敗しているというシグナルです。モデルのせいにする前に、履歴が混ざっていないか、読み込むファイルが実行ごとに変わっていないかを疑ってください。

この指標のよいところは、正解データが要らないことです。自己一致だけを見るので、導入コストがほぼゼロで済みます。

ゴールデンセットによる定点観測

もう一段進めるなら、過去に実際に起きた既知の不具合コミット(SQLインジェクション、権限設定の誤り、N+1)を固定のテストセットとして持ちます。CI でこれを定期的に流し、検知率を記録する。

検知率が下がったとき、原因は3つに絞れます。モデルが変わったか、指示書が壊れたか、入力スコープが変わったか。原因の候補が3つに絞れる状態を作れているかどうかが、運用できているかどうかの分かれ目です。


6. 導入順序

全部を一度にやる必要はありません。効果の大きい順に並べます。

第1週 — CLAUDE.md を削る
半年後も正しい文だけを残す。作業中の話、一時的な事情、「現在」という語を含む行をすべて消す。多くのプロジェクトで半分以下になります。ここが最も費用対効果が高い。

第2週 — 判断タスクのセッションを切る
レビュー、設計判断、リスク評価。これらを対話セッションから追い出し、ヘッドレス実行に移す。まずシェルスクリプト1本で構いません。

第3週 — 対称差分テストを回す
同じ入力で3回。結果を見る。ブレていたら入力制御を疑う。この時点で「効いているか分からない」状態を脱します。

第4週以降 — 規約を生成物にする
使っているAIツールが1つなら、公式の仕組み(Claude Code なら .claude/rules/)をそのまま使えば足ります。
2つ以上が混在しているなら、マスタから生成して --check を CI に入れる。ここで初めて自前の仕組みが要ります。


まとめ

AIエージェントの出力品質は、モデルの知能よりも、実行環境をどれだけ決定論的に制御できているかに依存します。

  • コンテキストは足すものではなく奪い合うもの。1行足すなら1行削る(公式の目安は200行未満)
  • 情報は変化の速度で層を分ける。作業中の話を不変層に書かない
  • コンパクション後に生き残るのはルートの CLAUDE.md だけ。局所規約は消えることを前提に設計する
  • AIに読ませる規約はドキュメントではなくコード。ツールが2つ以上あるならマスタから生成し、CIで検証する
  • 判断を伴うタスクは履歴ゼロのセッションで実行する
  • 対称差分で自己一致を測る。正解データが無くても劣化は検知できる

「意識して守る」で解決しようとしている限り、この問題は必ず再発します。課題を特定し、仕組みとしてコードに焼き付ける。エージェントを信頼できる同僚にする条件は、結局そこにあります。


使えるもの

規約の生成・配布(3節)

  • intellectronica/ruler — まずこれを検討してください。対応ツールが最も広い(MIT)
  • dyoshikawa/rulesync — 生成対象が最も広い(rules 以外に mcp・commands・subagents・hooks など。MIT)
  • ben-saito/ai-rules — Pythonだけで完結させたい場合の最小版。拙作(MIT)

劣化の検知(5節)

  • ben-saito/revi — 決定論的に制御するAIコードレビューCLI。拙作(MIT)
  • ai-rules stability — 正解データなしで High/Critical の一致を測る。任意のレビューコマンドに使えます

AIツールを1つしか使っていないなら、.claude/rules/ という公式の仕組みで足ります。 どれも要りません。

仕組みを入れても効かないとき

生成は ruler や rulesync で足ります。効かないのは、生成の問題ではありません。

仕組みが正しく動いていても規約が効かないケースがあります。原因はほぼ層の切り方です。
ツールを入れ替えても解けないのはこちら側で、ここだけはまだ誰も自動化していません。

層の判定基準、効かなくなる4パターンの見分け方(症状が似ていて対処が正反対)、構成パターン別の rules.yml、測定結果の読み方を本にまとめました。

AIコンテキスト設計ガイド ── 規約を配ったのに守られない理由(2,000円)

第3章「規約が効かなくなる4つのパターン」まで無料で読めます。
/compact の後もルートの CLAUDE.md は再注入されるが、サブディレクトリと paths: 付きは再注入されない ── 長いセッションで静かに落ちるのはどちらか、という章です。
第1章は仕組みを入れる前にやることで、コストがゼロで最も効きます。
そこだけ読んで終わりでも構いません。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?