mid-conversation effortの切り替えでオーケストレーションモードを作る
セッション単位でeffortとマルチエージェントのファンアウトを切り替える「オーケストレーションモード」を、公式ドキュメントの実装パターンから組み立てます。
Claude APIのエージェントに「今回は時間がかかっても徹底的にやってほしい」という要求だけを、セッション単位でオン・オフしたいことがあります。単一のAPIパラメータでは実現できません。effortの固定、mid-conversationシステムメッセージ、ツール説明への一文の3部品を組み合わせて作ります。ここでは公式の実装例をたどりながら、effortを切り替えた瞬間にプロンプトキャッシュが切れる問題と、その避け方まで組み立てます。
前提 — オーケストレーションモードは何でできているか
オーケストレーションモードとは、オンの間は徹底性を優先してWorkflowツールでのマルチエージェントファンアウト(fan-out。1つの要求を複数のサブエージェントに分けて同時に走らせること)を既定の挙動にし、オフの間は元どおり個別リクエストで都度の明示的な指示が要る状態(opt-in)に戻すセッション単位の切り替えです。APIパラメータではなく、次の3つのドキュメント化済みの部品だけで組み立てます。
- effortレベル:
xhighのような、公式に文書化されたeffort値でリクエストを実行します。この例ではリクエストのトップレベルで毎回指定し、ベータヘッダーは使いません。 - モードのリマインダー: mid-conversationシステムメッセージで、モードがオンであることをモデルに伝えます。数ターンごとに1行の再確認を送り、オフになったら終了通知を送ります。トップレベルの
systemフィールドは一度も変えないので、プロンプトキャッシュされたプレフィックスは保たれます。 - ツール説明への常時の許可(standing consent): オーケストレーション用ツールの説明文に、「モードがオンの間は毎回このツールで計画・実行してよい」という一文を入れます。
mid-conversationシステムメッセージが使えるモデルは、Claude Fable 5.1 / Mythos 5.1 / Fable 5 / Mythos 5 / Opus 5.5 / Opus 4.8 / Opus 5 / Sonnet 5.5です。Sonnet 5では使えません。その場合はトップレベルのsystemフィールドで代替します。この記事のcurl例はclaude-opus-5-5を使います。
ファンアウト自体はトークン消費を大きく増やします。1回のリクエストが多数のサブエージェント会話を同時に生むため、このモードはコストに見合う重い作業にだけ使います。effortレベルの基礎はClaude effortとはにあります。サブエージェントを起動する側の実装はAgent SDKのサブエージェント定義が扱っています。low effortで検索が減る場合の使い方はFable 5.1のlow effortで検索が減るときの直し方にあります。
事前に決めておく定数
実装を始める前に、ファンアウトの形とリマインダーの頻度を決める定数を用意します。公式サンプルの値は次のとおりです。
サンプルが使う定数の値
MAX_CONCURRENT
10
同時に走るサブエージェント数の上限(PHP版は逐次実行で無視)
MAX_TOTAL_SUBTASKS
200
1回のWorkflow呼び出しで積めるサブタスク数の上限
MAX_SUBAGENT_TURNS
15
1つのサブエージェントが使える最大ターン数
MAX_MAIN_TURNS
30
オーケストレーター本体が使える最大ターン数
TURNS_BETWEEN_REFRESHERS
10
MODE_REFRESHを再送するまでのユーザーターン数
MAX_CONCURRENTとMAX_TOTAL_SUBTASKSを分ける理由は、後の「よくあるつまずき」で扱います。
ステップ1: effortをリクエストのトップレベルで固定する
curl --fail-with-body -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d @- <<'EOF'
{
"model": "claude-opus-5-5",
"max_tokens": 64000,
"cache_control": {"type": "ephemeral"},
"output_config": {"effort": "xhigh"},
"system": "You are a helpful general-purpose agent. Answer the user's request directly.",
"tools": [
{"name": "Workflow", "description": "...", "input_schema": {"type": "object", "properties": {"subtasks": {"type": "array", "items": {"type": "string"}}}, "required": ["subtasks"]}},
{"type": "bash_20250124", "name": "bash"}
],
"messages": [
{"role": "user", "content": "現在のディレクトリを調査し、機能・コード品質の課題・具体的な改善案を徹底的にレビューして"},
{"role": "system", "content": "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than the fastest one. Use the Workflow tool on every substantive task."}
]
}
EOFoutput_config.effortはリクエストごとに指定する通常のパラメータで、ベータヘッダーは要りません。xhighでは思考と応答の合計が大きくなるため、max_tokensも大きく取ります。Opus 5の公式ガイドは、64kトークンから始めて調整するのを妥当な出発点としています。
cache_controlをトップレベルに置いているのは、キャッシュが働くのはcache_controlを付けたときだけだからです。mid-conversationシステムメッセージ自体はキャッシュエントリーを作らないので、付けなければ守るべきキャッシュが最初から存在しません。
effortの切り替え方は2つあり、キャッシュへの影響が違う
モードをオフに戻すときは、effortも通常の値へ戻したくなります。ここで選ぶ方法によって、キャッシュの扱いが変わります。
effortを途中で変える2つの方法
次のリクエストでトップレベルの値を変える
ベータヘッダーは不要です。ただしトップレベルのeffortはレンダリング後のプロンプトに影響するため、切り替えた時点で以前のターンのキャッシュ済みプレフィックスは引き継がれません。Fable 5.1については、以前の返答が前の水準で書かれているため、モデルがそれに合わせ続けやすく、水準の切り替えが効きにくいとも説明されています。
effortだけを載せたsystemメッセージを挟む
{"role": "system", "content": [], "output_config": {"effort": "medium"}}の形で送ります。次のuserターンから新しい水準になり、プロンプトキャッシュは保たれます。対応モデルはFable 5.1・Mythos 5.1・Opus 5.5・Opus 5・Sonnet 5.5です。mid-conversation-output-config-2026-07-01のベータヘッダーも要ります。
effortだけを載せたsystemメッセージは、テキストを持たないので配置ルールの対象外です。messagesの先頭でも、assistantターンと次のuserターンの間でも置けます。値はlow・medium・high・xhigh・maxの水準名です。
curl --fail-with-body -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: mid-conversation-output-config-2026-07-01" \
-H "content-type: application/json" \
-d @- <<'EOF'
{
"model": "claude-opus-5-5",
"max_tokens": 64000,
"cache_control": {"type": "ephemeral"},
"output_config": {"effort": "xhigh"},
"messages": [
{"role": "user", "content": "この設計を3ステップで示して"},
{"role": "assistant", "content": "1. 現状を調べる 2. 案を比べる 3. 実装する"},
{"role": "system", "content": [], "output_config": {"effort": "medium"}},
{"role": "user", "content": "要点を1文でまとめて"}
]
}
EOFここで落とし穴が2つあります。Sonnet 5.5でthinking: {"type": "between_tools"}を使っていると、いま有効な水準と異なるeffortを載せたsystemメッセージは400エラーになります。水準をターンごとに変えたいときは、適応型の思考(adaptive thinking)を使います。対応していないFable 5などは400エラーを返し、文言はoutput_config.effort requires a model that supports per-turn effort; this model does notです。Opus 4.8とFable 5は、モードのリマインダーには使えても、キャッシュを保ったeffort切り替えには使えません。
Opus 5.5のAPI既定のeffortはmediumで、Opus 5以前のOpusはhighです。モードをオフに戻すときの「通常の値」は、モデルごとの既定に合わせて決めます。
ステップ2: モードのリマインダーを3種類用意する
リマインダーは短く保ちます。重い指示はツールの説明側に置き、リマインダーはモードのオン・オフを伝えて説明への参照を促すだけにします。
MODE_ENTER = (
"Orchestration mode is on: optimize for the most exhaustive, correct answer rather than "
"the fastest one. Use the Workflow tool on every substantive task, sized to the problem's "
"natural decomposition rather than the maximum the tool allows. See the Workflow tool's "
"description for standing consent, granularity guidance, and quality patterns. Work solo "
"only on conversational or trivial turns."
)
MODE_REFRESH = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section."
MODE_EXIT = "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again."MODE_ENTERはモードがオンになった最初のターンで1回だけ、MODE_REFRESHは一定ターン数ごとに1行だけ、MODE_EXITはオフになったときに1回だけ送ります。3つともrole: "system"メッセージとして、userターンの後ろに置きます。この位置なら、そこより前のキャッシュ済みバイト列に一切触れず、配置ルールも満たせます。
配置ルールは次のとおりです。内容を持つsystemメッセージは、userターン(tool_resultを運ぶものを含む)の直後に置きます。サーバーツールの結果で終わるassistantターンの直後でも構いません。その後ろはassistantターンか配列の末尾です。tool_useとtool_resultの間に差し込むと400エラーになります。エージェントループでは、ユーザーの新しいターンが来た直後の1か所に差し込み処理をまとめておくと事故を減らせます。
ユーザーのターンごとに送るsystemメッセージ
- 1
ユーザーのメッセージを先に追加する
新しい
userターンをmessagesの末尾に足します。systemメッセージは必ずこの後ろに来ます。 - 2
その回に届ける通知を決めて続ける
終了通知の予約があれば
MODE_EXITを続けます。モードがオンで未アナウンスならMODE_ENTER、前回から規定ターンが過ぎていればMODE_REFRESHです。 - 3
トップレベルを変えずに送る
systemとtoolsは最初のリクエストのままです。変わるのはmessagesの末尾だけなので、そこまでのキャッシュが当たります。
状態管理は、「モードがオンか」「アナウンス済みか」「終了通知待ちか」の3つのフラグと、前回のリマインダーからのターン数のカウンターで足ります。モードをオフにする関数は、アナウンス済みなら終了通知を予約するだけで、その場では何も送りません。実際にMODE_EXITが出るのは次のユーザーターンです。切り替えはいつ呼んでもよく、送信タイミングは配置ルールに自動で合います。
公式サンプルの実行部分は、この切り替えを次のように使います。
agent = ModeAgent(MODEL)
print(agent.turn(task)) # モードがオン: ファンアウトで徹底的に調べる
agent.set_mode(False) # 終了通知を予約するだけ
print(agent.turn("Briefly summarize what you found above, no fan-out needed."))ステップ3: ツールの説明に常時の許可(standing consent)を明記する
Workflowツールの説明文自体に、モード中の既定挙動を明記します。次の例は、公式サンプルの説明文を短くしたものです。
Orchestrate a multiagent workflow: split a large task into independent subtasks
and run them as parallel agents, then collect their results. Opt-in: only use
this tool when the user explicitly asks for a workflow, or when a system message
confirms that orchestration mode is on. Granularity: scope each subtask to a
distinct concern rather than per line or file section; a focused review of a
module of a few hundred lines rarely needs more than about ten. Standing consent: while a
system message confirms orchestration mode is on, author and run a workflow for
every substantive task by default; work solo only on conversational turns or
trivial mechanical edits. When a system message says the mode is off, revert
to the opt-in rule above.リマインダー(ステップ2)は「モードがオンだ」という事実だけを伝え、振る舞いの詳細ルールはこの説明に集約します。モードの中身を変えたくなったら、ツール定義側だけを直せば足ります。
「Granularity」の指針は、サブタスクを行やファイル断片ではなく、独立した関心事の単位で切るというものです。数百行のモジュールを絞ってレビューするなら、10件前後で足りるという目安も添えています。この一文はモードのオン・オフに関わらず効く固定ルールで、サブタスクを細かく切りすぎないための指針です。末尾の一文は、systemメッセージがモードのオフを伝えたら、冒頭のopt-inの規則に戻るという意味です。
ステップ4: ファンアウトを再開可能にするジャーナル
数十のサブエージェントを起動するファンアウトは、途中で落ちたときに最初からやり直すコストが高くつきます。プロンプトのSHA-256をキーにしたローカルJSONファイルを噛ませるだけで、再実行時に完了済みの結果を再利用できます。
def journaled(prompt: str, compute) -> str:
"""このプロンプトのキャッシュ済み結果を返すか、計算して永続化する。
実行を中断して再実行しても、完了していないサブタスクだけが再計算される。"""
key = hashlib.sha256(prompt.encode()).hexdigest()
cached = _load_journal().get(key)
if cached is not None:
return cached
result = compute()
with _journal_lock: # fan-outは複数スレッドから書き込む
journal = _load_journal()
journal[key] = result
temp = f"{JOURNAL_PATH}.tmp"
with open(temp, "w") as file:
json.dump(journal, file)
os.replace(temp, JOURNAL_PATH) # POSIXでもWindowsでもアトミック
return resultジャーナルは実行を横断して重複を防ぐものであり、同一ファンアウト内の重複除去には使いません。やり直すときはジャーナルファイルを消すだけです。書き込みは一時ファイルへ書いてからリネームする形にしておくと、書き込み途中でプロセスが落ちてもジャーナルが壊れません。
ステップ5: 結果を検証する第2波を回す
1回目のファンアウトが終わったら、同じサブエージェントの仕組みを再利用して、各結果を「反証してみる」第2波を走らせます。検証エージェントに渡すプロンプトは次のように組み立てます。
def verify_prompt_for(subtask: str, result: str) -> str:
return (
"Adversarially verify the subagent result below: try to REFUTE it. "
"Re-derive the claims yourself with bash rather than trusting the "
"result, and look for evidence that contradicts them. Default to "
"refuted if uncertain."
)ポイントは「結果を鵜呑みにせず、自分でbashを使って主張を再導出する」ことと「確信が持てなければrefuted(反証)寄りに倒す」ことの2つです。検証エージェントも同じサブエージェントの実行経路を再利用するため、ファンアウト用に用意したジャーナルや失敗分離の仕組みがそのまま検証の第2波にも効きます。元の結果と検証結果の両方をオーケストレーターへ返し、両方を突き合わせて最終判断させます。1つのサブエージェントが失敗しても、そのタスクだけをエラー文字列に落として実行全体は止めない、という失敗の分離も同時に行います。
サブエージェントに渡すsubtasksの受け取り方にも一手間必要です。モデルは配列そのもの、配列をJSON文字列化したもの、改行区切りのプレーンテキストのいずれでも返してくることがあるため、パース前に形式を正規化します。
def normalize_subtasks(raw) -> list[str]:
"""モデルが返す形式(配列/JSON文字列化された配列/改行区切り)を吸収する"""
if isinstance(raw, str):
try:
raw = json.loads(raw)
except json.JSONDecodeError:
raw = raw.splitlines() if "\n" in raw else [raw]
if not isinstance(raw, list):
return []
return [task.strip() for task in raw if isinstance(task, str) and task.strip()]使い分け早見表
| 状況 | 向いている実装 |
|---|---|
| 単発の質問・雑談的なやり取り | 向いている実装通常のリクエスト(effort既定値、Workflowはopt-in) |
| 特定の1回だけ徹底的に調べてほしい | 向いている実装その1リクエストだけoutput_config.effortをxhighに上げる(次のリクエストでキャッシュは引き継がれない) |
| キャッシュを保ったまま、途中からeffortだけ上下させたい | 向いている実装effortだけを載せたsystemメッセージ(Opus 5.5・Opus 5・Sonnet 5.5・Fable 5.1・Mythos 5.1) |
| セッションを通じて「常に徹底的に・自動でファンアウトしてよい」状態にしたい | 向いている実装本記事のオーケストレーションモード(モードのリマインダー + ツール説明への常時の許可) |
| すでに手元でサブエージェントを個別に呼び分けている | 向いている実装Agent SDKのサブエージェント定義を直接使う構成に寄せる方が単純 |
よくあるつまずき
-
ファンアウトの上限を分けずに設計する: 「1回のWorkflow呼び出しで積める件数」(
MAX_TOTAL_SUBTASKS)と「同時に走らせる件数」(MAX_CONCURRENT)は別のノブです。前者だけだとモデルが大量のバックログを一度に投げつけ、後者だけだと大きな計画を立てられません。両方を別々にキャップします。 -
サブタスクの形式を1つしか受け付けない: モデルは配列・JSON文字列化された配列・改行区切りのリストのいずれでも
subtasksを返してくることがあります。正規化せずに配列決め打ちでパースすると、Workflow呼び出しがそのまま失敗します。 -
リマインダーに全ルールを詰め込む: モードのリマインダーを毎回長文にすると、そのたびにトークンを消費しキャッシュも当たりにくくなります。詳細ルールはツールの説明側(ステップ3)に置き、リマインダーは状態の通知に徹します。1ターンだけ見せたい内容なら、
clear_at: "next_user_message"を付けたターン限定のsystemメッセージ(ベータヘッダーmid-conversation-system-clear-at-2026-08-21、テキストのみでcache_controlは付けられません)という手もあります。 -
1つのサブエージェントの失敗で全体を止める: 分離せずに例外を伝播させると、数十件中の1件の失敗でファンアウト全体が失敗扱いになります。個々の失敗はエラー文字列として結果に含め、実行は継続させます。
-
送信済みのsystemメッセージを書き換える: 一度送ったmid-conversationシステムメッセージを編集・削除すると、そこから先のキャッシュが切れます。Fable 5.1・Opus 5.5・Sonnet 5.5では、以降の
assistantターンの思考ブロックも無効になります。モードの文面を変えたいときは、書き換えず新しいsystemメッセージを追記します。 -
テキスト付きのsystemメッセージを隣に置く: 連続するsystemメッセージは1つのまとまりとして扱われ、全体が同じ配置ルールに従います。effortだけのメッセージの隣にテキスト付きのメッセージを置くときは、配置ルールに合う位置に置きます。
-
サンドボックスなしのbashツールをそのまま使う: 公式サンプルのbashツールは、モデルが書いたコマンドを自分のマシンで直接実行し、ファンアウトがそれを並列で走らせます。試すときは、公開されて困らない専用ディレクトリで動かします。
本番運用へ向けて足す3点
このパターンをそのまま小さな検証に使うのはよいですが、本番のワークロードに載せるには次の3点を足す前提で設計します。
| 足すもの | 理由 |
|---|---|
| サンドボックス化されたオーケストレーションスクリプト | 理由サブタスクの平坦なリストだけでなく、分岐・ループ・reduceを含む計画をモデルに書かせ、隔離された環境で実行させる |
| 永続的なジャーナル | 理由ローカルJSONファイルではプロセス再起動や複数マシンからの同時書き込みに耐えないため、永続ストアへ置き換える |
| 予算の強制 | 理由1回のWorkflow呼び出し単位ではなく、セッション全体で起動したサブエージェント総数を追跡し、上限を超えたら拒否する |
リマインダー・常時の許可・ジャーナル・検証の第2波というパターン自体は、これらを足しても変わりません。変わるのは周りの実行基盤の堅牢さだけです。
まとめ
モードの切り替えは、リマインダーとツール説明だけならキャッシュを壊しません。壊すのはeffortの途中変更で、対応モデルならeffortだけのsystemメッセージで避けられ、非対応モデルでは切り替えの頻度を落とす設計になります。