Anthropic Claude Codeチームに所属するエンジニア Thariq が、2026年2月19日に公開した記事「Lessons from Building Claude Code: Prompt Caching Is Everything」が、X(旧Twitter)で232万インプレッションを記録し、AIエージェント開発者の必読文献として急速に拡散しました。Anthropic公式blogにも同タイトルで掲載された一次情報です。
主張は明快で、過激でもあります。「Cache Rules Everything Around Me」——Claude Codeのような長時間動くエージェントは、prompt cachingなしには成立しない、というのです。Anthropic社内ではキャッシュヒット率にアラートを仕掛け、低下するとSEV(Severity)扱いで対応する運用が走っています。
本記事は、その公式記事に書かれた5つの設計則を一つずつ丁寧に日本語化し、Claude Codeのソースコードに踏み込んだ実例とともに解説します。プロンプト順序、メッセージでの状態更新、モデル切替禁止、ツール固定、Cache-Safe Forking——どれもエージェントを本番運用するエンジニアにとって即実装可能な知見です。
Claude Code全体の使い方は Claude Code完全ガイド2026:インストールから本番運用まで をご覧ください。
01 Thariqが公式blogで明かしたキャッシュヒット率SEV運用
Thariqの記事の冒頭は、強烈な一文から始まります。Claude Codeのハーネス(モデルとツール・コンテキストを束ねる外殻)は、prompt cachingを前提にゼロから設計されている、という宣言です。これは「キャッシュを後付けで最適化した」のではなく、設計思想そのものがキャッシュ中心だ、という意味です。
重要:Anthropic社内ではClaude Codeのキャッシュヒット率を、サービスのアップタイムと同じレベルで監視しています。記事によれば、ヒット率が想定値を下回るとSEV(インシデント)として扱い、原因究明と修正が走るとのこと。これはキャッシュをコスト削減手段ではなく、プロダクトの可用性指標として位置づけているということです。
なぜそこまでするのか。理由はシンプルで、Claude Codeのようなエージェントは1セッションで何十回もモデルを呼び、合計数百万トークンを送ることが珍しくありません。キャッシュなしで毎回フル送信すれば、レスポンスは遅く、コストは10倍に膨らみます。キャッシュヒット率は「速さ」と「安さ」を同時に決める支配的な変数なのです。
Anthropic公式のPrompt Cachingドキュメントによれば、キャッシュヒット時の入力トークンは通常価格の10分の1(0.1倍)に割引されます。一方、キャッシュ書き込み時は1.25倍の追加コストがかかります。つまり「1回書き込んで10回以上ヒットさせる」のが採算ラインで、ハーネス設計はこのラインを越えるための工学なのです。
# Anthropic SDK:cache_controlの基本形
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": LARGE_STATIC_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"} # ここまでをキャッシュ
}
],
messages=[
{"role": "user", "content": user_query}
],
)
# レスポンスでキャッシュ統計を確認
print(response.usage.cache_creation_input_tokens) # 書き込み(1.25倍)
print(response.usage.cache_read_input_tokens) # 読み出し(0.1倍)
cache_control: {"type": "ephemeral"} を付けたブロックまでがキャッシュブレークポイントとなります。次回リクエストで同じプレフィックスが届いたら、サーバ側が一致部分を再利用し、新しい差分だけを処理します。
Claude Codeはprompt cachingを「最適化」ではなく「設計の前提」として組み立てている。Anthropicはキャッシュヒット率をSEV対象にする運用までしており、ヒット率=速さ+安さの支配変数。SDKでは `cache_control` でブレークポイントを明示する。
02 プレフィックスマッチを理解する——順序が全て
prompt cachingの本質を理解する上で、最も重要な事実が一つあります。それは、キャッシュは「前方プレフィックスマッチ」でしか効かない、ということです。
具体的に言えば、リクエストの先頭1万トークンが前回と完全一致していれば、その1万トークン分はキャッシュから読み出されます。しかし1001トークン目で1文字でも違うものを送れば、そこから先はすべて再計算になります。途中だけ同じ部分があってもダメで、必ず最初から連続して同じである必要があります。
警告:この「前方一致」の制約こそ、ハーネス設計のすべてを縛る最大のルールです。Thariqが繰り返し強調するのは、「キャッシュはプレフィックスマッチであり、プレフィックスのどこかが変わると全部無効になる」という一点です。
例えば、systemプロンプトの先頭に「現在時刻: 2026-05-10 14:23:17」と入れたとします。次のリクエストでは時刻が「14:23:22」に変わるため、先頭が違ってしまい、systemプロンプトもツール定義も会話履歴もすべてキャッシュ無効になります。たった1秒違うだけで、ヒット率がゼロに落ちる構造です。
# NG例:systemプロンプトに動的な時刻を入れる
import datetime
system_prompt = f"""
You are Claude Code. Current time: {datetime.datetime.now().isoformat()}
... (10000 tokens of static instructions)
"""
# 毎リクエストで時刻が変わる→キャッシュ完全無効
# 静的な10000トークンも毎回フル送信される
# OK例:時刻はメッセージ側で渡す
system_prompt = """
You are Claude Code.
... (10000 tokens of static instructions)
""" # 静的→キャッシュヒット
messages = [
{
"role": "user",
"content": f"<system-reminder>Current time: {datetime.datetime.now().isoformat()}</system-reminder>\n\n{user_query}"
}
]
同じ「時刻を伝える」処理でも、置く場所が違うだけでキャッシュ効率が劇的に変わります。ハーネスエンジニアリングとは、つまるところ「何を静的にし、何を動的にするか」を1トークン単位で設計する作業です。
ツール定義の順序も同様にクリティカルです。Claude APIではツール配列を tools=[...] で渡しますが、配列の順序が前回と違うと、それだけでプレフィックスが変わってしまいます。Pythonのdictをそのまま渡してハッシュ順で順序が揺らぐ、なんていう実装ミスは致命傷です。
# NG例:ツール順序が非決定的
tools = list(tool_registry.values()) # dictの順序に依存
# Python 3.7+ではinsertion orderが保たれるが、
# tool_registryの構築順が呼び出しごとに違うと崩壊
# OK例:明示的にソート
tools = sorted(tool_registry.values(), key=lambda t: t["name"])
1. systemプロンプト内の動的タイムスタンプ・ユーザー名・セッションID
2. ツール配列の順序が呼び出しごとに変わる
3. ツールパラメータのスキーマ更新(version変更含む)
キャッシュは前方プレフィックスマッチのみ。途中の1文字違いで以降全部が無効化される。動的データはsystemプロンプトに置かず、必ずmessages側で渡す。ツール定義の順序も決定的にソートする。
03 静的→動的:Claude Codeの4階層プロンプトレイアウト
Thariqの記事で最も実用的な部分が、Claude Codeが実際にどう4階層でプロンプトを並べているかを明かした箇所です。順序の論理は明確で、「変わりにくいもの→変わりやすいもの」の一方向です。
Claude Codeのプロンプト構造は次の順番で組み立てられます。
- 静的systemプロンプト + ツール定義(グローバル):Claude Code本体の挙動定義。バージョンアップでしか変わらない。
- CLAUDE.md(プロジェクトルール):プロジェクト固有の指示。プロジェクト内ではほぼ静的。
- セッションコンテキスト:開いているファイル・読んだ内容など、セッション中に蓄積していくもの。
- 会話メッセージ(ユーザー入力 + ツール結果):1ターンごとに増える完全動的な部分。
+ tool definitions
(全ユーザー共通)"] --> B["CLAUDE.md
(プロジェクト固有)"] B --> C["セッションコンテキスト
(開いたファイル・履歴)"] C --> D["会話メッセージ
(user + tool_result)"] A -.->|cache_control| A B -.->|cache_control| B C -.->|cache_control| C style A fill:#e8f5e9 style B fill:#fff9c4 style C fill:#ffe0b2 style D fill:#ffcdd2
各階層の境界に cache_control: {"type": "ephemeral"} を打ち、最大4箇所のキャッシュブレークポイントを設けます(Anthropic APIの上限が4個)。これによって、
- 1階層目だけ変わったとき:全層が再構築(最悪ケース)
- 4階層目だけ変わったとき:1〜3階層目はヒット(理想ケース)
となり、上位ほど安定的にキャッシュが効きます。Claude Codeはほとんどの呼び出しで1〜3階層目がフルヒットすることを目標に設計されています。
# Claude Codeの構造を模した4ブレークポイント例
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
system=[
# Layer 1: 静的system + tool定義の前段
{
"type": "text",
"text": GLOBAL_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}
},
# Layer 2: CLAUDE.md
{
"type": "text",
"text": project_claude_md,
"cache_control": {"type": "ephemeral"}
},
# Layer 3: セッションコンテキスト
{
"type": "text",
"text": session_context,
"cache_control": {"type": "ephemeral"}
},
],
tools=SORTED_TOOLS, # 順序固定
messages=conversation_history + [
{
"role": "user",
"content": [
{"type": "text", "text": user_input,
"cache_control": {"type": "ephemeral"}} # Layer 4
]
}
],
)
ポイント:4ブレークポイントの設計で、最も注意すべきは「ツール定義をどこに置くか」です。Anthropic APIではツール定義はsystemプロンプトとは別のフィールド(tools)で渡しますが、内部的にはsystemプロンプトの直後に展開されてキャッシュされます。つまりツール定義は実質的にLayer 1の一部として扱われ、ここが変わると全層が崩壊します。
Claude Codeチームは、ツール定義の安定性を保つため、次のような工夫をしています。
- ツール配列は名前でソートして決定的順序にする
- ツール説明文(description)に動的情報を入れない
- ツールスキーマのバージョン変更は破壊的更新とみなし、ロールアウトを慎重に行う
| 階層 | 内容 | 変更頻度 | 1セッションでの再構築コスト |
|---|---|---|---|
| Layer 1 | system + tools | バージョンアップ時のみ | 致命的(全層再計算) |
| Layer 2 | CLAUDE.md | プロジェクト編集時 | 大(Layer 2-4再計算) |
| Layer 3 | セッションコンテキスト | ファイル読込時 | 中(Layer 3-4再計算) |
| Layer 4 | 会話メッセージ | 毎ターン | 小(Layer 4のみ) |
Claude Codeは「静的system+tools→CLAUDE.md→セッション→会話」の4階層でプロンプトを並べ、各境界にcache_controlを置く。ツール定義は実質Layer 1の一部。動的データほど後ろに、静的データほど前に。これがprompt cachingの基本フォーマット。
04 system-reminderタグで「変更はメッセージで伝える」
ここまでで「systemプロンプトは静的に保つべし」と説いてきましたが、現実には「現在時刻」「現在の作業ディレクトリ」「ユーザーが開いているファイル」など、動的に伝えたい情報は山ほどあります。これをどう解決するのか。
Thariqの答えは明確です。「systemプロンプトを変えるな。messages側に <system-reminder> タグで挿入しろ」。
重要:Claude Codeのソースコードを覗くと、ユーザー入力やツール結果に <system-reminder>...</system-reminder> というXMLタグでラップされた追加情報がたびたび埋め込まれているのが見つかります。これはモデルに「これはユーザーの言葉ではなくシステムからの注意書きだ」と区別させるための慣習です。
# Claude Codeがやっているような system-reminder 注入
def build_user_message(user_input: str, context: dict) -> dict:
reminders = []
# 動的な情報をすべてここに集める
reminders.append(f"<system-reminder>Current time: {context['now']}</system-reminder>")
reminders.append(f"<system-reminder>cwd: {context['cwd']}</system-reminder>")
if context.get("recent_file_changes"):
reminders.append(
f"<system-reminder>Files changed since last turn: "
f"{', '.join(context['recent_file_changes'])}</system-reminder>"
)
return {
"role": "user",
"content": "\n".join(reminders) + "\n\n" + user_input
}
このパターンには3つの利点があります。
第一に、systemプロンプトとtool定義が完全に静的に保たれるため、Layer 1〜3のキャッシュが安定的にヒットします。動的情報はLayer 4(会話メッセージ)の中だけで動き、上位層を巻き添えにしません。
第二に、モデルが「何が固定の指示で、何が一時的な状態か」を区別できるようになります。systemプロンプトに「現在時刻: …」と書くと、モデルは「時刻はsystemルールの一部」と誤解しがちですが、<system-reminder> タグなら「これはこのターン限定の状態通知だ」と理解しやすい。
第三に、ターンごとに必要な情報だけ送れる。systemプロンプトに全情報を詰めると毎回送信ですが、reminderなら「ファイルが変わったときだけfile-changed reminderを送る」といった条件付きが可能です。
# ツール結果にもsystem-reminderを混ぜられる
def wrap_tool_result(tool_name: str, raw_result: str, side_effects: list) -> dict:
content_parts = [{"type": "text", "text": raw_result}]
for effect in side_effects:
content_parts.append({
"type": "text",
"text": f"<system-reminder>{effect}</system-reminder>"
})
return {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": content_parts
}
]
}
systemプロンプトのテンプレートに `{current_time}` や `{user_name}` のようなプレースホルダを埋め込んで毎回置換する実装。これはキャッシュを完全に殺す典型例。動的データは絶対にmessages側で渡す。
動的情報はsystemプロンプトに入れず、user message内に `<system-reminder>` タグで注入する。これでLayer 1〜3を静的に保ちながら、必要な状態をモデルに伝えられる。Claude Code自体がこの慣習を内部実装で多用している。
05 mid-sessionでツール・モデルを変えない設計
Thariqが特に強く戒めているのが、「セッション途中でツールセットやモデルを変えるな」というルールです。これはエージェント設計者が陥りがちな罠で、一見「賢い最適化」に見えるパターンが、実はキャッシュを完全に破壊しているケースが多いのです。
モデル切替の罠
ありがちな実装:「重い思考はOpus、軽い処理はHaikuに切り替えてコスト削減」。直感的には正しそうに見えます。しかし、Thariqの記事はこれをはっきり否定します。
警告:プロンプトキャッシュはモデル固有です。Opusで構築したキャッシュはHaikuでは使えず、Haiku側で同じプレフィックスを送っても、新規にキャッシュ書き込み(1.25倍)が発生します。1セッション中で頻繁に切り替えれば、両モデルでそれぞれキャッシュ書き込みが走り、合計コストはOpusのみで通すよりも高くなることが珍しくありません。
# NG例:mid-sessionでモデル切替
# Turn 1: Opus(10000トークン書き込み)
client.messages.create(model="claude-opus-4-5", ...)
# Turn 2: Haiku(同じ10000トークンを再書き込み!)
client.messages.create(model="claude-haiku-4-5", ...)
# Turn 3: Opus(Opus側のキャッシュは生きているので読み出し)
client.messages.create(model="claude-opus-4-5", ...)
# → Haikuターンで使った費用は完全な無駄
ではコスト最適化はどうするか。Thariqの答えはサブエージェントです。Claude Codeの「Explore」エージェントは、ファイルツリー探索という単純タスクのためHaikuで動きますが、これはメインセッションとは独立した別のキャッシュコンテキストとして扱われます。メインセッションは最後までOpusで通し、Haikuは別プロセスとして並行動作する。これが正解です。
ツール変更の罠
もう一つの罠が、「状況に応じてツールを動的に追加・削除する」設計です。「コーディングタスクではFile Editツール、データ分析ではPandas Toolを有効化」みたいな実装を考えてしまいがちですが——これもキャッシュ即死パターンです。
ツール定義は前章で説明したとおり、Layer 1の一部としてキャッシュプレフィックスに含まれます。1個でもツールを追加・削除すれば、ツール配列のハッシュが変わり、systemプロンプトもCLAUDE.mdもセッションコンテキストも、全部再構築されます。
| 設計パターン | キャッシュ有効性 | 1ターンの追加コスト |
|---|---|---|
| 全ツール常時ロード(固定) | 全層フルヒット | ベースライン |
| 状況に応じてツール追加 | 追加時に全層再構築 | 1.25倍×全層 |
| Plan Modeでツール置換 | 切替時に全層再構築 | 1.25倍×全層 |
| Tool Search(後述) | 全層フルヒット維持 | スタブ分のみ |
# NG例:mid-sessionでツール追加
turn1 = client.messages.create(
model="claude-opus-4-5",
tools=[tool_a, tool_b], # キャッシュ書き込み
messages=...
)
turn2 = client.messages.create(
model="claude-opus-4-5",
tools=[tool_a, tool_b, tool_c], # 配列が変わる→全層再構築
messages=...
)
ツール定義とモデル選択はキャッシュプレフィックスの根幹。mid-sessionで変えると全層が再計算され、コストはむしろ増える。モデル別最適化はサブエージェントで、ツールは最初から全部ロードしておくのが正解。
06 Plan Modeで学ぶ「状態をツールで表現する」
「ツール変更禁止」というルールに対して、すぐ反論したくなる人もいるはずです。「でもClaude CodeにはPlan Modeがあって、計画モードでは編集ツールが使えなくなるじゃないか」。
実はこれこそ、Thariqの記事の最も技巧的な箇所です。Claude Codeのチームは、「ツール定義は変えずに、ツール経由で状態遷移を表現する」という設計でこの問題を解いています。
Plan Modeの実装トリック
Plan Mode(計画モード)は、Claude Codeで「まず計画を立て、ユーザーが承認したら実行する」ためのモードです。計画中はファイル編集系ツールを呼んでほしくない。普通なら「Plan Mode開始時にEditツールを除外する」と実装したくなります。
しかしClaude Codeのアプローチは違います。ツールセットは常に全部ロードしたままで、代わりに EnterPlanMode と ExitPlanMode というツール自体を追加しています。モデルがPlan Modeに入りたいときは EnterPlanMode を呼ぶ。出るときは ExitPlanMode を呼ぶ。状態は会話履歴の中にツール呼び出しとして記録されます。
ポイント:この設計の美しさは、ツール定義(Layer 1)が一切変わらないことです。Plan Modeに入っても出ても、tools配列は完全に同じ。キャッシュは全層ヒットを維持し、状態遷移は会話履歴という動的レイヤー内だけで完結します。
# Plan Modeの状態管理:ツールは常に全部ロード
ALL_TOOLS = [
edit_file_tool,
read_file_tool,
bash_tool,
enter_plan_mode_tool, # ← モード切替もツールとして
exit_plan_mode_tool, # ← モード切替もツールとして
]
# モデルへのリクエスト:toolsは常に同じ
response = client.messages.create(
model="claude-opus-4-5",
tools=ALL_TOOLS, # 全セッション通じて固定
messages=conversation_history,
)
# Plan Mode中にEditが呼ばれたら、ハーネス側で拒否してエラー返す
def handle_tool_call(tool_use, state):
if state["plan_mode"] and tool_use["name"] == "edit_file":
return {
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use["id"],
"content": "Editing is disabled in Plan Mode. Call ExitPlanMode first.",
"is_error": True
}]
}
return execute_tool(tool_use)
ハーネス側でツール呼び出しを実行時に拒否することで、「ツール定義の安定性」と「モード別の挙動制約」を両立しています。これは「キャッシュ可能性のためなら、多少の冗長性は受け入れる」という設計判断の典型例です。
Tool Search:スキーマも遅延ロードする
もう一つの技巧が、Claude Codeで2026年に導入されたTool Search機構です。MCP(Model Context Protocol)経由で接続されるツールは数百個に達することがあり、全スキーマを毎回送ると数万トークンを消費します。
Tool Searchの解決策はこうです。ツールの「スタブ(名前と短い説明だけ)」を最初に送り、defer_loading: true フラグを付ける。モデルが特定のツールを使いたくなったら、fetch_tool_schema のような専用ツールでフルスキーマを動的に取得する。これにより、全層キャッシュを保ったまま、必要なツールだけを実コストで送れます。
# Tool Searchパターンの概念
deferred_tools = [
{
"name": "github__create_pr",
"description": "Create a GitHub pull request",
"input_schema": {"type": "object"}, # 空のスタブ
"_meta": {"defer_loading": True} # 遅延ロードフラグ
},
# ... 数百個のスタブが続く
]
# モデルが使いたくなったらフルスキーマを別ツールで取得
fetch_schema_tool = {
"name": "fetch_tool_schema",
"description": "Fetch full input schema for a deferred tool",
"input_schema": {
"type": "object",
"properties": {"tool_name": {"type": "string"}}
}
}
スタブだけなら数百ツールでも数千トークン程度で収まり、Layer 1のキャッシュを破壊しません。フルスキーマが必要になった瞬間だけ、ツール結果としてその場限りのコンテキストに展開されます。
1. ツール数100個でもLayer 1キャッシュが安定
2. 使われないツールのスキーマはコスト発生せず
3. ツール追加もスタブだけ更新すれば済む(ただし依然としてLayer 1の一部なので慎重に)
「ツールを変えずに状態を変える」のがClaude Code流。Plan Modeは EnterPlanMode/ExitPlanMode ツールで実装、ツール無効化はハーネス側で拒否。Tool Searchは defer_loading でスタブ送信し、必要時にスキーマフェッチ。すべてLayer 1キャッシュを死守するための工夫。
07 Compactionの罠とCache-Safe Forking
最後に取り上げるのが、長時間セッションで避けて通れないコンテキスト圧縮(Compaction)の問題です。会話が長くなると、コンテキスト長の上限(200K〜1Mトークン)に近づきます。古いやり取りを要約して新しいコンテキストにforkする必要が出てきます。
ここで素朴に「要約してから新しいセッションを始める」と実装すると、キャッシュが完全に切れます。なぜなら新セッションは別のsystemプロンプト構成・別のツール配列を持つ可能性があり、forkの瞬間にLayer 1から作り直しになるからです。
Cache-Safe Forking
Thariqが紹介する解決策が Cache-Safe Forking です。原則は「親会話と完全に同じプレフィックスを共有する」です。
具体的には次の3点を守ります。
- 親と同じsystem promptを使う(Layer 1を共有)
- 親と同じtool definitionsを使う(Layer 1を共有)
- 親のCLAUDE.mdとセッションコンテキストも引き継ぐ(Layer 2-3を共有)
- 会話履歴の最後にcompactionプロンプトを追加する(Layer 4だけ変える)
これによって、forkされた新セッションもLayer 1〜3のキャッシュをフルヒットできます。書き込みコストが発生するのは新規追加のcompactionプロンプト分だけです。
# Cache-Safe Fork:親のプレフィックスを完全に引き継ぐ
def cache_safe_compact(parent_session: dict) -> dict:
return {
"model": parent_session["model"], # ← 同じモデル必須
"system": parent_session["system"], # ← 同じLayer 1
"tools": parent_session["tools"], # ← 同じツール配列
"messages": parent_session["messages"] + [
{
"role": "user",
"content": (
"<system-reminder>"
"The conversation has reached compaction threshold. "
"Please summarize the key context, decisions, and pending tasks "
"in 500 tokens or less. The summary will become the new context "
"for continuation."
"</system-reminder>"
)
}
]
}
# モデルから要約を受け取ったら、それを新セッションのLayer 3に格納
def start_compacted_session(parent: dict, summary: str) -> dict:
return {
"model": parent["model"],
"system": [
*parent["system"],
# 要約はsessionコンテキストとして追加
{
"type": "text",
"text": f"<previous-session-summary>\n{summary}\n</previous-session-summary>",
"cache_control": {"type": "ephemeral"}
}
],
"tools": parent["tools"],
"messages": [] # 新規会話開始
}
重要:ここでmodelを変えてはいけません。前章で説明したとおり、キャッシュはモデル固有なので、Compaction時に「軽いHaikuで要約させよう」とやると、Opusで作った全層キャッシュが無効になります。要約のような単純タスクであっても、メインセッションと同じモデルで処理する方がトータルでは安くなります。
失敗パターンの典型
Cache-Safe Forkingを知らずに実装した場合の典型的な失敗例を表にまとめます。
| 失敗パターン | 何が起きるか | キャッシュ被害 |
|---|---|---|
| 別モデルで要約 | Opusキャッシュ無効化+Haiku書込 | 二重コスト |
| 新セッションで別system prompt | Layer 1から再構築 | 全層書込(1.25倍) |
| 要約後にツールを減らす | tools配列変更 | 全層書込 |
| systemプロンプトに要約を埋め込む | テンプレート変更 | 全層書込 |
| 要約をmessagesに置く(OK) | Layer 4のみ変更 | 最小(差分のみ) |
最後の行が正解パターンです。要約をsystemプロンプトに突っ込むのではなく、新しいuser messageの一部として渡すか、Layer 3のセッションコンテキストとして追加する。これでLayer 1のキャッシュは死守できます。
「要約だけ別モデル・別プロンプトで処理する」設計は、ほぼ確実にキャッシュを破壊する。Compactionは特別なフローではなく、「メインセッションの末尾に追加で1メッセージ送る」程度に軽量化するのが正解。
Compaction(コンテキスト圧縮)は親セッションと完全に同じmodel・system・toolsを引き継ぐCache-Safe Forkで実装する。要約はmessages側でリクエストし、得られた要約は新セッションのLayer 3として格納。これでforkコストを最小化。
5つの教訓——Thariqまとめの完全版
最後に、Thariq記事の末尾にまとめられた5つの教訓を、本記事の解説と照らして整理します。
| # | 原則 | 実装上の意味 |
|---|---|---|
| 1 | Prompt cachingはprefix matchである | プレフィックスのどこかが変わると全部無効。1トークン単位で並びを設計せよ |
| 2 | systemプロンプト変更ではなくメッセージで状態を伝える | <system-reminder> タグで動的情報をmessages側に注入 |
| 3 | mid-sessionでツール・モデルを変えるな | モデル切替はサブエージェントで、ツール変更は状態管理で代替 |
| 4 | キャッシュヒット率をuptimeのように監視せよ | Anthropic自身がSEV運用。あなたのプロダクトでも同様に |
| 5 | fork操作は親と同じプレフィックスを共有せよ | Cache-Safe Forkingで親のmodel・system・toolsを完全継承 |
これら5つはどれもバラバラの最適化テクニックではなく、「プレフィックスマッチを徹底的に活かす」という一つの原則から派生しています。エージェントを設計するとき、まず自問すべきは「このセッション内で、何が静的で何が動的か」です。それが整理できれば、5つのルールは自然に従えます。
ハーネスエンジニアリングの全体像については ハーネスエンジニアリング本番ガイド で、Claude Codeアーキテクチャ全体については Claude Codeアーキテクチャ完全解剖 で詳述しています。Thariq氏の他の発言を追いたい方は Claude Code開発者Thariqが語るエージェント設計の全貌 もあわせてどうぞ。Claude API側のキャッシュ料金や請求モデルの詳細は Claude API料金完全ガイド で扱っています。
エージェントを本番に出すとき、レイテンシとコストを左右する最大の変数はモデルではなく、ハーネスです。そしてハーネスの良し悪しは、prompt cachingをどこまで活かせるかでほぼ決まります。Thariqの記事は、その最前線をAnthropic自身が公開した稀有な一次資料です。本記事で扱った5つのルールを、ぜひあなたのエージェント設計にも適用してみてください。