6月のある朝、Crashlytics のトリアージレポートが空のまま届いていました。私は個人開発のアプリ運用で、毎朝6時に headless の Claude Code(claude -p)にクラッシュログの分類を任せているのですが、その日のログには overloaded_error(HTTP 529)が3回並び、リトライ上限に達して終了していました。再試行だけに頼った設計の弱さを突きつけられた出来事です。クラッシュの放置は AdMob の収益低下やストアレビューの悪化に直結するため、このトリアージは私の運用の中で最も止めたくないバッチでした。
直近30日のログを集計すると、529 で朝のバッチが空振りした日は4日ありました。月にして約13%の欠損です。人間が画面の前にいれば「少し待ってもう一度」で済みますが、無人実行ではその「少し待つ」をどれだけ重ねても、過負荷が続く朝には勝てません。そこで Claude Code に追加された fallbackModel 設定を使い、モデル自体を順次切り替える三段構成へ移行しました。この記事はその設計と、実際に運用して分かった注意点の記録です。
再試行ループだけでは越えられなかった壁
移行前の私のスクリプトは、よくある指数バックオフでした。
#!/bin/bash
# Before: 同一モデルへの再試行のみ。過負荷が続く朝は全滅する
PROMPT_FILE = " $HOME /ops/crashlytics_triage_prompt.md"
for i in 1 2 3 ; do
if claude -p "$( cat " $PROMPT_FILE ")" > /tmp/triage_result.md 2> /tmp/triage_err.log ; then
exit 0
fi
sleep $(( i * 60 )) # 1分 → 2分 → 3分
done
echo "triage failed after 3 attempts" >&2
exit 1
このコードの問題は、再試行しても同じモデルの同じ混雑 にぶつかり続けることです。529 はサーバー側の過負荷なので、数分のバックオフで解消する日もあれば、30分以上続く日もあります。空振りした4日のログを見返すと、3回の再試行はいずれも最初の失敗から6分以内に使い切られていました。混雑のピークはそれより長く続いていたわけです。
fallbackModel の配列指定で何が変わるのか
Claude Code の fallbackModel は、settings.json に配列で最大3モデルまで指定でき、プライマリモデルが過負荷などで応答できないときに別のモデルへ順次切り替えて 実行を続けます。再試行が「同じ窓口に並び直す」行為だとすれば、フォールバックは「空いている隣の窓口へ移る」行為です。混雑の質が違う窓口に移れることが、無人実行では決定的に効きます。
// After: .claude/settings.json — 過負荷時は別モデルへ順次切り替え
{
"model" : "claude-fable-5" ,
"fallbackModel" : [ "claude-opus-4-8" , "claude-sonnet-4-6" ]
}
設定はこれだけで、スクリプト側の変更は不要です。移行後の30日間で、プライマリが応答できずフォールバックが発動した朝は3回ありましたが、バッチが空振りした日はゼロ になりました。529 の発生そのものは止められなくても、結果の欠損は止められます。
一点、運用して分かった注意があります。fallbackModel が効くのは過負荷やモデル利用不可のエラーに対してであり、タイムアウトには効きません 。常時適応思考を持つモデルをプライマリに置くと、簡単に見えるタスクでも思考量が増えて所要時間が読みにくくなることがあります。私自身はバッチ全体のタイムアウトを従来の1.5倍に広げました。フォールバックだけでは対処できない失敗モードが残る前提で、タイムアウトは別の保険として設計することを推奨します。
三段構成の並べ方 — どのモデルを何番目に置くか
並び順は「品質の階段を一段ずつ降りる」ように決めました。私の基準は次の3つです。
1段目(プライマリ) : そのタスクで普段使いたい品質のモデル。私は導入期間中の Claude Fable 5 を試していますが、6月23日以降は usage credits が必要になるため、コストを見て Opus 4.8 へ戻す可能性も含めて決めます
2段目 : プライマリと出力傾向が近く、プロンプトをそのまま流用できるモデル。Opus 4.8 はトリアージの分類基準への追従が安定しており、ここに置いています
3段目 : 「結果ゼロよりは確実にまし」と言い切れる軽量モデル。Sonnet 4.6 は速く、分類の大枠は外しません
避けたほうがよいのは、コスト最優先で2段目にいきなり軽量モデルを置く構成です。フォールバックは数十日に数回しか発動しないので、発動時のコスト差は月額で見れば誤差ですが、品質の落差は翌朝の手戻りとして確実に返ってきます。発動頻度が低いからこそ、2段目は品質を優先する判断が割に合うというのが私の結論です。
なお、API 側で同じ思想を実装する場合のアーキテクチャはClaude API のモデル抽象レイヤー設計 — 世代交代に業務ロジックを巻き込まない内部アーキテクチャ に書いた抽象レイヤーの考え方が使えます。CLI の fallbackModel は、あの設計を設定一行で済ませてくれる存在だと捉えています。
どのモデルで実行されたかを記録する — stream-json から取り出す
フォールバックを入れて最初に困ったのは、「今朝の結果はどのモデルが書いたのか」が分からないことでした。フォールバックは静かに発動するため、記録を残さないと品質差の検証もコスト集計もできません。
私は出力形式を stream-json に変え、セッション開始時の system メッセージから実行モデルを取り出して CSV に追記しています。
#!/bin/bash
# 実行モデルを記録し、プライマリ以外なら印を付ける
PROMPT_FILE = " $HOME /ops/crashlytics_triage_prompt.md"
LOG = "/tmp/triage_stream.jsonl"
PRIMARY = "claude-fable-5"
claude -p "$( cat " $PROMPT_FILE ")" \
--output-format stream-json --verbose > " $LOG "
MODEL = $( jq -r 'select(.type == "system" and .subtype == "init") | .model' " $LOG " | head -1 )
RESULT = $( jq -r 'select(.type == "result") | .result' " $LOG " )
echo "$( date +%F),${ MODEL }" >> " $HOME /ops/triage_model_history.csv"
if [ " $MODEL " != " $PRIMARY " ]; then
# フォールバック発動日はレポート冒頭に明記して翌朝の自分に知らせる
printf '> 注: 本日は %s で実行されました\n\n%s' " $MODEL " " $RESULT " > /tmp/triage_result.md
else
printf '%s' " $RESULT " > /tmp/triage_result.md
fi
ポイントは、フォールバック発動をエラーではなくレポート自体への注記 として残すことです。発動日の出力は後述のとおり点検対象にしたいので、CSV だけでなく読む場所そのものに印を付けるほうが、無人運用では確実でした。
下位モデルに落ちた日の品質差にどう備えるか
3段目の Sonnet 4.6 まで落ちた朝が一度あったので、同じ日のクラッシュログ52件を後から Fable 5 でも分類し直し、突き合わせてみました。結果は47件が同じ分類で、一致率は約90%でした。違いが出た5件はいずれも「複数の原因が絡む低頻度クラッシュ」で、下位モデルは保守的に「要手動確認」へ寄せる傾向がありました。
この実測から、私は運用ルールを2つだけ決めました。
フォールバック発動日のレポートは、優先度「高」と判定された項目だけ人間が目視で再確認する(全件見直しはしない)
発動が月3回を超えたら、プライマリのモデル選定か実行時刻そのものを見直す
品質差への備えは「完璧な検証」ではなく「どこまで信用して、どこから自分の目を入れるか」の線引きだと考えています。一致率90%という数字は、その線を引くのに十分な根拠になりました。
deny ルールの glob 対応と組み合わせる — 無人実行の安全設計
フォールバックで実行が止まらなくなると、今度は「止まらないこと」自体がリスクになります。どのモデルに切り替わっても触ってほしくない領域は、permissions の deny ルールで固定しておきます。最近 glob 対応が入ったので、パターンでまとめて書けるようになりました。
// .claude/settings.json — モデルが何であれ越えてはいけない線
{
"permissions" : {
"deny" : [
"Write(~/ops/secrets/**)" ,
"Bash(rm -rf*)" ,
"Read(**/*.env)"
]
}
}
fallbackModel と deny ルールは対になる設計だと感じています。前者が「実行を続けるための柔軟さ」、後者が「何が実行されても守られる固さ」です。無人実行の許可設計全般についてはClaude Code Skill を無人で動かす設計 — 許可ダイアログで止まらない3つの実装パターン で詳しく書いています。
6/15 の月次クレジット移行とフォールバックの関係
headless の claude -p は6月15日から API レート準拠の月次クレジットへ移行します。フォールバック構成で見落としやすいのは、段によって消費の重みが変わる ことです。発動頻度が低いとはいえ、クレジット残量がぎりぎりの月末にプライマリの重いモデルで回り続ける構成は、フォールバック以前に実行自体が止まるリスクを抱えます。
私は月次クレジットの残量を週次で確認し、月末週だけプライマリを一段軽いモデルに入れ替える運用を試しています。工程ごとの委譲の見直しは月次クレジット移行を前に、自動パイプラインの工程配分を見直した記録 にまとめたので、コスト側の設計はそちらが参考になるはずです。
次にやること
手順は3つだけです。
直近30日のログから、無人実行が空振りした日数を数えます。月1回でもあれば移行する価値があります
settings.json に fallbackModel の配列を追加し、品質の階段を意識して3モデルを並べます
stream-json による実行モデルの記録を仕込み、最初のフォールバック発動日に品質差を一度だけ実測します
この3手順で、過負荷の朝に怯えない無人実行がひとまず完成します。同じように朝のバッチの空振りに悩んでいる方の参考になれば幸いです。