その他

【ClawRouter】判定基準をローカルLLMに託してみる

その他
この記事は約9分で読めます。

はじめに

みなさんこんにちは。株式会社インプルの岩崎です。

本日は、以前話題で取り上げたClawRouterをカスタマイズしてみたお話です!

現在のClawRouterの課題

さて、前回のClawRouterでご紹介した最大の特徴、

最大の特徴は、リクエスト内容を15次元でスコアリングし、そのタスクを処理できる「最も安価なモデル」に1ms以内で自動振り分けする点です。公称では最大92%のAPIコスト削減が可能とされています。

ですが、記事を作成した後、UIなどを作成しいろいろといじっていると、エラーが多発し、うまく制御ができない課題にぶつかりました。

ClawRouterの分類技術

社内で調査してみると、ClawRouterは「15次元スコアリング」と謳っていますが、実際の分類ロジックはプロンプト内のキーワードを部分文字列マッチングしてスコアを計算する仕組みのようでした。

これにより、前回も紹介したように、契約書の法的リスク分析をSIMPLEと誤判定してしまいました。これは、期待値として、本来はCOMPLEXと判定されるべきタスクでしたが、安いモデルに流れてしまいます。コスト削減が目的のルーターで、重要タスクが低品質なモデルに流れるのは本末転倒です。

[SIMPLE] google/gemini-2.5-flash $0.0102 (saved 90%)
→ Wallet empty, falling back to free model: free/gpt-oss-120b

詳しくはもう少しソースコードを調査してみる必要がありますが、分類が正しく行われていないのは大きな課題です。

日本語のプロンプトがもたらすバグ

日本語プロンプトを投げると Invalid character in header content ["x-clawrouter-reasoning"] というエラーが頻発しました。

[ClawRouter] Success with model: free/gpt-oss-120b
[ClawRouter] Error: Invalid character in header content ["x-clawrouter-reasoning"]

このログから「ヘッダーにASCII範囲外の文字が入ってエラーになった」という推測ができます。同じ日本語プロンプトでも成功するケースがあるため、日本語そのものが原因ではなく、特定の分類結果がヘッダーに書き込まれる際に発生する可能性があります。

課題に対しての解決策

さて、上記で紹介した日本語のプロンプトがうまく分類されず、エラーで返答される問題に対して、今回は下記のアプローチで解決を目指しました。

ローカルLLMで難易度判定

さて、今回のスコアリングではセマンティック的なマッチングでスコアを出していますが、これもLLMで難易度を出してしまおうという発想です。

ユーザーのプロンプト
        ↓
【新設】ローカルLLM(Ollama)で難易度判定
        ↓ SIMPLE / MEDIUM / COMPLEX を返す
Next.js Router
        ↓ 判定結果に応じてモデルを指定
ClawRouter
        ↓
最適なモデルへ振り分け

しかし、ここで有料モデルを使用してはコストがかかってしまい、ClawRouterの意味がなくなってしまいますので、無料で賢いモデルを使用していきます。

ローカルLLMの構成

そこで今回は、Ollama + gemma3:4b を採用しました。

項目内容
モデルサイズ  約3.3GB
起動コマンド           ollama run gemma3:4b                        
日本語対応  対応○             
コスト無料(ローカル動作)
レスポンス形式JSON形式で返答可能

日本語にも対応しており、先ほど課題に上がっていた日本語読み込みエラー問題は、これで解決するはずです。

ローカルLLMで試してみる

CLIでテスト

いきなり実装をする前に、まずは上記ローカルLLMの組み合わせでしっかりと動作をするのか、調査してみましょう。下記の難易度判定プロンプトを使用して、難易度を判定できるか調査していきます。

以下のユーザーの質問の難易度をSIMPLE/MEDIUM/COMPLEXの3段階で
判定してJSON形式で返してください。
質問:「{user_prompt}」

質問と期待値は、それぞれ下記の通りです。

SIMPLE:お腹がすいたを英語にして

COMPLEX:タスク管理APIをFastAPIで設計してください

MEDIUM:この契約書の法的リスクを分析してください。第3条:甲は乙に対し、成果物の著作権を無償で譲渡するものとする

準備とCLIでのテストは簡単で、コマンドを数回実行するだけです。

# Ollamaをインストール
brew install ollama

# モデルをダウンロード
ollama pull gemma3:4b

# 質問を実行
ollama run gemma3:4b "以下のユーザーの質問の難易度をSIMPLE/MEDIUM/COMPLEXの3段階で判定してJSON形式で返してください。質問:「お腹がすいたを英語にして」"

すると、最初に投げた質問には下記のように返答してきました。

{
  "question": "お腹がすいたを英語にして",
  "difficulty": "SIMPLE"
}

JSON形式で正しく返答し、SIMPLEと正しく判定していることが確認できました。他の2つの質問についても質問してみましょう。

ollama run gemma3:4b "以下のユーザーの質問の難易度をSIMPLE/MEDIUM/COMPLEXの3段階で判定してJSON形式で返してください。質問:「タスク管理APIをFastAPIで設計してください」"
json
{
  "question_difficulty": "COMPLEX"
}

**理由:**
この質問は、単にタスク管理APIを設計するだけでなく、以下の要素を理解している必要があるため、COMPLEXと判定しました。
*   **タスク管理APIの設計:** データの構造、エンドポイント、認証などの設計を理解している必要がある。
*   **FastAPI:** FastAPIの基本的な知識、ストラクチャリング、依存性注入などが必要。
*   **API設計:** RESTful API設計の原則、HTTPメソッド、ステータスコードの理解。
これらの要素を考慮すると、この質問は単純なレベルではなく、API設計とフレームワークに関するある程度の知識を必要とするため、COMPLEXと評価します。

---

ollama run gemma3:4b "以下のユーザーの質問の難易度をSIMPLE/MEDIUM/COMPLEXの3段階で判定してJSON形式で返してください。質問:「この契約書の法的リスクを分析してください。第3条:甲は乙に対し、成果物の著作権を無償で譲渡するものとする」"
json
{
  "difficulty": "MEDIUM"
}

**理由:**
*   **法的分析の必要性:** 質問は特定の条項(第3条)の法的リスクを分析することを求めており、単なる事実の確認ではなく、法律の知識を必要とする。
*   **条項の理解:** 契約条項の解釈、著作権譲渡の法的意味、甲乙間の権利義務関係などを理解する必要があるため、ある程度の法的な知識が求められる。
*   **リスクの特定:** 著作権譲渡という行為がもたらす可能性のあるリスク(例えば、権利の制限、将来的な利用上の問題など)を特定する必要がある。
これらの理由から、この質問は SIMPLE な質問よりも複雑で、COMPLEX な質問よりも直接的な理解と分析が求められるため、MEDIUM の難易度と判断しました。

ClawRouterがSIMPLEに誤分類していた契約書がMEDIUMに、タスク管理APIがCOMPLEXに正しく判定されています。

しかも判定理由まで説明してくれています。キーワードマッチングとは雲泥の差ですね。

ツールに組み込んでみる

さて、CLIではうまく動作したので、実際にUIでツールとして見れるようにしてみましょう。下記のような簡易的なチャットを作ってみました。

先ほどと同じように、下記の質問をそれぞれ投げてみましょう。

SIMPLE:お腹がすいたを英語にして

COMPLEX:タスク管理APIをFastAPIで設計してください

MEDIUM:この契約書の法的リスクを分析してください。第3条:甲は乙に対し、成果物の著作権を無償で譲渡するものとする

まず最初に、「お腹がすいたを英語にして」です。実行結果は画像のような感じ。正しく結果が表示されていますね。

こちらのチャットツールでは、判定と実行でそれぞれ分けて表示しています。今回の場合では、チャット画像紫色の判定(Ollamaによる難易度判定)も、緑色の実行(ClawRouterによるタスクの実行)も、SIMPLEで行われました。

判定: SIMPLE(Ollamaが正しく判断)
実行: SIMPLE(それに基づいてGPT-OSSが使用される)

続いて、「タスク管理APIをFastAPIで設計してください」を質問してみましょう。
すると、スクリーンショットを撮りそびれたのですが、レスポンス量が多すぎて返答に失敗しタイムアウトしちゃいました笑。これは、レスポンス量が多い場合、ファイル生成系はどう返せばいいか、また検討しなければですね。

[ClawRouter] safeWrite: socket not writable, dropping 20599 bytes

そこで、コードを生成しないタスクを投げてみることにしました。タスクの内容は、「Pythonのリストと辞書の違いを説明してください」というものです。すると、画像のような形に。

まだClawRouterには課金の設定をしていないので、紫色の判定はMEDIUMですが、実行はSIMPLEで実行されています。使用されたモデルもGPT-OSSですね。判定部分は正しく操作していることがわかります。

おわりに

いかがだったでしょうか。

判定部分もLLMにすることで、より正しく判定されるようになりました。これで通常のルーターのように、正しい難易度判定に基づいた効果的なAI利用ができるようになりましたね。
しかし、実験途中にあったレスポンスが肥大化することによるタイムアウトはまだまだ課題です。また、これを社内向けに展開していくには、いろいろなアレンジも加えられるようになるといいですね。
次回以降、このチャットの部分を深掘りしてみたいと思います!

ご覧いただきありがとうございました。