エンジニアの思い立ったが吉日

AI・生成AI・開発ツール・業務効率化のトピックを、20年エンジニア視点で ゆるく整理しているブログです。「現場で本当に効くか」を毎回自分なりに 見極めながら書いています

拒否はエラーで来ない、Fable 5フォールバックの実装勘所

昨日 Claude Fable 5 を取り上げたとき、「危ない話題は弱いモデル(Opus 4.8)に差し戻す」という安全設計を紹介した。あれを API で実装する側はどう書くのか、その技術ドキュメントが公開されたので読み込んだ。いちばん引っかかったのは、拒否(refusal)がエラーとして返ってこないこと。HTTP 200 の正常レスポンスとして、しれっと「中身が空の答え」が返る。ここを知らずに組むと、ユーザーに無言の空応答を返す事故になりかねない。Claude API を叩いている人向けに、勘所を整理する。

拒否はエラーじゃなく「成功レスポンス」で返ってくる

まず大前提。Fable 5 には安全分類器が入っていて、危ない要求は拒否する。問題はその返り方。普通の API なら 4xx や 5xx のエラーで来そうなところ、拒否は HTTP 200 の成功レスポンスとして返り、stop_reason"refusal" になるcontent は空っぽ。

{
  "model": "claude-fable-5",
  "content": [],
  "stop_reason": "refusal",
  "stop_details": {
    "type": "refusal",
    "category": "cyber",
    "explanation": "This request was declined because it could enable cyber harm."
  }
}

これは設計として理にかなっている。拒否は「システムの異常」ではなく「正常な判断の結果」だから、エラーにしない。課金も良心的で、出力前に拒否された場合は課金されず、レート制限にも計上されない。ただ、実装する側からすると罠でもある。try/catch でエラーを拾う作りだと、拒否はエラーじゃないので素通りしてしまう。気づかないと、空の content をそのままユーザーに見せる羽目になる。判定は必ず stop_reason"refusal" かどうかで行う、というのが第一の鉄則。

どんな話題で止まるか、cyber/bio/推論抽出の3分類

何が拒否されるのか。stop_detailscategory に、引っかかったポリシー領域が入る。ドキュメントで挙げられているのは 3 つ。

"cyber" は、マルウェアやエクスプロイト開発などサイバー被害につながりうる要求。"bio" は、危険な実験手法など生物学的被害につながりうる要求。そして "reasoning_extraction" は、モデルに内部の思考過程をそのまま吐き出させようとする要求(構造化された形で思考が欲しいなら adaptive thinking を使え、と案内されている)。昨日の発表スレッドにあった「蒸留(distillation)」の検知は、この推論抽出のカテゴリにおおむね対応すると読める。強いモデルの中身を引き抜いて別モデルに移すのを防ぐ、という狙い。

ここで正直に効いてくる注意書きがある。ドキュメント自身が「良性のサイバーセキュリティ業務でも cyber は発火しうる」「有益な生命科学の業務でも bio は発火しうる」と認めている。つまり、正当な防御研究や生物学の調べ物をしている開発者が、誤検知でこの網に引っかかる場面は普通に起きる。昨日の記事で「フォールバックは平均でセッションの 5% 未満」と書いたけど、自分の用途がたまたまこの帯に寄っていると、体感の発生率はもっと上がる。なお explanation の文面は安定しない(将来変わりうる)ので、表示用に使うのはいいが、文字列をパースして分岐するのは禁物。分岐は必ず構造化された categorystop_reason で。

弱いモデルに自動で回す3つの手

拒否されたら、同じ要求を別のモデルに投げ直せば、たいてい答えが返る。その「投げ直し(フォールバック)」の組み方が 3 通り用意されている。

いちばん楽なのが server-side fallback。リクエストに fallbacks を足して、beta ヘッダを付けるだけ。あとは API 側が、Fable 5 が拒否したら自動で次のモデルを同じ要求で走らせてくれる。1 回のリクエストで、ユーザーには 1 つの答えが返る。

await client.beta.messages.create({
  model: "claude-fable-5",
  max_tokens: 1024,
  messages,
  betas: ["server-side-fallback-2026-06-01"],
  fallbacks: [{ model: "claude-opus-4-8" }]
});

フォールバック先は最大 3 つまで、書いた順に試される。注意したいのは、フォールバックが発動するのは安全分類器による拒否のときだけだということ。レート制限や過負荷、サーバーエラーは、そのまま返ってくる(別モノとして自分で捌く必要がある)。それと、この server-side 方式は Claude API と AWS 上では使えるが、Message Batches や Bedrock・Vertex AI・Microsoft Foundry では使えない。そこでは 2 つ目の手、各言語 SDK のミドルウェア(client-side fallback)を使う。クライアントに一度設定すれば、どのプラットフォームでも自動でリトライしてくれる。3 つ目が、Ruby・PHP・生 HTTP 向けの手動リトライ。自分で「拒否を検知 → 別モデルに再送 → 以降もそのモデルを使い続ける」を書く。

誰が答えたかは応答が教えてくれる、課金もモデル別

フォールバックが走ると、「結局どのモデルが答えたのか」を知りたくなる。応答がちゃんと教えてくれる。トップレベルの model フィールドが、実際に答えたモデルになる。さらに、受け渡しが起きた地点に fallback という種類のコンテンツブロックが挟まり、from(拒否したモデル)と to(引き継いだモデル)が記録される。そして usage.iterations という配列に、試行のすべてが残る。拒否したモデルは普通の message エントリ、実際に答えたモデルは fallback_message エントリとして並ぶ。

課金も理屈が通っている。実際に答えたモデルのぶんだけ、各試行を個別のレートで課金。出力前に拒否しただけの試行は、ゼロ円。各試行はそのモデル自身のレート制限を食う。ここで地味に怖いのが、フォールバック先がレート制限や過負荷だと、フォールバック自体が行われず、元の拒否がそのまま返ること。stop_details.recommended_model に「直接これを試せ」というヒントは出るけど、フォールバック先の枠を、想定する拒否の量に見合うサイズで用意しておかないと、肝心なときに弱いモデルすら回らず拒否で終わる。

もうひとつ実装上ありがたいのが sticky routing。一度フォールバックした会話は、以降のリクエストも最初から答えたモデルに直接回される。毎ターン予測どおり拒否されるモデルに無駄打ちしないための仕組み。だいたい 1 時間、組織スコープで保持され、会話の中身そのものは保存せずハッシュで判定する。ただしベストエフォートなので「いつでも元のモデルが再び試されうる」前提でコードを書く必要がある(現状は非ストリーミングのみ)。

ハマりどころ、拒否は監視に映らない

ドキュメントが挙げる落とし穴が、どれも現場で刺さりそうなので拾っておく。

いちばん大事なのが監視の盲点。拒否は HTTP 200 なので、エラー率や 5xx をトリガーにした監視には一切引っかからない。何も対策しないと、ユーザーが静かに空応答や弱いモデルの答えを受け取っているのに、ダッシュボード上は健全に見える。対策は、拒否ごとに 1 イベント、フォールバックで答えたケース(さっきの fallback_message で識別)ごとに 1 イベントを自前で発火させて、その差分を監視すること。

ほかにも、リトライは必ず別モデルに(同じモデルに再送してもまた拒否される)、リトライ予算はセッション単位やターン単位ではなくリクエスト単位で(1 ターンでエージェント+サブエージェントが複数回拒否を出しうる)、サブエージェントの呼び出しには個別にフォールバックを設定(fallbacks はツール実行の中の呼び出しには伝播しない)、といった注意が並ぶ。手動リトライを書くなら、fallback credit(プロンプトキャッシュの二重課金を防ぐ仕組み)を必ず使え、という助言も。リトライのたびにキャッシュを書き直すと、余計なコストがかかるため。

まず一行足して、拒否を可視化するところから

Fable 5 のフォールバックは、能力の天井が上がったぶん「危ない帯は弱いモデルに回す」という割り切りを、API レベルできれいに実装できるようにしたもの。設計としてはよくできていると思う。一方で、拒否がエラーで来ない、誤検知が正当な業務でも起きうる、監視に映らない、という三点は、知らずに組むと確実にハマる。

Fable 5 に乗せ替えるなら、まず最小構成として、fallbacks に Opus 4.8 を一行足して beta ヘッダを付けるところから始めたい。そのうえで、拒否とフォールバックを自前のイベントとして計測し、自分のサービスで実際にどのカテゴリがどれくらい発火するかを掴む。そこを見てから、誤検知が多い用途ならプロンプトの書き方を調整したり、フォールバック先のレート枠を増やしたりする。能力の高いモデルを使うほど、「答えてくれなかったとき、どう優雅に二番手へ渡すか」の作り込みが、体験の質を分ける。派手な性能の話の裏で、この拒否ハンドリングを丁寧にやれるかどうかが、Fable 5 を実用に乗せる地味な分かれ目になりそうだと見ている。

engineer-kichizitsu.net

engineer-kichizitsu.net

engineer-kichizitsu.net

engineer-kichizitsu.net

engineer-kichizitsu.net

当サイトは、アフィリエイト広告を使用しています。