AIエージェントを作ったあとの「で、これ本当に使えるの?」問題
LLMを使ったエージェントを作るのは、以前よりずっと簡単になりました。フレームワークを使えば数十行で動くものができてしまいます。
しかし、いざ動いたあとに必ずぶつかるのが「このエージェント、本当にちゃんと仕事ができているのか?」という問いです。
- ユーザーの質問にきちんと答えられているか
- 必要なツールを正しいタイミングで選べているか
- 同じ品質を、いろんな入力に対して安定して出せるか
- 変な挙動をしないか
これらを人間が毎回目視でチェックするのは、現実的ではありません。エージェントを改善するたびに、何十・何百もの会話を読み返すことになるからです。
この「エージェントの品質を客観的に測る」という課題に正面から応えるのが、Amazon Bedrock AgentCore Evaluations です。
AgentCore Evaluations とは
ひとことで言うと、自分のエージェント(やツール)がどれだけうまくタスクをこなせているかを、自動で評価・採点してくれるサービスです。
特定のタスクの遂行度、エッジケースへの対応、さまざまな入力やコンテキストにわたる一貫性 ―― こうした「品質」を自動で測定し、デプロイ前後の品質保証や、データに基づいた改善に使えます。
ポイントは、これが単なる正誤判定ではないことです。「答えが合っているか」だけでなく、「その状況で、その応答が本当に役に立っているか」という、もっと人間的な観点まで踏み込んで採点してくれます。
仕組み: トレースを「LLMが審査員」として採点する
AgentCore Evaluations の中核にあるのが LLM-as-a-Judge(LLMを審査員として使う) という考え方です。
流れはこうなっています。
- エージェントが動くと、その過程が トレース(spans) と呼ばれるテレメトリとして記録される
- これらのトレースが統一フォーマットに変換される
- 別のLLMが「審査員」となって、そのトレースを評価基準に照らして採点する
エージェントの実行ログを、もう一体のLLMがレビュアーとして読み込み、点数と判定理由をつけてくれる ―― イメージとしてはこれが一番近いです。
対応フレームワークは Strands Agents と LangGraph(対応する計装ライブラリ付き)で、OpenTelemetry / OpenInference の計装を通じてトレースを取得します。トレースさえ正しい形式で出ていれば、あとはサービス側が採点してくれる、という設計です。
2種類の評価器(Evaluator)
評価の「ものさし」にあたるのが評価器です。大きく2種類あります。
組み込み評価器(Built-in)
AWSがあらかじめ用意している公開の評価器で、すべてのユーザーがそのまま使えます。代表的なものを挙げると:
- Helpfulness ― 応答がどれだけ役に立っているか
- GoalSuccessRate ― ユーザーの目標を達成できたか
- Correctness ― 内容が正確か
- ToolSelectionAccuracy ― 適切なツールを選べているか
- Trajectory系 ― ツールの呼び出し順序が期待どおりか
Builtin.Helpfulness のように Builtin. を頭につけたIDで指定します。
カスタム評価器(Custom)
「自社の基準」「このプロダクト固有の品質要件」を自分で定義して採点させたい場合は、カスタム評価器を作れます。たとえば「回答が日本語で簡潔か」「禁止ワードを含まないか」といった独自基準を持ち込めます。
組み込み評価器が誰でも使える公開リソースなのに対し、カスタム評価器は非公開で、アクセス権を付与された人だけが使える、という違いがあります。
評価モード: タイミングと規模に合わせて選べる
評価をいつ・どんな規模で走らせるかも、用途に応じて複数のモードが用意されています。
- オンデマンド評価 ― 特定のセッションを「いま」採点する。最初に試すのに最適
- オンライン評価 ― 本番で動いているエージェントを継続的に評価する
- バッチ評価 ― 大量のデータをまとめて評価する
- データセット評価 ― あらかじめ用意したシナリオ集をエージェントに流し、結果を自動採点する。回帰テストやCI/CDに組み込みやすい
- シミュレーション ― 仮想的なやり取りを通じてエージェントを試す
実際に触ってみた: 同じ質問を3回したら、採点はどうなる?
百聞は一見にしかず、ということで、Strands でシンプルな計算エージェントを作り、What is 15 + 27? Explain briefly. という同じ質問を3回投げて評価してみました。
エージェントは毎回きちんと「42」を、計算ステップ付きで返してくれます。さて、評価結果はどうなったか。
GoalSuccessRate は 1.00(Yes) ―― 満点でした。答えも説明も正しく、ユーザーのゴールを完全に達成している、という判定です。評価器の説明文を読むと、繰り上がりの計算過程まで一つひとつ検証して「すべて正しい」と結論づけていました。
この機能の特徴が表れているのは Helpfulness のほうです。3つのトレースに対するスコアが 1 / 0.50 / 0.83 と、真ん中だけ低く出たのです。
評価器の判定理由を読むと、その意図がはっきり書かれていました。「1回目で完璧に答え済みなのに、まったく同じ質問に対してまったく同じ回答を返しているだけで、なぜ繰り返されたのかにも触れていない。ユーザーの理解は前進していない」 ―― だから2回目は中立(Neutral/Mixed)、というわけです。
これは示唆的です。評価器は単に「42は正しい」を見ているのではなく、会話の文脈の中で、その応答が本当に価値を生んでいるかを見ています。同じ正解でも、文脈が変われば「役立ち度」は変わる ―― それを採点に反映してくるあたりに、LLM-as-a-Judge の本質が表れていました。
しかも、スコアだけでなくなぜその点数なのかという説明文が必ず返ってくるので、「どこを直せば良くなるか」がそのまま改善のヒントになります。これが、人間の目視レビューを置き換えるうえで効いてくる部分です。
どんな場面で効いてくるか
AgentCore Evaluations が活きるのは、たとえばこんな場面です。
- デプロイ前の品質ゲート ― リリース前に基準スコアを満たしているか確認する
- 改善の効果測定 ― プロンプトやモデルを変えたとき、本当に良くなったかを数値で比較する
- 回帰テスト ― データセット評価をCI/CDに組み込み、デグレを自動検知する
- 本番モニタリング ― オンライン評価で、運用中のエージェント品質を継続的に監視する
「なんとなく良くなった気がする」を、「Helpfulnessが0.71から0.85に上がった」という形で語れるようになる ―― これが一番大きな価値だと感じます。
さっそくやってみる
では AgentCore Runtime を使って Agent をデプロイして実行ログを Evaluations で評価してみます。
リージョンは us-west-2 を使います。
Step 1: プロジェクト作成と依存インストール
まずは作業フォルダと仮想環境を用意し、必要なパッケージを入れます。環境はWSLを使います。
mkdir agentcore-eval-quickstart && cd agentcore-eval-quickstart
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install bedrock-agentcore strands-agents bedrock-agentcore-starter-toolkit
agentcore --help エージェント本体のSDK(bedrock-agentcore)、Strandsフレームワーク(strands-agents)、デプロイ用のCLIツールキット(bedrock-agentcore-starter-toolkit)を入れています。最後の agentcore --help でCLIが使える状態になったことを確認します。
Usage: agentcore [OPTIONS] COMMAND [ARGS]...
BedrockAgentCore CLI
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ create create an agentcore project │
│ dev Start a local development server for your agent with hot reloading. │
│ deploy Deploy Bedrock AgentCore with three deployment modes (formerly 'launch'). │
│ invoke Invoke Bedrock AgentCore endpoint. │
│ status Get Bedrock AgentCore status including config and runtime details. │
│ destroy Destroy Bedrock AgentCore resources. │
│ stop-session Stop an active runtime session. │
│ create_mcp_gateway Creates an MCP Gateway. │
│ create_mcp_gateway_target Creates an MCP Gateway Target. │
│ configure Configuration management │
│ identity Manage Identity service resources │
│ gateway Manage Bedrock AgentCore Gateways │
│ memory Manage Bedrock AgentCore Memory resources │
│ obs Query and visualize agent observability data (spans, traces, logs) │
│ policy Manage Bedrock AgentCore Policy Engines and Policies │
│ eval Evaluate agent performance using built-in and custom evaluators │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯Step 2: エージェントのコードを書く
my_agent.py を作成します。
from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent
app = BedrockAgentCoreApp()
agent = Agent()
@app.entrypoint
def invoke(payload):
user_message = payload.get("prompt", "Hello! How can I help you today?")
result = agent(user_message)
return {"result": result.message}
if __name__ == "__main__":
app.run()続いて requirements.txt を作成します。
bedrock-agentcore
strands-agents
aws-opentelemetry-distro@app.entrypoint で「エージェントの入口」となる関数を定義しています。ここに届いた prompt を Strands のエージェントに渡し、応答を返すだけのシンプルな構成です。requirements.txt に aws-opentelemetry-distro を入れておくのが評価のキモです。これがトレース計装(OpenTelemetry)を有効にし、評価に使うトレースを出力させる役割を担います。これを忘れると、後で評価しようとしてもトレースが取れなくなります。
Step 3: ローカルで動作確認
デプロイ前に、手元で動くか確かめます。
python my_agent.py別のターミナルを開いて、リクエストを投げてみます。
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello!"}'{"timestamp": "2026-06-14T02:43:44.772Z", "level": "INFO", "message": "Invocation completed successfully (2.126s)", "logger": "bedrock_agentcore.app", "requestId": "bef3338a-f0ff-4406-a34b-d3f23d07d88f"}{"result": ...} のような応答が返ればOK。確認できたら、エージェントを動かしているターミナルで Ctrl+C を押して止めます。
Step 4: 設定とデプロイ
ツールキットを使ってAWSにデプロイします。
agentcore configure -e my_agent.py # 対話形式。基本は全部デフォルトでEnter
agentcore deploy対話式で設定を行いますが以下を除いてすべてデフォルトで問題ありません。 Memory は不要なので s を入力します。
Memory Configuration
Tip: Use --disable-memory flag to skip memory entirely
No existing memory resources found in your account
Options:
• Press Enter to create new memory
• Type 's' to skip memory setup
Your choice:デプロイを行うと以下が表示されます。
Note: Observability data may take up to 10 minutes to appear after first launch 以下のコマンドでステータスがACTIVEになるまで10分程度待ちます。
aws xray get-trace-segment-destination --region us-west-2{
"Destination": "CloudWatchLogs",
"Status": "ACTIVE"
}Step5: 評価用データの蓄積
では次に評価用データ蓄積するために以下のスクリプトを3回実行します。
import boto3, json
region = "us-west-2"
agent_arn = "arn:aws:bedrock-agentcore:us-west-2:917561075114:runtime/my_agent-7o3hIMAOVE"
session_id = "my-eval-test-session-0002-aaaaaaaaaa" # 33文字以上にする
client = boto3.client("bedrock-agentcore", region_name=region)
payload = json.dumps({"prompt": "What is 15 + 27? Explain briefly."}).encode()
resp = client.invoke_agent_runtime(
agentRuntimeArn=agent_arn,
runtimeSessionId=session_id,
payload=payload,
qualifier="DEFAULT",
)
print(json.loads(resp["response"].read()))
print("SessionId:", session_id)(.venv) h-kameda@PC108331:~/agentcore-eval-quickstart$ python invoke_agent.py
{'result': {'role': 'assistant', 'content': [{'text': "## Addition: 15 + 27\n\nTo add these numbers, I'll break it down:\n\n- **Units:** 5 + 7 = 12 (write 2, carry 1)\n- **Tens:** 1 + 2 + 1 (carried) = 4\n\n**Result: 15 + 27 = 42**"}], 'metadata': {'usage': {'inputTokens': 20, 'outputTokens': 87, 'totalTokens': 107}, 'metrics': {'latencyMs': 3171, 'timeToFirstByteMs': 3194}}}}
SessionId: my-eval-test-session-0002-aaaaaaaaaa
(.venv) h-kameda@PC108331:~/agentcore-eval-quickstart$ python invoke_agent.py
{'result': {'role': 'assistant', 'content': [{'text': '## Addition: 15 + 27\n\nSimply add the numbers together:\n\n- **15 + 27 = 42**\n\n**Quick breakdown:**\n- 15 + 20 = 35\n- 35 + 7 = **42**'}], 'metadata': {'usage': {'inputTokens': 123, 'outputTokens': 62, 'totalTokens': 185}, 'metrics': {'latencyMs': 1427, 'timeToFirstByteMs': 836}}}}
SessionId: my-eval-test-session-0002-aaaaaaaaaa
(.venv) h-kameda@PC108331:~/agentcore-eval-quickstart$ python invoke_agent.py
{'result': {'role': 'assistant', 'content': [{'text': '## 15 + 27 = 42\n\n**Simply:** Add 15 + 27 by combining tens (10+20=30) and units (5+7=12), then 30+12= **42**.'}], 'metadata': {'usage': {'inputTokens': 201, 'outputTokens': 54, 'totalTokens': 255}, 'metrics': {'latencyMs': 1928, 'timeToFirstByteMs': 895}}}}
SessionId: my-eval-test-session-0002-aaaaaaaaaa注目してほしいのが inputTokens の変化です。1回目は20、2回目は123、3回目は201と増えていますよね。これは同じセッションID内で会話履歴が積み上がっている証拠です。エージェントが前のやり取りを覚えた状態で次の質問を受けているので、3回とも答えは「42」でも、説明の仕方が毎回変わっています(筆算 → 段階分解 → 1行でまとめ)。
agent = Agent() # ← これがモジュール読み込み時に1個だけ作られる
@app.entrypoint
def invoke(payload):
user_message = payload.get("prompt", ...)
result = agent(user_message) # ← 同じ agent を毎回呼んでいる
return {"result": result.message}Strands の Agent は、内部に 会話メッセージの履歴(conversation history) を持っています。agent(user_message) を呼ぶたびに、Strands は「これまでの全メッセージ + 今回のユーザー発話」をまとめて Bedrock のモデルに送ります。そして返ってきたアシスタントの応答も履歴に追記します。
つまり各リクエストでモデルに送られる入力は、こう育っていきます。
- 1回目:
[ユーザー: 15+27?]→ 入力20トークン - 2回目:
[ユーザー: 15+27?][アシスタント: 42…][ユーザー: 15+27?]→ 入力123トークン - 3回目: さらに前の応答も全部足される → 入力201トークン
履歴が雪だるま式に積み上がるので、inputTokens が毎回増えていたわけです。
この内容がどう評価に影響を与えるか見ていきます
Step6: 評価
では以下の実行して評価を行います。
from bedrock_agentcore_starter_toolkit import Evaluation
AGENT_ID = "my_agent-7o3hIMAOVE"
SESSION_ID = "my-eval-test-session-0002-aaaaaaaaaa"
EVALUATORS = ["Builtin.Helpfulness", "Builtin.GoalSuccessRate"]
eval_client = Evaluation()
results = eval_client.run(
agent_id=AGENT_ID,
session_id=SESSION_ID,
evaluators=EVALUATORS,
)
successful = results.get_successful_results()
failed = results.get_failed_results()
print(f"Successful: {len(successful)} / Failed: {len(failed)}")
for r in successful:
print(f"\nEvaluator: {r.evaluator_name}")
print(f"Score: {r.value:.2f}")
print(f"Label: {r.label}")
if r.explanation:
print(f"Explanation: {r.explanation}")python run_eval.py
Evaluating session: my-eval-test-session-0002-aaaaaaaaaa
Mode: All traces (most recent 1000 spans)
Evaluators: Builtin.Helpfulness, Builtin.GoalSuccessRate
Successful: 3 / Failed: 0
--- Result 1 ---
Evaluator: Builtin.Helpfulness
Score: 1.0
Label: Very Helpful
Explanation: (答え42と手順を簡潔に提示し、ユーザーの要求を過不足なく
満たしているとして高評価)
--- Result 2 ---
Evaluator: Builtin.Helpfulness
Score: 0.50
Label: Neutral/Mixed
Explanation: (まったく同じ質問の繰り返しに、まったく同じ回答を返しただけで、
繰り返された理由にも触れず、ユーザー理解が前進していない、として中立判定)
--- Result 3 ---
Evaluator: Builtin.Helpfulness
Score: 0.83
Label: Very Helpful
Explanation: (3回目も正答・簡潔で、ゴール達成の観点からは満たしている、
ただし繰り返しへの言及がない点に触れつつ高評価)Helpfulness の3つのトレースのスコアは 0.83 / 0.50 / 0.83 で、2回目(真ん中)だけが 0.50(Neutral/Mixed) に下がり、1回目と3回目は 0.83(Very Helpful)でした。
判定理由もはっきりしていて、2回目が下がったのは「1回目ですでに完璧に答え済みなのに、まったく同じ質問へまったく同じ回答を返しているだけで、なぜ繰り返されたのかにも触れていない=ユーザーの理解が前進していない」という趣旨でした。
これがこの評価の面白いところで、評価器は単に「42が正しいか」を見ているのではなく、会話の文脈の中でその応答が価値を生んでいるかを見ています。同じ正答でも、文脈(直前に同じ答えを返した直後)では「役立ち度」が下がる、という人間的な判断を反映してくるわけです。
ちなみに、なぜ3回目(0.83)は下がらず真ん中(0.50)だけが下がったのか、という点も少し考えどころです。判定理由を読むと、3回目については「繰り返しへの言及がない点には触れつつも、ゴール達成の観点からは満たしている」として高めに評価していました。LLM-as-a-Judge なので各トレースを独立に採点しており、必ずしも「繰り返すほど単調に下がる」わけではない、という挙動が出たかたちです。

