ある朝、Anthropic の利用明細を眺めながら、ふと「これ、全部 Opus に投げる必要は本当にあるのか」と立ち止まったことがあります。個人開発で、長文生成をともなうバッチジョブを日次で回しています。その月の請求書を並べたとき、出力1トークンあたりの単価差を、ようやく実感の伴う数字として受け止めることができました。
ここで扱う「二段構え」は、Haiku 4.5 をオーケストレーター(指揮役)に置き、Opus 4.6 をワーカー(専門役)として呼ぶ構成です。安いモデルが上に立ち、必要なときだけ高価なモデルを呼びます。一見すると逆では、と思われるかもしれませんが、私の運用ではこの並びが結果として最も読み筋が安定しました。ここから先は、実コストの数字、判定基準、コード、そして実装中に詰まったいくつかの場面を順に整理していきます。
なぜオーケストレーターを Haiku に置くのか
ナチュラルな発想は「賢いモデルにルーティングさせて、安いモデルに作業させる」だと思います。私も最初はそうしていました。ところが Opus 4.6 にルーティング判定をさせると、判断の遅さと API 単価の二重の重さが効いてきて、入力 1M トークン換算で見ると指揮役のコストが本体作業を上回る場面が増えていきました。
オーケストレーション側の仕事は、実は「短い文脈で、JSON を返す」という制約の強い作業です。文章を生成する力よりも、構造化された出力が安定して返ってくることのほうが重要になります。Haiku 4.5 は短文の構造化出力で十分な精度を出してくれるため、ここでは Haiku のほうが用途に合っています。むしろ Opus は、長い文脈を抱えて創造的に書き下ろす、あるいは複雑な推論を逐次的に重ねる場面で本領を発揮します。
墨付けをする職人と、削り出す職人を分けるのと同じ考え方だと捉えています。判断の道具と作業の道具を別物として持っておくと、最終的な仕上がりは安定します。私自身、ルーター層を「判断専用のモデル」として切り出してから、出力品質のばらつきが判断側の問題なのか生成側の問題なのかを、はっきり切り分けられるようになりました。
実コストの内訳 — 公表単価に当てて数え直す
単価は変動しますので、ここでは 2026年9月時点の公表単価を明記したうえで計算します。消費トークン量は私の運用ログから取ったものです。
以前この節には「Opus 単独は入出力合算で約 $15 / 1M、二段構成は約 $4.50 / 1M」という、入出力を均した数字を並べておりました。いま読み返すと、これは自分を都合よく納得させる並べ方でした。長文生成では出力トークンが支配的で、しかも出力単価は入力単価の 5 倍です。均してしまえば、どのモデルの数字も「入出力が半々という架空のワークロード」の値段になってしまいます。
コストを比べる単位は $/MTok ではなく、自分のジョブ 1 件です。 入力と出力を分けて数え直したところ、削減率は私が言っていた 70% ではなく、59.6% でした。内訳は後述の月次試算に一行ずつ並べています。
エスカレーション率の 30% という数字は、私の生成バッチで実際に Opus へ回ったタスクの比率を、100件分のルーターログから測ったものです。残り 70% は Haiku がそのまま処理する、あるいはツール呼び出しを通じて軽量な後段に流すだけで完結します。
個人開発の規模では、この差はそのまま「試せる回数」に変わります。単価が落ちたぶんだけ、失敗を許容できる幅が広がりました。
数値だけ見ると「Haiku 単独でいいのでは」と思われるかもしれません。実際に試したのですが、私の用途では出力品質の差が無視できませんでした。論理展開、コード例のニュアンス、構造の切り方といった細部で Opus の出力がやはり一段抜けていて、成果物の水準を落とさないためには、ここを Haiku に統一しないほうが妥当だと判断しています。モデルそのものの選び分けについては、Claude Sonnet 4.6 と Opus 4.6 の使い分け にタスク別の基準をまとめています。
何をワーカーに任せるか — 5つの判定基準
Haiku から Opus を呼び出す判断は、何となく「難しそうだから Opus」では再現性がありません。私の運用では、ルーティング層のプロンプトに次の5つの基準を明示的に書き込み、JSON で出力させています。
出力長が 2,000 トークンを超える見込みか
5ホップ以上の論理連鎖を要するか
対象ドメイン固有の文脈・用語を強く反映する必要があるか
既存の出力と類似度が高く、差別化が必要か
長く参照される技術的判断(アーキテクチャ選択など)を含むか
このうち2つ以上が true の場合のみ Opus に回し、それ以外は Haiku の継続出力で完結させます。実装の中核を抜粋すると次のようになります。
import os
import json
from anthropic import Anthropic
client = Anthropic( api_key = os.environ[ "ANTHROPIC_API_KEY" ])
ROUTER_SYSTEM = """あなたはオーケストレーターです。タスク仕様を受け取り、
Opus 4.6 に処理を委譲すべきかを判定して JSON のみで返してください。
判定基準は次の通りです。
- output_long: 出力 2000 token を超える見込み
- multi_hop: 5 ホップ以上の論理連鎖が必要
- domain_voice: ドメイン固有の文脈/用語の再現が必須
- differentiation: 既存の類似出力との差別化が必要
- architecture: 長く参照されるアーキテクチャ判断を含む
2 つ以上が true なら escalate=true。
出力スキーマ:
{
"output_long": bool,
"multi_hop": bool,
"domain_voice": bool,
"differentiation": bool,
"architecture": bool,
"escalate": bool,
"reason": "短い日本語の根拠"
}
"""
def route (task_spec: dict ) -> dict :
response = client.messages.create(
model = "claude-haiku-4-5-20251001" ,
max_tokens = 400 ,
system = ROUTER_SYSTEM ,
messages = [{
"role" : "user" ,
"content" : json.dumps(task_spec, ensure_ascii = False )
}]
)
return json.loads(first_text(response))
def first_text (response) -> str :
for block in response.content:
if block.type == "text" :
return block.text
raise ValueError ( "text block not found in response" )
response.content[0].text と直接書かず、first_text() で text ブロックを探している点だけは譲れません。thinking が既定で有効なモデル(Sonnet 5・Opus 5 など)では content[0] が thinking ブロックになり、.text が AttributeError で落ちます。ルーターのモデルを世代ごと差し替えた日に、コストとは無関係な場所でパイプラインが止まる——これは後段の節で触れる「既定に寄りかかった実装」の、いちばん静かな現れ方だと感じています。
ポイントは、ルーター側で max_tokens=400 と短く絞り、出力フォーマットを JSON に固定していることです。Haiku 4.5 は短い構造化出力が安定して速いため、ここで余計な前置きを書かれないようにシステムプロンプトで制約しておきます。私の運用では、ルーティング判定の平均レイテンシが 1.2秒、コストが1判定あたり約 $0.0006 に収まっています。
ワーカー側の起動と結果の取り回し
ルーターが escalate=true を返した場合のみ Opus 4.6 を起動します。ここで重要なのは、Opus に渡す入力を「ルーターの判断ログ + 元のタスク仕様」の2層構造にしておくことです。これにより、Opus 側が「なぜ自分が呼ばれたか」を文脈として理解し、より深く踏み込んだ出力を返してくれるようになります。
def delegate (task_spec: dict , routing: dict ) -> str :
if not routing[ "escalate" ]:
return run_haiku_inline(task_spec)
response = client.messages.create(
model = "claude-opus-4-6" ,
max_tokens = 8000 ,
system = (
"あなたは熟練ライターです。前段のルーターが以下の理由であなたに"
"処理を委譲しました: "
f " { routing[ 'reason' ] }\n "
"この理由に応じて、長文・論理連鎖・個人体験の反映を意識して"
"成果物を作成してください。"
),
messages = [{
"role" : "user" ,
"content" : build_prompt(task_spec)
}]
)
return first_text(response)
実装当初、私はこの「ルーターの理由を Opus に渡す」工夫を入れていませんでした。当時の Opus は呼び出されると確かに高品質な出力を返してきましたが、ルーターの基準と微妙にずれた書き方をすることがあり、後段の品質ゲート(生成物を機械的に検収する自作スクリプト)で違反を拾われ、差し戻しになる頻度が無視できませんでした。
ルーター理由を渡すように変えてから、Opus 出力の品質ゲート一発通過率が 62% から 84% まで改善しました。これは月単位では「再生成のための追加課金」が大きく減るということで、コスト最適化の観点でも見過ごせない差です。差し戻し1件はトークンを二重に払うので、通過率はコスト指標でもあります。
ストリーミングとブロッキングの使い分け
ルーター層は短い JSON 応答を返すだけなので、ストリーミングを使う旨味は薄いです。1.2秒のレイテンシをさらに削るより、レスポンス全体を受け取ってから JSON パースに進むほうが、エラーハンドリングがシンプルになります。
一方で Opus 側はストリーミングを積極的に使います。本文生成では5,000〜8,000 トークンの出力が普通で、最初の数十トークンを受け取った時点で品質ゲート相当のキーワード検査を回し、明らかな違反パターン(テンプレート導入文、禁止語句、自慢調のタイトル)が混入していたら早期に abort する設計にしました。
def stream_with_early_abort (prompt: str , banned: list[ str ]) -> str :
parts: list[ str ] = []
window = ""
keep = max (( len (w) for w in banned), default = 1 ) - 1
seen = 0
intro_checked = False
with client.messages.stream(
model = "claude-opus-4-6" ,
max_tokens = 8000 ,
messages = [{ "role" : "user" , "content" : prompt}],
) as stream:
for text in stream.text_stream:
parts.append(text)
seen += len (text)
window += text
for word in banned:
if word in window:
stream.close()
raise EarlyAbort(word)
window = window[ - keep:] if keep > 0 else ""
if not intro_checked and seen > 600 :
intro_checked = True
if looks_like_template_intro( "" .join(parts)):
stream.close()
raise EarlyAbort( "template_intro" )
return "" .join(parts)
この形に落ち着くまでに、私は二度書き直しました。最初の版はチャンクを受け取るたびに "".join(buffer) で全文を作り直し、その全文を禁止語の数だけ走査していました。8,000 トークンの出力では、後半になるほど1チャンクあたりの走査量が積み上がっていきます。早期に止めて節約するための仕掛けが、止まらなかったときにいちばん重くなるという、居心地の悪い設計でした。
いまは禁止語の最大長ぶんだけ末尾を window に残し、チャンクをまたいで分断された語も拾えるようにしています。導入文の判定は本来 1 回で足りる検査ですから、600 文字を超えた最初の一度だけに絞りました。stream.close() を明示しているのは、例外で抜けたときに接続の後始末を取りこぼさないためです。
この早期 abort は、Opus 出力の品質ゲート一発通過率を上げると同時に、無駄なトークン課金を防ぐ効果がありました。実測で、abort が発火したジョブ1件あたり 3,500 トークン程度の節約になり、月単位では 5〜8% のコスト削減につながっています。
ハイブリッド構成で詰まった3つの落とし穴
理屈はきれいに整理できるのですが、実運用に乗せると思わぬ場所で詰まりました。同じ構成を採用する方のために、私が経験した3つを書き残しておきます。
1. ルーターが「全部 escalate=true」に寄ってしまう問題
最初の2週間、Haiku ルーターはほぼ全リクエストを Opus にエスカレーションしました。原因は、システムプロンプトに「迷ったら escalate=true」と書いていたことです。Haiku は迷うのが上手な性格のモデルなので、この一文があると全部 true に寄せます。
修正後は「迷ったら escalate=false。Haiku 自身で処理して構わない」と明示し、5基準の判定を「2つ以上 true」と数値化しました。これでエスカレーション率が 92% から 30% 前後に落ち、本来の二段構成として機能し始めました。
2. キャッシュキーの衝突によるレイテンシ悪化
Opus 側で prompt caching を効かせていたのですが、システムプロンプトに「ルーター判断ログ」を入れたことでキャッシュキーが毎回変わり、キャッシュ命中率がほぼゼロまで落ちました。実測でレイテンシが平均 4.8秒から 11.3秒まで悪化しました。
対処として、ルーター判断ログは messages 側に入れ、システムプロンプトは固定文字列を保ちました。ephemeral cache breakpoint をシステムプロンプト末尾に置き、cache_control を明示することで命中率は 78% まで戻り、レイテンシも 5.2秒前後に収まりました。キャッシュ層をさらに前段へ持ち上げたい場合は、Claude API のセマンティックキャッシュを本番投入する で扱った類似判定としきい値設計が接続点になります。
3. 二段ロギングがログ容量を圧迫する
ルーターと Opus の両方のレスポンスをそのまま保存していたら、月次のログ容量が前年比 2.4倍に膨らみました。Cloudflare R2 のストレージコストが意外に効いてくる規模です。
対処として、ルーターのフルレスポンスは保存せず、escalate / reason の2フィールドだけを残し、本体は Opus の出力に絞りました。デバッグ用にルーター生レスポンスが必要なときは、当日分のみ別のテンポラリ KV に置く構成にしています。これで月次ログ容量を前年並みに戻せました。
採用に至るまでに捨てた3つのアプローチ
二段構成にたどり着く前、私はいくつかの別構成を試して、最終的に捨てました。失敗の記憶は再現性のある教訓になるので、書き残しておきたいと思います。
ひとつめは「Opus 単独 + temperature を下げてコスト圧縮」というアプローチでした。出力を短くしてトークン消費を抑える発想です。実際に temperature を 0.2 まで下げると出力長は確かに 20% 程度減りましたが、表現の幅も同時に痩せ、成果物から手触りが抜け落ちました。単価はわずかに下がっても、後段で人が手を入れ直す時間が増え、総コストではむしろ悪化しています。
ふたつめは「Sonnet 4.6 単独で全タスクを処理」する構成でした。Opus より一桁安く、Haiku より一段賢いという中庸路線です。これは半年ほど採用しましたが、論理連鎖が複雑になる長文タスクで Opus 同等の深さが出ない場面が増え、品質ゲートで差し戻される頻度が上がりました。月次の再生成課金が積み上がり、結果として Opus を一切使わない月のほうがコスト総額が高くなる、という逆転が起きてしまったのです。
みっつめは「ルーター層を別アプリケーションに分離して Cloudflare Workers 上で動かす」構成でした。ルーティング判定はステートレスなので、Workers の Edge ロケーションで動かせばレイテンシをさらに削れるはず、という設計です。実装してみると確かにルーター単独のレイテンシは 0.4秒まで下がりましたが、Workers の制約(CPU 時間、メモリ)と Anthropic SDK の依存解決が想像以上に厄介で、運用負荷の増加に対してメリットが釣り合いませんでした。今は素直に Python ランタイム上で同居させています。
これらを捨てた経験から学んだのは、コスト最適化を「単価を下げる方向」だけで考えると視野が狭くなる、ということです。再生成課金、運用負荷、後段の手直し時間といった周辺コストを含めた総コストを最小化する観点で構成を選ぶと、二段構成のような少し回りくどい設計に落ち着きました。
観測すべき指標 — オーケストレーターを健康に保つために
二段構成は「動いている」ことの確認だけでは不十分です。私は次の指標を Grafana で毎日眺めるようにしました。
エスカレーション率(理想は 20〜40%)
ルーター判定の平均レイテンシ
Opus 出力の品質ゲート一発通過率
Opus 入力に占める prompt cache 命中率
ジョブ1件あたりの実コスト(Haiku + Opus 合算)
特に「エスカレーション率」は健康指標として大事で、20% を下回ると Haiku 単独で十分なケースが増えている可能性があり、40% を超えると Opus に頼りすぎている可能性があります。私の運用では、月次レビューでこの数値を確認し、ルーターのシステムプロンプトを微調整するというリズムが回っています。
月単位で見る実コスト試算 — 1日16件のジョブを回す場合
抽象的な単価より、自分の運用に当てはめた具体数字のほうが判断しやすいので、私の運用を例に置いて月次のコストを試算してみます。1日あたり16件、月当たり概ね 480 件の長文生成ジョブを回す規模を前提にします。
1件あたりの平均トークン消費は、ルーター入力 1,500、ルーター出力 300、Opus 入力 3,000、Opus 出力 5,000 程度になります。エスカレーション率を 30% と仮定すると、月当たりの消費は次のように積み上がります。
Haiku ルーター入力: 480件 × 1,500 = 720,000 トークン
Haiku ルーター出力: 480件 × 300 = 144,000 トークン
Opus 入力(30%エスカレ): 144件 × 3,000 = 432,000 トークン
Opus 出力(30%エスカレ): 144件 × 5,000 = 720,000 トークン
Haiku 本体処理(70%): 336件 × 4,500 = 1,512,000 トークン
Haiku 本体処理の 4,500 トークンは、入力 1,500・出力 3,000 の内訳です。ここまでを公表単価に当てると、次のように並びます。
同じ 480 件を Opus 4.6 単独(入力 3,000・出力 5,000)で回した場合は、入力 1.44M で $7.20、出力 2.4M で $60.00、合わせて $67.20 です。二段構成との差は月 $40.06、年間でおよそ $480、削減率にして 59.6% になります。
この表を作って初めて腑に落ちたことがあります。Opus 出力の $18.00 が、全体の 66% を占めているのです。指揮役を Opus から Haiku に替えたことによる節約は、金額でいえば月 1 ドルにも届きません。効いているのは「Opus に渡る出力トークンを 7 割減らしたこと」ただ一点で、それ以外の工夫はすべて誤差の範囲でした。
削るべきは呼び出し回数ではなく、高いモデルが吐く出力トークンです。 二段構成を入れるかどうかで迷っておられる方は、構成を組む前に、自分のジョブで出力トークンが請求額の何割を占めているかを数えてみていただければと思います。そこが 5 割に届かないワークロードであれば、この構成の手間は報われにくいのです。
エスカレーション率は固定ではなく、シーズンや扱うトピックで揺れます。私の場合、年末年始に販促まわりの長文を集中して回す期間は 45% まで上がり、同じ計算で月 $36 前後になります。逆に定型の短い出力が多い月は 18% 程度まで下がり、月 $20 前後に収まります。月次でこの数字を観察して、ルーター基準を微調整するリズムが運用の核になります。
ロギング設計 — ルーター出力の二次活用
ルーターが返す JSON は、エスカレーション判断のためだけに使うともったいないと感じています。私の運用では、ルーター出力を構造化ログとして Cloudflare D1 に蓄積し、後段の分析に使っています。
import datetime
import sqlite3
from contextlib import closing
def log_routing (task_id: str , task_spec: dict , routing: dict ,
escalated: bool , db_path: str = "routing.db" ) -> None :
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
with closing(sqlite3.connect(db_path)) as conn, conn:
conn.execute(
"""
INSERT INTO routing_log
(task_id, ts, topic, output_long, multi_hop, domain_voice,
differentiation, architecture, escalate, reason)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""" ,
(
task_id,
ts,
task_spec.get( "topic" , "" ),
int (routing[ "output_long" ]),
int (routing[ "multi_hop" ]),
int (routing[ "domain_voice" ]),
int (routing[ "differentiation" ]),
int (routing[ "architecture" ]),
int (escalated),
routing[ "reason" ][: 200 ],
),
)
ここも書き直した箇所です。以前は datetime.datetime.utcnow() でタイムスタンプを作っていましたが、これは Python 3.12 で非推奨になったうえ、返るのはタイムゾーンを持たない naive な datetime です。UTC のつもりで保存した文字列が、あとから読むときに何時なのか確定しない——月次レビューで「30日分」を切り出す集計を回している以上、ここが曖昧では困ります。いまは datetime.now(datetime.timezone.utc) に変えて、末尾に +00:00 が残る形にしています。
接続の扱いも改めました。conn.commit() だけでは、例外が飛んだときに接続が開いたまま残ります。closing() で確実に閉じ、with conn: でコミットとロールバックを任せる二段重ねが、SQLite ではいちばん取りこぼしがありません。
このログがあると、月末のレビューで「どの判定軸が最もエスカレーション判断に効いているか」を集計できます。私の運用では、domain_voice フラグが立った場合のエスカレーション率は実質 100% に近く、逆に output_long 単独では 32% 程度しか escalate されません。基準ごとの寄与度を測れると、ルーター基準そのものの調整が定量的にできるようになります。
論理連鎖(multi_hop)の判定はとくに難しく、Haiku が誤判定しやすいポイントでもあります。私はここを補強するために、過去30日分の multi_hop=true でエスカレーションされたジョブの要約を、ルーターのシステムプロンプトに数例だけ追記する Few-shot 化を試しました。これにより判定一致率が体感で2割ほど上がっています。
3段構成にする選択肢 — Sonnet 4.6 を挟む場合
ここまでは Haiku と Opus の2段で書いてきましたが、運用が成熟してくると Sonnet 4.6 を中段に挟む「3段構成」も選択肢になります。私の場合、中核に置いている長文ジョブのうち、Opus に丸投げするほど深掘りは不要だが Haiku では物足りない、という中間帯が見えてきました。
3段構成のルーティング基準は、2段構成の5基準に「output_long」のしきい値を3段化した形で実装しています。たとえば 1,200 トークン以下は Haiku、1,200〜3,500 は Sonnet、3,500 以上は Opus、というふうに段階を切る形です。実コストでは Sonnet 中段が約 $3.00 / 1M、Opus 単独より一桁安いので、ここを上手に使うとさらに月単位で 10〜15% の追加圧縮が見込めます。
ただ、3段にすると保守の負担も上がります。プロンプトのバージョン管理、各段の品質ゲート、エスカレーション率のモニタリングが、それぞれ独立して必要になります。私は最初の半年は2段に絞り、運用が安定して数字に余裕が出てから3段に拡張するほうが、現場の感覚としては無理がないと感じました。
判定基準を月次で読み直す — ルーターログからの基準改訂
二段構成の弱点は、ルーターの判定基準が「書いた時点の想定」のまま固まってしまうことです。扱うタスクの傾向は数週間で変わります。私の場合、ジョブ種別を一つ増やしただけで output_long の分布が右にずれ、エスカレーション率が 30% から 44% まで上がっていたことに、月末の請求額を見るまで気づけませんでした。
そこで、月に一度だけルーターログを読み直す時間を取るようにしています。やることは3つだけです。
直近30日のエスカレーション率を、タスク種別ごとに出します
Opus に上げたのに出力が短く済んだ案件(=上げる必要がなかった可能性)を抽出します
Haiku で処理したのに後段の品質ゲートで差し戻された案件(=上げるべきだった案件)を抽出します
2と3は、それぞれ偽陽性と偽陰性にあたります。ルーターの精度を一つの数字にまとめようとすると判断を誤りやすいため、私はこの二方向を分けて数えています。
前節の routing_log に、実際の出力トークン数と品質ゲートの結果を2列だけ足しておくと、この集計がそのまま SQL で書けます。
import sqlite3
REVIEW_SQL = """
SELECT
CASE
WHEN escalate = 1 AND actual_output_tokens < 1200 THEN 'false_positive'
WHEN escalate = 0 AND gate_rejected = 1 THEN 'false_negative'
ELSE 'ok'
END AS verdict,
COUNT(*) AS n,
ROUND(AVG(actual_output_tokens)) AS avg_tokens
FROM routing_log
WHERE ts >= date('now', '-30 day')
GROUP BY verdict
ORDER BY n DESC
"""
def monthly_review (db_path: str = "routing.db" ):
conn = sqlite3.connect(db_path)
rows = conn.execute( REVIEW_SQL ).fetchall()
total = sum (r[ 1 ] for r in rows) or 1
for verdict, n, avg_tokens in rows:
print ( f " { verdict :<15 } { n :>4 } 件 ( { n / total :.1% } ) 平均出力 { avg_tokens } tok" )
return rows
actual_output_tokens < 1200 というしきい値は、ルーターの output_long 判定に使っている値と同じものを置いています。判定に使った基準と、事後の検証に使う基準を別々の数字にしてしまうと、「基準がずれているのか、判定がずれているのか」が切り分けられなくなるためです。同じ数字を両側に置くことだけは、最初に決めておくことをお勧めします。
このスクリプトは判断を自動化しません。候補を並べるだけです。実際に基準を書き換えるかどうかは、必ず数件を自分の目で読んでから決めるようにしています。output_long のしきい値を 1,200 から 1,600 に引き上げた月は、偽陽性が 18 件から 5 件へ減り、品質ゲートの差し戻しは 2 件増えるにとどまりました。この程度のトレードオフなら受け入れる、という判断です。
基準を書き出し、実際の判定と突き合わせ、必要なら書き換えます。この往復を月に一度回しておくと、コスト構造が静かに悪化していく事態を、請求書より先に捕まえられます。
来なかった値上げと、実際に動いていた前提
ここまでの数字は 2026年5月時点の構成で測ったものです。その後に前提がいくつか動きましたので、二段構成を回している側で確かめ直した点を書き足しておきます。
最初にお伝えしたいのは、私の側の誤りです。私は「Sonnet 5 のプロモーション価格 $2 / $10 per MTok は 8月31日まで、9月1日から $3 / $15 per MTok に戻る」という前提で、三段構成の中段のしきい値を組み替えていました。中段が割高になるのだから帯を狭めておこう、という判断です。
その引き上げは行われませんでした。$2 / $10 は 8月10日に標準価格となり、9月1日に予定されていた改定は実施されない旨が Anthropic の料金ページ に明記されています。私は来ない値上げに備えて、二週間ほど中段を無駄に細らせていたことになります。請求額は下がらず、差し戻しだけが少し増えました。
予定された価格と、実施された価格は別のものです。 設計を動かす前に料金ページの現在値を開く——このひと手間を省いたぶんが、そのまま無駄な作業になりました。
実際に動いていたのは、単価ではなくモデル世代でした
日付を確認し直すと、私が「8月の改定」とひとまとめに捉えていたものは、単価ではなくモデル世代の入れ替わりでした。Sonnet 5(claude-sonnet-5)の提供開始は 6月30日、Opus 5(claude-opus-5)は 7月24日です。単価そのものは、Opus 5 が Opus 4.8 と同じ $5 / $25 per MTok に置かれています。
二段構成にとって厄介なのは、この種の変更が「設定を触っていないのに指揮役と作業役が同時に入れ替わる」形で効く点です。エスカレーション率は指揮役の判断傾向と作業役の実力の両方に依存する数字ですから、両方が同時に動くと、月次で見ていた 20〜40% のレンジが何を表していたのか読めなくなります。
対策は単純で、ルーター側もワーカー側も model を文字列で明示指定しておくことに尽きます。既定に寄りかかった実装は、ある朝の請求書で気づくことになります。予算側から二重に止める設計は、Claude API の本番運用で予算上限を確実に守る に置いたサーキットブレーカーが土台になります。
トークナイザが変わると、しきい値の物差しも変わります
単価表だけを眺めていたら見落としていたのが、こちらです。Claude 4.7 以降のモデル(Sonnet 5・Opus 5)は新しいトークナイザを使っており、同じ文章でおよそ 30% 多いトークン数になります 。Sonnet 4.6 以前(Sonnet 4.6・Haiku 4.5)は従来のトークナイザのままです。
これは二段構成に二重に効いてきます。
ひとつは費用です。ワーカーを Opus 4.6 から Opus 5 へ移すと、単価は $5 / $25 のまま据え置きですが、同じ原稿を通したときのトークン数が増えるぶん、請求額はおよそ 3 割上がります。「単価が同じなのだから乗り換えはコスト中立」という読みは、ここでは成り立ちません。
もうひとつはしきい値です。この記事のルーターは output_long を 2,000 トークン、月次レビューの検証側を 1,200 トークンというトークン数で 判定しています。指揮役を Haiku 4.5(従来のトークナイザ)に置いたまま作業役だけを新世代へ移すと、判定に使う物差しと実測に使う物差しが別物になります。同じ原稿が、判定では 1,900 で「短い」、実測では 2,500 で「長い」と出る——基準がずれているのか判定がずれているのかを切り分けられなくする、いちばん厄介な種類の不一致です。
移行するのであれば、しきい値は文字数で置き直すか、移行後に測り直したトークン数へ入れ替えます。私は当面ワーカーを Opus 4.6 に固定し、しきい値の棚卸しを済ませてから移す順番にしました。
中段を Sonnet 5 に置くと、いくらになるか
前節の月次モデル(月480件、エスカレーション率 30%)で、従来 Opus に上げていた 144 件のうち半分を中段へ降ろした場合を、公表単価で置き直します。入力 3,000・出力 5,000 トークンのプロファイルをそのまま当てると、中段が扱う量は入力 0.216M・出力 0.36M トークンです。
読みどころは 2 行目です。単価表だけを見れば Sonnet 5 は Sonnet 4.6 より 33% 安く映りますが、トークン数の増加を織り込むと差は 13% まで縮みます。中段として得であることは変わりませんが、得の大きさは単価表から受ける印象の半分以下でした。
もうひとつ、以前この記事に書いていた「同じ量を Opus に戻すと $8.64」という数字も取り下げます。あれは入出力を合算した $15 / 1M という概算から出したものでした。入力と出力を分けて公表単価に当て直すと $10.08 です。出力が支配的なワークロードで均した単価を使うと、高価なモデルのコストを 1 割以上小さく見積もることになります。
夜間バッチなら、Batch API が最大のレバーでした
ここまで積み上げてから気づいたのですが、私の用途でいちばん効くのは中段の帯ではありませんでした。この構成が回しているのは日次のバッチジョブで、結果を即座に返す必要がありません。Batch API は入力・出力とも 50% 引きですから、Opus に上げる 144 件をバッチへ寄せるだけで、前節の月次 $27.14 は $17.06 まで下がります。Opus 4.6 単独構成の $67.20 と比べれば 74.6% 減です。中段のしきい値をどれだけ丁寧に詰めても、ここには届きませんでした。
引き換えになるのは同期のレイテンシだけです。ルーター判定は同期のまま(1判定 1.2 秒)、ワーカーの長文生成だけをバッチへ寄せる——この分け方が、私の運用では無理のないところに落ち着きました。即応が要るジョブと、翌朝までに揃っていればよいジョブを、先に仕分けておくのが前提になります。
なお、inference_geo: "us" を指定して推論を米国内に限定する場合は、Claude 4.6 以降のモデルで全カテゴリ 1.1 倍が乗ります。データ所在の要件がある方は、ここも月次の試算に入れておくと後で驚かずに済みます。
価格・提供条件・既定モデルは変動します。数字をご自分の構成に当てる前に、料金ページで現在値をご確認いただければ幸いです。
どこから始めるか
これから二段構成を試す方には、いきなり全タスクに導入するのではなく、次の順序で段階的に切り替えることをお勧めします。
既存パイプラインのうち、定型的な分類タスク(タグ付け、カテゴリ判定、メタデータ抽出)を1つ選び、まず Haiku 単独に置き換えます
出力品質が落ちないことを2週間モニターします
出力品質に揺らぎが出るタスク(本文生成、コード生成)に対して、ルーター層を挟みます
エスカレーション率が想定範囲(20〜40%)に収まるまでシステムプロンプトを微調整します
prompt caching の命中率を高めるため、キャッシュ可能な前段を切り出します
私自身がこの順序を守れたわけではなく、いきなりルーターを差し込んで上記の落とし穴に全部踏みました。だからこそ、これから取り組まれる方には、もう少し穏やかな入り方をお勧めしたいのです。
二段構成の本当の価値は、コストが下がることそのものより、判断の道具と作業の道具が分かれることで、ふだん見えにくかった「判断の質」を別軸で測れるようになることだと感じています。
最初の一歩として今日できるのは、いま動かしているジョブのうち1つを選び、そこで走っているモデル指定が既定任せになっていないかを確認することです。文字列で固定されていなければ、そこを埋めるところからで十分です。エスカレーション率を測り始めるのは、その次で構いません。