[小ネタ] Strands Agents に Model Routing が追加されていたので試してみた

[小ネタ] Strands Agents に Model Routing が追加されていたので試してみた

Strands Agents の新機能「Model Routing」を試してみました!
2026.10.13

はじめに

こんにちは、運転練習中のコンサル部の神野です。駐車が一発で決まると気持ちいいですね・・・!

Strands Decider のハンズオン記事をこの前やっていたら、モデルの使い分けに ModelRouter というクラスが使われていました!

https://qiita.com/har1101/items/cf5e734358e09f2410d0

あれ、モデルのルーティングなんてできたっけ・・・?と見覚えがなかったので調べてみると、Strands Agents に Model Routing という機能が追加されていました!

https://strandsagents.com/docs/user-guide/sdk/model-providers/model-routing/

リリースノートをさかのぼると、ModelRouter が入ったのは 2026年8月12日の v1.52.0、ドキュメントのページが追加されたのは 9月8日の v1.55.0 でした。私が休んでいた間に、追加されていたようです・・・!見逃して悔しい・・・

早速試してみましょうか!

Model Routing の概要

Model Routing は呼び出しごとに「どのモデルに答えさせるか」を、候補の中から選ぶ仕組みです。候補のモデルを ModelRouter にまとめて Agent(model=router) に渡すだけで使えるので、Agent を呼び出す側のコードは今までと変わりません。

イメージとしては下記のような感じです。
デフォルトはSonnetで繋がらない時はHaikuみたいなこともできますし、内容によってルーティングしたりなども可能です!

Model Routing の構成

これまではメインのモデルが失敗したら別のモデルで呼び直す処理や、依頼の内容を見てモデルを選んでから Agent を作る処理を自分で書く必要がありましたが、これを SDK に任せられるのが嬉しいところですね!

どのモデルを選ぶかはルーティング戦略(RoutingStrategy)で指定します。用意されている戦略は次の3種類です。

戦略 選び方
FallbackStrategy デフォルト。宣言した順に試し、失敗したら次の候補へ切り替える
ClassifierStrategy 判断役のモデルが依頼の内容を見て、最初に答えるモデルを選ぶ。失敗しても切り替えない
独自の RoutingStrategy 自分で書く。上の2つを組み合わせることもできる

なお、候補にできるのは会話履歴をプロバイダー側に持たない(ステートレスな)モデルです。

前提

今回は下記前提で進めていきます!

項目 バージョン・設定
Python 3.13.13
strands-agents 1.59.0
モデル Amazon Bedrock の Claude Haiku 4.5 / Claude Sonnet 4.6
リージョン ap-northeast-1
環境のセットアップ
uv init --python 3.13 strands-model-routing
cd strands-model-routing
uv add strands-agents

FallbackStrategy で失敗したら予備のモデルに切り替える

戦略を指定しない場合に使われるのが FallbackStrategy です。宣言した順にモデルを試し、呼び出しに失敗したら次の候補へ切り替えます。

切り替わるところを見たいので、1番手にわざと存在しないモデル ID を指定しました。

fallback.py
import logging

from strands import Agent
from strands.models import BedrockModel, ModelRouter

logging.basicConfig(format="%(name)s | %(message)s")
logging.getLogger("strands.models.routing").setLevel(logging.INFO)

# わざと存在しないモデルIDを1番手にする
primary_model = BedrockModel(model_id="jp.anthropic.claude-not-exist-v1:0")
backup_model = BedrockModel(model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0")

router = ModelRouter(models=[primary_model, backup_model], max_switches=1)
agent = Agent(model=router, callback_handler=None)

result = agent("日本の首都を一言で答えてください。")
print(result)

max_switches は、1回の呼び出しの中で候補を切り替えてよい回数の上限です。
試してみましょう!

実行コマンド
uv run fallback.py
実行結果
strands.models.routing.router | strategy=<FallbackStrategy>, candidate=<BedrockModel/jp.anthropic.claude-not-exist-v1:0>, model=<BedrockModel/jp.anthropic.claude-not-exist-v1:0> | candidate selected
strands.models.routing.router | from_candidate=<BedrockModel/jp.anthropic.claude-not-exist-v1:0>, to_candidate=<BedrockModel/jp.anthropic.claude-haiku-4-5-20251001-v1:0>, error=<ValidationException> | model call failed, switching candidate
東京

1行目で存在しないモデルが選ばれ、2行目で ValidationException を受けて Haiku 4.5 に切り替わっています。エラーにならずに「東京」と返ってきました!自前でハンドリングしなくていいのは楽ですね。

ClassifierStrategy で依頼の内容に応じて振り分ける

続いて依頼の内容でモデルを振り分けたい場合はこちらを試してみます。判断役のモデルに依頼の内容と候補の説明を渡し、どの候補に答えさせるかを選ばせる仕組みです。今回は判断役に Haiku 4.5 を使い、簡単な依頼は Haiku 4.5、考える量が多い依頼は Sonnet 4.6 に振り分けるようにしました。

classifier.py
import logging

from strands import Agent
from strands.models import BedrockModel, ClassifierStrategy, ModelRouter, RoutingCandidate

logging.basicConfig(format="%(name)s | %(message)s")
logging.getLogger("strands.models.routing").setLevel(logging.INFO)

# 振り分けを判断するモデル
classifier_model = BedrockModel(
    model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
    max_tokens=64,
    streaming=False,
    temperature=0,
)

router = ModelRouter(
    models=[
        RoutingCandidate(
            BedrockModel(model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0"),
            name="fast",
            description="あいさつ、短い質問、簡単な翻訳など、すぐ答えられる依頼",
            metadata={"cost": "low", "latency": "low"},
        ),
        RoutingCandidate(
            BedrockModel(model_id="jp.anthropic.claude-sonnet-4-6"),
            name="strong",
            description="複数の条件を踏まえた設計、比較検討、デバッグなど、考える量が多い依頼",
            metadata={"cost": "high", "latency": "medium"},
        ),
    ],
    strategy=ClassifierStrategy(classifier_model),
)

requests = [
    "「おはようございます」を英語にしてください。",
    "マルチリージョンのアクティブ/アクティブ構成で、DynamoDBの書き込み競合をどう設計すべきか比較してください。",
]
for request in requests:
    print(f"\n### {request}")
    agent = Agent(model=router, callback_handler=None)
    result = agent(request)
    print(str(result)[:80].replace("\n", " "), "...")

候補モデルは RoutingCandidate で指定し、名前・説明・metadata を付与します。判断役のモデルはこの3つを根拠に振り分け先を選ぶので、どんな依頼に向いたモデルかを具体的に書いておきましょう。

metadata にはコストやレイテンシのような基準を入れられ、キーは文字列、値は JSON にできるものにします。

実行コマンド
uv run classifier.py
実行結果(回答は先頭だけ表示)
### 「おはようございます」を英語にしてください。
strands.models.routing.router | strategy=<ClassifierStrategy>, candidate=<fast>, model=<BedrockModel/jp.anthropic.claude-haiku-4-5-20251001-v1:0> | candidate selected
「おはようございます」の英語訳は:  **Good morning.**  より丁寧な表現としては: - **Good morning, sir/madam.* ...

### マルチリージョンのアクティブ/アクティブ構成で、DynamoDBの書き込み競合をどう設計すべきか比較してください。
strands.models.routing.router | strategy=<ClassifierStrategy>, candidate=<strong>, model=<BedrockModel/jp.anthropic.claude-sonnet-4-6> | candidate selected
# DynamoDB マルチリージョン アクティブ/アクティブ 書き込み競合設計  ## 競合が発生するメカニズム  ``` Region A (ap-nort ...

翻訳の依頼は fast(Haiku 4.5)、DynamoDB の設計の比較は strong(Sonnet 4.6)が選ばれました。狙いどおりに振り分けられていますね!

ClassifierStrategy は本回答の前に判断役のモデルを1回呼び出すため、その分の待ち時間とトークンが増えます。選んだモデルが失敗しても別の候補には切り替えません。この辺りはトレードオフな関係なのでやりたいことと要求されているスピードを鑑みて採用を考えたいですね。

独自の RoutingStrategy で振り分けと切り替えを両立する

組み込みの戦略では、振り分けと失敗時の切り替えを同時には使えません。例えば両方ほしいときは、RoutingStrategy として select メソッドを1つ持つクラスを自分で書きます

ドキュメントの例にならって、最初の選択は ClassifierStrategy、失敗したあとの選択は FallbackStrategy に任せるクラスを作りました。まだ試行がなければ(context.attempts が空なら)最初の選択と判断できます。

custom.py(戦略の部分)
class ClassifyThenFallback:
    def __init__(self, classifier: ClassifierStrategy) -> None:
        self._classifier = classifier
        self._fallback = FallbackStrategy()

    async def select(self, context: RoutingContext, **kwargs: Any) -> RoutingCandidate | None:
        # 最初の選択は分類、失敗後の選択はフォールバックに任せる
        if not context.attempts:
            return await self._classifier.select(context, **kwargs)
        return await self._fallback.select(context, **kwargs)

ModelRouter には strategy=ClassifyThenFallback(ClassifierStrategy(classifier_model)) を渡します。切り替わるところを見たいので、strong 側のモデル ID はわざと存在しないものにしました。

custom.py の全体
custom.py
import logging
from typing import Any

from strands import Agent
from strands.models import (
    BedrockModel,
    ClassifierStrategy,
    FallbackStrategy,
    ModelRouter,
    RoutingCandidate,
    RoutingContext,
    RoutingStrategy,
)

logging.basicConfig(format="%(name)s | %(message)s")
logging.getLogger("strands.models.routing").setLevel(logging.INFO)

class ClassifyThenFallback:
    def __init__(self, classifier: ClassifierStrategy) -> None:
        self._classifier = classifier
        self._fallback = FallbackStrategy()

    async def select(self, context: RoutingContext, **kwargs: Any) -> RoutingCandidate | None:
        # 最初の選択は分類、失敗後の選択はフォールバックに任せる
        if not context.attempts:
            return await self._classifier.select(context, **kwargs)
        return await self._fallback.select(context, **kwargs)

classifier_model = BedrockModel(
    model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
    max_tokens=64,
    streaming=False,
    temperature=0,
)

strategy: RoutingStrategy = ClassifyThenFallback(ClassifierStrategy(classifier_model))

router = ModelRouter(
    models=[
        RoutingCandidate(
            BedrockModel(model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0"),
            name="fast",
            description="あいさつ、短い質問、簡単な翻訳など、すぐ答えられる依頼",
        ),
        RoutingCandidate(
            # わざと存在しないモデルIDにして、選ばれたあとに失敗させる
            BedrockModel(model_id="jp.anthropic.claude-not-exist-v1:0"),
            name="strong",
            description="複数の条件を踏まえた設計、比較検討、デバッグなど、考える量が多い依頼",
        ),
    ],
    strategy=strategy,
    max_switches=1,
)

agent = Agent(model=router, callback_handler=None)
result = agent("マルチリージョンのアクティブ/アクティブ構成で、DynamoDBの書き込み競合をどう設計すべきか比較してください。")
print(str(result)[:80].replace("\n", " "), "...")
実行コマンド
uv run custom.py
実行結果(回答は先頭だけ表示)
strands.models.routing.router | strategy=<ClassifyThenFallback>, candidate=<strong>, model=<BedrockModel/jp.anthropic.claude-not-exist-v1:0> | candidate selected
strands.models.routing.router | from_candidate=<strong>, to_candidate=<fast>, error=<ValidationException> | model call failed, switching candidate
# DynamoDBマルチリージョン構成での書き込み競合設計  マルチリージョンのアクティブ/アクティブ構成における書き込み競合対策を、複数のアプローチで比較し ...

1行目で、DynamoDB の設計の比較は分類によって strong に振り分けられました。その strong が ValidationException で失敗すると、2行目で fast に切り替わり、無事に回答が返ってきました!

これ以外にも自前でルーティングロジックを組みたい時には使えますね!

おわりに

休みの間に追加されていた Model Routing を試してみました!
普通に便利そうで嬉しい機能ですね。

冒頭のハンズオンでは、振り分けの判断を Strands Decider に任せる例も紹介されていました。判断役を LLM にするか Jev や Decider にするかも考えてみると面白いですね!

本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事