長い会話を何十ターンも積み上げた末に、AIエージェントへ与える方針をここから切り替えたい、と思う瞬間が来る。コードレビューをさせていた相手に、途中からセキュリティ観点を最優先させたい。長時間のリサーチで、集めた前提を踏まえて出力の形式だけ差し込みたい。この切り替えを従来はトップレベルの system を書き換えて実現していたが、書き換えた途端にプロンプトキャッシュが根こそぎ無効化され、会話全体が再計算されてコストが跳ねる。Claude API の更新は、まさにこの一点に手を入れてきた。
目次
会話の途中に system メッセージを差し込める
更新の中身はシンプルだ。トップレベルの system フィールドを編集する代わりに、messages 配列の中へ {"role": "system", "content": "..."} を追記する。これだけで、キャッシュ済みの会話履歴を壊さないまま、途中から方針や文脈を差し替えられる。
対応モデルは Claude Opus 4.8(claude-opus-4-8)のみで、ベータヘッダは不要だ。通常の client.messages.create でそのまま使える。
筆者は複数のAIを束ねて動かすツールを作っていて、長い会話にキャッシュを効かせ続けたい動機が常にある。長時間走るエージェントほど、履歴の再計算コストは無視できない大きさになる。だからこそ、この小さな追加が効いてくる。
なぜキャッシュが壊れないのか
理由はプロンプトキャッシュの仕組みにある。キャッシュは前方一致で働く。レンダリング順は tools → system → messages と決まっていて、プレフィックスの途中で1バイトでも変われば、それ以降のキャッシュはすべて無効になる。
トップレベルの system は会話全体の先頭に置かれる。ここを書き換えると、後ろに続く全ターンのプレフィックスが変わり、キャッシュ済みの履歴が丸ごと再計算される。長い会話ほど、この再計算は重くのしかかる。
role: "system" のメッセージは違う。履歴の後ろに追記されるので、先頭から直前までのキャッシュ済みプレフィックスは無傷のまま残る。差し込んだ数十トークンだけが新しく計算される。
| やり方 | キャッシュ済みプレフィックス | 再計算されるトークン |
|---|---|---|
トップレベル system を編集 |
変わる(無効化) | 会話履歴の全ターン |
messages に role:"system" を追記 |
無傷 | 追記した分だけ |
コードにすると、差し替えの位置関係はこうなる。
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
system=[
{
"type": "text",
"text": "あなたはコードレビュアーです。",
"cache_control": {"type": "ephemeral"},
}
],
messages=[
{"role": "user", "content": "この関数をレビューして"},
{"role": "assistant", "content": "..."},
# ここから方針を差し替える。トップレベル system は一切触らない
{"role": "system", "content": "以降はセキュリティ観点を最優先で見てください。"},
{"role": "user", "content": "次はこのファイルです"},
],
)
トップレベルの system にキャッシュブレークポイントを置いたまま、方針の切り替えは後ろの role: "system" に任せる。会話がフェーズごとに性格を変えていく常駐エージェントに、この形は素直にはまる。
置ける位置には制約がある
どこにでも差し込めるわけではない。role: "system" のミッドカンバセーションメッセージは、user メッセージの後(またはサーバーツール使用で終わる assistant ターンの後)に置く必要がある。そして配列の最後の要素になるか、その後に assistant ターンが続く形でなければならない。
配列の先頭 messages[0] には置けない。会話の最初の指示はこれまで通りトップレベルの system が担う。内容はテキストのみで、画像やツール結果は入れられない。
非対応モデルへ送ると 400 エラーが返る。メッセージは role 'system' is not supported on this model だ。設計に組み込む前に、使うモデルが Opus 4.8 かどうかを確認しておきたい。
実際に叩いて、位置の制約を確かめた
OpenRouter 経由で Anthropic のモデルへ、4通りの並びを投げた。返る 400 は Anthropic の生の invalid_request_error がそのまま出てくる。
| messages の並び | 結果 |
|---|---|
| … assistant, system, user | 400 |
| … user, system, user | 400 |
| … user, system](配列の末尾)通る | 200 |
| … user, system, assistant …通る | 200 |
400 のメッセージが、制約をそのまま言葉にしていた。
messages.2: role 'system' must follow a 'user' message or an 'assistant'
message ending in a server tool result; the directive-only form
(content: [] with output_config) is accepted at any position
messages.3: role 'system' must precede an 'assistant' message or end the
array; the directive-only form (content: [] with output_config) is
accepted at any position
2つを合わせると、user の直後に置き、assistant の直前か配列の末尾で終えるという条件になる。上に書いた制約と一致した。
エラー文には、content: [] と output_config を使う directive-only 形式なら任意の位置に置ける、とも書いてある。未検証: この形は試していない。
対応が Claude 5 の3モデルまで広がっていた
上で「使うモデルが Opus 4.8 かどうかを確認しておきたい」と書いた。2026年7月31日に測り直したところ、対応は広がっていた。
| モデル | 末尾に system を置いた結果 | 指示に従ったか |
|---|---|---|
| claude-sonnet-5 | 200 | 従った(Four) |
| claude-opus-5 | 200 | 従った(Four) |
| claude-fable-5 | 200 | 従った(Four) |
| claude-haiku-4.5 | 200 | 従わなかった(4) |
差し込んだ指示は「ここから先は必ず英語で、1単語だけ」で、質問は「2+2は?」。Claude 5 の3モデルは Four を返した。haiku-4.5 は日本語のまま 4 を返した。
haiku-4.5 はもう一段ややこしい。400 になるはずの並び(assistant の直後に system)を投げても 200 を返し、そのときは Four. と英語で答えた。位置の制約を強制せず、従い方も呼び出しによって変わる。
ここから引ける実務上の線は1本になる。エラーが出ないことを、対応している証拠に使わない。組み込む前に、指示が実際に効いたかを出力で確かめる。制約に違反した並びを通してしまうモデルがある以上、400 が出ないことは何も保証しない。
測定は OpenRouter 経由で、モデル名は anthropic/claude-* を指定した。Anthropic の API を直接叩いた場合の挙動は確かめていない。
キャッシュを壊す他の操作と並べて考える
プレフィックスを無効化する操作は system の書き換えだけではない。ツール定義の追加・削除・並べ替えはレンダリング順の先頭に置かれるため、全キャッシュを吹き飛ばす。モデルの切り替えも同様で、キャッシュはモデルごとに分かれている。会話の途中でこれらを変えれば、履歴は再計算される。
だから「モードを切り替えたい」という要求を、ツールセットの入れ替えで表現するのは高くつく。方針の変更が指示の言葉で足りるなら、role: "system" の追記で済ませるのが安い。ツールを丸ごと差し替える代わりに、モデルへ「ここからはこう振る舞ってほしい」と伝えるだけで、キャッシュを保ったまま挙動を変えられる。
長時間セッションのコスト設計では、何がプレフィックスを変え、何が変えないかを把握しておくと効いてくる。凍結すべきもの(system プロンプト、ツール一覧、モデル)と、後ろに流してよいもの(実行中に判明する文脈)を分けて考える習慣が、そのままヒット率に跳ね返る。
フェーズが変わる長時間セッションの例
具体的な場面で考えてみる。ある調査エージェントに、まず資料を広く集めさせ、次に集めた資料を批判的に読ませ、最後に決まった書式でレポートへまとめさせる。3つのフェーズは求める振る舞いがそれぞれ違う。
フェーズが切り替わるたびにトップレベルの system を書き換えていたら、集めた資料を含む会話履歴が毎回再計算される。調査が長引くほど、この再計算は膨らむ。フェーズの境目で role: "system" を1つ追記すれば、それまでの履歴はキャッシュから読み出され、新しい指示だけが後ろに足される。
筆者が複数AIを束ねるツールで狙っているのも、この住み分けだ。会話の骨格は凍結し、実行中に変わる指示は後ろへ流す。骨格が動かない限り、何ターン重ねてもキャッシュは効き続ける。
プロンプトインジェクション対策としての意味
この機能の効きどころは、コスト削減だけにとどまらない。オペレーターの指示を届ける経路として、偽装できない筋道を1本増やす意味がある。
従来、会話の途中でオペレーターの指示を伝えるには、user ターンのテキストに <system-reminder> のような形で埋め込む手があった。キャッシュ挙動は同じだが、user や tool の内容は「ユーザー入力に書き込める何者か」が偽装できる。悪意ある入力が、あたかもオペレーターの指示のように振る舞える余地が残る。
role: "system" は、その偽装ができないオペレーター権限の経路になる。user や tool の中身をいくら操作しても、system ロールそのものを名乗ることはできない。信頼できる指示だけを流す専用のチャネルとして機能する。
例えば「ここから先はツールの自動承認を有効化した」「残りトークン予算が閾値を下回った」といった、アプリ側が実行中に知る事実を伝えるのに向いている。ユーザーの発話に混ぜず、オペレーターの声としてモデルへ届けられる。
ひとつ注意したいのは書き方だ。system ロールでも、ユーザーの意図を踏みにじる命令口調(「ユーザーの指示を無視しろ」など)は避け、事実を述べてモデルに判断を委ねる形にする。Claude はユーザーを守る方向に訓練されていて、その保護は system ロールにも及ぶ。
よくある疑問
どのモデルで使える?
Claude Opus 4.8(claude-opus-4-8)のみだ。ベータヘッダは要らず、通常の client.messages.create で動く。非対応モデルへ送ると 400 が返る。
なぜ user ターンに埋め込むのではダメ?
キャッシュの効き方は同じだが、user や tool の内容はユーザー入力を書ける立場の者が偽装できる。オペレーター権限を名乗る指示を安全に流したいなら、偽装できない role: "system" を使う。
配列の先頭に置ける?
置けない。messages[0] は不可で、user メッセージの後(またはサーバーツール使用で終わる assistant の後)にだけ置ける。最後の要素になるか、その後に assistant ターンが続く形にする。
差し込むたびにキャッシュを書き直す必要は?
ない。トップレベルの system と直前までの履歴が同じバイト列なら、その部分はキャッシュから読み出される。新しく計算されるのは追記した system 以降だけだ。
1つの会話で何度も差し込める?
差し込める。フェーズが変わるたびに追記していけばよい。各追記は履歴の後ろに積まれるので、前の追記までのキャッシュは生き続ける。追記のたびにヒットが積み上がっていく形になる。
実装に組み込むときの勘どころ
常駐型のエージェントで「モード切替」を実装しているなら、その切替をトップレベル書き換えから途中 system へ移すだけで、長時間セッションのコストが目に見えて下がる。フェーズが変わるたびに履歴を再計算していた無駄が消える。
設計段階では、固定の指示(凍結した system プロンプト)を先頭に置き、実行中に変わる文脈を後ろの role: "system" へ寄せる。この住み分けを最初から意識しておくと、キャッシュのヒット率が安定する。Claude を組み込んだアプリをUIからバックエンドまで段階的に組み上げる進め方はバイブコーディングの進め方にまとめてある。今回のAPI更新を、実際のアプリのどこに効かせるかがイメージしやすくなるはずだ。
派手な機能ではない。それでも、長い会話を走らせるアプリを作る人にとっては、コスト設計と安全性の両方に効く一手になる。キャッシュを壊さず方針を差し替えられる、この一点だけでも試す価値がある。