Claude Media
mid-conversation system messageとは — 配置制約とclear_atの仕様

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_addition
  • tool_removal
  • output_config
  • cache_control

もう1つの制約が、送り続ける義務です。クリア済みになった後も、以降のリクエストでmessagesにバイト単位で同じまま含めなければなりません。クライアント側で履歴を保存するときは、clear_atフィールドと本文をそのまま保つ実装にします。履歴の圧縮や整形の処理が、クリア済みだからと削ってしまうと制約に反します。

外部テキストを入れてはいけない理由

Claudeはsystemの内容を運営者の指示として扱い、従います。ツールの生の出力、検索で取得した文書、Web上のコンテンツなど、会話の外から来たテキストをsystemメッセージに直接入れると、そのテキストに運営者権限を与えることになります。

入れてよいもの入れてはいけないもの
アプリ自身が観測した事実(設定の切り替え、残りトークン予算)入れてはいけないものツールの生出力
会話の当事者であるエンドユーザーの入力の中継入れてはいけないもの取得した文書・Webページの本文
アプリが決めたポリシーや期限入れてはいけないもの第三者が書けるテキスト全般

外部データはtool_resultブロックに入れたままにします。これは、プロンプトインジェクション対策の基本と同じ考え方です。ユーザー入力を中継する形は認められていますが、ツール出力や第三者コンテンツの受け渡しには使いません。

キャッシュとの関係

mid-conversation system messageは、キャッシュを有効にしたときに意味を持ちます。押さえる点は4つです。

  1. リクエストにcache_controlがなければ、何もキャッシュされず、会話全体を毎回通常の入力料金で処理します。
  2. cache_controlは、リクエスト間で変わらない最後のブロックに置きます。
  3. システムメッセージはそのブレークポイントの後ろに足します。プレフィックスのハッシュは変わりません。
  4. 一度送ったシステムメッセージは、次のターンから安定した履歴の一部としてキャッシュから読まれます。

送信済みのシステムメッセージを編集・削除すると、その位置以降のキャッシュが無効になります。指示を更新したいときは、書き換えず、新しいシステムメッセージを追記します。

キャッシュには最小のキャッシュ可能プロンプト長もあります。短い会話では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に置いたままにします。

この記事を共有:XはてブLinkedIn