App Store のレビュー返信の下書きを作る小さなループを、私は Messages API を直接叩いて書いております。数年ぶんのレビューを相手にするので会話が長くなり、system プロンプトには「本日は 2026-09-02 です」のような行を毎回組み立てて入れておりました。日付が変われば文面のトーンも変わりますから、当然の実装のつもりでいたのです。
その「毎回組み立て直す」が、9 月 1 日から意味を持つようになりました。Claude Fable 5.1 では、thinking ブロックより前の会話を書き換えたリクエストが検査に引っかかります。私の実装は、まさに引っかかる側でした。
最初にお伝えしたいのは、慌てて全部を直す話ではないということです。適用されるのは Fable 5.1 だけで、しかも 2026 年 8 月 31 日 00:00 UTC 以降に作られた新しいアカウントに限られます。Claude Code・Claude Cowork・claude.ai、そして他社製品経由の利用は対象外です。ただし今後のモデルでは全アカウントに広がると公式が明記していますから、いま自分のループを一度点検しておくのは、無駄にならない作業だと思っております。
弾かれるのは「thinking ブロックより前」を書き換えたときです
Claude は応答の前に推論を出し、API ではそれが thinking ブロックとして返ります。マルチターンでは、このブロックを署名つきでそのまま送り返します。API 側はその署名を使って、そのブロックを生んだときと同じ system・tools・messages が送られてきているかを検証するようになりました。食い違えば 400 です。
messages.1.content.0: Invalid `signature` in `thinking` block. The block is bound to a
different conversation. Remove the block, or set
`thinking.block_binding.prefix_mismatch_behavior` to "drop_block".
検査の対象は 3 つあります。モデルが同じか新しいこと、ブロックより前の system・tools・messages が変わっていないこと、そして前の thinking ブロックの連なりが途切れていないことです。3 つめは見落としやすいところで、履歴の先頭から thinking ブロックを外すのは問題ありませんが、途中から1 つ抜くと、それより後ろの thinking がすべて無効になります。
何が「書き換え」に当たるかは、公式が表で示しております。日々の実装で迷いやすいところだけ抜き出します。
| 2 回のリクエストのあいだで起きたこと | 後続の thinking ブロック |
|---|---|
| 末尾にメッセージを追加した | 有効 |
max_tokens や tool_choice など、system・tools・messages 以外のパラメータを変えた | 有効 |
cache_control の位置を足した・動かした・外した | 有効 |
| サーバー側の compaction や context editing が中身を削った | 有効(送った内容で照合されるため) |
| 過去の user / assistant / system メッセージを編集・並べ替え・削除した | 無効 |
トップレベルの system の文字列やブロックを変えた | 無効 |
tools にツールを足した・外した・名前や定義を変えた | 無効 |
| 履歴の途中から thinking ブロックを 1 つ抜いた | それより後ろがすべて無効 |
| 画像や文書の URL が、次のリクエストで別のバイト列を返した | 無効 |
最後の行は盲点でした。「最新のスクリーンショット」を返すエンドポイントを毎ターン参照していると、URL の文字列は同じでも中身が変わりますから、後続の thinking が無効になります。同じファイルを跨って参照するなら Files API の file_id か base64 に寄せるのが安全です。
自分のループが該当するかは、1 リクエストで分かります
推測で直し始める前に、実際に検査を通してみるのが早道です。ベータヘッダ thinking-binding-controls-2026-08-01 を付けて prefix_mismatch_behavior を "drop_block" にすると、アカウントの新旧に関係なく検査が有効になります。古いアカウントから、新しいアカウントの利用者が見るものを先に見られる、ということです。
curl https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: thinking-binding-controls-2026-08-01" \
-d '{
"model": "claude-fable-5-1",
"max_tokens": 16000,
"thinking": {
"type": "adaptive",
"block_binding": { "prefix_mismatch_behavior": "drop_block" }
},
"system": "You are a review-reply assistant.",
"messages": [
{ "role": "user", "content": "この星1レビューに返信を書いてください。" },
{
"role": "assistant",
"content": [
{ "type": "thinking", "thinking": "", "signature": "EqQBCkYIBxgCKkD..." },
{ "type": "text", "text": "レビュー本文を貼っていただけますか。" }
]
},
{ "role": "user", "content": "「課金したのに広告が消えません」" }
]
}'この設定で通常どおり数ターン回すと、レスポンスの先頭に input_transformations が付いてきます。ここを毎ターン記録するだけで、自分の実装が append-only かどうかが判定できます。
{
"input_transformations": [
{
"type": "thinking_dropped",
"path": "messages.1.content.0",
"reason": "prefix_binding_mismatch"
}
]
}読み分けは 2 通りです。prefix_binding_mismatch なら、path が指すブロックより前で何かを変えています。前回と今回のリクエストボディを取っておいて、system・tools・その手前までの messages を差分で突き合わせれば、犯人はすぐ出ます。一方 model_binding_mismatch は、ルーターやフォールバックで古いモデルへ移ったという意味で、実装の欠陥ではありません。こちらはブロックを送り続けて、読めない分を API に落とさせるのが正しい振る舞いです。
私は前回リクエストのボディを 1 つだけ手元に持っておいて、次のリクエストの直前に system と tools を突き合わせる 10 行ほどの関数を足しました。CI では "error" にして、履歴を書き換えたら赤くなるようにしております。
私が直したのは、system プロンプトの組み立て直しでした
日付を毎回入れ直していた実装は、会話中システムメッセージに置き換えれば済みました。トップレベルの system はセッション開始時に固定し、途中で伝えたいことができたら messages の中に role: "system" のメッセージを追記します。前の内容は 1 バイトも動きません。
# 変更前: 毎リクエストで system を組み立て直していた(後続の thinking がすべて無効になる)
system = f"You are a review-reply assistant. Today is {date.today()}."
# 変更後: system はセッション開始時に固定し、変化は messages へ追記する
system = "You are a review-reply assistant."
messages.append({
"role": "system",
"content": "日付が変わりました。本日は 2026-09-02 です。以降の返信の日付表記はこれに合わせてください。",
})
messages.append({"role": "user", "content": next_review_text})この role: "system" メッセージは Fable 5.1 では安定機能で、ベータヘッダは要りません。モデルは system プロンプトと同じ強さで扱います。ツールループの中に置く場合は tool_result を含む user メッセージの後ろに置いてください。assistant の tool_use とその tool_result のあいだに挟むことはできません。
ツールの出し入れも同じ考え方です。tools 配列を直接いじる代わりに、全部をセッション開始時に宣言しておいて、tool_addition と tool_removal で途中から提供・撤回します。ツール定義の変更がプロンプトキャッシュごと壊す話は会話の途中でツールを差し替えるときのレジストリ設計にも書きましたが、今回の検査で「キャッシュが冷える」だけでは済まなくなりました。
履歴の切り詰めは、形によって可否が分かれます
もう 1 つよくある書き換えが、長くなった会話の切り詰めです。ここは全部が禁止されたわけではなく、書き換えた履歴の後ろに thinking ブロックを残さないという一点だけが条件になります。
| 圧縮の形 | 検査との相性 | 必要な手当て |
|---|---|---|
| サーバー側の compaction / context editing | 通ります | 不要(送った内容で照合されるため) |
| 単純圧縮(全体を 1 通の要約にして、次のターンから始め直す) | 通ります | 不要。過去の thinking を一切引き継がない形です |
| 末尾保持圧縮(古い分を要約し、直近のターンはそのまま残す) | 落ちます | 持ち越す assistant ターンから thinking と redacted_thinking を外し、text と tool_use は残します |
| バックグラウンド圧縮(裏で要約を作って差し替える) | 落ちます | 差し替え前に生まれた thinking を含むリクエストで "drop_block" を送るか、同期的に圧縮します |
| 履歴の途中から特定のターンだけ抜く | 落ちます | クライアント側で回避する形はありません。会話中システムメッセージかサーバー側の context editing へ寄せます |
意外だったのは、公式が単純圧縮を推奨の形として挙げていることでした。要約 1 通と次の指示だけで messages を作り直し、過去のターンも thinking も一切送り返さない形です。手の込んだ方式と比べても多くのワークロードで同等に働く、と公式が述べております。階層的に要約を積む設計を私はチャット履歴の階層サマライズで試したことがありますが、あれを維持する手間を思うと、単純圧縮に寄せる判断は十分あり得ます。
ひとつだけ守るべき順序があります。ツールラウンドの途中では圧縮しないでください。tool_use が tool_result を待っている assistant ターンは、thinking を付けたまま送り返して、モデルにそのラウンドを最後まで考えさせるのが筋です。
本番では error と drop_block のどちらに倒すか
prefix_mismatch_behavior の既定は "error" です。実装が append-only になったあとであれば、不一致が起きること自体が自分のコードの欠陥を意味しますから、既定のまま 400 で気づくのが健全だと思います。私も CI ではこちらにしております。
"drop_block" を選ぶのは、落ちるより落とすほうがましな場面です。ルーターやフォールバックでモデルが動く構成、あるいは複数のクライアントが同じ会話を触る構成では、こちらのほうが運用が静かになります。ただし黙って消えるので、input_transformations の記録は必ずセットにしてください。記録しないと、品質が落ちた理由が後から分からなくなります。
本番で 400 を掴んだときの復旧手順も決めておくと落ち着きます。同じリクエストをそのまま再送しても通りません。ベータヘッダを付けて "drop_block" で送り直し、そのセッションの残りは "drop_block" を送り続けます。ベータを使えない事情があるなら、履歴からすべての thinking と redacted_thinking を外し、各ターンの text と tool_use を残して 1 回だけ再送します。そのうえで、書き換えていた箇所を直します。
副産物として、思考のコストにも効きます。前置きが 1 バイトも動かない会話はプロンプトキャッシュがそのまま効きますから、拡張思考を本番で回すときの費用にも跳ね返ります。この辺りの費用感は拡張思考を本番導入して見えたコストに整理しております。
私はこの一件を、制約というより設計の指針として受け取りました。会話履歴は書き足す場所であって、書き直す場所ではありません。 日付もモード切り替えもツールの増減も、過去に手を入れずに「いまから、こうです」と足していけば表現できます。言われてみれば当たり前のことなのに、そう気づいたのは、毎リクエスト system を組み立て直すコードを何年も書き続けたあとでした。
まずは手元のループを 1 セッションだけ "drop_block" で通し、input_transformations が空のまま終わるかを見てみてください。空でなければ、path が最初の 1 行を教えてくれます。私もそこから始めました。最後までお読みくださり、ありがとうございました。