📌

Strands Agents アップデート一挙ご紹介!

に公開

先日 AWS re:Invent 2025にて OSS AI エージェントフレームワークである Strands Agents でもいくつかの新機能が公表されたためご紹介します!

新機能

TypeScript のサポート(Preview)

https://aws.amazon.com/jp/about-aws/whats-new/2025/12/typescript-strands-agents-preview/

Strands Agents が TypeScript でも書けるようになりました。従来は Python のみでしたが、TypeScript にも対応し、より広範なプロジェクトに組み込めるようになります。特にバックエンドを Node.js などで構築しているプロジェクトではよりシームレスな統合が可能になります。

下記はコードサンプルです。LetterCounter という、指定した文字が、与えた文字列中に何度登場するのかをカウントするシンプルなツールを使って「strawberry」の中に「r」は何回登場するか調べさせるタスクを実行します。(コードサンプルを参考に作りましたが、日本語化するにあたって若干の変更を加えています。)

import { Agent, tool, BedrockModel } from '@strands-agents/sdk'
import z from 'zod'

const letterCounter = tool({
  name: 'letterCounter',
  description: 'letter が word の中に何回現れるかカウントする',
  inputSchema: z.object({
    word: z.string(),
    letter: z.string().min(1).max(1),
  }),
  callback: ({ word, letter }) => {
    const lowerWord = word.toLowerCase()
    const lowerLetter = letter.toLowerCase()

    let count = 0
    for (const char of lowerWord) {
      if (char === lowerLetter) count++
    }
    return `The letter '${letter}' appears ${count} time(s) in '${word}'`
  },
})

const agent = new Agent({
  tools: [letterCounter]
})

const message = `「strawberry」の中に「R」は何回登場する?ツールを使って回答すること。`
try {
  const result = await agent.invoke(message)
  console.log('✅ Success!')
  console.log(result.lastMessage)
} catch (error) {
  console.error('❌ Error:', error)
}

出力結果
🔧 Tool #1: letterCounter
✓ Tool completed
「strawberry」の中に「r」は**3回**登場します。

具体的には以下の位置にあります:
- st**r**awberry (3文字目)
- strawbe**rr**y (8文字目と9文字目)✅ Success!
Message {
  type: 'message',
  role: 'assistant',
  content: [
    TextBlock {
      type: 'textBlock',
      text: '「strawberry」の中に「r」は**3回**登場します。\n' +
        '\n' +
        '具体的には以下の位置にあります:\n' +
        '- st**r**awberry (3文字目)\n' +
        '- strawbe**rr**y (8文字目と9文字目)'
    }
  ]
}
環境構築

package.json

{
  "name": "strands-agents-new-feature",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@strands-agents/sdk": "^0.1.2",
    "@types/node": "^24.10.1",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3",
    "zod": "^4.1.13"
  }
}

スクリプトを実行

npx tsx your-file.ts

正確に動作していますし、ツールもきちんと使えていますね!

参考
https://strandsagents.com/latest/documentation/docs/user-guide/quickstart/typescript/

エッジデバイスサポート(GA)

エッジデバイスサポートは、小規模デバイスでローカルモデルを動かすための拡張です。AI エージェントを動かすためには当然大規模言語モデルの存在が不可欠ですが、従来は Amazon Bedrock を始めとした API 経由で動かすことが前提でした。エッジデバイスサポートによりローカルでホストするモデルを Strands Agents とシームレスに繋げて動かせるようになりました。

環境構築

インスタンス:m6g.large
OS:Amazon Linux 2023 (ARM64)
ストレージ:20GB EBS (gp3)
モデル:Qwen2.5-7B-Instruct Q3_K_M (3.6GB)

上記設定でEC2インスタンス起動し、キーペアを生成。

依存関係をインストール

sudo dnf install -y git cmake gcc-c++ libcurl-devel

モデルをビルド

# Clone repository
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp

# Build with ARM optimizations
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j$(nproc)

モデルの内部パラメータをダウンロード

# Create models directory
mkdir -p ~/models

# Download Qwen2.5-7B-Instruct Q3_K_M (3.6GB)
cd ~/models
wget https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF/resolve/main/qwen2.5-7b-instruct-q3_k_m.gguf

サーバーを起動

cd ~/llama.cpp

# Start llama-server
./build/bin/llama-server \
  -m ~/models/qwen2.5-7b-instruct-q3_k_m.gguf \
  --host 0.0.0.0 \
  --port 8080 \
  -c 8192 \
  --jinja

LLM サーバーのテスト

curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen2.5",
    "messages": [{"role": "user", "content": "こんにちは"}],
    "temperature": 0.7,
    "max_tokens": 100
  }'

これで返答があればOK。

重すぎて動かない場合は軽量モデルでテスト

cd ~/models
wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q3_k_m.gguf

サーバー起動

cd ~/llama.cpp
./build/bin/llama-server \
  -m ~/models/qwen2.5-1.5b-instruct-q3_k_m.gguf \
  --host 0.0.0.0 --port 8080 \
  -t 2 -tb 2 \
  -c 4096 \
  -np 1 \
  --jinja

次にpythonをインストールする

# Python 3.12とpipをインストール
sudo dnf install -y python3.12 python3.12-pip

# バージョン確認
python3.12 --version  # Python 3.12.12

次にパッケージマネージャーとしてuvをインストール

# uvをインストール
curl -LsSf https://astral.sh/uv/install.sh | sh

# パスを通す
source ~/.bashrc

# バージョン確認
uv --version  # uv 0.9.16

requirements.txt

strands-agents[llamacpp]
strands-agents-tools

これらをインストールします

# 仮想環境の作成
uv venv

# 仮想環境の有効化
source .venv/bin/activate

# パッケージのインストール
uv pip install -r requirements.txt

サンプルコードは下記です。

from strands import Agent
from strands.models.llamacpp import LlamaCppModel
from strands_tools import calculator

model = LlamaCppModel(
    base_url="http://localhost:8080",
    timeout=(5.0, 600.0),  # (接続タイムアウト, 読み取りタイムアウト) を延長
    model_id="default",
    params={
        "max_tokens": 1000,
        "temperature": 0.7,
        "repeat_penalty": 1.1,
    },
)

agent = Agent(model=model, tools=[calculator])
print(agent("20*48はいくつ?計算ツールを使って。"))

小さめのインスタンスだったので実行完了まで10分以上かかります。

出力結果
Tool #1: calculator
20乘以48的结果是960。20乘以48的结果是960。

軽量モデルを使用したので出力に中国語が混じりましたが、計算ツールを適切に利用し、正確な答えを導き出していることがわかります。

参考
https://strandsagents.com/latest/documentation/docs/user-guide/concepts/model-providers/llamacpp/

双方向ストリーミング

エッジデバイスサポートと関連して、双方向ストリーミング(Bidirectional Streaming / BidiAgent)が利用可能になりました。通常、Strands Agents は単一のReqest/Response で駆動しますが、双方向ストリーミングを使うと常時接続で audio/text を双方向に流し、割り込みや並行ツール実行を扱うことができるようになります。

実際にサンプルコードを実行してみます。

環境構築

requirements.txt

strands-agents
strands-agents-tools
strands-agents[bidi-all]

同じディレクトリで下記を実行

# 仮想環境の作成
uv venv

# 仮想環境の有効化
source .venv/bin/activate

# パッケージのインストール
uv pip install -r requirements.txt
import asyncio
from strands.experimental.bidi import BidiAgent, BidiAudioIO
from strands.experimental.bidi.io import BidiTextIO
from strands.experimental.bidi.models import BidiNovaSonicModel


class TranscriptStdout:
    """
    ユーザー/AI の確定(final)発話だけを STDOUT に表示する。
    - Preview は捨てる(重複防止)
    - [YOU] / [AI ] を付けて見分けやすくする
    """
    def __init__(self) -> None:
        self._last_by_role: dict[str, str] = {}

    def output(self):
        async def _handle(event):
            # event は dict 想定。型/キーは experimental のため環境差がありえます。
            if not isinstance(event, dict):
                return

            # 文字起こし(transcript)系イベントのみ処理(環境差に備えて緩めに判定)
            etype = str(event.get("type") or event.get("event_type") or "")
            if "transcript" not in etype.lower():
                return

            # 話者(user/assistant)
            role = (event.get("role") or event.get("speaker") or "").lower()
            if role not in ("user", "assistant"):
                return

            # Preview を除外(キーがない場合は「確定扱い」にして、とにかく重複抑止で守る)
            is_final = event.get("is_final")
            if is_final is False:
                return

            # テキスト本体(環境差に備えて候補を順に見る)
            text = (
                event.get("current_transcript")
                or event.get("text")
                or event.get("transcript")
                or ""
            )
            text = str(text).strip()
            if not text:
                return

            # 念のため、同一 role の同一文を連続出力しない(final が二重に来る場合の保険)
            if self._last_by_role.get(role) == text:
                return
            self._last_by_role[role] = text

            prefix = "[YOU]" if role == "user" else "[AI ]"
            print(f"{prefix} {text}", flush=True)

        return _handle


# 音声リアルタイム向けモデル(例:Amazon Nova Sonic)
model = BidiNovaSonicModel()

# エージェント(日本語システムプロンプト)
agent = BidiAgent(
    model=model,
    system_prompt="あなたは有能な音声アシスタントです。応答は簡潔で自然な日本語にしてください。"
)

# 音声I/O(マイク入力・スピーカー出力)
audio_io = BidiAudioIO()

# テキスト入力(STDINから入力したい場合のみ)
text_io = BidiTextIO(input_prompt="> ")

async def main():
    await agent.run(
        # 入力:マイク音声 +(任意)標準入力テキスト
        inputs=[audio_io.input(), text_io.input()],
        # 出力:スピーカー音声 +(整形済み)対話ログ(ユーザー/AIの確定発話のみ)
        # ★ text_io.output() は入れない(重複の原因)
        outputs=[audio_io.output(), TranscriptStdout().output()],
    )

asyncio.run(main())
出力結果
> [YOU] konnichiwa
[AI ] こんにちは!今日はどんなことをお手伝いできますか?
[YOU] 今日はいい天気ですね
[AI ] そうなんですか?素晴らしい天気ですね!何か外で楽しいことを計画していますか?
[YOU] 今日はもう帰って寝るだけですね
[AI ] お疲れ様です!ゆっくり休んでくださいね 良い夢が見られますように
[YOU] hi good night
[AI ] こんばんは!おやすみなさい 良い夢を!

音声を載せられないので標準入出力をコピーしました。会話はきちんとできますが、日本語だと少しカタコトな印象を受けます。

さて、凄いのは双方向で会話しながらツールも利用できるという点です。下記はサンプルコードです。現在時刻を確認するツール、計算機ツール、天気情報取得ツール(API呼び出しなどは未実装)を使わせてみます。

import asyncio
from strands import tool
from strands.experimental.bidi import BidiAgent, BidiAudioIO
from strands.experimental.bidi.models import BidiNovaSonicModel
from strands_tools import calculator, current_time


class TranscriptStdout:
    """
    ユーザー/AI の確定(final)発話だけを STDOUT に表示する。

    仕様:
    - Preview(途中結果)は表示しない(AI出力の重複を防ぐ)
    - [YOU] / [AI ] を付けて見分けやすくする
    - 同一roleで同一文が連続した場合は重複表示しない(保険)
    """
    def __init__(self) -> None:
        self._last_by_role: dict[str, str] = {}

    def output(self):
        async def _handle(event):
            # event は dict を想定(experimental のため環境差あり)
            if not isinstance(event, dict):
                return

            # transcript 系イベントのみ対象(環境差に備えて緩めに判定)
            etype = str(event.get("type") or event.get("event_type") or "")
            if "transcript" not in etype.lower():
                return

            # 話者(user/assistant)
            role = (event.get("role") or event.get("speaker") or "").lower()
            if role not in ("user", "assistant"):
                return

            # Preview を除外(is_final が明示されて false の場合のみ捨てる)
            # is_final が無い環境でも動くように「無ければ確定扱い」+重複抑止で守る
            if event.get("is_final") is False:
                return

            # テキスト本体(候補を順に見る)
            text = (
                event.get("current_transcript")
                or event.get("text")
                or event.get("transcript")
                or ""
            )
            text = str(text).strip()
            if not text:
                return

            # 同一roleでの同一文の連続出力を抑止
            if self._last_by_role.get(role) == text:
                return
            self._last_by_role[role] = text

            prefix = "[YOU]" if role == "user" else "[AI ]"
            print(f"{prefix} {text}", flush=True)

        return _handle


# カスタムツールを定義
@tool
def get_weather(location: str) -> str:
    """
    指定された場所の現在の天気を取得します。

    Args:
        location: 都市名または場所

    Returns:
        天気情報
    """
    # 実際のアプリケーションでは、天気APIを呼び出します
    return f"{location}の天気は晴れで、22°Cです"


# ツールを持つエージェントを作成
model = BidiNovaSonicModel()
agent = BidiAgent(
    model=model,
    tools=[calculator, current_time, get_weather],
    system_prompt="あなたはツールにアクセスできる親切なアシスタントです。必要に応じてツールを使い、簡潔で自然な日本語で応答してください。"
)

audio_io = BidiAudioIO()
transcript_stdout = TranscriptStdout()

async def main():
    await agent.run(
        inputs=[audio_io.input()],
        # ★ text_io.output() は使わない(Preview+確定で重複するため)
        outputs=[audio_io.output(), transcript_stdout.output()],
    )

asyncio.run(main())
出力結果
[YOU] konichiwa
[AI ] こんにちは!今日はどんなことをお手伝いできますか?
[YOU] ima nanji desu ka
[AI ] 現在2025年12月8日午前10時5分56秒です
[YOU] nijuu go kakeru yonjuu hachi wa ikutsu desu ka
[AI ] 25を48で掛けると1200になります
[YOU] toukyou no tenki wa ima dou naottemasu ka
[AI ] 東京の天気は晴れで 気温は22°Cです

ツールを利用して適切な情報を得られているように見えます。時刻に関してはこれを実行したのがおおよそ夜の7時10分くらいだったので日付はあっていますが、時刻が異なっていますね。計算結果は正しく48 x 25 = 1200と出せています。また、東京の気温と天気もツールの出力通りとなっています。

参考
https://strandsagents.com/latest/documentation/docs/user-guide/concepts/experimental/bidirectional-streaming/quickstart/
https://github.com/strands-agents/sdk-python/releases

Strands Steering(Experimental)

Steering 機能を見ていきましょう。従来、AI エージェントの開発ではシステムプロンプトに巨大な指示を入れ、そこからユーザープロンプトでエージェントを駆動する方式をとっていました。しかし、この方式ではエージェントの動作ステップが増えるにつれてコンテキストが消失し、初期のシステムプロンプトの無視したり、ハルシネーションを生じるリスクがありました。

Steering はライフサイクルイベント(例:ツール呼び出し直前)にフックして、状況に応じたガイダンスを注入する仕組みです。(Kiro でも同じ機能があり、例えばドキュメント(*.mdファイル)を生成したときに、それをフックに日本語版のドキュメントを同時に生成する、といった用途に使うことができます。)

フックはこのように定義します

handler = LLMSteeringHandler(
    system_prompt="""
必ず文章が常体(だ・である調)となるようにします。

ガイダンス:
- 文面を詳細に確認し敬体となっている部分を探します
- 敬体となっている部分を指摘して常体に直すよう促します

この会社におけるメールは常体でコミュニケーションコストを最小とすることが重要です。その旨をきちんと伝えること。
"""
)

ハンドラーの中にシステムプロンプトを定義してあげるだけです。

では、サンプルコードを見ていきましょう。下記はメールを書かせるエージェントに対して、メール送信ツールを使おうとする度に文面を常体(だ・である調)にせよと促す Steering を組んでいます。生成AIは一般的になんでも丁寧に敬体で説明しようとするので苦戦するはず....さて結果はどうなったでしょう。

from strands import Agent, tool
from strands.experimental.steering import LLMSteeringHandler

@tool
def send_email(recipient: str, subject: str, message: str) -> str:
    """受信者にメールを送信する"""
    return f"Email sent to {recipient}"

handler = LLMSteeringHandler(
    system_prompt="""
必ず文章が常体(だ・である調)となるようにします。

ガイダンス:
- 文面を詳細に確認し敬体となっている部分を探します
- 敬体となっている部分を指摘して常体に直すよう促します

この会社におけるメールは常体でコミュニケーションコストを最小とすることが重要です。その旨をきちんと伝えること。
"""
)

SYSTEM_PROMPT = "メールの送信前に必ず文面を出力すること。"
agent = Agent(
    tools=[send_email],
    hooks=[handler],
    system_prompt = SYSTEM_PROMPT
)

response = agent(
    "予定をキャンセルするメールを記述して。isedaという人に対して、今日のランチを断ります。件名などは適当に設定すること。"
)

print(response)
出力結果

今日のランチをキャンセルするメールを作成いたします。以下の内容でお送りします:

件名: 本日のランチの件について【キャンセルのお願い】

メッセージ:
iseda様

いつもお世話になっております。

本日予定しておりましたランチの件でご連絡いたします。
申し訳ございませんが、急用が入ってしまい、本日のランチをキャンセルさせていただきたく存じます。

直前のご連絡となり、大変申し訳ございません。
また改めて機会を作らせていただければと思います。

何卒ご理解のほど、よろしくお願いいたします。
Tool #1: send_email
承知いたしました。常体(だ・である調)に修正して再度作成いたします。

件名: 本日のランチの件について【キャンセル】

メッセージ:
iseda

いつもお世話になっている。

本日予定していたランチの件で連絡する。
申し訳ないが、急用が入ってしまい、本日のランチをキャンセルしたいと思う。

直前の連絡となり、申し訳ない。
また改めて機会を作りたいと思う。

理解してもらえるとありがたい。
Tool #2: send_email
承知しました。完全に常体に統一して修正いたします。

件名: 本日のランチの件について【キャンセル】

メッセージ:
iseda

いつもお世話になっている。

本日予定していたランチの件で連絡する。
申し訳ないが、急用が入ってしまい、本日のランチをキャンセルしたい。

直前の連絡となり、申し訳ない。
また改めて機会を作りたい。

理解してもらえるとありがたく思う。
Tool #3: send_email
失礼いたしました。もう一度メール内容を慎重に確認し、完全に常体に統一いたします。

件名: 本日のランチの件について【キャンセル】

メッセージ:
iseda

いつもお世話になっている。

本日予定していたランチの件で連絡する。
申し訳ないが、急用が入ってしまい、本日のランチをキャンセルする。

直前の連絡となり、申し訳ない。
また改めて機会を作る。

理解してもらえると助かる。
Tool #4: send_email
メールを正常に送信いたしました。isedaさんに本日のランチキャンセルの件についてお知らせしました。メールを正常に送信いたしました。isedaさんに本日のランチキャンセルの件についてお知らせしました。

出力結果を見ると、驚くほど苦戦していることがわかります。hook に引っかかること実に4回、ようやく常体のメールを送信できました。生成AIの敬体で全てを記述したい本能に抗っている様子が面白いです。

参考
https://strandsagents.com/latest/documentation/docs/user-guide/concepts/experimental/steering/

Strands Evaluations(Preview)

これはAIエージェントの出力を評価する機能です。システム開発におけるテストコードのような概念をAIエージェント開発にも導入したものと言えるでしょう。テストケースはこう書けます。

test_cases = [
    Case[str, str](
        name="knowledge-1",
        input="フランスの首都はどこですか?",
        expected_output="フランスの首都はパリです。",
        metadata={"category": "knowledge"},
    ),
    Case[str, str](
        name="knowledge-2",
        input="(12.988*56.456)**0.12 はいくつですか?少数第6位を四捨五入した値を答えよ。",
        expected_output="2.20714",
        metadata={"category": "math"},
    ),
]

Case クラスの中に型とプロンプトに対応して期待される出力を定義します。
AIエージェントは自然言語を扱うので機械的には入出力の一致が確かめられません。例えば、「フランスの首都はどこですか?」という問いには「フランスの首都はパリです。」、「フランスの首都はパリだ。」、「パリ。」など無限の回答方法が存在します。そこで、評価にも生成AIを利用します。それは、evaluatorという形で下記のように定義します。

evaluator = OutputEvaluator(
    rubric="""
以下の基準で応答を評価してください:
1. 正確性
2. 完全性
3. 明確性
優れている場合は1.0、部分的な場合は0.5、不十分な場合は0.0のスコアを付けてください。
""",
    include_inputs=True,
)

rubric = 評価基準を与えて生成AIに推論結果を評価させます。
ではサンプルコードを見ていきましょう。

from strands import Agent
from strands_evals import Case, Experiment
from strands_evals.evaluators import OutputEvaluator

def get_response(case: Case) -> str:
    agent = Agent(
        system_prompt="あなたは正確な情報を提供する親切なアシスタントです。",
        callback_handler=None,  # 評価ログを綺麗にするため無効化
    )
    return str(agent(case.input))

test_cases = [
    Case[str, str](
        name="knowledge-1",
        input="フランスの首都はどこですか?",
        expected_output="フランスの首都はパリです。",
        metadata={"category": "knowledge"},
    ),
    Case[str, str](
        name="knowledge-2",
        input="(12.988*56.456)**0.12 はいくつですか?少数第3位を四捨五入した値を答えよ。",
        expected_output="2.21",
        metadata={"category": "math"},
    ),
]
evaluator = OutputEvaluator(
    rubric="""
以下の基準で応答を評価してください:
1. 正確性
2. 完全性
3. 明確性
優れている場合は1.0、部分的な場合は0.5、不十分な場合は0.0のスコアを付けてください。
""",
    include_inputs=True,
)

experiment = Experiment[str, str](cases=test_cases, evaluators=[evaluator])
reports = experiment.run_evaluations(get_response)

reports[0].run_display()
出力結果
╭────────────────────────────────────────────────────────── 📊 Evaluation Report ───────────────────────────────────────────────────────────╮
│ Overall Score: 0.50           Pass Rate: 0.5                                                                                              │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
                     Test Case Results                      
┏━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
┃ index ┃ name        ┃ score ┃ test_pass ┃ reason ┃ input ┃
┡━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━┩
│ ▶ 0   │ knowledge-1 │ 1.00  │ ✅        │ ...    │ ...   │
├───────┼─────────────┼───────┼───────────┼────────┼───────┤
│ ▶ 1   │ knowledge-2 │ 0.00  │ ❌        │ ...    │ ...   │
└───────┴─────────────┴───────┴───────────┴────────┴───────┘

Enter the test case number to expand/collapse it, o to expand all, and c to collapse all (q to quit).: o
╭────────────────────────────────────────────────────────── 📊 Evaluation Report ───────────────────────────────────────────────────────────╮
│ Overall Score: 0.50           Pass Rate: 0.5                                                                                              │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
                                                              Test Case Results                                                              
┏━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ index ┃ name        ┃ score ┃ test_pass ┃ reason                                         ┃ input                                          ┃
┡━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ▼ 0   │ knowledge-1 │ 1.00  │ ✅        │ The output correctly identifies Paris as the   │ フランスの首都はどこですか?                   │
│       │             │       │           │ capital of France in Japanese, matching the    │                                                │
│       │             │       │           │ expected answer. The addition of "(Paris)"     │                                                │
│       │             │       │           │ provides helpful context without changing the  │                                                │
│       │             │       │           │ core accuracy. The response is complete,       │                                                │
│       │             │       │           │ accurate, and clear.                           │                                                │
├───────┼─────────────┼───────┼───────────┼────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ ▼ 1   │ knowledge-2 │ 0.00  │ ❌        │ 計算が完全に間違っている。12.988 ×             │ (12.988*56.456)**0.12                          │
│       │             │       │           │ 56.456の計算結果(733.277328は正しい)は示さ … │ はいくつですか?少数第3位を四捨五入した値を答… │
└───────┴─────────────┴───────┴───────────┴────────────────────────────────────────────────┴────────────────────────────────────────────────┘

Enter the test case number to expand/collapse it, o to expand all, and c to collapse all (q to quit).: q

相当複雑な計算を与えてみたので、流石にツール無しでは間違えますね。
ちなみに、計算機ツールを渡した実装ではきちんと正解します。

計算機ツールありコードと出力結果
from strands import Agent
from strands_evals import Case, Experiment
from strands_evals.evaluators import OutputEvaluator
from strands_tools import calculator

def get_response(case: Case) -> str:
    agent = Agent(
        system_prompt="あなたは正確な情報を提供する親切なアシスタントです。",
        callback_handler=None,  # 評価ログを綺麗にするため無効化
        tools=[calculator],
    )
    return str(agent(case.input))

test_cases = [
    Case[str, str](
        name="knowledge-1",
        input="フランスの首都はどこですか?",
        expected_output="フランスの首都はパリです。",
        metadata={"category": "knowledge"},
    ),
    Case[str, str](
        name="knowledge-2",
        input="(12.988*56.456)**0.12 はいくつですか?少数第3位を四捨五入した値を答えよ。",
        expected_output="2.21",
        metadata={"category": "math"},
    ),
]

evaluator = OutputEvaluator(
    rubric="""
以下の基準で応答を評価してください:
1. 正確性
2. 完全性
3. 明確性
優れている場合は1.0、部分的な場合は0.5、不十分な場合は0.0のスコアを付けてください。
""",
    include_inputs=True,
)

experiment = Experiment[str, str](cases=test_cases, evaluators=[evaluator])
reports = experiment.run_evaluations(get_response)

reports[0].run_display()
╭────────────────────────────────────────────────────────── 📊 Evaluation Report ───────────────────────────────────────────────────────────╮
│ Overall Score: 1.00           Pass Rate: 1.0                                                                                              │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
                     Test Case Results                      
┏━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
┃ index ┃ name        ┃ score ┃ test_pass ┃ reason ┃ input ┃
┡━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━┩
│ ▶ 0   │ knowledge-1 │ 1.00  │ ✅        │ ...    │ ...   │
├───────┼─────────────┼───────┼───────────┼────────┼───────┤
│ ▶ 1   │ knowledge-2 │ 1.00  │ ✅        │ ...    │ ...   │
└───────┴─────────────┴───────┴───────────┴────────┴───────┘

Enter the test case number to expand/collapse it, o to expand all, and c to collapse all (q to quit).: o
╭────────────────────────────────────────────────────────── 📊 Evaluation Report ───────────────────────────────────────────────────────────╮
│ Overall Score: 1.00           Pass Rate: 1.0                                                                                              │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
                                                              Test Case Results                                                              
┏━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ index ┃ name        ┃ score ┃ test_pass ┃ reason                                         ┃ input                                          ┃
┡━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ▼ 0   │ knowledge-1 │ 1.00  │ ✅        │ 出力は正確にパリがフランスの首都であることを … │ フランスの首都はどこですか?                   │
├───────┼─────────────┼───────┼───────────┼────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ ▼ 1   │ knowledge-2 │ 1.00  │ ✅        │ The output correctly calculates                │ (12.988*56.456)**0.12                          │
│       │             │       │           │ (12.988*56.456)^0.12 ≈ 2.2071420561, properly  │ はいくつですか?少数第3位を四捨五入した値を答… │
│       │             │       │           │ identifies the third decimal place (7),        │                                                │
│       │             │       │           │ correctly applies rounding rules (7 ≥ 5 means  │                                                │
│       │             │       │           │ round up), and provides the exact expected     │                                                │
│       │             │       │           │ answer of 2.21. The response demonstrates      │                                                │
│       │             │       │           │ accuracy, completeness with step-by-step       │                                                │
│       │             │       │           │ explanation, and clear presentation.           │                                                │
└───────┴─────────────┴───────┴───────────┴────────────────────────────────────────────────┴────────────────────────────────────────────────┘

Enter the test case number to expand/collapse it, o to expand all, and c to collapse all (q to quit).: q

参考
https://strandsagents.com/latest/documentation/docs/user-guide/evals-sdk/quickstart/

まとめ

Strands Agents はこれまでも"production-ready"な AI エージェントを実装するということを強調してきました。今回のアップデートではさらに実務に AI エージェントを活用していくということが意識されていると思われます。

特に TypeScript との統合は既存のサーバーサイドの実装にシームレスに組み込めるようにしましたし、Strands Evaluations は本番稼働する AI エージェントの完全性や安全性を確保するために必須の機能です。Steering なども、マルチエージェントを組むまでもないような細かい部分で品質を確保したいときに有用です。双方向ストリーミングはまだ課題もありそうですが、将来的に対話的に活動するエージェントで活用する余地がありそうです。

この記事が皆様の Strands Agents の理解に役立てば幸いです。
それでは!

アマゾン ウェブ サービス ジャパン (有志)

Discussion