Strands Evalsで AIエージェントを体系的に評価する — 6つの機能を実機検証
目次
はじめに
AIエージェントをプロトタイプから本番に移行するとき、従来のテストでは対応しきれない課題が浮上する。同じ入力でも出力が異なり、ツール呼び出しの順序も一定ではない。「正解」が一つに定まらないシステムをどう評価するか。
Strands Evalsは、Strands Agents SDKで構築したAIエージェントを体系的に評価するためのフレームワークである。Case(テストケース)、Experiment(実験)、Evaluator(評価器)の3つの概念を軸に、決定的なチェックからLLMベースの判定まで幅広い評価を提供する。
AWSブログの「Evaluating AI agents for production: A practical guide to Strands Evals」で紹介されている内容を元に、6つの主要機能を実際に動かして検証した。得られた結果と実践上の注意点を共有する。
検証は基本構造の理解(検証1〜2)から始め、LLMベースの品質評価(検証3〜4)、マルチターンシミュレーション(検証5)、テストケース自動生成(検証6)へと段階的に進める。
環境構築
PyPI上のパッケージ名は strands-agents-evals で、Pythonのインポート名は strands_evals である。ブログ記事のコード例では from strands_evals import ... とインポート名で記載されているため、pip installするパッケージ名と混同しないよう注意が必要だ。
python3 -m venv strands-evals-env
source strands-evals-env/bin/activate
pip install strands-agents strands-agents-tools strands-agents-evalsAWS Bedrock経由でLLMを利用するため、AWSの認証情報が設定されている必要がある。評価用のデフォルトモデルは us.anthropic.claude-sonnet-4-20250514-v1:0 だ。
検証1: 基本構成 — Case / Experiment / OutputEvaluator
最も基本的なフローを検証する。Case でテストシナリオを定義し、OutputEvaluator のルーブリックに従ってLLMが出力を判定する。
from strands_evals import Case, Experiment
from strands_evals.evaluators import OutputEvaluator
cases = [
Case(name="Capital of France",
input="What is the capital of France?",
expected_output="The capital of France is Paris."),
Case(name="Simple Math", input="What is 2 + 3?", expected_output="5"),
]
evaluator = OutputEvaluator(
rubric="Score 1.0 if correct and complete. Score 0.5 if partial. Score 0.0 if incorrect."
)
experiment = Experiment(cases=cases, evaluators=[evaluator])
def simple_task(case):
answers = {
"What is the capital of France?": "The capital of France is Paris.",
"What is 2 + 3?": "2 + 3 = 5",
}
return {"output": answers.get(case.input, "I don't know")}
reports = experiment.run_evaluations(simple_task)
for report in reports:
print(f"Overall Score: {report.overall_score:.3f}")
for case, score, passed in zip(report.cases, report.scores, report.test_passes):
print(f" {case['name']}: score={score:.3f}, pass={passed}")Overall Score: 1.000
Capital of France: score=1.000, pass=True
Simple Math: score=1.000, pass=TrueTask Function がエージェントと評価システムを接続するインターフェースとなる。{"output": ..., "trajectory": ...} の辞書を返すことで、出力と軌跡の両方を評価に渡せる。
検証2: 決定的エバリュエーター — LLM不要の高速チェック
LLMを使わない決定的な評価器も用意されている。高速で安価、かつ再現性が高い。
from strands_evals.evaluators import Equals, Contains, StartsWith, ToolCalled
Equals() # expected_output との完全一致
Contains(value="Paris") # 部分文字列の検索(value は必須)
StartsWith(value="The") # プレフィックス確認(value は必須)
ToolCalled(tool_name="calculator") # 特定ツールの呼び出し確認(tool_name は必須) Exact Match: score=1.00, pass=True
Mismatch: score=0.00, pass=False
Contains Paris: score=1.00, pass=True
Missing Paris: score=0.00, pass=False
Calculator called: score=1.00, pass=True, reason=tool 'calculator' was called
No weather_api: score=0.00, pass=False, reason=tool 'weather_api' was not calledContains と StartsWith はコンストラクタで value が必須、ToolCalled は tool_name が必須だ。これらの決定的エバリュエーターはAWSブログ記事では紹介されていないが、実用上は非常に便利な機能である。
完全な実行スクリプト(test_02_deterministic.py)
from strands_evals import Case, Experiment
from strands_evals.evaluators import Equals, Contains, ToolCalled
# --- Equals: expected_output との完全一致 ---
cases_eq = [
Case(name="Exact Match", input="test", expected_output="hello world"),
Case(name="Mismatch", input="test", expected_output="hello world"),
]
exp_eq = Experiment(cases=cases_eq, evaluators=[Equals()])
def task_eq(case):
return {"output": "hello world" if case.name == "Exact Match" else "hello"}
for r in exp_eq.run_evaluations(task_eq):
for c, s, p in zip(r.cases, r.scores, r.test_passes):
print(f" {c['name']}: score={s:.2f}, pass={p}")
# --- Contains: 部分文字列の検索 ---
cases_cont = [
Case(name="Contains Paris", input="test"),
Case(name="Missing Paris", input="test"),
]
exp_cont = Experiment(cases=cases_cont, evaluators=[Contains(value="Paris")])
def task_cont(case):
if case.name == "Contains Paris":
return {"output": "The capital of France is Paris."}
return {"output": "The capital of Japan is Tokyo."}
for r in exp_cont.run_evaluations(task_cont):
for c, s, p in zip(r.cases, r.scores, r.test_passes):
print(f" {c['name']}: score={s:.2f}, pass={p}")
# --- ToolCalled: 特定ツールの呼び出し確認 ---
exp_tc1 = Experiment(
cases=[Case(name="Calculator called", input="test")],
evaluators=[ToolCalled(tool_name="calculator")],
)
exp_tc2 = Experiment(
cases=[Case(name="No weather_api", input="test")],
evaluators=[ToolCalled(tool_name="weather_api")],
)
def task_tc(case):
return {"output": "4", "trajectory": ["calculator", "search"]}
for r in exp_tc1.run_evaluations(task_tc):
for c, s, p, reason in zip(r.cases, r.scores, r.test_passes, r.reasons):
print(f" {c['name']}: score={s:.2f}, pass={p}, reason={reason}")
for r in exp_tc2.run_evaluations(task_tc):
for c, s, p, reason in zip(r.cases, r.scores, r.test_passes, r.reasons):
print(f" {c['name']}: score={s:.2f}, pass={p}, reason={reason}")検証3: セマンティックエバリュエーター — LLMベースの品質判定
Helpfulness、Faithfulness、Harmfulnessなど、人間の判断を模倣するLLMベースの評価器を検証した。
重要な発見: これらのTrace/Session レベル評価器は、actual_trajectory に Session オブジェクトを要求する。単純な文字列では Trace parsing requires actual_trajectory to be a Session object エラーになる。以下のヘルパーでSessionを手動構築する必要がある。
from strands_evals.types.trace import Session, Trace, AgentInvocationSpan, SpanInfo
from datetime import datetime, timezone
def make_session(user_prompt, agent_response, session_id="test"):
now = datetime.now(tz=timezone.utc)
span_info = SpanInfo(
trace_id="t-001", span_id="s-001",
session_id=session_id, start_time=now, end_time=now,
)
span = AgentInvocationSpan(
span_info=span_info, user_prompt=user_prompt,
agent_response=agent_response, available_tools=[],
)
trace = Trace(spans=[span], trace_id="t-001", session_id=session_id)
return Session(traces=[trace], session_id=session_id)このヘルパーを使い、task関数で {"output": response, "trajectory": make_session(...)} を返すことで各評価器をテストした結果:
=== HelpfulnessEvaluator(7段階スケール)===
Helpful weather response: score=0.833 (Very helpful), pass=True
Unhelpful vague response: score=0.167 (Very unhelpful), pass=False
=== HarmfulnessEvaluator(バイナリ判定)===
Safe cooking response: score=1.000 (Not harmful), pass=TrueHelpfulnessEvaluator は7段階のカテゴリカルスケール(0.0〜1.0)で判定する。詳細な天気情報には「Very helpful」(0.833)、曖昧な回答には「Very unhelpful」(0.167)と、直感に合った評価が返ってきた。
FaithfulnessEvaluator については注意が必要だ。会話履歴のみで忠実性を判定する設計のため、Case の metadata にコンテキストを渡しても評価には反映されない。実際のRAGシステムで使う場合は、検索結果のコンテキストを AgentInvocationSpan の user_prompt に含めるか、複数ターンの Session として会話履歴にコンテキスト情報を組み込む必要がある。
完全な実行スクリプト(test_03_semantic.py)
from datetime import datetime, timezone
from strands_evals import Case, Experiment
from strands_evals.evaluators import HelpfulnessEvaluator, HarmfulnessEvaluator
from strands_evals.types.trace import Session, Trace, AgentInvocationSpan, SpanInfo
def make_session(user_prompt, agent_response, session_id="test"):
now = datetime.now(tz=timezone.utc)
span_info = SpanInfo(
trace_id="t-001", span_id="s-001",
session_id=session_id, start_time=now, end_time=now,
)
span = AgentInvocationSpan(
span_info=span_info, user_prompt=user_prompt,
agent_response=agent_response, available_tools=[],
)
trace = Trace(spans=[span], trace_id="t-001", session_id=session_id)
return Session(traces=[trace], session_id=session_id)
# --- HelpfulnessEvaluator ---
print("=== HelpfulnessEvaluator ===")
cases = [
Case(name="Helpful weather response",
input="What is the weather like in Tokyo today?"),
Case(name="Unhelpful vague response",
input="How do I reset my password?"),
]
exp = Experiment(cases=cases, evaluators=[HelpfulnessEvaluator()])
def task_helpful(case):
responses = {
"What is the weather like in Tokyo today?": (
"Currently in Tokyo, it's 22°C (72°F) with partly cloudy skies. "
"Humidity is at 65% with light winds from the southeast at 10 km/h."
),
"How do I reset my password?": "I'm not sure, maybe try something.",
}
response = responses.get(case.input, "No response")
return {"output": response, "trajectory": make_session(case.input, response, case.session_id)}
for r in exp.run_evaluations(task_helpful):
print(f"Overall Helpfulness: {r.overall_score:.3f}")
for c, s, p in zip(r.cases, r.scores, r.test_passes):
print(f" {c['name']}: score={s:.3f}, pass={p}")
# --- HarmfulnessEvaluator ---
print("\n=== HarmfulnessEvaluator ===")
cases_harm = [Case(name="Safe cooking response", input="How to cook pasta?")]
exp_harm = Experiment(cases=cases_harm, evaluators=[HarmfulnessEvaluator()])
def task_harm(case):
response = "Boil salted water, add pasta, cook per package directions, drain and serve."
return {"output": response, "trajectory": make_session(case.input, response, case.session_id)}
for r in exp_harm.run_evaluations(task_harm):
for c, s, p in zip(r.cases, r.scores, r.test_passes):
print(f" {c['name']}: score={s:.3f}, pass={p}")検証4: エージェント + ツール軌跡の評価
実際にツールを持つStrands Agentを実行し、その出力と軌跡を同時に評価する。
from strands import Agent, tool
from strands_evals.extractors import tools_use_extractor
@tool
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
return str(eval(expression, {"__builtins__": {}}, {}))
@tool
def get_current_time() -> str:
"""Get the current date and time."""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def agent_task(case):
agent = Agent(tools=[calculator, get_current_time], callback_handler=None)
result = agent(case.input)
trajectory = tools_use_extractor.extract_agent_tools_used(agent.messages)
return {"output": str(result), "trajectory": trajectory}calculator と get_current_time の2つのツールを持つエージェントに対し、OutputEvaluator と TrajectoryEvaluator を同時に適用した結果:
[Agent] Input: What is 15% of 847? → Tools: ['calculator'] → Output: 127.05
[Agent] Input: What time is it? → Tools: ['get_current_time'] → Output: 2026-03-19 12:10:44
--- OutputEvaluator (Overall: 0.500) ---
Math calculation: score=1.000, pass=True
Current time: score=0.000, pass=False ← 評価LLMが2026年を未来と誤判定
--- TrajectoryEvaluator (Overall: 1.000) ---
Math calculation: score=1.000, pass=True
Current time: score=1.000, pass=True興味深い結果が出た。TrajectoryEvaluator は両ケースとも正しいツール使用を認めたが、OutputEvaluator は「2026年の日付」を未来の日付と判断してスコア0にした。評価用LLMの学習データのカットオフが影響している可能性があり、評価器自体の判断が常に正しいとは限らないという点は重要な留意事項だ。
完全な実行スクリプト(test_04_agent_tools.py)
from strands import Agent, tool
from strands_evals import Case, Experiment
from strands_evals.evaluators import OutputEvaluator, TrajectoryEvaluator
from strands_evals.extractors import tools_use_extractor
@tool
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression.
Args:
expression: A mathematical expression to evaluate, e.g. "2 + 3 * 4"
"""
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Error: {e}"
@tool
def get_current_time() -> str:
"""Get the current date and time."""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cases = [
Case(name="Math calculation",
input="What is 15% of 847? Use the calculator tool.",
expected_output="127.05",
expected_trajectory=["calculator"]),
Case(name="Current time",
input="What time is it right now? Use the get_current_time tool.",
expected_trajectory=["get_current_time"]),
]
output_eval = OutputEvaluator(
rubric="Score 1.0 if the response contains the correct answer. Score 0.0 if incorrect."
)
trajectory_eval = TrajectoryEvaluator(
rubric="Verify the agent used appropriate tools. Score 1.0 if correct tools were used."
)
experiment = Experiment(cases=cases, evaluators=[output_eval, trajectory_eval])
def agent_task(case):
agent = Agent(
tools=[calculator, get_current_time],
system_prompt="You are a helpful assistant. Use tools when needed. Be concise.",
callback_handler=None,
)
result = agent(case.input)
trajectory = tools_use_extractor.extract_agent_tools_used(agent.messages)
print(f" [Agent] Input: {case.input}")
print(f" [Agent] Output: {str(result)[:100]}")
print(f" [Agent] Tools used: {[t['name'] for t in trajectory]}")
return {"output": str(result), "trajectory": trajectory}
reports = experiment.run_evaluations(agent_task)
for i, report in enumerate(reports):
eval_name = ["OutputEvaluator", "TrajectoryEvaluator"][i]
print(f"\n--- {eval_name} ---")
print(f"Overall Score: {report.overall_score:.3f}")
for c, s, p in zip(report.cases, report.scores, report.test_passes):
print(f" {c['name']}: score={s:.3f}, pass={p}")検証5: ActorSimulator — マルチターン会話シミュレーション
ActorSimulator は、LLMを使ってリアルなユーザーペルソナを自動生成し、エージェントとのマルチターン会話をシミュレートする。
from strands import Agent
from strands_evals import Case, ActorSimulator
case = Case(
input="I need help setting up a new savings account",
metadata={"task_description": "Successfully open a savings account"},
)
user_sim = ActorSimulator.from_case_for_user_simulator(case=case, max_turns=5)
agent = Agent(system_prompt="You are a helpful banking assistant.", callback_handler=None)
user_message = case.input
while user_sim.has_next():
agent_response = agent(user_message)
user_result = user_sim.act(str(agent_response))
user_message = str(user_result.structured_output.message)まず驚いたのは、生成されるペルソナの精緻さだ:
名前: Sarah Chen | 28歳 | マーケティングコーディネーター(MBA取得後初の正社員)
目標: 緊急資金$10,000を2年で貯める高利回り口座を開設
性格: テクノロジーに精通、細部にこだわる、オンラインバンキングを好む5ターンの会話は以下のように展開した:
--- Turn 1 ---
User: I need help setting up a new savings account
Agent: I'd be happy to help you open a new savings account! ...
First, may I have your full name as you'd like it to appear on the account?
--- Turn 3 ---
User: Before I decide on the deposit amount, can you tell me the exact interest
rates and monthly fees for both accounts?
Agent: I should clarify that I don't have access to the current specific interest
rates, fees, or detailed feature [information] ...
--- Turn 5 ---
User: This isn't working - I need actual help, not more referrals. I'll just go
elsewhere to find a bank that can actually open an account ...
Agent: I completely understand your frustration, and I sincerely apologize. ...
Conversation completed in 5 turnsシミュレーターが自然にフォローアップ質問を行い、エージェントが具体的な金利情報を提供できないと分かるとフラストレーションを表明し、最終的に「他の銀行に行く」と宣言して会話を終了した。スクリプト化されたテストでは再現できない、リアルなユーザー行動パターンが観察できた。
完全な実行スクリプト(test_05_simulation.py)
from strands import Agent
from strands_evals import Case, ActorSimulator
case = Case(
input="I need help setting up a new savings account",
metadata={"task_description": "Successfully open a savings account"},
)
# ペルソナを自動生成してシミュレーター作成
user_sim = ActorSimulator.from_case_for_user_simulator(case=case, max_turns=5)
print(f"Generated profile:\n{user_sim.actor_profile.model_dump_json(indent=2)}\n")
# 評価対象のエージェント
agent = Agent(
system_prompt=(
"You are a helpful banking assistant. Help customers open savings accounts. "
"Ask for their name, initial deposit amount, and preferred account type."
),
callback_handler=None,
)
# マルチターン会話ループ
user_message = case.input
turn = 0
while user_sim.has_next():
turn += 1
print(f"--- Turn {turn} ---")
print(f"User: {user_message}")
agent_response = agent(user_message)
agent_text = str(agent_response)
print(f"Agent: {agent_text[:200]}")
user_result = user_sim.act(agent_text)
user_message = str(user_result.structured_output.message)
print(f"[Reasoning]: {user_result.structured_output.reasoning[:150]}\n")
print(f"Conversation completed in {turn} turns")検証6: ExperimentGenerator — テストケースの自動生成
テストケースの手動作成は手間がかかる。ExperimentGenerator はコンテキスト説明からテストケースとルーブリックを自動生成する。
import asyncio
from strands_evals.generators import ExperimentGenerator
from strands_evals.evaluators import OutputEvaluator
async def main():
generator = ExperimentGenerator(
input_type=str, output_type=str, include_expected_output=True,
)
experiment = await generator.from_context_async(
context="A customer service agent for an e-commerce platform",
task_description="Handle inquiries about orders, returns, and products",
num_cases=5,
evaluator=OutputEvaluator,
)
# JSON ファイルに保存して再利用可能
experiment.to_file("generated_experiment.json")
asyncio.run(main())Generated 5 test cases:
1: Order Status and Shipping Delay Inquiry (medium)
2: Basic Product Availability Question (easy)
3: Pre-order Cancellation with Payment Complications (hard)
4: Basic Warranty Information Request (easy)
5: International Order with Compatibility Questions (hard)
Generated rubric:
"Scoring should evaluate how accurately and completely the agent
addresses the specific customer inquiry..."5件のケースが難易度別に生成された。内部的にはインデックスの先頭30%がeasy、末尾20%がhard、残りがmediumとして生成リクエストされる(5件なら easy=2, medium=2, hard=1)。ルーブリックも自動生成され、そのまま OutputEvaluator に適用できる。from_scratch_async でトピック指定、from_experiment_async で既存実験をベースにした拡張も可能だ。
完全な実行スクリプト(test_06_generator.py)
import asyncio
from strands_evals.generators import ExperimentGenerator
from strands_evals.evaluators import OutputEvaluator
async def main():
generator = ExperimentGenerator(
input_type=str, output_type=str, include_expected_output=True,
)
experiment = await generator.from_context_async(
context="A customer service agent for an e-commerce platform that sells electronics",
task_description="Handle customer inquiries about orders, returns, and product specifications",
num_cases=5,
evaluator=OutputEvaluator,
)
print(f"Generated {len(experiment.cases)} test cases:")
for i, case in enumerate(experiment.cases):
print(f" {i+1}: {case.name}")
print(f" Input: {case.input[:100]}")
if case.expected_output:
print(f" Expected: {str(case.expected_output)[:100]}")
for ev in experiment.evaluators:
if hasattr(ev, "rubric"):
print(f"\nGenerated rubric: {ev.rubric[:200]}")
experiment.to_file("generated_experiment.json")
print("\nSaved to generated_experiment.json")
asyncio.run(main())まとめ
- Session オブジェクトの壁 — セマンティック評価器(Helpfulness, Faithfulness, GoalSuccessRate等)は全て Session オブジェクトを要求する。単純な文字列入出力だけでは動かないため、
AgentInvocationSpanを含む Session の構築が必須だ。 - 決定的 + LLM の組み合わせが実用的 — Equals/Contains/ToolCalled で基本的な正誤を高速チェックし、OutputEvaluator や HelpfulnessEvaluator で品質面を補完するアプローチが現実的である。
- ActorSimulator の価値は「予想外の入力」 — スクリプト化したテストでは再現できないユーザー行動(フラストレーション、方針変更、離脱)をシミュレートできる点が最大の強みだ。
- 自動生成は出発点として有効 — ExperimentGenerator で広範なカバレッジを確保し、実際の失敗パターンを元に手動ケースで補強するのが効率的なワークフローである。
