mid-conversation system messageとは — 配置制約とclear_atの仕様
messagesの途中にrole: systemを置くmid-conversation system messageの配置制約、clear_atによるターン限定、外部テキストを入れてはいけない理由、キャッシュとの関係を表で示します。
mid-conversation system messageは、Claude APIのmessages配列の途中に{"role": "system"}のメッセージを差し込み、会話の途中から運営者(オペレーター)の指示を追加する仕組みです。トップレベルのsystemフィールドを編集せずに済むので、キャッシュ済みのプレフィックスはそのまま使えます。代わりに、置ける場所・書ける中身・送り続け方に制約があります。この記事は、その制約を一覧にして示します。
mid-conversation system messageとは何か
通常のシステム指示は、リクエストの先頭にあるトップレベルのsystemフィールドに書きます。プロンプトキャッシュはリクエストのプレフィックスをtools、system、messagesの順にハッシュするため、systemを1文足しただけでも、その後ろのキャッシュはすべてミスになります。
mid-conversation system messageは、その指示を履歴の末尾側に足す方法です。それより前のメッセージは一字も変わらないので、既存のキャッシュエントリは一致し続けます。新しく処理されるのは追加した1件だけです。
contentはuserやassistantと同じく、文字列またはコンテンツブロックで書けます。指示が衝突したときは、後ろのシステムメッセージが前のものに勝ちます。また、mid-conversation system messageは、それより後のターンについてトップレベルのsystemに優先します。
対応モデルとプラットフォーム
この機能はClaude API、Amazon Bedrock、Google Cloudで使えます。対応モデルはClaude Fable 5.1、Claude Mythos 5.1、Claude Fable 5、Claude Mythos 5、Claude Opus 5.5、Claude Opus 4.8、Claude Opus 5、Claude Sonnet 5.5です。基本のシステムメッセージにベータヘッダーは要りません。
Claude Sonnet 5では使えません。Sonnet 5で同じことをするには、トップレベルのsystemフィールドを使います。
userメッセージではなくsystemを使う理由
同じ文をuserメッセージで送っても、Claudeは従います。違いは優先度です。userはエンドユーザーの発言として扱われ、systemは運営者の発言として扱われます。両者が食い違えばsystemが優先されます。
エンドユーザーが別の頼み方をしても守らせたい制約や、アプリ側が観測した事実(設定の切り替え、残りトークン予算など)は、systemロールで送る用途です。
配置制約の一覧
配置の規則は、次の表のとおりです。守らないと400エラーになります。
| 位置・中身 | 可否 | 補足 |
|---|---|---|
messagesの先頭 | 可否不可 | 補足中身のあるsystemメッセージは先頭に置けない。冒頭からの指示はトップレベルのsystemへ |
userターンの直後 | 可否可 | 補足tool_resultブロックを持つuserターンも含む |
サーバーツールの結果で終わるassistantターンの直後 | 可否可 | 補足textブロックは可 |
tool_useと対応するtool_resultの間 | 可否不可 | 補足400エラー |
直後がassistantターン、または配列の末尾 | 可否可 | 補足どちらかでなければならない |
空のcontentでoutput_config.effortだけを設定する | 可否どこでも可 | 補足先頭や、assistantとuserの間にも置ける |
置ける場所を一言でまとめると、「userターンの直後で、次がassistantか配列の末尾」です。エージェントのループでは、ツール結果を返すuserメッセージの直後に置けます。
ツール結果の直後に置く
ツールを回している最中の典型的な形は、次のとおりです。
[
{ "role": "user", "content": "Run the test suite and fix any failures." },
{
"role": "assistant",
"content": [{ "type": "tool_use", "id": "toolu_01", "name": "run_tests", "input": {} }]
},
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_01", "content": "12 passed, 0 failed" }
]
},
{
"role": "system",
"content": "The user sent the following message while you were working: also update the changelog before you finish."
}
]この配置は、ユーザーがツール実行中に打ち込んだ追加入力を中継するときにも使えます。新しい依頼として作業を切り替えさせず、進行中の作業に取り込ませる形です。
書き方にもコツがあります。「ユーザーの発言を無視せよ」のような上書き命令より、何が変わったかという事実を書くほうが効きます。Claudeはユーザーに不利に働く指示に抵抗するよう訓練されていて、その保護はsystemロールにも及びます。
連続したシステムメッセージは1つのまとまりとして判定される
systemメッセージを連続して置くことは許されていて、まとめて1つのシステム区画として扱われます。配置規則もまとまり全体に適用されます。
効果だけを設定する空のメッセージ(output_config.effortのみ)はどこにでも置けますが、その隣にテキストを持つメッセージを置くと、グループ全体が「中身あり」の規則に従います。effortの切り替えはeffort切り替えの解説記事が詳しく、ツールの追加・撤回はツール変更の解説記事が詳しく扱っています。
ツール変更ブロックにはさらに制約がある
tool_additionとtool_removalブロックも、テキストと同じ配置規則に従います。加えて、一時停止(pause_turn)したassistantターン、つまりサーバーツールの結果で終わるターンの直後には置けません。textブロックなら置けます。一時停止したターンを先に再開し、ツール変更は次のsystemメッセージで送る形になります。
clear_atでターン限定にする
clear_atは、systemメッセージの表示範囲を決めるフィールドです。値は2つあります。
| 値 | 挙動 |
|---|---|
"never"(既定) | 挙動含まれるすべてのリクエストで、その位置に表示され続ける。フィールドを省略しても同じ |
"next_user_message" | 挙動ターン限定。後ろにrole: "user"のメッセージがない間だけテキストが表示される |
後ろにuserメッセージができると、そのメッセージは「クリア済み」になります。配列には残りますが何も表示されず、入力トークンも消費しません。その状態は、以後のリクエストでも続きます。
判定で注意したいのは、tool_resultだけを持つuserメッセージもuserメッセージとして数えられる点です。ツールループの1往復ごとに、直前のターン限定メッセージは消えます。
ターン限定メッセージはベータ機能です。ベータヘッダーmid-conversation-system-clear-at-2026-08-21が必要で、ヘッダーなしでclear_atを送ると未知のフィールドとして拒否されます。
{
"role": "system",
"clear_at": "next_user_message",
"content": "First privately list what you need next; then request every item that doesn't depend on another's result in this one response."
}毎ターンのリマインダーが積み上がらない
主な用途は、ツールループでのターンごとのリマインダーです。tool_resultのメッセージの後ろに毎回リマインダーを追加し、過去のコピーはそのまま残します。モデルに見えるのは最後のuserメッセージより後ろにあるコピーだけなので、リマインダーが積み上がりません。
それより前のmessagesは変わらないため、プロンプトキャッシュも一致し続けます。過去のリマインダーを削除すると履歴が変わりますが、クリア済みメッセージは配列に残るので、履歴は変わりません。
Claude Fable 5.1、Claude Opus 5.5、Claude Sonnet 5.5では、後ろのthinkingブロックが有効なままになる効果もあります。リマインダーを削除すると、それより後のthinkingブロックの手前で会話が変わり、会話の一致検査に失敗します。thinkingブロックの保護はPreserved thinkingの解説記事で扱っています。
ターン限定メッセージはテキストのみで、そのまま送り続ける
clear_at: "next_user_message"のメッセージには、次のものを持たせられません。
tool_additiontool_removaloutput_configcache_control
もう1つの制約が、送り続ける義務です。クリア済みになった後も、以降のリクエストでmessagesにバイト単位で同じまま含めなければなりません。クライアント側で履歴を保存するときは、clear_atフィールドと本文をそのまま保つ実装にします。履歴の圧縮や整形の処理が、クリア済みだからと削ってしまうと制約に反します。
外部テキストを入れてはいけない理由
Claudeはsystemの内容を運営者の指示として扱い、従います。ツールの生の出力、検索で取得した文書、Web上のコンテンツなど、会話の外から来たテキストをsystemメッセージに直接入れると、そのテキストに運営者権限を与えることになります。
| 入れてよいもの | 入れてはいけないもの |
|---|---|
| アプリ自身が観測した事実(設定の切り替え、残りトークン予算) | 入れてはいけないものツールの生出力 |
| 会話の当事者であるエンドユーザーの入力の中継 | 入れてはいけないもの取得した文書・Webページの本文 |
| アプリが決めたポリシーや期限 | 入れてはいけないもの第三者が書けるテキスト全般 |
外部データはtool_resultブロックに入れたままにします。これは、プロンプトインジェクション対策の基本と同じ考え方です。ユーザー入力を中継する形は認められていますが、ツール出力や第三者コンテンツの受け渡しには使いません。
キャッシュとの関係
mid-conversation system messageは、キャッシュを有効にしたときに意味を持ちます。押さえる点は4つです。
- リクエストに
cache_controlがなければ、何もキャッシュされず、会話全体を毎回通常の入力料金で処理します。 cache_controlは、リクエスト間で変わらない最後のブロックに置きます。- システムメッセージはそのブレークポイントの後ろに足します。プレフィックスのハッシュは変わりません。
- 一度送ったシステムメッセージは、次のターンから安定した履歴の一部としてキャッシュから読まれます。
送信済みのシステムメッセージを編集・削除すると、その位置以降のキャッシュが無効になります。指示を更新したいときは、書き換えず、新しいシステムメッセージを追記します。
キャッシュには最小のキャッシュ可能プロンプト長もあります。短い会話ではcache_creation_input_tokensとcache_read_input_tokensが0のままです。
送信前に配置を検査するコード
配置違反は400エラーで初めて分かるので、履歴を組み立てる側で先に確かめておくと原因を追いやすくなります。次の関数は、この記事の表を検査に書き直した例です。公式のコードではなく、独自に組んだ簡易版です。
def check_system_placement(messages: list[dict]) -> list[str]:
"""中身のある system メッセージの配置を検査する簡易版。"""
errors = []
for i, m in enumerate(messages):
if m["role"] != "system":
continue
# 連続する system は 1 グループとして直前を見る
j = i
while j > 0 and messages[j - 1]["role"] == "system":
j -= 1
if j == 0:
errors.append(f"[{i}] 先頭には置けない")
continue
if messages[j - 1]["role"] == "assistant":
errors.append(f"[{i}] user ターンの直後ではない")
nxt = messages[i + 1]["role"] if i + 1 < len(messages) else None
if nxt == "user":
errors.append(f"[{i}] 直後が assistant でも末尾でもない")
if m.get("clear_at") == "next_user_message":
content = m.get("content")
if not isinstance(content, str) and any(
b.get("type") != "text" for b in content
):
errors.append(f"[{i}] ターン限定はテキストのみ")
return errorsこの検査は、空のcontentでeffortだけを設定するメッセージと、サーバーツールの結果で終わるassistantターンの直後を扱っていません。実運用では、公式の制約表と照らして拡張します。
使い分けの早見表
制約を踏まえた選び方は、次のようになります。
| 目的 | 向く手段 |
|---|---|
| 会話の冒頭から効かせる方針 | 向く手段トップレベルのsystem |
| 途中から加わる恒久的な制約 | 向く手段clear_atなしのmid-conversation system message |
| 毎ターン出したいが積み上げたくない注意 | 向く手段clear_at: "next_user_message" |
| ツール出力や取得文書の受け渡し | 向く手段tool_resultブロック |
| エンドユーザーが言った追加要望の中継 | 向く手段ツール結果直後のsystemメッセージ(事実として書く) |
Sonnet 5を使っている環境では、この機能自体がないのでトップレベルのsystemが唯一の手段です。
参考: よくある400エラーの原因
配置の問題は、大半が次の3つに収まります。
- 会話の先頭にsystemメッセージを置いた
tool_useを含むassistantターンとtool_resultの間に挟んだ- systemメッセージの直後にもう1つuserメッセージを置いた
いずれも「userターンの直後で、次がassistantか末尾」という基本形に戻せば解消します。
まとめ
mid-conversation system messageは、キャッシュを壊さずに運営者権限の指示を後から足す仕組みです。代償として、先頭には置けず、userターンの直後だけに置け、ターン限定版はテキストのみでバイト単位に同じまま送り続ける必要があります。中に入れるのは、アプリ自身が観測した事実か、エンドユーザー本人の入力に限ります。外部のテキストはtool_resultに置いたままにします。