Semantic Routerによるインテントルーティングのレイテンシとコスト最適化:ベクトルベースAIルーティング実践ガイド

AI tutorial - IT technology blog
AI tutorial - IT technology blog

課題:LLMによるインテントルーティングに潜むレイテンシとコスト

会話型AIエージェントは、ユーザーからプロンプトが送信された瞬間に必ず重要な分岐点に直面します。「次は何を実行すべきか?」。そのクエリはSQL検索が必要なのか、ドキュメントを対象としたベクトル検索なのか、Pythonコードのサンドボックス実行なのか、それとも単なる定型文の挨拶なのか、といった判断です。

多くの開発チームは、プロンプトをGPT-4o-miniやClaude 3.5 Haikuなどの軽量モデルに渡すことでこの課題を解決しています。LLMがテキストを解析し、インテントラベルを抽出してJSONペイロードを返します。

この手法は機能こそしますが、本番環境において2つの重大なボトルネックを生み出します。

  • 深刻なレイテンシ:どれほど高速なホステッドモデルであっても、ルーティング用のトークンを1つ返すだけで300ms〜800msを要します。この遅延は、コアとなる検索やエージェントロジックが開始される前段階で発生してしまいます。
  • 累積するAPIコスト:1日あたり20万件のクエリをLLM分類器に通すと、費用が膨大になります。1リクエストあたり約150プロンプトトークンを消費する場合、「こんにちは」や「請求書はどこですか?」といった基本的な挨拶や定型文の分類だけでも、軽量モデルで月額180ドル以上が無駄になります。GPT-4oなどのプレミアムモデルを使用した場合、その請求額は1,500ドルを超えます。

二者択一や数系統の分岐を判断するためだけに、テキスト生成を行う必要はありません。処理の分岐を選ぶために、数百億パラメータを持つ自己回帰型デコーダは不要です。必要なのは幾何学的な類似度だけです。

Semantic Routerは、このLLM呼び出し全体をベクトル計算へと置き換えます。クエリの埋め込みベクトルと事前定義されたルートクラスタをコサイン類似度で比較することにより、外部APIのクォータを消費することなく、アプリケーションローカルでわずか5ms〜20msでルーティング判定を完了できます。

クイックスタート(5分):Semantic Routerのセットアップ

Pythonで実際に動作するプロトタイプを作成してみましょう。受信したユーザーリクエストを「請求(billing)」、「技術サポート(tech_support)」、「一般的な雑談(chitchat)」の3つのルートに振り分けます。

1. 依存パッケージのインストール

まず、semantic-routerパッケージとsentence-transformersをインストールします。アプリケーションコンテナ内のインメモリで完全にルーティングを実行できるよう、軽量なローカル埋め込みモデルを使用します。

pip install semantic-router sentence-transformers

2. ルートと発話サンプルの定義

router_demo.pyというスクリプトを作成します。ユーザーの意図を捉えた代表的なフレーズ(発話サンプル: utterances)をいくつか指定して、各ルートを定義します。

from semantic_router import Route
from semantic_router.encoders import HuggingFaceEncoder
from semantic_router.layer import RouteLayer

# サンプルフレーズを指定してルートを定義
billing_route = Route(
    name="billing",
    utterances=[
        "最新の請求書が必要です",
        "登録しているクレジットカードを変更できますか?",
        "今月二重に請求されているのはなぜですか?",
        "サブスクリプションを今すぐ解約してください",
        "請求書の領収書はどこからダウンロードできますか?"
    ]
)

chitchat_route = Route(
    name="chitchat",
    utterances=[
        "こんにちは!",
        "調子はどうですか?",
        "面白いジョークを言って",
        "おはよう、ボットさん",
        "あなたを作ったのは誰ですか?"
    ]
)

tech_support_route = Route(
    name="tech_support",
    utterances=[
        "APIがステータスコード500を返してきます",
        "Webhookのペイロード検証に失敗します",
        "Dockerコンテナが起動時にクラッシュします",
        "データベース接続がタイムアウトしました",
        "エンドポイントから認証エラー(Unauthorized)が返され続けます"
    ]
)

routes = [billing_route, chitchat_route, tech_support_route]

3. ルーティングレイヤーの初期化

次に、エンコーダを読み込んでRouteLayerを構築します。ここでは一般的なCPU環境でも快適に動作する80MBの軽量モデルall-MiniLM-L6-v2を使用します。

# 軽量で高速なローカル埋め込みモデルを使用
encoder = HuggingFaceEncoder(name="sentence-transformers/all-MiniLM-L6-v2")

# ルーティングレイヤーの構築
router = RouteLayer(encoder=encoder, routes=routes)

# クエリでテスト
queries = [
    "先週の支払いのPDFを発行してもらえますか?",
    "サーバーがポート5432への接続を拒否します",
    "やあ、調子はどう?",
    "ワシの平均的な翼開長はどのくらいですか?"
]

for query in queries:
    decision = router(query)
    print(f"クエリ: '{query}' -> ルート: {decision.name}")

スクリプトを実行すると、即座に決定論的な分類結果が得られます。

クエリ: '先週の支払いのPDFを発行してもらえますか?' -> ルート: billing
クエリ: 'サーバーがポート5432への接続を拒否します' -> ルート: tech_support
クエリ: 'やあ、調子はどう?' -> ルート: chitchat
クエリ: 'ワシの平均的な翼開長はどのくらいですか?' -> ルート: None

ワシに関するクエリはNoneと判定されました。この質問は3つのルートすべての類似度しきい値に達しなかったため、ルーターはこれをオープンドメインのフォールバック対象として安全にフラグ付けしました。

詳細解説:ベクトル埋め込みがLLMなしでインテントをルーティングする仕組み

内部のメカニズムを理解することで、信頼度しきい値の調整やルーティングレイヤーのスケールが格段に容易になります。

幾何学的なルーティングの仕組み

Semantic Routerはテキスト生成パイプラインを完全に省略し、密な表現空間(Dense Representation Space)を利用します。リクエストが処理される流れは以下のとおりです。

  1. 事前計算:アプリケーションの起動時に、ルーターはすべての発話サンプルを384次元のベクトル埋め込みにエンコードし、メモリ内にキャッシュします。
  2. クエリのベクトル化:ユーザーメッセージを受信すると、ローカルモデルがテキストを全く同一のベクトル空間にマッピングします。一般的な2コアのクラウドインスタンスであれば、この処理にかかる時間はわずか4ms〜12msです。
  3. 類似度スコアの計算:エンジンは、受信したクエリベクトルとキャッシュされている各ルートベクトルとの間のコサイン類似度を計算します。
  4. しきい値によるフィルタリング:最も類似度の高いスコアがルートのしきい値(例:0.75)を超えている場合、ルーターはそのルート名を即座に返します。すべてのスコアがしきい値未満だった場合、デフォルトのNoneを返します。

LLM分類器とSemantic Routerの比較

両者のアプローチを比較すると、なぜベクトルルーティングがLLMによるトリアージプロンプトを急速に置き換えているのかが明確になります。

  • レイテンシ:LLMプロンプトによる分類は平均350ms〜800msを要します。対してSemantic Routerは5ms〜20msで処理を完了します。
  • 運用コスト:LLMルーティングは100万トークンあたり0.15ドル〜5.00ドルの費用がかかります。一方、Semantic Routerはプロセス内メモリでローカル実行されるため、API費用は0ドルです。
  • 決定性(再現性):LLMはスキーマのドリフト、予期せぬフォーマットの変更、JSONパースエラーなどのリスクを常に抱えています。ベクトル間の距離計算は数学的に一意であり、完全な決定性を持ちます。
  • スループット:月額10ドル程度のクラウドコンテナ(2 vCPU、4GB RAM)1基でも、レート制限に引っかかることなく、毎秒1,200件以上のルーティング評価を容易に処理できます。

応用編:動的しきい値とハイブリッドルーティング

本番環境のワークロードにはきめ細かな制御が求められます。影響度の高い処理には厳格な一致条件が必要ですが、一般的な会話ルートにはより広い許容度を持たせることができます。

1. ルート感度の微調整

破壊的なアクション(アカウント削除や返金リクエストなど)は、誤検知(False Positive)を排除するために高めの信頼度しきい値を設定する必要があります。一方で、一般的な問い合わせはしきい値を低めに設定して間口を広げたほうが効果的です。感度は各ルートに直接設定できます。

from semantic_router import Route
refund_route = Route(
    name="critical_refund",
    utterances=[
        "全額返金を求めます",
        "購入代金を直ちに返金してください",
        "この請求書について返金してほしいです"
    ],
    score_threshold=0.82  # 厳格:誤検知による誤発火を防止
)

faq_route = Route(
    name="general_faq",
    utterances=[
        "営業時間は何時ですか?",
        "オフィスはどこにありますか?",
        "営業担当者にはどう連絡すればよいですか?"
    ],
    score_threshold=0.68  # 緩和:幅広い表現のバリエーションを拾う
)

2. フォールバックを備えたエージェントディスパッチャーの構築

定型的なタスクはローカルで高速に解決し、判断が難しい曖昧なクエリだけを高コストなLLM呼び出しに委ねる、実践的なディスパッチャーの実装例を紹介します。

import time
from semantic_router.layer import RouteLayer
from semantic_router.encoders import HuggingFaceEncoder

class IntelligentAgentDispatcher:
    def __init__(self, routes):
        self.encoder = HuggingFaceEncoder(name="sentence-transformers/all-MiniLM-L6-v2")
        self.router = RouteLayer(encoder=self.encoder, routes=routes)
        
    def dispatch(self, user_prompt: str):
        start_time = time.perf_counter()
        match = self.router(user_prompt)
        latency_ms = (time.perf_counter() - start_time) * 1000
        
        if match.name == "billing":
            return self.handle_billing(user_prompt, latency_ms)
        elif match.name == "tech_support":
            return self.handle_support_ticket(user_prompt, latency_ms)
        elif match.name == "chitchat":
            return {"handler": "static_cache", "response": "こんにちは!どのようなご用件でしょうか?", "latency": f"{latency_ms:.2f}ms"}
        else:
            return self.delegate_to_llm(user_prompt, latency_ms)
            
    def handle_billing(self, prompt, latency):
        return {"handler": "sql_billing_service", "action": "query_invoices", "latency": f"{latency:.2f}ms"}

    def handle_support_ticket(self, prompt, latency):
        return {"handler": "jira_integration", "action": "open_ticket", "latency": f"{latency:.2f}ms"}

    def delegate_to_llm(self, prompt, latency):
        # セマンティックルートで一致しなかった場合のみ、高コストな生成処理にフォールバック
        return {"handler": "heavy_llm_agent", "action": "generate_response", "routing_latency": f"{latency:.2f}ms"}

実践的なTips:本番導入から得られた知見

ベクトルベースのルーティングを本番環境で運用することは、明確な成果をもたらします。高トラフィックな複数のクライアント環境において、プロンプトベースの分類を置き換えたことで、p95レスポンスタイムが平均420ms短縮されました。また、ボット1台あたり毎月約240ドルの分類トークン費用が削減されました。

Semantic Routerを導入する際は、以下の運用ガイドラインを念頭に置いてください。

  • 各ルートの発話数は10〜15件に抑える:多ければ多いほど良いわけではありません。何百もの発話を追加すると初期化時間が長くなり、クラスタの境界が曖昧になる可能性があります。特徴的でシグナルの強いフレーズに絞り込むことが重要です。
  • 適切な埋め込みモデルを選定する:英語のワークロードであれば、sentence-transformers/all-MiniLM-L6-v2が速度(CPU上で10ms未満)と精度のバランスに最も優れています。多言語対応が必要な場合は、BAAI/bge-m3やparaphrase-multilingual-MiniLM-L12-v2の利用を検討してください。
  • ルーティングされなかったクエリを体系的に分析する:Noneが返されたクエリを確認できるシンプルなダッシュボードを構築しましょう。これらのログから、新たなユーザー意図の発生や、既存のルート定義における語彙の死角を発見できます。
  • ステージング環境でしきい値を調整する:まずはベースラインとして0.70から開始します。ステージングのログで誤検知が見られる場合は、マッチングが正確かつ高信頼になるまで0.03刻みでしきい値を引き上げてください。
Share: