
NVIDIA LLM Router を自分のペルソナに合わせて再訓練してみた(訓練編)
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。
基礎編 で NVIDIA LLM Router v3 を一通り動かしてみたあと、しばらく自分の作業で default checkpoint を使ってきました。Nemotron / GPT-OSS / Qwen / GPT-5 / Opus 系の 9 モデル汎用 pool(v1-9models-qwen08b.yaml)が標準で配布されていて、これだけで routing 判定はちゃんと動きます。
しばらく触ってみて気になってきたのが、この default pool が「自分の好みからは少しずれている」という点でした。最新世代の Opus 4.8 / Sonnet 4.6 / Gemini 3.5 Flash がまだ入っていなかったり、普段呼びたい新興系のモデル群(DeepSeek V4 / Qwen 3.7 / Kimi K2.6 / GLM 4.7 など)とラインナップが噛み合わなかったり。MLP の訓練データも汎用前提で組まれているので、自分の使い方(重い設計議論と軽い壁打ちが両極に分かれる傾向)にチューニングされているわけでもなさそうです。
幸い、NVIDIA LLM Router v3 には collect → train → evaluate の 3 ステップで自分用 checkpoint を作り直すリソースが公式に揃っています。pool YAML を自分好みに組み直して、普段投げている質問を訓練データに落とせば、その人専用の routing をゼロから組める。せっかく仕組みが用意されているので、自分のペルソナに寄せて一度組み直してみよう、というのが本記事のスタート地点です。
具体的には、自分のペルソナ(普段の Claude Code / Codex の使い方、執筆中のブログ記事、Hermes Agent や NemoClaw 周りの設計議論など)を 480 問の訓練データに落として、新しい 9-model pool(最新 Opus 4.8 / Sonnet 4.6 / Gemini 3.5 Flash 込み)の checkpoint をゼロから組み直してみました。品質維持型の routing(重い質問はちゃんと Opus、軽い質問は安いモデルへ)という方向で設計したところ、Opus 4.8 の採用率を 43.1% まで引き上げつつ、結果として軽中量質問では 98〜99% 台のコスト削減(最大 99.3%)を実証できる構成にたどり着きました。
なお LLM のコスト最適化だと、MindStudio の「Run Local AI Models with Claude Code」解説のようにタスク種別ベースのルールルーティングで済ませる流れもあります。今回 NVIDIA LLM Router v3 を選んだのは、同種タスク内の品質×価格トレードオフ(同じコード生成でも依存関係が深いものは Opus、軽い one-liner は gpt-oss-120b)を Qwen3.5-0.8B エンコーダ + PCA + MLP に学習させたかったため。補助タスクをローカルに逃したいだけならルール routing で十分なので、用途で使い分けるのが現実的かなと思っています。本記事では「Claude Code を Opus 前提で使ってきた人が、品質を落とさずに routing 化したい」というユースケースに絞って話を進めます。
Pool 再設計の方針
旧 5-model 構成での失敗から
最初に組んだ 5-model pool(Local + gpt-oss-120b + Kimi K2.7 + GLM 5.2 + Opus 4.6)では、5 ユースケース投入で軽量以外の 4 件が全部 gpt-oss-120b に集約されてしまいました。原因を眺めてみると pricing のラダーがいびつで、gpt-oss-120b ($0.05 / $0.21) と次に高い kimi ($0.74 / $3.50) のあいだに 14 倍のコストの崖が空いていたんです。Opus との AUC 差は 5 ポイント程度しかなく、MLP は経済性込みで「中量帯は gpt-oss-120b で十分」と素直に学習してくれました。
これは LLM Router を組むときに地味に効いてくる罠で、隣り合うモデル同士のコスト差が大きすぎると MLP は安い側に寄せがちです。1.5〜3 倍程度の刻みで連続性を持たせるのが品質維持型の routing を作るうえでの基本作法だと、ここで体感しました。
9-model ラダー
そこで組み直した最終構成が以下です。
| Slot | Model | OpenRouter slug | output $/M | 隣接倍率 | 役割 |
|---|---|---|---|---|---|
| 1 | Nemotron 3 Nano 30B-A3B (Local) ⁽*⁾ | openrouter/nvidia/nemotron-3-nano-30b-a3b |
0 ⁽*⁾ | — | ローカル無料アンカー、軽量 reasoning |
| 2 | DeepSeek V4-Flash | deepseek/deepseek-v4-flash |
0.18 | — | 軽量汎用(新興系 1 / コード / 長 ctx 1M) |
| 3 | GLM 4.7 Flash | z-ai/glm-4.7-flash |
0.40 | 2.22x | 軽量(新興系 2 / 中国語精度 / 202k ctx) |
| 4 | DeepSeek V4-Pro | deepseek/deepseek-v4-pro |
0.87 | 2.18x | 中量推論(新興系 3、コスパ / 長 ctx 1M) |
| 5 | Qwen 3.7-Plus | qwen/qwen3.7-plus |
1.28 | 1.47x | 中量汎用(新興系 4 / 最新世代 / 長 ctx 1M) |
| 6 | Kimi K2.6 | moonshotai/kimi-k2.6 |
3.50 | 2.73x | 中重量汎用(新興系 5、汎用性高い) |
| 7 | Gemini 3.5 Flash | google/gemini-3.5-flash |
9.00 | 2.57x | 中重量 reasoning + マルチモーダル独自経路 |
| 8 | Claude Sonnet 4.6 | anthropic/claude-sonnet-4.6 |
15.00 | 1.67x | 重量品質ミドル、Opus への橋渡し |
| 9 | Claude Opus 4.8 | anthropic/claude-opus-4.8 |
25.00 | 1.67x | 最重量、品質アンカー |
⁽*⁾ Slot 1 は連載の再現性を上げるため OpenRouter Nano paid 版 ($0.05 / $0.20) で動かしていますが、pool YAML の cost_per_m_*_tokens は 0 のまま維持しています。ローカル環境でモデルを動かせる環境を持っている方は litellm_model を openai/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 + api_base: http://localhost:8000/v1 に差し替えれば、Slot 1 をローカル運用に切り替えられます。MLP のラベルは local-nemotron-3-nano 据え置きで、再 train なしでも routing 判定はそのまま流用できます。
なお slug 表記が provider ごとにバラついている(Anthropic / Google / Moonshot / Z.AI はドット形式、DeepSeek / NVIDIA はハイフン形式)のは OpenRouter 側の仕様です。本表は OpenRouter /api/v1/models で実体確認した slug をそのまま転載しています。基礎編の default pool 表ではハイフン形式の Anthropic slug (anthropic/claude-opus-4-6) を載せていますが、これは default config v1-9models-qwen08b.yaml が Part 1 公開時の表記そのままだったためです。
設計のポイントは隣接倍率を最大 2.73x に抑えたところです。旧 5-model の 14 倍ジャンプから 5 倍ほど緩和して、MLP が「コストの崖を理由に一段下のモデルに寄せる」現象を防ぐ狙いです。
※ Mermaid 内の表示は output 単価のみ。input 単価は前出の表を参照。
2026 世代ベンチの実態
最終構成を決める前に、9 モデルそれぞれの実力を整理しておきたかったのですが、2026 年世代のフロンティアモデルは、旧来の MMLU / HumanEval / MBPP / Aider polyglot を公式ベンチから外しているようでした。Anthropic / DeepMind / Z.AI / Moonshot / Qwen がほぼ全社、公式申告から外しています。指標としては GPQA Diamond / AIME / HLE / SWE-bench Verified / MMMU-Pro / LiveCodeBench v6 / Arena Elo の 7 軸あたりに集約されているのかな、というのが実態でした。
実機検証用に GPQA Diamond と Arena Elo を中心に並べ直したのが以下です(執筆時 2026-06 時点、出典は各社 2026-06 公式リリースおよび lmarena.ai)。
| モデル | GPQA Diamond | SWE-bench Verified | MMMU-Pro | Arena Elo |
|---|---|---|---|---|
| Nemotron 3 Nano 30B-A3B | 71.9 | 38.8 | N/A | N/A |
| DeepSeek V4-Flash | 87.4 | ~74 | (text) | N/A |
| GLM 4.7-Flash | 75.2 | 59.2 | (text) | N/A |
| DeepSeek V4-Pro | 90.1 | 80.6 | (text) | 1467 |
| Qwen 3.7-Plus | 90.3 (vendor) | ~78 | 未公開 | N/A |
| Kimi K2.6 | 90.5 | 80.2 | 79.4 | 1466 |
| Gemini 3.5 Flash | 90.4 (親) | 78 (自社) | 83.6 | 1480 |
| Claude Sonnet 4.6 | 89.9 | 79.6 | 74.5 | 1467 |
| Claude Opus 4.8 | 93.6 | 88.6 | N/A | 1512 |
※ (vendor) はベンダー自己申告値、(親) は親モデル(Gemini 3 Pro 系)の同等値を流用、(自社) は自社内部評価。
並べてみて驚いたのが、新興系上位 3 機種(V4-Pro / Qwen 3.7-Plus / Kimi K2.6)が Sonnet 4.6 と GPQA / Arena でほぼ同点ということです。価格差は V4-Pro $0.87 vs Sonnet $15 で 17.2 倍あるので「Sonnet を pool から外す」議論もあり得ましたが、9 slot 想定で動かす運用都合と、Anthropic 系の長文整合性や安全性ニュアンスで差が出たら拾いたい、という判断で残しました。後述の通り、結果として Sonnet 4.6 は 23.1% の採用率で「中重量品質ミドル」の役割をしっかり担ってくれることになります。
Gemini 3.5 Flash の役割とマルチモーダル経路の確保
Pool 設計でもうひとつ悩ましかったのが Gemini 3.5 Flash の扱いです。マルチモーダル(MMMU-Pro 83.6)が際立って強い反面、NVIDIA LLM Router v3 の Prefill Router は設計上テキストオンリー(Qwen3.5-0.8B エンコーダで prompt の hidden states を取る作り)で、画像 / 音声 / 動画 block は判定に効きません。「以下の画像を分析してください」のような短い prompt だと、routing が DeepSeek や Local Nemotron を選んだ瞬間に OpenRouter から 404 No endpoints found that support input image が返ってきます。
ここはテキスト系の役割(Google grounding / 図解構造化 / Arena Elo 1480 の汎用品質)を訓練データで担保しつつ、マルチモーダル経路は litellm adapter 層に薄い shim を追加して別レイヤーで取りに行く設計にしました。9-model pool 用の静的な capability lookup(image → {gemini-3.5-flash, sonnet-4.6, opus-4.8} / audio・video → {gemini-3.5-flash})で動かしていて、実機 probe では 10×10 PNG / 1 秒 WAV / 1 秒 MP4 がいずれも Gemini 3.5 Flash に振られ、画像・音声・動画の解析が返ってきます。この shim は upstream に PR #34 として proposal 投げているので、興味のある方はそちらもどうぞ。CLIP ベースの本格的なマルチモーダル routing(NVIDIA AI Blueprint v2 の Auto-Router)を組みたい場合は別の話になりますが、「画像 / 音声 / 動画リクエストはちゃんと対応 upstream に流す」という実用ニーズはこの薄い shim で十分カバーできるかなと思っています。
Collect 質問データ設計
全体の構成
訓練データは合計 480 問で、内訳は次の通りです。
| カテゴリ | 件数 | 主役モデル |
|---|---|---|
| 個人ペルソナ(curated 重量寄り 40 + 新規重量 60) | 100 | 重量 60 は Sonnet / Opus 寄り |
| Opus 優位質問(重量シグナル補強用) | 150 | Opus 4.8 一強 |
| Gemini 優位質問(テキスト系のみ) | 30 | Gemini 3.5 Flash 独自 |
| 軽量・中量質問(curated 軽量 60 + generated 40) | 100 | Nemotron / V4-Flash / GLM |
| 公開データセット(MMLU 30 + HumanEval 15 + GSM8K 15 + DollyJA 40) | 100 | bias 回避 |
Opus 優位 150 は「新興系の経済合理性に MLP が押し負けないように、Opus でしか正解しない重量シグナルを濃く入れる」狙いで、本構成の核です。Gemini 優位 30 はテキスト系の独自性(grounding / 図解 / 構造化)を学習させる補強。Sonnet 優位質問は意図的に入れていません(新興系上位と GPQA 同点で、差別化質問を作っても効果が読みにくいため自然分布に任せる方針)。重量寄り 210 問(44%)+ Gemini 寄り 30 問(6%)という比率になります。
個人ペルソナ 100 問の作り方
自分が普段 Claude Code に投げている質問の傾向を一度言語化しておくと、Opus 優位質問とのバランスが取りやすくなります。
| サブカテゴリ | 件数 | 内容例 |
|---|---|---|
| 既存(記事執筆 / Codex / tailscale / Hermes / NemoClaw 等) | 40 | 旧版 curated から重量寄り 40 を選別 |
| 大規模リファクタリングプラン | 15 | 「50 kLoC monolith を DDD + 非同期化」のような multi-axis、コード文脈長め |
| インフラ構築プラン | 15 | 「マルチリージョン Kubernetes + IaC + 監視設計」「DGX Spark + on-prem GPU クラスタ構築」など |
| PR レビュー | 15 | 実コード diff(50-200 行)+ レビュー観点(セキュリティ / 性能 / 保守性) |
| Issue トリアージ | 15 | 「再現条件不明な flaky test を切り分け」「production incident の root cause 分析」など |
後半の 60 問(リファクタ / インフラ / PR / Issue)は明確に重量質問なので「重量=Opus」のシグナルとして強く効きます。
Opus 優位質問 150 の作り方
Opus 4.8 が新興系上位を明確に上回る場面を特性別に整理し、150 問の内訳を組みました。
| 特性 | 件数 | 例 |
|---|---|---|
| 哲学・倫理ジレンマ | 30 | Trolley / Frankfurt / 安楽死など複数原則の衝突 |
| 長文整合性 | 25 | 4000-5000 字の文脈で登場人物の発言と過去設定の矛盾を指摘 |
| 多段 reasoning | 25 | ベイズ更新、因果推論、線形計画の双対 |
| リファクタリング | 20 | 50 kLoC monolith を DDD で再設計、移行リスクとロールバック戦略込み |
| 創作・ペルソナ模倣 | 20 | 夏目漱石風、特定のキャラクター口調を長く維持 |
| 制約付き判断 | 15 | 10+ の制約を全部守らせるコード・設計 |
| 文化的繊細さ・敬語 | 15 | 日本のビジネス慣習で上司に丁寧に反論する英文 |
詳しい質問例は GitHub の nvidia-llm-router-v3-training/data/questions-opus-favor-150.txt に置く予定です。質問を作る側で気をつけたのは「抽象的にしすぎない」点で、「○○について説明してください」では新興系上位と差が出にくく、「○○の X 側面を Y と Z の制約のもとで N 段階に分けて 600 字で論じてください」と制約を含めると Opus の真価が出やすくなります。
Gemini 優位質問 30 の作り方(テキスト系のみ)
Gemini 3.5 Flash の独自性を MLP に学習させたい部分です。マルチモーダル経路自体は前述の adapter shim で別レイヤーに切り出したので、訓練データの方は MLP に学習させたいテキスト系のシグナル(grounding / 図解 / 構造化)に絞って 30 問用意しました。
| カテゴリ | 件数 | 内容例 |
|---|---|---|
| Google 検索 grounding 必須 | 10 | 「2026 年 6 月時点の AWS Bedrock Claude Sonnet 4.5 のオンデマンド料金を引用付きで」 |
| Mermaid 図解出力 | 8 | 「NVIDIA LLM Router v3 のリクエストフローを Mermaid sequenceDiagram で」 |
| 多段構造化 markdown | 7 | 「Kubernetes Operator パターンを 5 章構成で、テーブルとコールアウト併用で」 |
| 引用付き要約 | 5 | 「Anthropic Constitutional AI を [1][2][3] 形式で 500 字要約」 |
「2026 年 6 月以降」「最新の」など、grounding が必要なキーワードを明示しているのがポイントです。Prefill Router 自体は呼び出し前のテキストでしか判定できませんが、これらのキーワードが乗っているプロンプトには「Google 検索 grounding を要する性質のテキスト」という特徴量が乗るので、MLP が間接的に Gemini を引きやすくなる、というシグナルとして効きます。実際に呼び出された Gemini 側で grounding が走る、という流れです。
公開データセット 100 問のサンプリング
bias 回避用に MMLU 30 + HumanEval 15 + GSM8K 15 + DollyJA 40 の組み合わせで 100 問サンプリングしています。事前に構築した 300 問プールから決定的に(seed 固定で)抽出していて、再現性が取れる形にしました。
訓練パイプライン実行
Step 1. 環境前提と疎通確認
DGX Spark(aarch64、GB10、128GB UMA)+ vLLM Nemotron 3 Nano 30B-A3B NVFP4(ローカル)+ OpenRouter(残り 8 モデル)の構成です。probe-models.py で各モデルに Reply with the single word 'pong' を投げて HTTP 200 / latency / cost を確認します。
# 本記事の検証スクリプト一式はお手元の作業ディレクトリに合わせて読み替えてください
cd workspace/blog/scripts/nvidia-llm-router-v3-training
uv run --with httpx --with pyyaml scripts/probe-models.py \
--config configs/my-pool-9models.yaml
9/9 OK になれば次へ進めます。
Step 2. dry-run(10 問 × 9 モデル)
本番 collect の前に 90 calls で dry-run を回し、コストと latency を見積もります。ここで thinking 周りの罠を 2 つほど踏みましたが、extra_body.reasoning.enabled: false で thinking を切る方針 + Gemini 3.5 Flash だけ max_tokens: 2048 の例外、という形で 3 試行目に 9/9 全 OK 構成が固まりました。実コストは事前概算 $45-55 の 1/4、$11.27 で 480q に振り切れる見込みです。
dry-run 3 試行と thinking 罠の詳細
最初に thinking on のまま動かしたところ、GLM 4.7-Flash / Kimi K2.6 / Gemini 3.5 Flash / DeepSeek V4-Pro が max_tokens=1024 を thinking で全部消費して実回答が空、という現象が多発しました。reply_len=0 のレコードが大量に出てきて、これは品質シグナルとしては使えません。
そこで extra_body.reasoning.enabled: false を OpenRouter 6 モデルに統一で入れる方針にしたところ、latency が劇的に変わりました。
| モデル | thinking on | thinking off | 倍率 |
|---|---|---|---|
| GLM 4.7-Flash | 29.75s | 5.69s | 5.2x |
| Qwen 3.7-Plus | 34.03s | 2.94s | 11.6x |
| Kimi K2.6 | 13.40s | 3.51s | 3.8x |
ところがここでもう一つ罠が待っていました。Gemini 3.5 Flash は mandatory reasoning で、reasoning: { enabled: false } を渡すと HTTP 400 を返します("Reasoning is mandatory for this endpoint and cannot be disabled.")。Gemini だけは thinking on のまま max_tokens: 2048 に上げて実回答用 tokens を確保する例外対応にしました。
3 試行のコスト推移は次の通りで、max_tokens=1024 + thinking off の組み合わせがここまで省コストとは思っていなかったので、地味にありがたい発見でした。
| 試行 | コスト(90 calls) | 480q 見積もり |
|---|---|---|
| 1st (thinking on) | $0.265 | $12.70 |
| 2nd (thinking off 全モデル) | $0.148 | $7.12 |
| 3rd (thinking off + Gemini 例外) | $0.235 | $11.27 |
Step 3. judge=vote の罠と judge=llm への切替
dry-run 完了後、judge=vote のまま本番 collect を投入したら per-model accuracy が「Local Nemotron 99.8% / 他全機種 0-0.2%」という目を疑う数字に。原因は collect.py の _judge_vote のロジックで、各モデルが違う自然言語応答を返すと Counter が同数になり insertion order で pool 先頭の Local Nemotron が常に「正解」扱いされる、という実装でした。MMLU 系の選択肢一致なら成立しますが、生成タスクには使えません。
そこで judge=llm + Sonnet 4.6 judge に切り替えて再 collect(7h15m)。Sonnet 自身が judge なので自己採点バイアスは残りますが、Opus が頂点、新興系上位 3 機種が団子、Local / GLM が下位、という妥当な分布が出てきて、品質シグナルとして使える状態になりました。
| モデル | accuracy (Sonnet judge) | avg tokens |
|---|---|---|
| Opus 4.8 | 69.17% | 738 |
| Sonnet 4.6 ⁽*⁾ | 65.2% | 709 |
| DeepSeek V4-Flash | 63.1% | 724 |
| DeepSeek V4-Pro | 61.0% | 718 |
| Kimi K2.6 | 60.0% | 946 |
| Qwen 3.7-Plus | 56.5% | 741 |
| Gemini 3.5 Flash | 54.6% | 1,719 |
| GLM 4.7-Flash | 39.0% | 742 |
| Local Nemotron | 38.3% | 940 |
⁽*⁾ Sonnet 4.6 が judge を兼ねているため自己採点バイアスあり
judge=vote バグの実装詳細と再 collect コマンド
最初の judge=vote 結果は次の通りで、ローカル無料モデルが他 8 機の最強モデルを全部踏み倒して 99.8% 勝利するという、明らかにロジック側の問題でした。
| モデル | accuracy |
|---|---|
| Local Nemotron | 99.8% |
| DeepSeek V4-Flash | 0.0% |
| GLM 4.7-Flash | 0.2% |
| DeepSeek V4-Pro | 0.2% |
| Qwen 3.7-Plus | 0.2% |
| Kimi K2.6 | 0.0% |
| Gemini 3.5 Flash | 0.0% |
| Claude Sonnet 4.6 | 0.0% |
| Claude Opus 4.8 | 0.0% |
collect.py の _judge_vote を読みに行くと、原因がすぐ判明しました。
def _normalize(text: str) -> str:
return " ".join(re.split(r"\s+", text.strip().lower()))
def _judge_vote(outputs: list[str]) -> str:
normalized = [_normalize(o) for o in outputs]
counts = Counter(normalized)
return counts.most_common(1)[0][0]
各モデルが異なる自然言語応答を返すと normalized 後も全部 unique な文字列になります。Counter は同数の場合 insertion order を尊重するので、最初に登場した model(Local Nemotron が pool YAML の Slot 1)の応答が常に majority になるわけです。Local の応答と一致するモデルだけが「正解」と判定されて、それ以外は全部 0%。
再 collect のコマンドは次の通り(judge 並列化 patch を当てて max_workers=5、OpenRouter の rate limit に当たらない上限を狙う)。
model-router collect \
--config configs/my-pool-9models.yaml \
--questions data/questions-9models.txt \
--judge llm \
--judge-model openrouter/anthropic/claude-sonnet-4.6 \
--output data/collected-my-pool-9models.csv
他のユースケースで自分色 routing を作る方は同じ落とし穴に注意してみてください。MMLU 系(A/B/C/D の選択肢が文字列で返る)ならそのまま使えますが、生成タスクなら judge=llm を選ぶのが安全です。
Step 4. train(5 分で完了)
model-router train \
--config configs/my-pool-9models.yaml \
--data data/collected-my-pool-9models.csv \
--output-dir checkpoints/my-router-9models/
train は accelerate 入り、9 dim MLP、Qwen/Qwen3.5-0.8B encoder で実行されます。Stage 1〜6(Load → Extract → PCA → MLP ensemble → Calibrate → Save)まで通って 5 分 1 秒で完了しました。
完成した checkpoint の per-model AUC は次の通りです。
| モデル | AUC (Shared trunk ensemble) |
|---|---|
| Qwen 3.7-Plus | 0.9430(9 モデル中最高) |
| Gemini 3.5 Flash | 0.9250 |
| Claude Sonnet 4.6 | 0.9162 |
| Claude Opus 4.8 | 0.9103 |
| DeepSeek V4-Flash | 0.9065 |
| GLM 4.7-Flash | 0.8937 |
| Local Nemotron | 0.8837 |
| DeepSeek V4-Pro | 0.8757 |
| Kimi K2.6 | 0.8517 |
全モデル AUC 0.85 以上で、想定していた目標値(per-model AUC 0.5-0.9)を上回りました。default checkpoint の p_max が 0.07-0.10 だったのと比べると、訓練データを揃えるだけでこれだけ MLP の予測が締まるんだなと。
Step 5. evaluate(数字で見る品質改善)
model-router evaluate \
--config configs/my-pool-9models.yaml \
--checkpoint checkpoints/my-router-9models/prefill_router.pt \
--data data/collected-my-pool-9models.csv \
--output results/eval-my-router-9models.json
| 指標 | 値 |
|---|---|
| Oracle accuracy | 0.9250 |
| Best single model | 0.6917(Opus 4.8 単体) |
| Router accuracy (argmax) | 0.7937(+10.20pp vs Opus 単体) |
| Headroom captured | 43.7% |
「Opus 単体で 480 問解いた場合の accuracy 69.17% と比べて、router は 79.37% と 10 ポイント以上改善」というのが定量的に見える数字です。Oracle accuracy(毎回最善のモデルを選んだ場合の上限)92.5% との差分のうち、router が 43.7% を埋めている計算になります。
evaluate の argmax 分布も眺めておきましょう。
| モデル | 選ばれた数 | 比率 | 選ばれた時の正解率 |
|---|---|---|---|
| Claude Opus 4.8 | 207 | 43.1% | 64.7% |
| Claude Sonnet 4.6 | 111 | 23.1% | 99.1% |
| Gemini 3.5 Flash | 59 | 12.3% | 98.3% |
| DeepSeek V4-Pro | 33 | 6.9% | 69.7% |
| Qwen 3.7-Plus | 31 | 6.5% | 90.3% |
| DeepSeek V4-Flash | 23 | 4.8% | 82.6% |
| Kimi K2.6 | 16 | 3.3% | 56.3% |
| Local Nemotron | 0 | 0.0% | — |
| GLM 4.7-Flash | 0 | 0.0% | — |
Opus 4.8 が 43.1% で最大採用、その次に Sonnet 4.6 が 23.1% で 99.1% の正解率を叩き出しています。Gemini 3.5 Flash も 12.3% で 98.3%。設計時に「Sonnet は選ばれることがあれば程度」と言っていたのが、ふたを開けてみると中重量品質ミドルの主役として安定して選ばれているのが意外でした。
Opus 64.7% が Sonnet 99.1% より低く見えるのは直感に反しますが、これは「argmax で選ばれた集合の難易度が違う」だけです。Opus が argmax で選ばれる 207 問は MLP が「Opus でないと P(correct) が高くならない」と判定した難問クラスタなので、Opus でも judge から正解と認められるハードルが上がって 64.7% に落ちます。Sonnet / Gemini の 99.1% / 98.3% は「Opus でなくても解ける」と MLP が判定した比較的易しい質問が振られた結果。前述の Opus 単体 69.17% は 480 問全体での集計なので、ここでの 64.7%(207 問サブセット)とは集合が違う点だけ補足しておきます。
逆に Local Nemotron と GLM 4.7-Flash は argmax だと 0% です。accuracy 自体は 38-39% あるのですが、tolerance=0 の argmax 判定だと「同じ質問で他のモデルがより高い P(correct) を出してしまう」と必ず負ける構造なので、訓練データが重量寄り 44% に振れているこの構成では出番がなくなります。tolerance を 0.2 まで上げれば軽量質問でちゃんと拾われるようになります。
ペルソナ最適化 checkpoint の振る舞い
tolerance 別の routing 実演(bench_persona_3tol)
bench_persona_3tol.py は 5 種類の代表質問(雑談 / コード生成 / 技術説明 / 数学証明 / 哲学)を、tolerance を 3 段階(0.05 / 0.10 / 0.20)で投げて、それぞれの routing 分布とコストを比較するスクリプトです。新 checkpoint を model-router serve --port 8204 で立てて流してみました。
| tolerance | 採用された主なモデル | 合計コスト(5 問) | 削減率(All-Opus 比) |
|---|---|---|---|
| 0.05 | deepseek-v4-flash 一強 | $0.00055 | 99.3% |
| 0.10 | deepseek-v4-flash + glm-4.7-flash | $0.00080 | 99.0% |
| 0.20 | 軽量帯 + Local Nemotron + glm | $0.00133 | 98.3% |
| (All-Opus) | Opus 4.6 直叩き(bench スクリプト固定) | $0.07664 | 基準 |
(All-Opus) の行は bench スクリプト側の比較基準モデルが anthropic/claude-opus-4-6 で固定されているのをそのまま流用したので、新 pool の Slot 9 (Opus 4.8) ではなく旧 Opus 4.6 になっています。output 単価は近いので比較基準としては問題ありませんが、桁を厳密に合わせたい場合は bench スクリプトの OPUS_MODEL を claude-opus-4.8 に書き換えてください。
bench の 5 質問は全部「軽量〜中量」なので、Opus が出てこないのは想定通りです。tol=0.05 でも軽量帯の deepseek-v4-flash 一強になるのは、evaluate (480 質問 / argmax) で V4-Flash が 4.8% しか選ばれていなかったのと比べるとギャップがありますが、これは bench の 5 問が evaluate の難易度分布の左裾(軽量寄り)に偏っているためです。tolerance=0.20 まで上げると Local Nemotron も拾われるようになって、軽量質問は実質コスト 0 にできます。
routing 分布の解釈と Sonnet の役割
evaluate(480 問 / argmax)と bench(5 軽中量質問 / tolerance あり)で routing の挙動がまったく違って見える、という点を整理しておきます。
| 評価軸 | 環境 | 主採用モデル | 解釈 |
|---|---|---|---|
| evaluate | 480 質問 / argmax (tol=0) | Opus 43.1% + Sonnet 23.1% + Gemini 12.3% | 品質維持型 routing |
| bench_persona_3tol | 5 軽中量質問 / tol 0.05-0.20 | deepseek-v4-flash + glm-4.7-flash + Local | コスト削減型 routing |
routing が質問の難易度と tolerance に応じて自動で切り替わる、という設計の理想形がそのまま実現している状態です。重量質問は Opus / Sonnet で品質を守り、軽量質問は軽量モデルでコストを削る、というメリハリが効いています。
設計段階では「Sonnet は新興系上位(V4-Pro / Qwen 3.7-Plus / Kimi K2.6)と GPQA 同点なので、訓練データで差別化質問は入れない、選ばれることがあれば程度」というスタンスでしたが、evaluate では 23.1% 採用 / 正解率 99.1% という結果が出ました。argmax (tol=0) にはコストが入らないので「安いから選ばれた」では説明できず、考えられるのは (1) judge に Sonnet 4.6 を使った自己採点バイアスで Sonnet の正解ラベルが底上げされ MLP の Sonnet P(correct) が高めに学習された、(2) 選ばれた 111 問では MLP の Sonnet 予測が実際に Opus を上回っていた、のいずれかです。別 judge(GPT-5.4 や Gemini 3.5 Pro)で再 collect しないと切り分けはできませんが、結果として Sonnet が中重量品質ミドルとして安定して機能している事実は変わらず、「品質維持しつつコストも下がる」設計の理想形が定量的に効く形になっています。
Local Nemotron の採用率を上げたい場合
evaluate の argmax だと Local Nemotron は 0%、bench tol=0.20 でやっと 2/5 採用、という結果でした。「もっと壁打ち系で Local に振りたい」場合は、(1) request 単位で extra_body.routing.tolerance: 0.20 を body に入れて tolerance を上げる(最も手軽、再起動不要)、(2) --models で pool を軽量帯だけに絞ったサーバを別 port で立てる、(3) 訓練データに雑談 / 短文翻訳 / 簡易メモ整理を 50〜100 問追加して再 collect / re-train する、の 3 段階で調整できます。実用上は (1) が一番手軽で、Hermes Agent の profile 別に「雑談用は tolerance=0.20、Claude Code 経由のコード生成は 0.05」と使い分けると 1 つの routing service で複数用途を回せます。Hermes 側で動かしているフローと連動させる話は実践編で扱う予定です。
旧 5-model checkpoint との対比で見えるコスト崖
参考として、Pool 再設計の冒頭で触れた旧 5-model pool(Local Nemotron / gpt-oss-120b / Kimi K2.7 Code / GLM 5.2 / Opus 4.6)の routing 分布も載せておきます。5 ユースケースを投入すると軽量以外の 4 件すべて gpt-oss-120b に集約されていて、p_max 0.79-0.94 と確信度は高いものの、Opus / Kimi / GLM は永遠に呼ばれない構造になっていました。
| ユースケース | 選ばれたモデル | p_max |
|---|---|---|
| 雑談(軽量) | local-nemotron-3-nano | 0.94 |
| コード生成(中量) | gpt-oss-120b | 0.87 |
| 技術説明(中量) | gpt-oss-120b | 0.83 |
| 数学証明(中重量) | gpt-oss-120b | 0.79 |
| 哲学議論(重量) | gpt-oss-120b | 0.81 |
新 9-model ラダーと並べると、原因の 14 倍ジャンプが視覚的にもはっきり見えます。
※ Mermaid 内の単価はすべて output 単価で統一しています。input 単価は前出の各表を参照してください。
自分色 routing を組む場合の基本作法として「隣接モデル間のコスト倍率は 3 倍以内、できれば 2 倍前後に揃える」を最初に意識すると、こうしたコストの崖を避けやすくなります。
まとめ
NVIDIA LLM Router v3 の default checkpoint を一通り触ってみて感じた「自分の好みと少し違うかも」というところから、9-model pool をゼロから組み直して 480 問でペルソナ訓練を回すところまで一気に進めてみました。結果として Opus 4.8 採用率 43.1% で品質を守りつつ、軽中量質問では tolerance 0.05-0.20 で 98〜99% 台のコスト削減(最大 99.3%)まで持っていけて、品質維持型 routing の手応えが見えてきました。途中で踏んだ罠(thinking on で max_tokens を全消費、judge=vote の insertion order バグ、Gemini の mandatory reasoning、コスト崖の 14 倍ジャンプ)は他のユースケースで自分色 routing を組む方の参考になればと思って具体的に書き残しています。とくに pool 設計の「隣接モデル間のコスト倍率は 3 倍以内、できれば 2 倍前後」は、最初に意識しておくと良い基本作法かなと思っています。
「作って終わり」にしないためには、checkpoint を運用に乗せたあとの観測も大事になってきます。本記事で使っている routing service には Langfuse callback と routing decision の stdout ログを薄く挟んでいて、どのモデルが何回呼ばれてどんなコストになっているか、tolerance を変えたときに分布がどう動くかを後追いで確認できるようにしています。このあたりのオブザーバビリティの話は、別の記事でゆっくり扱いたいなと考えています。
Reference implementation only と書かれていてもここまで遊べる v3、訓練を経て自分のペルソナにすっと馴染んでいく感触は、触っていて地味に楽しいですね。
参考リンク
NVIDIA LLM Router
- NVIDIA-AI-Blueprints/llm-router (GitHub) — 本記事で扱った v3 ブランチを含むリポジトリ
- NVIDIA LLM Router で LLM の用途別使い分け環境を構築してみた(基礎編) — 連載 Part 1、default checkpoint の挙動と OpenAI 互換 endpoint まわりの基礎
本記事の検証で投げた upstream PR
- PR #32: forward all OpenAI-compatible fields to upstream —
toolsなどの OpenAI 互換フィールドが drop されていた問題の修正提案 - PR #33: translate upstream errors to OpenAI-compatible responses — upstream 4xx エラーが 500 として返っていた問題の修正提案
- PR #34: route image/audio/video requests to capability-matched models — マルチモーダル content を capability-matched モデルに振る adapter shim の提案
関連トピック
- OpenRouter — 本記事 pool の upstream に使った API gateway
- Langfuse — observability 用に LiteLLM callback で統合
- lmarena.ai — 本記事のベンチ表で参照した Arena Elo の出典







