@shinyaz

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-evals

AWS Bedrock経由でLLMを利用するため、AWSの認証情報が設定されている必要がある。評価用のデフォルトモデルは us.anthropic.claude-sonnet-4-20250514-v1:0 だ。

検証1: 基本構成 — Case / Experiment / OutputEvaluator

最も基本的なフローを検証する。Case でテストシナリオを定義し、OutputEvaluator のルーブリックに従ってLLMが出力を判定する。

test_01_basic.py
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=True

Task Function がエージェントと評価システムを接続するインターフェースとなる。{"output": ..., "trajectory": ...} の辞書を返すことで、出力と軌跡の両方を評価に渡せる。

検証2: 決定的エバリュエーター — LLM不要の高速チェック

LLMを使わない決定的な評価器も用意されている。高速で安価、かつ再現性が高い。

Python
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 called

ContainsStartsWith はコンストラクタで value が必須、ToolCalledtool_name が必須だ。これらの決定的エバリュエーターはAWSブログ記事では紹介されていないが、実用上は非常に便利な機能である。

完全な実行スクリプト(test_02_deterministic.py)
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_trajectorySession オブジェクトを要求する。単純な文字列では Trace parsing requires actual_trajectory to be a Session object エラーになる。以下のヘルパーでSessionを手動構築する必要がある。

Python
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=True

HelpfulnessEvaluator は7段階のカテゴリカルスケール(0.0〜1.0)で判定する。詳細な天気情報には「Very helpful」(0.833)、曖昧な回答には「Very unhelpful」(0.167)と、直感に合った評価が返ってきた。

FaithfulnessEvaluator については注意が必要だ。会話履歴のみで忠実性を判定する設計のため、Case の metadata にコンテキストを渡しても評価には反映されない。実際のRAGシステムで使う場合は、検索結果のコンテキストを AgentInvocationSpanuser_prompt に含めるか、複数ターンの Session として会話履歴にコンテキスト情報を組み込む必要がある。

完全な実行スクリプト(test_03_semantic.py)
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を実行し、その出力と軌跡を同時に評価する。

Python
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}

calculatorget_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)
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を使ってリアルなユーザーペルソナを自動生成し、エージェントとのマルチターン会話をシミュレートする。

Python
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)
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 はコンテキスト説明からテストケースとルーブリックを自動生成する。

Python
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)
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 で広範なカバレッジを確保し、実際の失敗パターンを元に手動ケースで補強するのが効率的なワークフローである。

共有する

田原 慎也

田原 慎也

ソリューションアーキテクト @ AWS

AWS ソリューションアーキテクトとして金融業界のお客様を中心に技術支援を行っています。クラウドアーキテクチャや AI/ML に関する学びをこのサイトで発信しています。このサイトの内容は個人の見解であり、所属企業の公式な意見や見解を代表するものではありません。

関連記事