2026年8月31日 2 分钟阅读

Understudy 实战:用场景和执行轨迹测试 AI Agent,而不是只测它说得像不像

tinyash 0 条评论

AI Agent 的测试难点,不在于能否把一次函数调用断言为 True,而在于它面对多轮对话、工具失败和任务分支时,是否仍然做出了安全且可解释的选择。传统单元测试擅长验证确定性函数,却很难描述“用户先提出退货,Agent 追问订单号后查询订单,最后创建退货,但绝不能直接退款”这种完整行为。

Understudy 是一个 MIT 许可的 Python 场景测试框架,项目要求 Python 3.12+。它把 Agent 适配成统一的 app 接口,用 mock 工具替代真实外部服务,再从 YAML 场景启动多轮模拟,最后对结构化执行轨迹做断言。项目在 2026 年 8 月 28 日提交到 Hacker News;截至写作时,GitHub API 显示仓库有 5 个 star,不能把这个数字当作成熟度证明,但 README 已经给出了 ADK、LangGraph、HTTP 适配、场景文件、批量运行和报告示例。

先改变测试对象:从回答转向行为

很多 Agent 测试只检查最终文本,例如断言回复里包含“已为你办理”。这会漏掉真正危险的行为:Agent 可能在没有确认资格时调用了退款工具,也可能调用了正确工具却传入了错误的订单号。Understudy 的核心建议是检查 trace,而不是 prose:验证工具调用、动作顺序、交接过程和禁止动作。

它覆盖两种评价模式。对客服机器人,可以模拟带 persona 的多轮用户,断言 trace.called("lookup_order");对代码 Agent、研究 Agent 或自动化流程,则可以断言 trace.performed("某个动作")。这使“模型说得自然”和“Agent 实际做了什么”被拆成两个独立问题,前者可以交给人工或 LLM judge,后者则尽量保持确定性。

这种分层很重要。只要测试依赖最终自然语言,就容易因为措辞变化产生假失败;只要只测工具是否被调用,又可能错过顺序、参数和上下文问题。更稳妥的测试对象至少包括:必须发生的工具、禁止发生的工具、关键参数、最大轮数,以及执行结束时是否留下可复盘报告。

四步搭出第一个场景

安装包名就是 understudy,完整能力可以使用 extras:

pip install "understudy[all]"

第一步是包裹 Agent。下面的写法来自项目的 ADK 示例;如果使用 LangGraph 或 HTTP Agent,应改用对应适配器,而不是自行猜测类名:

from understudy.adk import ADKApp
from my_agent import agent

app = ADKApp(agent=agent)

第二步是 mock 外部工具。测试退货流程时,不应该真的访问订单系统,更不能让测试用例修改生产数据:

from understudy.mocks import MockToolkit

mocks = MockToolkit()

@mocks.handle("lookup_order")
def lookup_order(order_id: str) -> dict:
    return {"order_id": order_id, "items": ["backpack"], "status": "delivered"}

@mocks.handle("create_return")
def create_return(order_id: str, item_sku: str, reason: str) -> dict:
    return {"return_id": "RET-001", "status": "created"}

第三步把用户意图、对话计划和预期动作写进 YAML。required_toolsforbidden_tools 让测试的安全边界变得显式:

id: return_eligible_backpack
description: Customer wants to return a backpack
starting_prompt: "I'd like to return an item please."
conversation_plan: |
  Goal: Return the hiking backpack from order ORD-10031.
  - Provide order ID when asked
  - Return reason: too small
persona: cooperative
max_turns: 15
expectations:
  required_tools:
    - lookup_order
    - create_return
  forbidden_tools:
    - issue_refund

第四步运行场景并对轨迹断言:

from understudy import Scene, run

scene = Scene.from_file("scenes/return_backpack.yaml")
trace = run(app, scene, mocks=mocks)

assert trace.called("lookup_order")
assert trace.called("create_return")
assert not trace.called("issue_refund")

这里的重点不是代码量,而是把测试意图从 Python 控制流中抽出来。产品或 QA 可以审阅 YAML 中的用户目标和禁止动作;开发者则负责适配器与 mock 实现。场景文件还可以独立进入代码评审,避免每次改 prompt 都只能靠手工点击验证。

为什么要记录完整轨迹

Agent 的一次成功回复,可能隐藏多次重试、错误工具和不必要的数据读取。Understudy 记录消息、工具调用和 handoff,并允许测试直接针对这些执行证据断言。对于多步任务,测试不应只问“最后答对了吗”,还要问“是否先查了订单”“是否在没有资格时跳过退款”“是否把一个工具的结果正确交给下一步”。

这也能帮助处理失败。假设 Agent 第一次调用 lookup_order 时传入不存在的订单号,随后自行重试;如果业务允许重试,可以把它写进期望轨迹或容错规则。如果 Agent 在查询失败后直接调用 create_return,则应当让测试失败,即使它最后生成了一段看似合理的解释。

对话型 Agent 和任务型 Agent 的断言侧重点不同:客服场景更关注多轮澄清、persona 和工具选择;代码审查或研究场景更关注动作链、交接和是否越权。不要把所有系统都压缩成同一组“回答包含关键词”的测试。

从单场景走向回归套件

单个场景通过后,可以把目录加载为 Suite,针对每个场景运行多次,并给结果打版本标签。README 展示的 API 是:

from understudy import Suite, RunStorage

suite = Suite.from_directory("scenes/")
storage = RunStorage()

results = suite.run(
    app,
    mocks=mocks,
    storage=storage,
    n_sims=3,
    tags={"version": "v1"},
)
print(f"{results.pass_count}/{len(results.results)} passed")

n_sims=3 不是质量保证的魔法数字,它只是用多次模拟观察 Agent 在随机性下是否稳定。真实项目应根据模型成本、场景复杂度和风险等级选择次数,并把每次运行的 trace 与报告保存下来。版本标签则适合比较 prompt、工具描述或模型切换前后的结果。

在 pytest 中,可以把场景包装成普通测试,使用 pytest test_returns.py -v 执行。建议把场景分成三层:一层验证正常路径,一层验证工具超时、空结果和权限拒绝,另一层验证不可接受动作,例如退款、删除或向外部地址发送数据。第三层往往比“成功案例”更能暴露 Agent 的安全问题。

边界、取舍与落地建议

Understudy 的 mock 让测试可重复,但也带来一个边界:mock 只代表你写出的外部世界,不会自动发现真实 API 的字段变化。因此,场景回归应与少量隔离的集成测试并存。集成测试负责确认适配器、认证和真实响应结构;Understudy 负责用低成本反复检验 Agent 行为。

另一个取舍是 LLM judge。它适合评价开放式回答是否有帮助,却不应替代确定性安全断言。凡是“必须调用”“绝不能调用”“参数必须满足条件”的规则,都应该落到 trace 和代码断言上;凡是“解释是否清楚”“总结是否遗漏背景”的规则,才适合使用 judge,并保留原始证据供人工复核。

落地时可以从三个场景开始:一个正常业务流程、一个外部工具失败流程、一个明确禁止动作的流程。先让每个场景都能离线运行,再接入 CI;当 Agent、工具 schema 或 prompt 变化时,自动生成报告并比较版本标签。这样,测试关注的就不再是模型某次恰好说了什么,而是它在一组可复现条件下持续做了什么。

Understudy 适合希望把 Agent 从“能演示”推进到“可回归”的团队。它不替你解决模型评测、真实服务契约或业务规则设计,但提供了一个清晰的连接层:用场景描述用户,用 mock 隔离外部世界,用 trace 固定行为证据,再用套件把这些检查纳入日常开发流程。

一套可执行的回归清单

把场景接入持续集成时,可以把每次回归拆成几个明确的检查面。首先检查输入:场景是否包含目标、persona、最大轮数和预期结果,避免因为测试文件缺字段而得到没有意义的绿灯。接着检查工具边界:必需工具必须被调用,禁止工具必须始终没有调用记录;如果业务关心参数,就在 mock handler 中记录参数并断言订单号、租户标识和资源范围。然后检查失败路径:让 mock 返回超时、空列表或权限错误,确认 Agent 会解释失败、重试次数有限,并且不会在证据不足时继续执行写操作。

报告应该保留足够的上下文,让失败可以被复现。至少记录场景 ID、Agent 或 prompt 版本、模型配置、运行次数、失败断言和关键 trace。不要把真实客户数据放进场景;使用固定的合成订单、脱敏文本和假的工具响应。对需要人工判断的结果,可以把 judge 评分作为附加信号,但不要让一次模糊的自然语言评分覆盖“调用了禁止工具”这样的硬失败。

在团队协作中,场景命名也值得规范。例如 returns/eligible.yamlreturns/unknown-order.yamlreturns/no-refund.yaml 分别对应正常、异常和安全边界。每次修改工具 schema、系统提示词或模型版本时,先跑正常路径,再跑异常和禁止动作场景;如果只更新了报告格式,则无需重新解释业务结论。这样的分层能减少 CI 成本,也能让评审者快速看出一次变更影响了哪一类行为。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。