ローカル LLM Fusion Router の組み方と判断ロジック
Qwen、LFM JP、GLM、VibeThinker を役割分担させ、候補生成、構造化 judge、outer 最終回答、 deterministic な検証で Fusion を組んだ流れを整理します。
何を作ったか
前回の比較では、ローカル LLM を単体で呼ぶ場合と、複数モデルを Fusion Router として組み合わせる場合を同じ課題で比べました。 この記事では、点数ではなく、その Fusion をどう組んでいるかを整理します。
やっていることは、単純な多数決ではありません。 複数モデルに同じ課題を投げ、候補を並べ、別の段階で候補の矛盾や抜けを分析し、最後に元の課題へ戻って最終回答を作ります。 その後、計算結果、文字数、並び順、JSON 形状、コード修正の形など、機械的に確認できる条件はスクリプトで採点します。
大事にしているのは、モデルを増やすことより責務を分けることです。 候補を出す役、候補を疑う役、日本語を整える役、最後に検証しながら回答する役を分け、どこで誤答が混ざるかを見えるようにしています。
用意したスキル群
ここでいうスキル群は、Codex の .codex/skills と、常駐ローカルモデルに割り当てた役割の両方を指します。
モデルそのものを「賢い順」に並べるのではなく、仕事の種類で分けています。
| 役割 | モデル | ポート | 使いどころ |
|---|---|---|---|
| primary analysis | Qwen3.6 27B | 8080 | 仕様理解、実装方針、候補の主軸、judge と outer |
| Japanese normalizer | LFM JP | 8081 | 日本語入力の整流、曖昧さの確認、最終文面の整え |
| non-Qwen critic | GLM-4.7-Flash | 8082 | Qwen とは違う観点からの反論、リスク発見 |
| fast critic | VibeThinker 3B | 8083 | 小さな反例、境界条件、テストケースの確認 |
この上に、Codex 側の運用スキルを置いています。
qwen-local-review は、ローカルモデルを補助役として使うための中心スキルです。
ここでは、用途ごとにモードを分けています。
Lightweight、Review、Implementation precheck、Fusion Router、Read-only Tool Executor、Japanese Normalization です。
普段は小さく Qwen だけ、批判が必要なら Qwen + GLM、日本語の揺れが問題なら Qwen + LFM、実験や高リスクの確認では VibeThinker も足す、という使い方です。
qwen-git-operations は、git 状態の読み取りや差分要約をローカルモデルへ補助させるためのスキルです。
ただし、ステージング範囲やコミット判断は Codex 側に残します。
Fusion Router 本体ではありませんが、ローカルモデルを「安い作業者」ではなく「限定された観察者」として扱うための境界を作っています。
現在の再現用ソースは、兄弟プロジェクト ../tyto-lab-assets/tools/local-llm に分離しました。旧LaunchAgentテンプレートは同プロジェクトの archive/local-llm に保存しています。
4 つのモデルは OpenAI 互換の /v1/chat/completions を出し、Qwen については Codex の Responses 経由で read-only tool executor として使える道も残しています。
ただし、Fusion の評価スクリプトでは、基本的に chat completions で候補を取り、必要な結果だけをスコアリングしています。
呼び分けの考え方
最初から全モデルを呼ぶわけではありません。 Fusion は待ち時間を買って精度を上げる手段なので、タスクが軽いなら単体 Qwen で十分です。
運用上は、まずタスクの境界を決めます。 計算、並び順、JSON 変換、コード修正のように正誤を機械的に確かめられるものか。 日本語コピーのように、必須語や文字数は機械的に見られるが、トーンは最後に人間が見るものか。 要件矛盾やレビューのように、複数観点があるものか。 ここを分けてから、呼ぶモデルを決めます。
目安は次の通りです。
| モード | 呼ぶモデル | 目的 |
|---|---|---|
| single | Qwen など 1 モデル | 軽い候補出し、低レイテンシの確認 |
| Qwen + GLM | Qwen、GLM | Qwen の見落としを別系統で批判する |
| Qwen + LFM | Qwen、LFM JP | 日本語の曖昧さや文面品質を整える |
| Qwen + LFM + GLM | Qwen、LFM JP、GLM | 実務寄りのレビューや要件確認 |
| all panel | Qwen、LFM JP、GLM、VibeThinker | 評価実験や、失敗コストが高い判断 |
比較記事で使った router_fusion_qwen_all は、最後の all panel です。
これは常用の推奨ルートではなく、上限を見るための実験構成です。
実行フロー
評価スクリプトでは、1 つの課題に対して次の順番で処理します。
- 対象タスクと対象パターンを決める。
- 必要なモデルが起動しているか preflight する。
- panel に入るモデルへ候補回答を並列で投げる。
- pattern ごとに
single、judge、router_fusionを実行する。 - 最終回答だけを取り出す。
- タスク専用 checker でスコアを付ける。
- JSON と Markdown レポートへ保存する。
候補生成は並列です。 Qwen、LFM JP、GLM、VibeThinker は別ポートの常駐サーバなので、同じ課題を同時に投げられます。 一方で、Fusion Router の judge analysis と outer answer は直列です。 outer は judge analysis を読んで最終回答を書くため、ここは順番を入れ替えられません。
途中停止に備えて、1 モデルの呼び出しや 1 pattern の評価が終わるたびに JSON を保存します。
--resume を付けて再実行すると、成功済みの候補や pattern は再利用します。
クラウドモデルを含む比較では、途中でレートリミットに当たっても再開できることが重要です。
Fusion Router の中心ロジック
Fusion Router は、次の 3 段階です。
panel responses
-> structured judge analysis
-> outer final answer
-> deterministic checker
panel は、各モデルが元の課題へ直接答える段階です。 ここでは、正しい候補と間違った候補をどちらも保存します。 候補段階では、誤答を消そうとしません。 あとで judge が比較できるように、どのモデルが何を言ったかを残します。
judge analysis は、候補を比較する段階です。 ここで重要なのは、judge に最終回答を書かせないことです。 judge には、合意点、矛盾、抜け、固有の気づき、盲点、検証が必要な項目、推奨ソースだけを JSON で返させます。
outer final answer は、元の課題と judge analysis だけを読みます。
候補の文章を丸ごと混ぜるのではなく、judge が構造化した分析を材料として使います。
そして、最終回答の answer にはユーザーへ返す本文だけを入れます。
候補名、監査ログ、自己訂正の跡は入れません。
擬似コードにすると、実装はだいたいこうです。
for (const task of selectedTasks) {
const candidates = await panel(task);
const analysis = await judge({
task,
candidates,
});
const final = await outer({
task,
analysis,
});
const answer = extractAnswer(final);
const score = task.score(answer);
persist({ task, answer, score });
}
この形にした理由は、直接 Fusion の失敗が分かりやすかったためです。 候補をそのまま合成する方式では、正しい答えを含んでいても危険です。 途中の誤った金額や選択肢を残しやすくなります。 採点側は「最初に出てきた金額」や「明示された選択肢」も見ます。 そのため、最終回答に監査ログや迷いが混ざると、正しい値を含んでいても減点されます。
Fusion Router では、judge が「どこが危ないか」を分析し、outer が「最終回答だけ」を作ります。 この 2 段階に分けることで、候補の誤答をそのまま本文へ混ぜにくくしています。
judge が見るもの
judge のプロンプトでは、特に次の条件を強くしています。
- 元の課題を正とする
- consensus を証拠にしない
- 計算、並び順、コード、JSON、硬い制約は独立検証の対象として扱う
- 最初に誤った答えを出して後から自己訂正した候補は信頼性を下げる
- 最終回答を書かず、分析だけを返す
出力スキーマも固定しています。
{
"consensus": ["..."],
"contradictions": ["..."],
"coverage_gaps": ["..."],
"unique_insights": [
{
"source": "qwen|lfm|glm|vibe",
"insight": "..."
}
],
"blind_spots": ["..."],
"verification_required": ["..."],
"recommended_sources": ["qwen|lfm|glm|vibe"],
"confidence": 0.0
}
この JSON は、答えではなく作業メモです。 outer が見るための材料であり、そのままユーザーに出すものではありません。
outer が見るもの
outer は、最終回答を書く役です。 ここでも judge analysis は参考情報に過ぎません。 元の課題の方を強く扱います。
outer のルールは次のようにしています。
- 元の課題と judge analysis から最終回答を書く
- judge analysis に矛盾がある場合、元の課題から検証できない値はコピーしない
- 計算、並び順、コード、JSON、硬い制約は最終回答前に確認する
answerには最終回答だけを入れるused_analysisとverification_performedに、使った分析と確認内容を残す
出力はこの形です。
{
"answer": "...",
"used_analysis": ["..."],
"verification_performed": ["..."],
"confidence": 0.0
}
もし outer が answer を持たない JSON や配列を返した場合は、スクリプト側で一度だけリトライします。
リトライ時は、前回の出力が schema に合っていなかったこと、answer を持つ JSON object を返すこと、監査ログを混ぜないことを明示します。
採点ロジック
最後の採点は LLM に任せていません。 課題ごとに JavaScript の checker を書いています。
請求額なら、最終金額 12,826 円が最初の金額として出ているか、割引、送料、税の順序が含まれているか、中間値が含まれているかを見ます。
JavaScript のバグ特定なら、sort 行を見ているか、降順に直しているか、最小修正が b.score - a.score かを見ます。
日本語コピーなら、90 文字以上 110 文字以下か、必須語をすべて含むか、禁止語を避けたか、静かな信頼感があるかを見ます。
並び順制約なら、最初に明示された選択肢が正しいか、理由が制約を説明しているかを見ます。
ここが重要です。 Fusion は最終回答の候補を作る仕組みであって、正しさの保証ではありません。 正しさを機械的に確認できるところは、別の checker に渡します。 モデル同士が一致しても、checker が落とすなら失敗です。
なぜ改善したのか
改善版で効いたのは、候補を増やしたことではありません。 効いたのは、最終回答を作る前に、何を検証すべきかを judge analysis へ明示し、outer がその検証を実際に踏んだことです。
前回の日本語制約コピーでは、Fusion Router は必須語と禁止語をかなり見ていました。
しかし、最終回答が 58 文字になり、90 文字以上 110 文字以下という条件を外しました。
つまり、分析段階で気づいていても、最後の answer に反映できなければ失敗します。
改善版では、outer が文字数、必須語、禁止語を確認し直し、98 文字の回答を出しました。
この 1 点で、追加測定した 3 課題の平均は 0.900 から 1.000 へ上がりました。
直接 Fusion や Best-of-N が弱い場面もここにあります。 正しい値を含む候補を選べても、本文に間違った途中値を残すと減点されます。 Fusion Router は、候補の良い部分を選ぶだけでなく、最終回答の形から不要な監査ログや誤答を落とすための構造です。
まだ残る弱点
この仕組みでも、すべてを任せられるわけではありません。
まず、遅いです。 panel は並列にできますが、judge と outer は直列です。 全モデルを呼ぶ構成では、軽い確認に使うには重すぎます。
次に、outer が検証を空振りすることがあります。
プロンプトに「確認せよ」と書いても、実際の answer が条件を満たすとは限りません。
だから、文字数、JSON、計算、並び順のような条件は、生成後の deterministic checker で見る必要があります。
また、弱い候補を入れれば必ず良くなるわけでもありません。 VibeThinker は速い反例チェックには使えますが、未完成の reasoning や空出力が混ざることもあります。 LFM JP は日本語の整流に強い一方で、技術的な正誤判断の主役にはしません。 GLM は Qwen と違う視点をくれますが、最終判断は checker と Codex 側で行います。
実務での使い方
普段の流れでは、いきなり all panel を使いません。 最初は単体 Qwen か、Qwen + GLM くらいで十分です。 日本語の表現が問題になる時だけ LFM JP を入れます。 VibeThinker は、小さな反例やテストケースを見たい時に限定します。
Fusion Router は、次のような場面に絞ります。
- 候補同士の補完が効く課題
- 失敗時のコストが高い判断
- 要件、コード、文面の複数観点を同時に見るレビュー
- 生成後に deterministic checker を通せる課題
逆に、軽い要約、単純な下読み、短い確認では使いません。 そこでは Fusion の待ち時間の方が大きくなります。
自分の中では、Fusion Router は「賢い 1 つの答えを作る魔法」ではなく、「候補、批判、最終回答、検証を分けるための作業台」です。 ローカル LLM を実務に入れるなら、この分離がいちばん大事だと感じています。