はじめに
2026年10月1日、AWSが Strands Decider 2B という小さなAIモデルを公開しました。Apache 2.0ライセンスで、Hugging Faceから無料でダウンロードできます。
このモデルは文章を書けません。
用意した選択肢から1つ選ぶか、「はい/いいえ」や段階で点数をつけることしかしません。
そのかわり自分のパソコンで動き、答えは選択肢のどれかになります。
この記事は、中学生でも手を動かしながら理解できることを目標にしたハンズオンです。
コードはすべて記事の中にあるので、上から順にコピーして実行すれば同じ結果がでるはずです。
- ステップ1〜4: Strands Decider だけで動かします。AWSアカウントは要りません
- ステップ5: AIエージェントの「門番」にします。前半はAWSなし、後半は Amazon Bedrock を使います
- ステップ6: しゃべるAI(Claude Haiku 4.5)と速さを比べます。Amazon Bedrock を使います
Strandsのアイコン(キャラクター)がカエルに変わっています
必要なもの
- Mac(M1/M2/M3などのチップを積んだもの)、またはNVIDIAのGPU(画像処理用の部品)があるLinuxのパソコン
公式によるとCPUだけでも動きますが、遅くなります - 空き容量10GB程度と、インターネット環境
- ステップ5-2・6だけ、AWSアカウント
記事のコマンドはMacとLinux用です。
Windowsでは試していません。
動作確認した環境は次のとおりです。
- Apple M1・メモリ16GBのMac
- Python 3.12、strands-decider 0.1.0、strands-agents 1.57.1
- 確認日: 2026年10月2日
しゃべるAIとえらぶAI
ChatGPTやClaudeのようなAIは、作文のテストを受けている生徒に似ています。
質問されると自由に文章を書いて答えます。
何でも書けるかわりに、書き終わるまで時間がかかり、答えの書き方も毎回少しずつ変わります。
Strands Decider 2B は、マークシートのテストを受けている生徒です。
- 答えは、用意された選択肢のどれか1つしか出しません
- すべての選択肢に「どれくらいありそうか」を確率(%)でつけます
- 作文をしないぶん体が小さく、自分のパソコンで動きます。
パラメータ(AIの中にある調整つまみ)は約19億個です
「えらぶだけ」のAIを使う場面
AIエージェント(道具を使いながら自分で仕事を進めるAI)は、仕事の途中で小さな判断を何度もしています。
- この問い合わせは、どの担当に回すか
- この道具を、いま使ってよいか
- このお願いは危なくないか
この小さな判断のたびに大きなAIへ作文させると、時間もお金もかかります。
そこで判断だけを小さなAIに任せます。
AWSはこのようなモデルを「System One」のモデルと呼んでいます。
System One は、人間の素早い直感的な判断を指す言葉です。
中身のしくみ
この節は少し難しいので、読み飛ばしても先に進めます。
Strands Decider 2B は、Qwen3.5-2B-Base という「しゃべるAI」を土台にしています。
- しゃべるAIから「次の言葉を書く部品」を取り外す
- かわりに「選択肢を指さして点数をつける部品」(ポインターヘッド)を取り付ける
- 選ぶ練習をたくさんさせる
文章を1文字ずつ書くのではなく、選択肢の一つひとつに点数をつけて1回で答えを出します。
準備
uv のインストール
uv は、Pythonとライブラリをまとめて準備してくれる道具です。
ターミナル(文字でパソコンに命令するアプリ)を開いて、次を実行します。
すでに入っている場合は飛ばしてください。
curl -LsSf https://astral.sh/uv/install.sh | sh
Strands Decider のインストール
作業フォルダをつくり、その中にPythonの環境を用意します。
mkdir strands-decider-handson
cd strands-decider-handson
uv venv -p 3.12 .venv
source .venv/bin/activate
uv pip install strands-decider
source .venv/bin/activate は、このハンズオン専用のPython環境に入るコマンドです。
新しいターミナルを開いたときは、作業フォルダでもう一度実行してください。
最初の1問
コマンド1行で質問できます。
最初の1回はモデルのダウンロードが入るので時間がかかります。
手元のMacでは約85秒かかり、ダウンロード量は合計で約9GBでした。
strands-decider ask StrandsAgents/strands-decider-2B-hobson-v19 \
--state "Help! My payouts have been failing for 3 days!" \
--choice "Which team should handle this?=billing,sales,retail"
choice_0 -> billing (confidence 0.766)
billing 0.844
retail 0.093
sales 0.063
「3日間も支払いが失敗している!」という問い合わせを、billing(お金の担当)に回すと選びました。
billing の確率は84%です。
1行目の confidence 0.766 は、いちばん高い確率を、選択肢の数に合わせて0〜1に直した値です。
この記事では、わかりやすい確率(%)のほうを見ていきます。
サーバーとして起動
毎回モデルを読み込むと遅いので、別のターミナルでサーバー(質問を受け付けて答えを返し続けるプログラム)として起動したままにします。
cd strands-decider-handson
source .venv/bin/activate
strands-decider serve StrandsAgents/strands-decider-2B-hobson-v19 --port 8099
Uvicorn running on http://127.0.0.1:8099 と表示されたら準備完了です。
このターミナルは閉じずに置いておきます。
質問用の道具 kantan.py
作業フォルダに kantan.py というファイルをつくり、次の内容を貼りつけます。
サーバーに質問を送るだけの小さな道具です。
"""Strands Decider にかんたんに質問するための小さな道具。
先に別のターミナルでサーバーを起動しておく:
strands-decider serve StrandsAgents/strands-decider-2B-hobson-v19 --port 8099
"""
import json
import os
import urllib.request
URL = os.environ.get("STRANDS_DECIDER_URL", "http://127.0.0.1:8099") + "/v1/systemone"
def ask(state, questions):
body = json.dumps({"state": state, "questions": questions}).encode()
req = urllib.request.Request(URL, data=body, headers={"content-type": "application/json"})
with urllib.request.urlopen(req, timeout=120) as res:
return json.loads(res.read())
def hai_iie(state, question, true=None, false=None):
"""はい/いいえ。「はい」である確率(0〜1)を返す。"""
q = {"type": "noul", "instructions": question}
if true and false:
q["criteria"] = {"true": true, "false": false}
r = ask(state, {"q": q})
return r["answers"]["q"]["noul"], r["latency_ms"]
def erabu(state, question, options):
"""選択肢から1つ選ぶ。options は {名前: 説明} の辞書。"""
r = ask(state, {"q": {"type": "choice", "instructions": question, "criteria": options}})
return r["answers"]["q"], r["latency_ms"]
def tensuu(state, question, levels):
"""低い順に並べた段階で点数をつける。"""
r = ask(state, {"q": {"type": "score", "instructions": question, "criteria": levels}})
return r["answers"]["q"], r["latency_ms"]
def bar(p, width=20):
n = round(p * width)
return "█" * n + "·" * (width - n)
Strands Decider に送るものは2つだけです。
- 状況(state): 判断の材料になる文章
例「朝から頭が痛くて、熱っぽいです。」 - 質問(questions): 何を判断してほしいかという選択肢
例「どこに相談に行けばいい?」保健室/職員室/図書室
質問の形は3種類あります。
| 形 | コードでの名前 | 返ってくるもの |
|---|---|---|
| はい/いいえ | noul | 「はい」の確率 |
| 選ぶ | choice | 選んだもの+全選択肢の確率 |
| 点数 | score | 段階のどのあたりか(小数) |
ステップ1: はい/いいえで答えてもらう
step1_hai_iie.py をつくって実行します。
この後のスクリプトも、すべて kantan.py と同じフォルダに置きます。
# ステップ1: 「はい/いいえ」で答えてもらう
from kantan import hai_iie, bar
bunshou = [
"今日の給食、めっちゃおいしかった!",
"もう最悪。傘忘れてびしょぬれ。",
"明日は月曜日です。",
]
for s in bunshou:
p, ms = hai_iie(s, "この文を書いた人はうれしそうですか?")
print(f"{bar(p)} はい {p:.0%} ({ms:.0f}ミリ秒) {s}")
python step1_hai_iie.py
██████████████████·· はい 90% (1644ミリ秒) 今日の給食、めっちゃおいしかった!
███················· はい 17% (472ミリ秒) もう最悪。傘忘れてびしょぬれ。
████················ はい 20% (470ミリ秒) 明日は月曜日です。
日本語の文でも読めています。
「はい」か「いいえ」だけでなく、「はい」の確率が数字で出ます。
最初の1回だけ時間がかかりました(準備の処理が入るためと考えられます)。
2回目からは0.5秒前後でした。
ステップ2: 選択肢から選んでもらう
学校の「相談窓口の振り分け係」をやってもらいます。
# ステップ2: 選択肢から1つ選んでもらう(相談窓口の振り分け)
from kantan import erabu, bar
madoguchi = {
"保健室": "体調が悪い・けがをした",
"職員室": "先生への連絡・提出物・忘れ物",
"図書室": "本を借りたい・調べものをしたい",
}
soudan = [
"朝から頭が痛くて、熱っぽいです。",
"数学のプリントを家に忘れてしまいました。",
"夏休みの自由研究で恐竜について調べたい。",
]
for s in soudan:
a, ms = erabu(s, "どこに相談に行けばいい?", madoguchi)
print(f"\n{s} → {a['choice']} ({ms:.0f}ミリ秒)")
for name, p in a["probabilities"].items():
print(f" {name:<4} {bar(p)} {p:.0%}")
朝から頭が痛くて、熱っぽいです。 → 保健室 (459ミリ秒)
保健室 ████████████████████ 99%
職員室 ···················· 1%
図書室 ···················· 0%
数学のプリントを家に忘れてしまいました。 → 職員室 (492ミリ秒)
保健室 ···················· 1%
職員室 ████████████████████ 98%
図書室 ···················· 0%
夏休みの自由研究で恐竜について調べたい。 → 図書室 (479ミリ秒)
保健室 ···················· 2%
職員室 █··················· 3%
図書室 ███████████████████· 95%
3問とも、迷わず正しい窓口を選びました。
選択肢はコードの中で自由に書きかえられます。
AIを作り直す(再学習する)必要はありません。
madoguchi に「体育館」「事務室」などを足して、自分の学校版をつくってみてください。
ステップ3: 点数をつけてもらう
段階を低い順に並べて渡すと、どのあたりかを小数で返します。
# ステップ3: 段階で点数をつけてもらう
from kantan import tensuu
levels = ["とても暗い", "ふつう", "とても明るい"] # 低い順に並べる
for s in ["今日の給食、めっちゃおいしかった!", "もう最悪。傘忘れてびしょぬれ。", "明日は月曜日です。"]:
a, ms = tensuu(s, "気持ちはどのくらい明るい?", levels)
print(f"点数 {a['score']:.2f} (0=とても暗い, 2=とても明るい) {s}")
点数 1.54 (0=とても暗い, 2=とても明るい) 今日の給食、めっちゃおいしかった!
点数 0.41 (0=とても暗い, 2=とても明るい) もう最悪。傘忘れてびしょぬれ。
点数 0.99 (0=とても暗い, 2=とても明るい) 明日は月曜日です。
「明日は月曜日です。」は、ほぼ真ん中の「ふつう」になりました。
なお公式の評価資料には、点数(score)は弱いと書かれています。
大事な判断には「はい/いいえ」か「選ぶ」を使うほうが安心です。
ステップ4: わざと困らせてみる
AIがどこでつまずくかを知っておくと、うまく使えるようになります。
3つの実験をまとめて行います。
# ステップ4: わざと困らせてみる(弱点さがし)
from kantan import erabu
madoguchi = {
"保健室": "体調が悪い・けがをした",
"職員室": "先生への連絡・提出物・忘れ物",
"図書室": "本を借りたい・調べものをしたい",
}
def show(title, a):
probs = " ".join(f"{k} {v:.0%}" for k, v in a["probabilities"].items())
print(f"{title} → {a['choice']} | {probs}")
print("【実験A】選択肢の説明を消すと? 相談:「体操服を忘れました」")
show("説明なし", erabu("体操服を忘れました", "どこに相談に行けばいい?", {k: k for k in madoguchi})[0])
show("説明あり", erabu("体操服を忘れました", "どこに相談に行けばいい?", madoguchi)[0])
print("\n【実験B】どれにも当てはまらない相談をすると? 相談:「お腹すいた。」")
show("3択のまま", erabu("お腹すいた。", "どこに相談に行けばいい?", madoguchi)[0])
show("「どれでもない」を足す", erabu("お腹すいた。", "どこに相談に行けばいい?",
{**madoguchi, "どれでもない": "上のどれにも当てはまらない"})[0])
print("\n【実験C】質問を逆の意味にすると? 相談:「朝から頭が痛くて、熱っぽいです。」")
s = "朝から頭が痛くて、熱っぽいです。"
show("どこに相談に行けばいい?", erabu(s, "どこに相談に行けばいい?", madoguchi)[0])
show("行ってはいけない場所はどこ?", erabu(s, "絶対に行ってはいけない場所はどこ?", madoguchi)[0])
【実験A】選択肢の説明を消すと? 相談:「体操服を忘れました」
説明なし → 保健室 | 保健室 93% 職員室 6% 図書室 1%
説明あり → 職員室 | 保健室 4% 職員室 95% 図書室 0%
【実験B】どれにも当てはまらない相談をすると? 相談:「お腹すいた。」
3択のまま → 保健室 | 保健室 88% 職員室 4% 図書室 8%
「どれでもない」を足す → どれでもない | 保健室 17% 職員室 1% 図書室 2% どれでもない 81%
【実験C】質問を逆の意味にすると? 相談:「朝から頭が痛くて、熱っぽいです。」
どこに相談に行けばいい? → 保健室 | 保健室 99% 職員室 1% 図書室 0%
行ってはいけない場所はどこ? → 保健室 | 保健室 98% 職員室 1% 図書室 1%
実験A: 選択肢の説明で答えが変わる
「保健室・職員室・図書室」と名前だけを渡すと、体操服を忘れた相談を保健室(93%)に回しました。
「体操服」という言葉が、体育や体調を連想させたのかもしれません。
各選択肢に「先生への連絡・提出物・忘れ物」のような説明をつけると、職員室(95%)に変わりました。
選択肢の説明は、このAIにとって大切な手がかりです。
実験B: 逃げ道がないと無理やり選ぶ
マークシートには「わからない」の欄がありません。
「お腹すいた。」という、どの窓口にも合わない相談でも、保健室を88%で選びました。
選択肢に「どれでもない」を足すと、81%でそちらを選びました。
逃げ道の選択肢を用意しておくのは、使う人間の仕事です。
実験C: 質問文はあまり読んでいない
「行ってはいけない場所はどこ?」と逆の意味で聞いても、答えは保健室(98%)のままでした。
これは公式の説明にも書かれている弱点です。
状況と選択肢を同じにしたまま質問文だけを変えても、約94%は同じ答えになるそうです。
このAIは、質問文よりも、状況の文章と選択肢のほうを強く見ています。
公式のおすすめは、「〜ではないのは?」のような逆の質問を避けることです。
ふつうに読んだ意味が、そのまま聞きたいことになる質問にします。
確率は「正しさ」ではない
実験A・Bでは、間違った答えにも88〜93%の確率がついていました。
%は「AIがどれくらいありそうだと考えたか」であって、「どれくらい正しいか」ではありません。
大事な判断に使う前には、自分のデータで何問か試して、何%なら信じてよいかを決めます。
次のステップ5で、実際にこれをやってみます。
ステップ5: AIエージェントの門番にする
ここが応用編です。
「天気を調べる道具」を持ったAIエージェントに、都市名を言わずに「天気どう?」と聞きます。
AIエージェントの仕組みは、Strands Agents というライブラリで作ります。
このエージェントには、「都市がわからなければ勝手に東京だと決めつけろ」という、わざと悪いクセをつけておきます。
Strands Decider を門番として置き、道具を使う直前に「その都市名は、ユーザーが本当に言ったものか」をチェックさせます。
公式リポジトリにも同じ考え方のサンプル(examples/strands/tool_call_intervention.py)があります。
この記事では、それを短くしたものを使います。
公式サンプルとの違いは次の2つです。
- 公式サンプルは「根拠があるか」と「聞き返す前に使うのは早すぎないか」の2つを質問します
この記事では1つ目だけを使います - 公式サンプルは門番に渡す文章の先頭に、会話の説明をする1文を入れています
この記事では省いています
5-1: 質問としきい値を決める(AWSなし)
しきい値は、「何%以上なら通す」と決める境目の数字です。
いきなりエージェントに組み込む前に、門番の質問を6つの例で試して、この境目を決めます。
比べるのは、公式サンプルの質問と、もっと具体的に聞く質問の2つです。
# ステップ5-1: 門番の質問としきい値を、エージェントなしで決める(AWS不要)
import json
from kantan import ask
def jokyo(user, city):
"""「ユーザーの発言」と「AIが使おうとしている道具」を1つの文章にまとめる"""
return (f"--- CONVERSATION ---\nuser: {user}\n\n--- PROPOSED TOOL CALL ---\n"
f"tool: get_weather\narguments: {json.dumps({'location': city}, ensure_ascii=False)}")
shitsumon = {
"公式の質問": {
"type": "noul",
"instructions": "Are the tool's argument values grounded in facts the user actually provided?",
"criteria": {
"true": "every argument value traces back to something the user said",
"false": "an argument value was guessed or invented, not stated by the user",
},
},
"具体的な質問": {
"type": "noul",
"instructions": "道具に渡す都市名を、ユーザーは自分で言いましたか?",
"criteria": {
"true": "道具に渡す都市名が、ユーザーの発言の中にある",
"false": "ユーザーはその都市名を一度も言っていない",
},
},
}
# (ユーザーの発言, AIが使おうとした都市名, 正解: 通すべきか)
tests = [
("天気どう?", "東京", False),
("今日の天気は?", "名古屋", False),
("天気教えて", "福岡", False),
("大阪の天気どう?", "大阪", True),
("札幌って今雨降ってる?", "札幌", True),
("明日の京都の天気は?", "京都", True),
]
print("発言 都市 通すべき 公式の質問 具体的な質問")
for user, city, ok in tests:
a = ask(jokyo(user, city), shitsumon)["answers"]
print(f"{user:<16} {city:<5} {'はい' if ok else 'いいえ':<5} "
f"{a['公式の質問']['noul']:>8.0%} {a['具体的な質問']['noul']:>11.0%}")
発言 都市 通すべき 公式の質問 具体的な質問
天気どう? 東京 いいえ 22% 47%
今日の天気は? 名古屋 いいえ 22% 46%
天気教えて 福岡 いいえ 23% 41%
大阪の天気どう? 大阪 はい 61% 88%
札幌って今雨降ってる? 札幌 はい 42% 85%
明日の京都の天気は? 京都 はい 62% 87%
公式サンプルの質問は「道具に渡す値(引数)は、ユーザーが実際に言った事実にもとづいていますか?」という、どんな道具にも使える聞き方です。
公式サンプルは、この確率が0.45未満なら止める設定になっています。
この記事の簡略版(前置きの1文なし・質問1つ)で測った表に0.45を当てはめると、「札幌って今雨降ってる?」(42%)は止められてしまいます。
ユーザーは札幌と言っているのにこのようになってしまいました。
「道具に渡す都市名を、ユーザーは自分で言いましたか?」と具体的に聞くと、結果がはっきり分かれました。
- 言っていない3件: 41〜47%
- 言った3件: 85〜88%
この2つのグループの間に線を引けば、6件とも正しく分けられます。
この記事では、しきい値を 0.65にしました。
ステップ4の実験Cで「質問文はあまり読まない」と書きましたが、何を聞くかを具体的にすることには意味がありました。
逆さまの聞き方は通じなくても、聞く中身を絞ると答えははっきりします。
5-2: エージェントに組み込む(Amazon Bedrockが必要)
ここからは、AIエージェント本体に Amazon Bedrock の Claude Haiku 4.5 を使います。
- AWSアカウントが必要です。Amazon Bedrock の利用料が少しかかります
- AWS CLI v2 を入れておきます。手順は公式のAWS CLI のインストールガイドにあります
aws login は新しいバージョンにしかないコマンドなので、古い場合は更新します(手元では 2.35.15 で確認しました) - ターミナルで aws login を実行し、開いたブラウザで承認してログインします
ライブラリを追加します。
uv pip install strands-agents "botocore[crt]"
botocore[crt] は、aws login でログインした状態をPythonから使うための追加部品です。これがないと、エージェントを動かしたときに MissingDependencyException というエラーになりました。
コードの中の Monban は、InterventionHandler という「道具を使う直前に呼ばれる係」を受け継いだクラスです。
before_tool_call の中で Decider に質問し、Proceed(通す)か Guide(やり直させる)を返します。
# ステップ5: AIエージェントの「門番」にする(Amazon Bedrock が必要)
# 公式サンプル examples/strands/tool_call_intervention.py を短くし、質問としきい値をステップ5-1の結果に合わせて変えたもの
import json
from strands import Agent, tool
from strands.models import BedrockModel
from strands.interventions import Guide, InterventionHandler, Proceed
from kantan import ask
QUESTIONS = {
"konkyo": { # ステップ5-1で決めた「具体的な質問」
"type": "noul",
"instructions": "道具に渡す都市名を、ユーザーは自分で言いましたか?",
"criteria": {
"true": "道具に渡す都市名が、ユーザーの発言の中にある",
"false": "ユーザーはその都市名を一度も言っていない",
},
},
}
HAI = 0.65 # ステップ5-1の結果から決めた境目。決めるのはモデルではなく、このコード
class Monban(InterventionHandler):
name = "monban"
def before_tool_call(self, event, **kwargs):
# 門番に見せるのはユーザーの発言だけ(AIの「東京を調べますね」が混ざると判定がぶれる)
talk = "\n".join(
f"user: {b['text']}"
for m in event.agent.messages if m["role"] == "user"
for b in m.get("content", []) if "text" in b
)
state = (f"--- CONVERSATION ---\n{talk}\n\n--- PROPOSED TOOL CALL ---\n"
f"tool: {event.tool_use['name']}\narguments: {json.dumps(event.tool_use.get('input') or {}, ensure_ascii=False)}")
r = ask(state, QUESTIONS)
p = r["answers"]["konkyo"]["noul"]
print(f"[門番] {event.tool_use['name']}({event.tool_use.get('input')}) ユーザーが言った {p:.0%} ({r['latency_ms']:.0f}ミリ秒)")
if p < HAI:
print("[門番] → 待った! ユーザーに聞き直させる")
return Guide(feedback="その都市名はユーザーが言っていません。どの都市か聞いてください。")
print("[門番] → 通ってよし")
return Proceed()
@tool
def get_weather(location: str) -> str:
"""指定した都市の天気を調べる。
Args:
location: 都市名
"""
return f"{location}は晴れ、21度です。"
SYSTEM_PROMPT = ("あなたはせっかちな天気アシスタントです。都市が書かれていなければ"
"聞き返さずに東京だと決めつけて、すぐ get_weather を呼んでください。")
for q in ["天気どう?", "大阪の天気どう?", "札幌って今雨降ってる?"]:
print(f"\n========== ユーザー: {q}")
agent = Agent(model=BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0", region_name="us-east-1"), tools=[get_weather],
interventions=[Monban()], system_prompt=SYSTEM_PROMPT, callback_handler=None)
print(f"AIの最終回答: {agent(q)}")
python step5_monban.py
========== ユーザー: 天気どう?
[門番] get_weather({'location': '東京'}) ユーザーが言った 47% (1449ミリ秒)
[門番] → 待った! ユーザーに聞き直させる
AIの最終回答: すみません!どの都市の天気が知りたいですか?
========== ユーザー: 大阪の天気どう?
[門番] get_weather({'location': '大阪'}) ユーザーが言った 88% (987ミリ秒)
[門番] → 通ってよし
AIの最終回答: 大阪は現在**晴れ**で、気温は**21度**ですよ!いい天気ですね。
========== ユーザー: 札幌って今雨降ってる?
[門番] get_weather({'location': '札幌'}) ユーザーが言った 84% (789ミリ秒)
[門番] → 通ってよし
AIの最終回答: 札幌は今**晴れ**ですよ!気温は21度。雨は降っていません。過ごしやすい天気ですね!
勝手に「東京」と決めつけた呼び出しは止められ、エージェントは聞き直しました。
都市名を言った大阪と札幌は、そのまま通りました。
門番づくりでつまずいた点
最初は公式サンプルと同じく、会話の全部(ユーザーの発言とAIの発言)を門番に渡していました。すると、「天気どう?」で決めつけた東京が77%となり、通ってしまいました。
門番に渡している文章を表示してみると、原因がわかりました。
--- CONVERSATION ---
user: 天気どう?
assistant: 東京の天気を確認しますね!
--- PROPOSED TOOL CALL ---
tool: get_weather
arguments: {"location": "東京"}
AIが自分で「東京の天気を確認しますね!」と言っていたため、「会話の中に東京が出てくる」と判断されていました。
そこで、門番にはユーザーの発言だけを見せるように変えています(コードの「門番に見せるのはユーザーの発言だけ」の部分)。
門番のしくみで押さえておくこと
- 止めるだけでなく、やり直させることもできます
Guide を返すと、エージェントにアドバイスを渡してもう一度考えさせます
いきなり拒否するのではなく、正しい方向へ戻す仕組みです - 最後に決めているのは人間が書いたコードです
Decider は「47%」という数字を出すだけで、「0.65未満なら止める」と決めたのは if 文です - しきい値は、自分の例で決めます。
公式サンプルの0.45を簡略版の門番に当てはめると、札幌を止めてしまいました
6件の表をつくるだけで、使える値が見つかりました
ステップ6: しゃべるAIと速さを比べる
同じ「相談窓口の振り分け」を、しゃべるAI(Claude Haiku 4.5)にも解かせて、時間を比べます。ステップ5-2と同じく Amazon Bedrock を使います。
# ステップ6: 同じ問題を「しゃべるAI」(Claude Haiku 4.5)と比べる(Amazon Bedrock が必要)
import time, statistics, boto3
from kantan import erabu
madoguchi = {"保健室": "体調が悪い・けがをした", "職員室": "先生への連絡・提出物・忘れ物",
"図書室": "本を借りたい・調べものをしたい"}
soudan = ["朝から頭が痛くて、熱っぽいです。", "数学のプリントを家に忘れてしまいました。",
"夏休みの自由研究で恐竜について調べたい。", "体操服を忘れました", "体育でひざをすりむいた"]
br = boto3.client("bedrock-runtime", region_name="us-east-1")
def llm(s):
prompt = (f"相談:「{s}」\nどこに相談に行けばいい? 次から1つだけ、名前だけを答えて。\n"
+ "\n".join(f"- {k}: {v}" for k, v in madoguchi.items()))
t = time.perf_counter()
r = br.converse(modelId="us.anthropic.claude-haiku-4-5-20251001-v1:0",
messages=[{"role": "user", "content": [{"text": prompt}]}],
inferenceConfig={"maxTokens": 20})
return r["output"]["message"]["content"][0]["text"].strip(), (time.perf_counter() - t) * 1000
d_ms, l_ms = [], []
for s in soudan:
a, ms1 = erabu(s, "どこに相談に行けばいい?", madoguchi)
ans, ms2 = llm(s)
d_ms.append(ms1); l_ms.append(ms2)
print(f"{s:<24} Decider: {a['choice']}({ms1:.0f}ms) LLM: {ans}({ms2:.0f}ms)")
print(f"\n中央値 Decider {statistics.median(d_ms):.0f}ms(このMacの中) / LLM {statistics.median(l_ms):.0f}ms(ネット越し)")
2回実行したうちの2回目の結果です。1回目は、最初の呼び出しに準備の処理が入ってDeciderの1問目だけ遅くなったため(1829ミリ秒)、ふだんの速さに近い2回目を載せています。
朝から頭が痛くて、熱っぽいです。 Decider: 保健室(536ms) LLM: 保健室(1114ms)
数学のプリントを家に忘れてしまいました。 Decider: 職員室(585ms) LLM: **職員室**(785ms)
夏休みの自由研究で恐竜について調べたい。 Decider: 図書室(516ms) LLM: 図書室(753ms)
体操服を忘れました Decider: 職員室(493ms) LLM: 職員室(748ms)
体育でひざをすりむいた Decider: 保健室(505ms) LLM: 保健室(785ms)
中央値 Decider 516ms(このMacの中) / LLM 785ms(ネット越し)
わかったことは3つです。
- 答えは5問とも同じでした。この程度の判断なら、小さなAIでも大きなAIと同じ答えが出ます
- 速さの差は思ったより小さいです。M1のMacでは1問0.5秒ほどで、ネット越しのClaude Haiku 4.5(約0.8秒)より少し速い程度でした。公式の値は、NVIDIA RTX 3090で115ミリ秒、M3 ProのMacで153ミリ秒(短い質問の場合)です(どちらも中央値)
パソコンの性能で大きく変わります - しゃべるAIは答えの書き方がぶれることがあります
2問目では、Claudeが「**職員室**」と太字の記号つきで返してきました
プログラムで受け取るなら、記号を取り除く処理が必要です
Decider は選択肢の名前そのものを返します
速さ以外にも
- 会話の中身をパソコンの外に出さずに判断できる
- Decider の判断には1回ごとの利用料がかからない
という違いがあります。
つまずきやすいところ
実際に手を動かして引っかかった点をまとめておきます。
- 最初の1回が遅い: モデルのダウンロード(合計約9GB)と準備が入ります
2回目からは速くなります - MissingDependencyException が出る: aws login でログインしている場合、botocore[crt] を入れます
- The provided model identifier is invalid が出る: AWSのデータセンターの場所(リージョン)の設定が東京になっていると、アメリカ向けの名前(us. で始まるモデルID)が使えません
コードのように region_name="us-east-1" を明示します - サーバーが落ちる、Connection refused になる: 同じポートで2つのサーバーを起動しようとしていないか確認します
手元では、別の作業で起動していたサーバーとポート8099がぶつかり、サーバーが止まりました
別のポートで起動した場合は、STRANDS_DECIDER_URL=http://127.0.0.1:ポート番号 を設定してからスクリプトを実行すると、kantan.py の送り先が変わります
試してわかったこと
最初に「えらぶだけのAIを、判断だけに使う」と書きました。
手を動かしてみると、上手に使うためのコツは、AIよりも人間側の準備にありました。
- 選択肢には説明をつける(実験A)
- 「どれでもない」のような逃げ道を用意する(実験B)
- 質問は具体的に、まっすぐ聞く(実験C、ステップ5-1)
- 何%で信じるかは、自分の例で表をつくって決める(ステップ5-1)
どれも、マークシートの問題をつくる先生がやっていることと同じです。
えらぶAIは、よい問題をつくれば、手元のパソコンで素早く正確に答えてくれます。
最後まで読んでいただきありがとうございました。
参考
- Introducing Strands Decider 2B: a small, open source, decision model(Strands Agents 公式ブログ)
- strands-labs/strands-decider(GitHub)
- StrandsAgents/strands-decider-2B-hobson-v19(Hugging Face モデルカード)

