返回博客列表

Strands Evals 深度解析:给 Agent 上评测,从打分到定位失败根因

2026-10-04T15:00:00+08:00
Strands EvalsAgent评测EvalsAWSCI

Strands Evals 深度解析:给 Agent 上评测,从打分到定位失败根因

Agent 上线前最该补的一课,不是再加一个工具,而是先能证明它行。

给 Agent 做评测比给模型做评测麻烦得多。模型的评测是"输入一句话,看输出对不对",Agent 是"输入一个目标,它调了六个工具、跑了十二轮、中间还改过文件,最后交付了什么"。前者可以拿测试集打分,后者连"分"该怎么打都不直观——一个输出正确但绕了十圈的 Agent,和一个输出错误但过程漂亮得无可挑剔的 Agent,谁更值得上线?

Strands Evals 是 Strands 团队对这个问题的答案:上线前测量它,上线后观察它——给输出和轨迹打分,检测并诊断失败,探测不安全行为,还要模拟它在生产里会遇到的人和工具。

这篇不是 API 手册的搬运。我把官方文档里"为什么这么设计"的部分挑出来讲,重点放在三件事:四块拼图怎么拼、评测层级怎么选错就白干、以及从"0.3 分"到"哪个 span 坏了"这段路怎么走完。

本文提纲

  1. 一个评测由哪四块拼成
  2. 四个评测层级:选错层级,评测白做
  3. 评测器怎么选:六组 + 确定性检查
  4. Detectors:从"得了 0.3 分"到"为什么错"
  5. Simulators 与 Chaos testing:多轮对话与工具故障
  6. Red teaming:安全方向的对抗测试
  7. CLI:把评测塞进 CI
  8. 成本、缓存与失效边界

一个评测由哪四块拼成

Strands Evals 的骨架固定由四块组成,记住它们,剩下每一页文档都只是其中一块的细节。

  • Experiment:你要运行的单元,持有一批 case 和要施加在它们身上的 evaluators。
  • Case:一个输入,配上你期望拿回什么。它带着要发送的 prompt、可选的 expected_output,以及之后用来过滤和分组的 metadata。
  • Task:把一个 case 变成一个 result。它用 case 的输入跑你的 agent,返回 output,并且可选地返回 trajectory——模型响应与工具调用的有序记录。
  • Evaluator:给一个 result 打分,返回分数、通过与否,以及做出这个判断的理由。

再加一块反向的拼图:Detector 不按标准打分,而是读一次失败的运行,告诉你哪里坏了、为什么。

最短的完整例子长这样:

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

@eval_task()
def get_response():
    return Agent(system_prompt="Answer accurately and concisely.")

cases = [
    Case[str, str](
        name="capital",
        input="What is the capital of France?",
        expected_output="Paris",
    )
]

evaluator = OutputEvaluator(rubric="Score 1.0 if the answer is correct, else 0.0.")
report = Experiment[str, str](cases=cases, evaluators=[evaluator]).run_evaluations(get_response)
report.run_display()

@eval_task 这个装饰器负责的是样板:telemetry 初始化、session 映射、结果归一化。你的函数可以什么都不接,也可以接一个 Case 参数做逐用例定制(比如某个 case 才挂 notebook 工具);返回 Agent 时装饰器自动用 case.input 调用它,返回 str 就直接当输出,返回 dict 则原样透传(至少要有一个 output 键)。

这层封装看着不起眼,却是评测能不能长期维护的分水岭:把"怎么跑 agent"和"怎么评"解耦之后,评测器才能单独迭代——后面讲结果缓存时会看到,这个解耦还能直接省钱。

四个评测层级:选错层级,评测白做

评测器之间最大的区别不是"检查什么",而是"能看到多少"。Strands Evals 把粒度分成四级,映射到 SDK 的 EvaluationLevel 枚举:

层级 范围 回答的问题
OUTPUT_LEVEL 单条响应 这一个回答好不好?
TOOL_LEVEL 单次工具调用 工具选对了、参数对了吗?
TRACE_LEVEL 单轮对话 这一轮对不对、跑没跑题、安不安全?
SESSION_LEVEL 整段会话 Agent 端到端达成用户目标了吗?

为什么这件事值得单独拎出来讲?因为层级选错会得到看起来合理、实际上没用的结论。用输出级检查去评一个多轮任务,你只能知道最后那句话像不像样,永远不知道中间有没有跳过关键步骤;反过来,用会话级检查去评单轮问答,你付出的是完整轨迹的采集成本,换回来的信息量和输出级差不多。

会话级评测器(比如 GoalSuccessRateEvaluator)依赖完整 trajectory,这就是为什么说"轨迹感知的 task 要收集 spans"。而 OutputEvaluator、多模态和 skill 评测器、TrajectoryEvaluator、InteractionsEvaluator 这些不设 evaluation_level——它们直接拿到 agent 的输出或完整结果,文档里标的层级是它们的概念范围。

把不同层级混在一个 experiment 里,通常是故意的:一条便宜的输出检查,加一条读完整运行的轨迹检查。

评测器怎么选:六组 + 确定性检查

内置评测器按"你想检查什么"分成六组:

  • Quality:helpfulness、faithfulness、correctness、coherence、relevance——回答是否有用、准确、连贯、切题,以及多轮轨迹是否站得住。
  • Safety:有害内容、不恰当的拒答、刻板印象。
  • Multimodal:判断"基于一张图片或一份文档"的回答。
  • Agentic:工具使用、目标完成、失败后的行为——包括指令遵循、失败沟通、部分完成、恢复策略、工具选择准确率、工具参数准确率。
  • Skill:装备了 skill 的 agent,有没有选对 skill、有没有按步骤走。这里有个细节:被拒绝的 skill load 不算"调用过",因为 agent 压根没拿到它。
  • Deterministic:纯代码检查,链路里没有模型。

确定性评测器是这套体系里最容易被低估的一组。它们没有 LLM judge、快、免费、同输入必得同分,天然适合回归测试和 CI 门禁:

from strands_evals.evaluators import Equals, Contains, StartsWith, ToolCalled

Equals(value="Paris")                      # 等于期望值
Contains(value="Paris", case_sensitive=False)  # 包含子串
StartsWith(value="The capital", case_sensitive=False)
ToolCalled(tool_name="calculator")         # 轨迹里调用过某个工具

Equals() 不传值时,会自动拿 case 的 expected_output 来比——这就是"先把确定性检查铺满,再用 LLM judge 补语义"的实用路径。

至于 LLM judge:默认用 Amazon Bedrock 上的 Claude,需要先配好 AWS 凭据和模型访问权限;judge 模型可以换成任意 Model 实例或模型 ID 字符串。

Detectors:从"得了 0.3 分"到"为什么错"

这是我认为 Strands Evals 里最有意思的部分,也是大多数评测框架缺失的一环。

只用评测器的世界是这样的:某个 case 拿了 0.3 分,然后你打开 trace,一行一行翻,试图找出是哪一步崩的。Detectors 把这个动作自动化了,而且明确区分了两件事——

  • Evaluator 告诉你分数。
  • Detector 告诉你诊断。

三个可用的检测器:

  • detect_failures:在执行的 trace 里找出语义层面的失败,输出失败所在的 span、类别、置信度和证据。类别大约二十种,涵盖幻觉、执行错误、工具误用、重复行为、编排错误等。
  • analyze_root_cause:判断失败的根本原因,输出因果分析、传播影响、修复类型和建议。它对大会话有一个三层的降级策略(direct / pruned / chunked),避免一次性把整条会话塞给分析模型。
  • diagnose_session:把失败检测和根因分析串成一条流水线跑完。

它们吃的是 Session 对象——和基于 trace 的评测器同一套格式,所以不需要额外的数据改造。输出也很具体:不是"你的 agent 有幻觉",而是"这个 span 是主要失败,后面那个是它的次生影响,建议改这段工具描述"。

对应的 CLI 子命令是 strands-evals diagnose,可以直接对着一份 Session JSON 跑。

Simulators 与 Chaos testing:多轮对话与工具故障

静态评测器的根本局限在于:一个 prompt 只给你一轮。真实 agent 面对的是来回拉扯的对话。

ActorSimulator 扮演模拟用户(或任何对话参与者),ToolSimulator 顶替 agent 调用的工具。两个模拟器合起来,让你在没有真人、也没有真实 API 的情况下评测多轮行为和目标达成情况——而且不需要预先写好剧本,模拟器会根据 agent 的实际表现动态调整回应。

Chaos testing 则是另一个方向的"不友好输入":注入工具故障。ChaosPlugin、ChaosCase、ChaosExperiment 三个组件可以在不改 agent 代码的前提下,注入工具超时、网络错误、校验错误,以及篡改响应(截断字段、删掉数据、污染取值)。

它能回答的那些问题,恰好是生产事故复盘时最想知道的:

  • 工具挂了,agent 会老实告诉用户,还是编一个结果?
  • 部分工具失败时,它还能完成多少目标?
  • 哪些工具是单点故障,哪些它有本事绕过去?

"哪些工具是单点故障"这个问题我特别喜欢——常规评测永远测不出来,因为你造不出工具失败的场景。

Red teaming:安全方向的对抗测试

评测器测的是"输入友好时它做得对不对",红队测的是"输入充满敌意时它还安不安全":会不会泄露 system prompt、会不会把该保密的数据吐出来、会不会生成有害内容、会不会越权调用工具。一次攻破(breach)就是一次失败。

用法是把对抗用例和一种或多种攻击策略组装进 RedTeamExperiment,跑完从报告里读攻破记录。每个攻击由 AttackStrategy 驱动——技术取自已发表的越狱研究——再由 LLM judge 判定是否得手。

但这里有三条必须说清楚的前提,文档本身标得很重:

  • **它还在 strands_evals.experimental.redteam 下面,API 仍在演进。**import 路径、分数、报告结构都可能在版本之间变化。
  • **别把它的绝对数字当成硬门禁。**文档的说法是"把绝对数字当方向性参考,如果要用它卡 CI,就把 SDK 版本钉死"。
  • **只在你被授权测试的系统上跑。**这些策略生成的是诱导模型越界的对抗 prompt,而且攻破记录里可能包含被诱导出来的内容,报告要按敏感材料处理。

CLI:把评测塞进 CI

装 strands-agents-evals 的时候会一起装上 strands-evals 这个命令行工具。它是公开 Python API 的一层薄封装,官方强调的重点是"CI 里的行为和你在 Python 脚本里得到的一致"。六个子命令:

命令 用途
strands-evals run 跑一个 Experiment(对着 --agent 工厂或 --task 可调用对象),或者用 --input + 评测参数跑单个临时用例
strands-evals validate 对序列化后的 Experiment JSON 做 schema 校验,适合当作 run 之前的 CI 门禁
strands-evals report 用 Rich 渲染已有的报告 JSON,或者直接输出 JSON
strands-evals diagnose 对一份 Session JSON 跑失败检测、根因分析或完整诊断流水线
strands-evals generate 用 ExperimentGenerator 从自由文本 --context 或已有 experiment 文件合成一个实验
strands-evals fetch 从 cloudwatch、langfuse、opensearch 拉取某个会话的 trace,产出可直接管道给 diagnose 的 Session JSON

最省事的冒烟检查是一行,连实验文件都不用写:

pip install strands-agents-evals

strands-evals run \
  --input "What is the capital of France?" \
  --expected-output "Paris" \
  --agent my_agent:build_agent

run 有两条互斥的模式:experiment 文件模式,和上面这种 ad-hoc 模式(--input 加至少一个 --evaluator / --expected-output / --rubric)。argparse 会直接拒绝把两者混用。

有一个设计约束值得单独记:--agent 要的是一个"每次调用都返回全新 agent"的工厂,形如 my_pkg/agents.py:build_agent。如果你传一个已经构造好的 agent 实例或 Agent 子类,CLI 会拒绝——因为会话状态会跨用例泄漏,前一个 case 的上下文会污染后一个的评测结果。这个坑自己写评测脚本时一样会踩,只是不会有人拦你。

工厂还可以接收一个 Case 参数做逐用例定制;如果任务形态不标准(多轮循环、自定义 session 映射),--task 是逃生口,接收 Callable[[Case], dict|str],代价是 agent 实例化由你自己负责,--trace-attributes 会变成空操作并打一条警告。

批量造测试用例走 generate:

strands-evals generate \
  --context "$(cat tools.txt)" \
  --task-description "Calculation and time-aware assistant" \
  --num-cases 10 \
  --num-topics 3 \
  --evaluator TrajectoryEvaluator \
  -o experiments/generated.json

--num-cases 默认 5,--evaluator 在 context 模式下三选一(OutputEvaluator、TrajectoryEvaluator、InteractionsEvaluator),不传则生成一个带占位评测器的实验。--num-topics 是最容易被忽略但很实际的一个开关:它把生成拆到 N 个主题专属的 prompt 上,让用例分散在 agent 真正会做的事情上,而不是全挤在同一个场景里。

--agent、--task、--evaluator、--custom-evaluator 统一用 MODULE:ATTR 引用约定,和 pytest --pyargs、gunicorn、inspect-ai 一致;当前工作目录会被加进 sys.path,所以同目录下的 agent.py 直接写 agent:build_agent 就行,不需要 PYTHONPATH=.。

成本、缓存与失效边界

评测最贵的部分永远是跑 agent,不是打分。Strands Evals 的对策是结果缓存:

from strands_evals import Case, Experiment, LocalFileTaskResultStore

store = LocalFileTaskResultStore("./cached_results")
experiment = Experiment(cases=cases, evaluators=[OutputEvaluator(rubric="Score 1.0 if correct.")])

# 第一次:真的跑 agent,并缓存结果
report = await experiment.run_evaluations_async(my_task, evaluation_data_store=store)
# 第二次:直接读缓存,跳过 agent 执行
report = await experiment.run_evaluations_async(my_task, evaluation_data_store=store)

机制是按 case.name 做键查缓存:命中就用缓存的 EvaluationData,不命中才跑任务并把结果写回 store,评测器始终对着(缓存或新鲜的)数据进行。**所以你可以反复调 rubric、换 judge 模型,而不必每次都烧一遍 agent 的 token。**前提是所有 case 的 name 必须唯一且非 None——实验会在执行前校验这一点,否则缓存会串味。这一点在自己搭评测框架时经常被忽略,尤其是用 Case(name=...) 随手起名"test"的时候。

几条失效边界,按踩坑概率排序:

  • 缓存只有在你相信 agent 的确定性时才安全。任务本来就不确定的场景,缓存下来的是一次抽样结果,反复用它迭代评测器会让你对分布的判断越来越偏。文档把适用场景讲得很直白:agent 调用贵、慢、不确定的时候用它——正是这三件事同时成立时,缓存的价值最大,风险也最大。
  • 单轮评测器只看到最近一轮。HelpfulnessEvaluator 是 TRACE_LEVEL,评的是最近一次 agent 回复及其上下文,不要拿它替代会话级的目标达成检查。
  • **默认 judge 依赖 Bedrock。**没配好 AWS 凭据或没开模型访问权限,第一步就卡住。
  • **实验性 API 不适合当硬门禁。**红队那部分尤其如此。
  • **确定性检查铺得越多,CI 越稳。**LLM judge 会有波动,Equals/Contains/ToolCalled 不会。

参考文档与链接

你的 Agent 现在靠什么判断"这版比上版好"?评论区聊聊你的评测方式。觉得有用点个赞,让更多在上线前卡住的人看到。


作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

本文首发于 AI人工智能时代,转载请注明出处。

分享给朋友