Agent 测试策略:让能力可回归
echo-agent 前身为 2025 年 11 月启动的个人助理项目 fubot,最初面向长期陪伴型个人智能体,围绕认知记忆、上下文延续、用户偏好沉淀、任务闭环与持续自我优化展开。随着真实场景迭代,项目逐步形成多入口接入、统一事件模型、消息总线、Agent Loop、多模型抽象、工具调用、MCP 接入、任务调度、权限审批、运行轨迹、长期记忆和受控自演进等能力。目前已支持微信、QQ、CLI、Gateway、Webhook、Cron 等入口,服务用户超过 20 万、累计下载超过 50 万,是面向长期运行、记忆增强和可持续成长智能体的开源 Agent Runtime。
你改了一段工具描述,Agent 终于会主动调用 knowledge_search 了。又顺手改了上下文压缩模板,长对话看起来更省 token。
第二天,另一个用例开始失败:模型把工具参数拼错,审批逻辑没有触发,Gateway 的 wait 请求迟迟等不到 final event。
这就是 Agent 测试的真实难点。它不是“有没有测试”,而是测试什么才算覆盖了系统真正的风险。
本篇只讲一个点:Agent 测试的目标不是追求漂亮覆盖率,而是把模型、工具、状态、权限和通道共同组成的行为,固定成可回归的工程契约。
问题入口
普通 Web 服务的行为边界相对清楚:请求进来,读写数据库,返回 HTTP 响应。测试可以围绕输入、数据库状态和输出结构展开。
Agent 不一样。用户说“帮我修复测试失败”,系统可能经过上下文构造、模型推理、工具选择、审批判断、命令执行、会话保存、消息投递和后台整理。最终回答只是结果的一部分,很多风险藏在过程里。
如果只断言最终文本等于某句话,测试会非常脆弱。模型表达略有变化就失败,但真正危险的工具越权可能没有被发现。
如果只测底层函数,又覆盖不到 Agent 的真实工作方式。PathPolicy 单独能拒绝越界路径,不代表模型工具调用进入执行器前一定经过了审批。
会返回正确文本,只说明这一轮看起来没错;能否长期演进,要看关键能力有没有进入可回归边界。
所以 Agent 测试不能只问“答案对不对”。它还要问:是否调用正确工具,是否遵守权限,是否保存状态,是否处理异步取消,是否能解释每次行动依据。
测试分层
Agent 系统仍然需要传统测试金字塔,但这座金字塔在 Agent 里会变形。
底层是确定性测试。路径规则、状态迁移、配置覆盖、数据模型序列化、文本拆分、token 估算,这些行为不应该随着模型变化而变化。
中间是集成测试。MessageBus、Session、Storage、Pipeline、Tools、Security、Memory、Skills、Gateway、Scheduler、Tasks、MCP 这些模块必须一起验证契约。
上层是少量端到端测试。它验证真实入口是否连通:CLI 启动、Gateway 请求、WebSocket 推送、调度任务投递、多 Agent 委派、MCP 工具接入。
Agent 还需要第四层:行为评估。它处理的是不适合精确断言的语义质量,例如回答是否包含关键事实,是否引用知识库结果,是否在合理迭代次数内完成。
| 层级 | 主要对象 | 断言方式 | 边界 |
|---|---|---|---|
| 单元测试 | 规则、状态机、数据模型 | 精确断言 | 不判断自然语言质量 |
| 集成测试 | 模块契约、事件流、存储协作 | 结构化断言 | 不覆盖全部真实平台 |
| 端到端测试 | 用户可见主链路 | 入口到出口的链路断言 | 不负责细粒度定位 |
| 行为评估 | 工具选择、事实覆盖、效率 | 指标与数据集 | 不替代安全测试 |
为了不停留在抽象层面,下面以 echo-agent 的实现为例。
echo-agent 当前测试目录覆盖 bus、session、storage、pipeline、tools、security、memory、skills、knowledge、models、mcp、gateway、scheduler、tasks、planning、a2a、evaluation 等子系统。
这份结构的价值不是文件多,而是每个测试文件都在回答一个问题:这个模块给系统新增了哪类不可破坏的契约?
契约优先
Agent 测试不应该机械地按“新增一个类就测一个类”组织,而应从契约开始。
新增工具的契约是:schema 是否准确,参数是否能校验,副作用是否经过审批,执行结果是否结构化,失败是否能被模型和人理解。
新增通道的契约是:平台消息是否能无损变成 InboundEvent,统一输出是否能按平台能力表达,通道失败是否不破坏消息总线。
新增 provider 的契约是:不同厂商响应能否稳定转成统一的 LLMResponse 和 ToolCallRequest,错误、重试、流式输出和 usage 统计是否能被上层一致处理。
新增长期状态的契约是:状态能否持久化、迁移、并发访问和异常恢复,并且不会污染不该污染的上下文。
最有价值的测试不是证明某段代码存在,而是证明某个架构边界没有被破坏。
以“修复测试失败”为例:意图是修复失败;状态包括仓库文件、测试日志、依赖版本、历史会话;能力包括读文件、运行测试、改代码、提交补丁。测试不能只看最后有没有一句“已修复”,而要验证模型是否看到必要上下文,是否调用允许工具,工具结果是否回灌到下一轮推理,失败时是否留下可诊断信息。
测试替身
Agent 测试必须承认模型输出不完全确定。即使 temperature 为 0,不同 provider、不同模型版本、不同上下文压缩结果,也可能让输出发生变化。
解决办法不是把所有行为绑定在固定自然语言上,而是把不确定性关进边界。
FakeProvider 固定模型响应,FakeTool 固定工具结果,tmp_path 固定文件系统范围,mock channel 固定平台回执,mock approval 固定审批结果,短周期 scheduler 或 fixed clock 固定时间推进。
一个最小测试可以写成这样:
provider = FakeProvider([
LLMResponse.tool_call("read_file", {"path": "tests/test_app.py"}),
LLMResponse.text("已定位断言失败"),
])
tool = FakeTool("read_file", result="assert actual == expected")
approval = MockApproval(allow_readonly=True)
event = InboundEvent.text_message(
channel="test",
sender_id="u1",
chat_id="c1",
text="帮我分析测试失败",
)
result = await agent_loop.handle_event(event)
assert approval.checked("read_file")
assert tool.called_with(path="tests/test_app.py")
assert result.saved_to_session is True
assert result.outbound_event.kind == "final"
这段伪代码不关心模型最后怎么措辞。它关心的是系统契约:工具有没有进入审批,参数有没有传对,结果有没有保存,最终事件有没有投递。
fake provider 的核心价值,是把“模型是否聪明”从“系统是否正确”中分离出来。系统正确性先由测试替身测稳,真实模型质量再交给评估集观察。
阶段边界
旧式 AgentLoop 常把上下文构造、模型调用、工具执行、历史保存和消息发送写在一个大函数里。这种写法能跑,但测试失败时很难定位。
echo-agent 将处理过程拆成 ContextStage、InferenceStage、ResponseStage,测试边界因此更清晰。
ContextStage 测系统如何“看见世界”:输入事件如何加载会话,历史消息如何进入上下文,记忆、知识、技能和运行元数据如何注入,token 压力下是否触发压缩。
InferenceStage 测系统如何“推理与行动”:ready tools 是否经过过滤,模型路由候选是否按健康状态生成,工具调用循环是否遵守最大轮数,工具结果是否以正确格式反馈给模型。
ResponseStage 测系统如何“落盘与输出”:最终回答是否保存到会话,输出是否通过 MessageBus 投递,后台整理任务是否在回答后启动,错误响应是否保留足够上下文。
阶段化测试的收益很直接:失败时能判断问题来自哪个阶段。
安全回归
Agent 安全测试要默认怀疑模型。模型可能调用错误工具,传错路径,重复执行,相信注入内容,忽略审批提示。
echo-agent 中,ApprovalGate 位于工具执行前,综合风险分类、权限策略、路径策略、工具策略、Smart Approval 和审批结果。这里的测试重点不是证明“正常操作能成功”,而是证明“危险操作能被拒绝”。
| 安全对象 | 允许路径 | 拒绝路径 |
|---|---|---|
| 路径策略 | 工作区内读取 | 父目录跳转、符号链接越界、敏感路径 |
| 工具权限 | 低风险只读工具 | 不在 allowed_tools 中的工具 |
| 审批系统 | 已批准中风险操作 | 高风险工具、审批超时、用户拒绝 |
| MCP 接入 | 只读 server 工具 | 注入式描述、冲突工具名、协议异常 |
| 多 Agent | worker 使用交集工具 | worker 调用 blocked tools、递归越界 |
拒绝路径必须返回可解释错误,不能崩溃,也不能沉默。允许路径也要测,因为过度保守会让 Agent 不可用。
生产级 Agent 至少要有这些可检验项:tool call trace,工具风险分级,只读/低风险写/高风险写区分,审批节点,路径策略,失败重试与取消处理,任务状态持久化,评估集与回归测试,以及每次行动依据的可复盘记录。
长期状态
记忆、技能、任务、工作流和调度器让 Agent 不再是一次性问答系统,也让测试多了时间维度。
记忆测试要使用临时存储,覆盖写入、检索、更新、删除、压缩、审查和后台 consolidation。尤其要验证无关记忆不会被检索进上下文,低置信度或过期记忆不会影响回答,用户记忆和环境记忆不会混淆。
技能测试要覆盖 agentskills.io 兼容布局、名称规则、只读 builtin 根、用户可写根、允许子目录、辅助文件大小限制、原子写入和删除。上下文注入时只应注入技能摘要,不能把所有 SKILL.md 正文无条件塞进模型。
任务、工作流与调度测试要覆盖状态迁移、重试、取消、暂停、恢复、step 依赖和 CRON、INTERVAL、ONCE、EVENT、CONDITION 触发器,并避免依赖真实长时间等待。
这些测试保护的是 Agent 的长期行为,避免系统在夜间任务、重试、长会话压缩和状态迁移中悄悄退化。
CI 与回放
测试数量上来以后,不能每次提交都跑所有东西。分层 CI 是 Agent 工程的现实需要。
快速层适合每次提交,覆盖单元测试、配置、数据模型、路径策略和文本处理。集成层适合 PR,覆盖 bus、pipeline、tools、session、storage、memory、skills、workflow、gateway。安全层应在 PR 和发布前运行,路径越界、危险工具、审批、MCP 安全和权限策略不应被跳过。扩展层适合定时或发布前运行,覆盖真实 provider smoke test、端到端 Gateway、长会话压缩和较大 eval 数据集。
如果 CI 时间过长,优先拆层,而不是删除测试。Agent 的错误往往跨模块传播,删掉回归测试会让后续演进成本迅速上升。
回放测试也应该进入长期策略。线上 trace 或录制轨迹可以重新执行,用来检查新版本是否保持行为:消息、上下文、工具调用、审批结果、状态变化和最终输出是否仍然符合预期。
回放不要求完全复现外部世界。工具响应可以录制,模型输出可以固定或重新生成,外部时间可以模拟,用户审批可以脚本化。关键是保留轨迹结构,让复杂多轮问题能沉淀为可重复测试。
小结
Agent 测试不是把随机性消灭掉,而是把随机性关进明确边界。
确定性规则用单元测试固定;模块协作用集成测试固定;入口链路用少量端到端测试固定;语义质量用评估集管理;真实失败用回放测试沉淀;安全边界用允许路径和拒绝路径同时校准。
echo-agent 的测试策略背后有一个朴素判断:Agent 架构会持续变化,但系统契约不能随意变化。只要工具执行前必须审批、危险路径必须拒绝、会话状态必须保存、最终事件必须投递、评估集必须回归,这个系统就有持续演进的底座。
测试最终服务的不是测试报告,而是架构信心。它告诉开发者哪里可以安全修改,哪里必须阻断发布,哪里只是模型波动,哪里暴露了结构问题。没有这套回归机制,Agent 越强,越难维护;有了它,能力增长才不会变成不可控的复杂度。
(全篇完)
本文为 echo-agent 设计笔记系列第 30 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣,欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《Agent 架构的长期演进与维护》,敬请期待。
更多推荐

所有评论(0)