問い合わせを「請求」「営業」「技術サポート」に振り分けたい。必要なのは、用意した候補から一つ選ぶことです。
こうした分類を試せるのが、Strands Deciderです。自由な文章を生成する代わりに、選択肢の判定や尺度に沿った評価を返します。問い合わせの担当部署を決めたり、緊急性を調べたりする用途が想定されています。
この記事では、Apple silicon搭載MacでPythonの仮想環境を作り、CLIとローカルHTTP APIから問い合わせを判定する手順を紹介します。
JEVとの比較
Strands Decider(Amazon / AWS公開)とJev(TypeSafe提供)は、いずれも自律型AIエージェントのワークフローにおいて「自由文の生成」ではなく「候補肢の選択やスコアリング(判断)」に特化したDecision Model(判断専用モデル)です。
使うときの大きな違いは、モデルを自分の環境で動かすか、提供されているAPIを呼び出すかです。
提供形態と運用の違い
| 項目 | Strands Decider (2B) | Jev (TypeSafe) |
|---|---|---|
| 開発元 | Amazon / AWS (Strands Agentsチーム) | TypeSafe |
| 提供形態 | オープンソース(重み・コード・学習レシピ公開) | ホステッドAPI(プロプライエタリ) |
| 実行環境 | ローカル環境(MacBook、自前のGPUサーバーなど) | クラウドAPI(外部エンドポイントへリクエスト) |
| 課金方式 | モデル無料(自前の計算リソース費用のみ) | API利用料が発生(料金は公式の最新情報を確認) |
| カスタマイズ性 | 公開コードや学習レシピを使った変更・追加学習が可能 | 公開されたAPIの機能範囲で利用 |
| プライバシー / セキュリティ | ローカル構成では判定対象を外部APIへ送らずに処理できる | データが外部APIに送信される |
| セットアップの手間 | 推論インフラの構築・保守が必要 | モデルの配備は不要。APIの認証と呼び出し処理を設定する |
Strands Deciderでできること
公開モデルのStrands Decider 2B v19は、約19億パラメータの判断モデルです。質問に応じて、次の3種類の結果を返します。
| 種類 | 問い合わせでの用途 | 返される内容 |
|---|---|---|
choice |
担当部署を候補から選ぶ | 選択結果、候補ごとの確率、confidence |
noul |
緊急性があるか調べる | Yesに相当する確率。0に近ければNo寄り、1に近ければYes寄り |
score |
不満の程度を段階で評価する | 順序付き尺度の期待値、各段階の確率、confidence |
scoreは、例えば「落ち着いている・不満がある・強く怒っている」の3段階を0・1・2として扱います。返される値は小数にもなり、必ず一つの段階名を選ぶわけではありません。noulには独立したconfidenceフィールドはありません。
仕組みとしては、Qwen3.5-2B-Baseの文章生成用ヘッドを、選択肢を評価する小さなpointer headへ置き換えています。次の単語を順番に生成するループを使わず、入力と候補を計算して判定します。ただし、入力の読み込みとモデル計算には時間がかかります。
詳しい出力の定義は、公式のアーキテクチャ資料にあります。
どのような場面でメリットがあるか
「生成LLMを使うかどうか」だけでなく、現在の振り分け方法と比べると、使いどころを判断しやすくなります。
| 比較する方法・状況 | Strands Deciderを検討する理由 | 比較するときに見る点 |
|---|---|---|
| 分類のたびに外部の生成LLM APIを呼んでいる | ローカルで判定を完結できれば、その分類に使っていたAPI呼び出しを減らせる | モデルを動かす計算資源、保守の手間、精度が必要。総コストが下がるかは件数にもよる |
| 自由文で返されたカテゴリ名を取り出している | 用意した候補を評価するので、文章からラベルを抽出する処理を減らせる | 生成LLMにも構造化出力がある。出力形式が整っていても分類の正しさは別に確認する |
| キーワードや正規表現だけで分類している | 表現の揺れや文脈を含む入力を、質問と候補を指定して判定できる | 固定ルールで十分なら、そのほうが軽く、判定理由も追いやすい |
| 専用の分類器を自分で学習しようとしている | 候補をリクエスト側で指定して試せるため、最初から用途別の学習を用意せずに評価を始められる | 専用分類器のほうが精度や速度で有利な場合もある |
例えば、担当部署の振り分けをDeciderで試し、判断が曖昧な入力を人や生成LLMに回す構成が考えられます。返信文の作成は生成LLMが担当します。どの程度の呼び出しを減らせるかは、実際の問い合わせで測る必要があります。
速度とメモリは、測定条件と一緒に見る
公式の評価結果では、v19をM3 Pro・メモリ36GB・MPSで実行した測定が公開されています。
| 条件 | 公式の測定値 |
|---|---|
| 300トークン未満の入力、同じ長さでの繰り返し実行 | 中央値153ms |
| JevBench全体、繰り返し実行 | 中央値234ms、95パーセンタイル2,628ms |
| MPSでのピークメモリ | 約5.4GB |
これはその環境での測定です。長い入力や初回の処理では時間が増えます。モデルのダウンロードと読み込みも別に必要なので、CLIを起動して終了するまでの時間を、そのまま推論時間として比較しないようにします。
手元のMacBook Pro(M2 Pro・メモリ16GB)でも、以下に掲載したCLIの判定が動きました。今回確認したのは、担当部署を選ぶ単独の質問と、3種類の質問をまとめた判定です。処理速度とピークメモリは測定していないため、上の公式測定値とは分けて扱います。
1. Pythonの仮想環境を作る
以下はmacOSのターミナルで実行します。Apple silicon搭載MacとPython 3.12が用意されている前提です。パッケージの設定ファイル上のPython要件は3.10以上ですが、ここでは公式のMac向け手順に合わせて3.12を使います。
python3.12 --version
mkdir -p ~/strands-decider-demo
cd ~/strands-decider-demo
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
仮想環境は、この作業用のPythonパッケージを入れる場所です。次のコマンドで確認できます。
python -c "import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)"
Pythonのパスに .venv/bin/python が含まれ、続いて True が出れば、仮想環境のPythonを使っています。python3.12: command not found が出た場合は、Python 3.12をインストールしてから進めます。
2. パッケージをインストールする
仮想環境を有効にした状態で実行します。
python -m pip install strands-decider
strands-decider --help
ヘルプが表示されれば、CLIを呼び出せています。まだモデルの推論は行っていません。初回の判定ではHugging Faceからモデル関連ファイルがダウンロードされるため、インターネット接続と保存領域が必要です。
公式資料によると、公開チェックポイントとは別に、約4.5GBのベースモデルが取得されます。仮想環境を作っても、モデルのキャッシュがすべて .venv の中に保存されるわけではありません。
ここではMPSを使います。MPSはMacのGPUで計算するためのPyTorchの仕組みです。
python -c "import torch; print(torch.backends.mps.is_available())"
True が出ることを確認してください。False の場合は、MacやPython、PyTorchの対応状況を確認します。CPUで試す場合は、後述の --device mps を --device cpu に変更しますが、速度やメモリ使用量は異なります。
MLXという別の実行方式も公式資料にあります。基本の動作確認にはMPSを使い、後半にMLXでの実行と速度比較の手順を追加しています。インストールで依存関係のエラーが出たときは、公式の推論手順と配布版を確認してください。
3. CLIで問い合わせを判定する
まずは担当部署を選ぶ質問を一つ送ります。
strands-decider ask StrandsAgents/strands-decider-2B-hobson-v19 \
--device mps \
--state "支払いが3日前から失敗し続けています。対応をお願いします。" \
--choice "どの部署が担当すべきですか?=billing,sales,tech_support"
--stateが判定対象の文章、--choiceが質問と選択肢です。質問のあとに = を置き、候補をカンマで区切ります。
通常のCLI出力には、choice_0、選ばれた候補、confidence、候補ごとの確率が表示されます。このコマンドの通常出力はJSONではありません。
手元のMacでの検証結果です。
Fetching 37 files: 100%|█████████████████████| 37/37 [00:00<00:00, 2961.85it/s]
Download complete: | 0.00B
Reconstruction complete: | | 0.00B / 0.00B
Loading weights: 100%|█████████████████████| 320/320 [00:00<00:00, 8367.53it/s]
[transformers] `causal_conv1d_fn` is falling back to its reference PyTorch implementation because `causal_conv1d` is not installed. This is correct but much slower; install `causal_conv1d` for the optimized kernel.
choice_0 -> billing (confidence 0.727)
billing 0.818
tech_support 0.164
sales 0.019
ログには、最適化されたカーネルがないため参照用のPyTorch実装に切り替える警告が出ています。今回の実行では、警告が出たあとも判定は完了しました。
この結果では、billing の確率が0.818、confidenceが0.727です。両者は別の値で、confidenceは候補数を考慮して計算されます。例えば3候補では、(3 × 最大確率 − 1) / 2です。表示値は丸められているため、表示された確率から計算した値とはわずかにずれる場合があります。
選択結果が billing でも、正しい振り分けとは限りません。「支払い失敗は請求担当か、システム担当か」といった業務上の定義を先に決め、その基準に沿って結果を確認します。
緊急性と不満の程度も、同じ入力についてまとめて質問できます。
strands-decider ask StrandsAgents/strands-decider-2B-hobson-v19 \
--device mps \
--state "支払いが3日前から失敗し続けています。対応をお願いします。" \
--choice "どの部署が担当すべきですか?=billing,sales,tech_support" \
--noul "この問い合わせには緊急性がありますか?" \
--score "書き手の不満はどの程度ですか?=calm,frustrated,furious"
scoreの候補は、弱いものから強いものへ並べています。担当部署のように順序のない候補には choice を使います。同じ入力への質問をまとめると、入力の読み取りを共有できる仕組みがあります。
手元のMacでの検証結果です。
noul_0 noul = 0.691
choice_0 -> billing (confidence 0.733)
billing 0.822
tech_support 0.160
sales 0.019
score_0 score = 1.08 (confidence 0.477)
0: calm 0.191
1: frustrated 0.539
2: furious 0.270
4. ローカルHTTP APIから呼び出す
この節は公式資料に基づく利用手順です。今回掲載した実機ログはCLIの結果で、HTTP APIの確認結果は含めていません。
他のプログラムから結果を受け取る場合は、サーバーを起動します。
strands-decider serve StrandsAgents/strands-decider-2B-hobson-v19 \
--device mps \
--port 8000
このターミナルは開いたままにします。別のターミナルから、起動状態を確認します。
curl --fail --show-error http://127.0.0.1:8000/health
モデル情報が返ったら、問い合わせを送ります。判定用のURLは /v1/systemone です。
curl --fail --show-error http://127.0.0.1:8000/v1/systemone \
-H 'Content-Type: application/json' \
-d '{
"state": "支払いが3日前から失敗し続けています。対応をお願いします。",
"questions": {
"is_urgent": {
"type": "noul",
"instructions": "この問い合わせには緊急性がありますか?"
}
}
}'
応答はJSONで返されます。answers.is_urgent.noulを確認すると、緊急性があるという判定の確率を読み取れます。値の意味やリクエスト形式は、公式のサーバー仕様を参照してください。
接続できない場合は、サーバー側のターミナルでモデルの読み込みが完了したか、エラーが出ていないかを確認します。ポート8000がほかのアプリで使われている場合は、サーバーとcurlの両方でポート番号を変更してください。
サーバーは標準で 127.0.0.1 に待ち受け、認証機能はありません。ここではローカルの動作確認に使います。停止するには、サーバーを起動したターミナルで Ctrl+C を押します。
5. MLXでの実行とMPSとの速度比較(実機検証前)
MPSで動作確認できたので、次はMLXで同じ問い合わせを処理し、速度と判定結果を比較します。MLXはApple silicon向けの計算フレームワークです。Strands Deciderには、モデル本体をMLXで動かす実行方式が用意されています。
この節は公式のMLX実行手順をもとにした検証用の手順です。手元のM2 Proでは、まだMLXの動作と速度改善を確認していません。 警告が消えるかだけでなく、同じ入力で結果を返すか、処理時間が短くなるかを確認します。
MLX用の仮想環境を用意する
公式資料では、MLX用の追加依存関係はソースからインストールする方法が案内されています。今回は、既存の環境を残し、比較用に新しい仮想環境を作ります。Gitが必要です。
cd ~/strands-decider-demo
python3.12 -m venv .venv-mlx
source .venv-mlx/bin/activate
python -m pip install --upgrade pip
python -c "import platform; print(platform.machine())"
git clone https://github.com/strands-labs/strands-decider.git strands-decider-source
cd strands-decider-source
python -m pip install -e '.[mlx]'
strands-decider ask --help
platform.machine()の出力が arm64 であることを確認します。x86_64 の場合は、Rosetta経由のターミナルやIntel版Pythonを使っていないか確認してください。すでに同名のソースフォルダがある場合は、再度cloneせず、そのフォルダを使います。
比較に使うバージョンも記録しておきます。MPSとMLXは、この同じ環境・同じソースで測定します。以前掲載したCLIログと今回の測定は、パッケージ版が異なる可能性があります。
git rev-parse HEAD > ../mlx-source-commit.txt
python -m pip freeze > ../mlx-environment.txt
sw_vers > ../mlx-macos.txt
MLXで判定する
strands-decider ask StrandsAgents/strands-decider-2B-hobson-v19 \
--device mlx \
--state "支払いが3日前から失敗し続けています。対応をお願いします。" \
--choice "どの部署が担当すべきですか?=billing,sales,tech_support"
結果が返ったら、選択された部署とconfidence、警告の有無を記録します。MLXとMPSでは計算方法が異なるため、確率の小数点以下が完全には一致しない場合があります。エラーが出た場合は、全文と上で記録したバージョンを残します。
起動済みサーバーで比較する
CLIの起動から終了までを測ると、モデルの読み込み時間も含まれます。ここでは、モデルを読み込んだサーバーへ同じ入力を繰り返し送り、ローカルHTTP通信と判定処理を含む応答時間を比較します。モデル内部だけの推論時間ではありません。
まず、比較用の環境でMPSサーバーを起動します。ほかに動かしているDeciderサーバーがあれば停止してください。16GBのMacでは、両方式を同時に起動せず、一つずつ測ります。
strands-decider serve StrandsAgents/strands-decider-2B-hobson-v19 \
--device mps --port 8000
別のターミナルで、次を実行します。
cd ~/strands-decider-demo
source .venv-mlx/bin/activate
curl --fail --show-error http://127.0.0.1:8000/health
モデル情報が返ったら、以下を compare_decider.py という名前で、~/strands-decider-demo に保存します。追加のPythonパッケージは不要です。
import json
import statistics
import sys
import time
from pathlib import Path
from urllib.request import Request, urlopen
label = sys.argv[1]
if label not in {"mps", "mlx"}:
raise SystemExit("使い方: python compare_decider.py mps または mlx")
base_url = "http://127.0.0.1:8000"
with urlopen(base_url + "/health", timeout=30) as response:
health = json.load(response)
print("起動中のサーバー:", health)
payload = {
"state": "支払いが3日前から失敗し続けています。対応をお願いします。",
"questions": {
"department": {
"type": "choice",
"instructions": "どの部署が担当すべきですか?",
"criteria": {"billing": None, "sales": None, "tech_support": None},
}
},
}
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
def ask():
request = Request(
base_url + "/v1/systemone",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
start = time.perf_counter()
with urlopen(request, timeout=180) as response:
result = json.load(response)
elapsed_ms = (time.perf_counter() - start) * 1000
if "department" not in result.get("answers", {}):
raise RuntimeError(f"判定結果を確認できません: {result}")
return elapsed_ms, result
print("ウォームアップ: 5回(集計には含めません)")
for _ in range(5):
ask()
records = []
for index in range(20):
elapsed_ms, result = ask()
records.append({"elapsed_ms": elapsed_ms, "response": result})
print(f"{index + 1:02d}: {elapsed_ms:.1f} ms", flush=True)
median_ms = statistics.median(row["elapsed_ms"] for row in records)
report = {
"label": label,
"health": health,
"request": payload,
"warmup_count": 5,
"measured_count": 20,
"median_ms": median_ms,
"records": records,
}
path = Path(f"comparison_{label}.json")
path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"{label}: 中央値 {median_ms:.1f} ms")
print("最後の判定:", records[-1]["response"]["answers"]["department"])
print("保存先:", path.resolve())
MPSを測定します。表示されるサーバー情報で、MPSで起動していることも確認してください。
python compare_decider.py mps
完了したら、サーバー側のターミナルで Ctrl+C を押します。次に、同じ環境でMLXサーバーを起動します。
strands-decider serve StrandsAgents/strands-decider-2B-hobson-v19 \
--device mlx --port 8000
モデルの読み込みが完了したら、測定用のターミナルで実行します。
python compare_decider.py mlx
測定結果は comparison_mps.json と comparison_mlx.json に保存されます。判定結果とサーバー情報も含むので、速度だけでなく選択された部署や確率の違いも確認できます。
confidenceを自動実行の条件にする前に
confidenceは、モデルが返す分布から計算される値です。「confidenceが0.9なら、どんな入力でも90%正しい」という保証ではありません。
公式資料には、confidenceに応じて自動処理・確認・人への引き継ぎを分ける運用例があります。ただし、採用するしきい値は、自分の問い合わせデータと誤判定の影響に合わせて決めます。日本語の業界用語や社内ルールを含む入力で、同じ精度になるかも確認が必要です。
部署の候補に該当しない入力も用意しておくと、振り分けを見直しやすくなります。choiceは指定された候補から選ぶため、必要なら「その他・要確認」の候補を追加します。
また、ツールを選ぶ判定と、実行権限の確認は別の処理です。削除や送金などを行うツールでは、confidenceだけを実行許可の代わりにしないようにします。
終了と再開
作業を終えたら、仮想環境を解除します。
deactivate
後日再開するときは、同じフォルダで有効にします。毎回インストールし直す必要はありません。
cd ~/strands-decider-demo
source .venv/bin/activate
参考資料
- Strands Decider公式リポジトリ
- 公開モデル:Strands Decider 2B v19
- インストール・CLI・HTTP API
- モデル構成とconfidenceの定義
- 精度・速度・メモリの評価結果
リポジトリのコードはApache License 2.0で公開されています。モデルの利用・再配布にあたっては、公開モデルとベースモデルのライセンス、付属の通知も確認してください。
