mobile wallpaper 1
mobile wallpaper 2
mobile wallpaper 3
mobile wallpaper 4
4043 字
11 分钟
写了半年 Agent,我才明白为什么「评估」不是「测试」
2026-09-10

在过去大半年里,我自己写过 Agent 玩具,读过不少开源框架源码(比如 pi、Claude Code、LangGraph),也和做工程的朋友交流时经常聊到一个核心话题:

“大模型的输出明明每次都随机、不确定,那针对 Agent 项目,到底该怎么做自动化测试?”

最开始折腾 Agent 时,我的第一反应和大多数刚入门的同学一样:弄几十个 Prompt,写个脚本批量调接口,然后拿另一个大模型(LLM-as-a-Judge)打个分,算一下胜率(Win-rate)或者准确率,分高就觉得“这次优化成功了”。

但随着项目从一个简单的 Demo 往具有多轮工具调用、状态暂存、中断恢复的复杂系统演进,这种做法立刻就行不通了:

  • 每次改两行代码想验证一下,跑一次脚本要等几分钟,一不小心几十块钱 API 额度就没了;
  • 脚本跑出来这次 82 分,下次 79 分,根本不知道是因为模型天然的随机波动,还是刚写的代码里真的引入了 Bug;
  • 最要命的是,哪怕平均分是 90 分,代码里一个工具调用的参数解析越界了,一运行直接整机抛异常崩溃,评分再高有什么用?

痛苦反思之后我意识到:我们不能把学术圈做科研、打榜评测的思维,硬套在软件工程的质量防线上。

如果我们想把 Agent 当成一个严肃的软件系统去构建,就必须站在“宏观系统设计者”的角度去重新拆解:什么是测试(Testing)?什么是评估(Evaluation)?我们究竟该怎么从零设计一套靠谱的测试体系?


💡 观念破局:别再把“评估”当成“测试”了#

要想理清测试体系,第一步必须在认知上把这两件事彻底割裂开来。

在很多讨论里,大家会把“写 Evals”和“写 Tests”混在一起说。但从系统设计的视角来看,它们解决的完全是两类不同性质的问题:

graph TD subgraph Testing [工程测试 Testing: 系统的硬门禁] T1[输出: 严格布尔值 Pass / Fail] T2[运行: 本地几秒跑完 / 不花钱 / 断网可跑] T3[目的: 抓逻辑 Bug / 防越权 / 保状态机无死锁] T4[对象: 代码、状态流转、协议解析、工具实现] end subgraph Evaluation [效果评估 Evaluation: 算法的体检单] E1[输出: 概率与统计分布 分数 / 召回率 / 胜率] E2[运行: 耗时较长 / 消耗 Token / 需网络与模型] E3[目的: 比较 Prompt 好坏 / 检验模型能力泛化] E4[对象: 语义质量、生成风格、意图理解广度] end
维度工程测试(Testing)体系评估(Evaluation / Evals)
回答的问题系统有没有按契约坏掉?(Bug / Crash)模型这波回答得漂不漂亮?(Quality)
判定结果确定性的二元结果:Pass / Fail(1 或 0)统计学分布:评分、胜率、区间命中率
执行代价几乎为零:毫秒级本地单测,离线无外部依赖昂贵:成百上千次 API 调用,耗时数十分钟
拦截意义阻断门禁:测试不通过,代码严禁合并上线演进罗盘:指导后续怎么微调或改 Prompt

举个通俗的例子:

  • 测试就像是汽车出厂前的安全质检:刹车踩下去有没有制动力?电门踩死会不会短路?转向机会不会卡死?有一项不过,这辆车就绝对不准出厂。
  • 评估就像是在测试这辆车在不同路况下开起来舒不舒服、油耗是不是百公里 6 升。

如果连底盘螺栓有没有拧紧(代码与契约测试)都没确认,天天测百公里加速度和内饰质感(跑 Eval 打分),这就是本末倒置。


🏛️ 从零推导:Agent 系统的四层测试架构#

既然模型输出是不确定的,那到底该怎么测?

经过阅读大量优秀开源实现并亲手推导后,我总结出一个心智模型:“把非确定性的模型剥离出去,把确定性的逻辑死死锁在测试网内。”

一个鲁棒的 Agent 系统,测试应该自底向上分成四层:

┌────────────────────────────────────────────────────────┐
│ Layer 4: 影子运行与只读金丝雀(线上真实环境校验) │
├────────────────────────────────────────────────────────┤
│ Layer 3: 录制回放与离线沙盒集成测试(VCR / Snapshot) │
├────────────────────────────────────────────────────────┤
│ Layer 2: 运行时循环与状态机契约测试(Mock Provider) │
├────────────────────────────────────────────────────────┤
│ Layer 1: 工具与边界防线的纯函数单测(Schema & Utils) │
└────────────────────────────────────────────────────────┘

Layer 1:工具与边界防御的纯函数单测(最轻、最快、最硬)#

在 Agent 架构里,工具(Tool Call)是 Agent 唯一能改变真实外部世界的“手”。

这一层完全不需要联网,更不需要大模型介入,全部是纯函数级的逻辑测试:

  1. Schema 容错与反序列化:当模型漏传必要参数,或者把数字当成字符串传过来时,我们的 Pydantic / Zod 校验能不能安全拦截,并返回清晰易懂的错误说明?
  2. 反越权与参数清洗:如果让 Agent 读文件,传入路径是 ../../etc/passwd,防御层是否能在执行前把它拦下来?
  3. 幂等键生成:相同业务上下文下多次计算出的业务唯一键是否稳定?

这一层测试跑起来只需几毫秒,是保护系统的第一道绝对防护网。

Layer 2:运行时循环与状态机契约测试(核心主战场)#

在之前关于 腾讯面试状态设计复盘 的文章中,我详细拆解过 Agent 的三层状态模型。

代码最容易出 Bug 的地方,恰恰是 Agent 循环(Agent Loop)和状态流转。这一层我们要测试的是代码的骨架,做法是编写一个可编程的 Mock Model(模拟模型)

一个极为优雅的现实工业范例,就是我在精读 pi 的源码 时发现的 fauxProvider。pi-ai 在设计最底层时,没有把测试留给真实网络,而是内置了一个高拟真的假模型供应器:

  • 你可以通过 faux.setResponses([...]) 给它预设一套确定性的「剧本」——第 1 轮返回思考(fauxThinking)和工具调用(fauxToolCall),第 2 轮拿到工具结果后返回总结(fauxText);
  • 它甚至支持配置 tokensPerSecond,把 Token 拆成半个词半个词(chunk)往外吐,真实模拟流式推送;
  • 剧本队列掏空后,自动派发明确的 No more faux responses queued 错误,而不是引发未捕获异常。

借由类似 fauxProvider 这样的剧本驱动器,我们在本地单测里就可以把运行时的各种极端边界测个底朝天:

  • 截断防御:模拟模型吐出半截被截断的 JSON({"order_id": ),校验循环层是否会崩溃,能否正确拦截并提示参数非法;
  • 死循环熔断:给模型配置无限次循环调用同一无效工具的剧本,测试执行预算(max_turns / Token 限制)能否强行熔断并把状态置为 failed
  • 并发中断与取消:当底层正在模拟执行耗时工具时,通过外部注入 AbortSignal,验证状态机能否平滑切入 cancelled,并丢弃滞后的工具返回值;
  • 工具异常写回:模拟工具在特定轮次抛出 500 异常,验证循环层能否将其格式化为标准的 tool_error 喂回上下文供下一步推理,而不是导致整个 Node 进程崩掉。

这一步能够把绝大多数“服务假死”、“死循环刷爆账单”、“状态机逻辑互锁”的问题在本地彻底消灭。

Layer 3:录制与回放的沙盒集成测试(Deterministic Replay)#

很多时候我们确实需要测试完整的端到端链路:比如“用户提问 \rightarrow 规划任务 \rightarrow 调用只读工具查库 \rightarrow 结合数据综合回答”。

如果每次跑集成测试都去打真实的 OpenAI 或 Anthropic API,会立刻面临三个工程死穴:

  1. Flaky Tests(测试抖动):公网网络丢包、供应商偶发 503 限流,导致原本正确的代码在 CI 上随机标红;
  2. 极慢与极贵:一个稍微复杂的多轮 Agent 跑完要几十秒,积累上百个用例后,跑一次 CI 要半小时、刷掉几十刀额度,开发最后只能跳过测试直接合并;
  3. 输出微小漂移导致断言失效:模型换个修辞、多吐两个无关标点,基于文本严格匹配的集成断言就会全线崩溃。

解决这个问题的经典工业方案,是借鉴网络测试中的 VCR 录制与回放机制(Deterministic Replay)

1. 录制期(Record Phase):捕获 Golden Snapshot#

在受控环境下,输入一个经典的端到端 Prompt(比如“查询订单 1024 并给出履约状态”),让 Agent 与真实的 LLM 完成交互。
在这个过程中,底层网络适配器把所有出入报文抓取下来,序列化存为一份快照文件(Cassette):

  • 包含用户输入的原始消息与系统 Prompt;
  • 包含大模型返回的全部流式事件序列(TTFT、Token 流、Tool Call 报文、Stop Reason);
  • 包含工具执行的 Mock 响应与消耗的时间戳。

2. 回放期(Replay Phase):离线环境下的确定性穿越#

在日常提交代码、本地运行 pnpm test 或 CI 触发时,系统彻底切断外网连接,网络请求被重定向到本地的快照文件:

  • 本地运行时就像真实面对一个毫秒级响应、完全免费且 100% 确定的大模型;
  • 断言重心从“文字像不像”转移到“协议与副作用对不对”
    • 断言输入消息拼装出的 Context 结构是否符合新版约定;
    • 断言解析器派发出的状态事件流(started \rightarrow tool_called \rightarrow completed)顺序是否严格单调递增;
    • 断言工具接收到的实际参数对象(深度比对)是否符合预期。

3. 快照更新策略(Snapshot Maintenance)#

代码重构或修复了某个状态流转 Bug 时,原有的回放应当直接绿灯通过;只有当我们主动升级了基座模型(如从 Claude 3.5 升级到 3.7)、大幅重构了系统 Prompt,或者重构了工具接口协议时,才由专人运行带有 --record 标志的命令重新录制一份基线快照。

Layer 4:影子运行与只读金丝雀(生产环境的最后屏障)#

大模型的概率特性决定了:就算你在本地用 fauxProvider 测满了所有状态机边、用 VCR 回放验证了协议无损,真实用户千奇百怪的提问依然可能触发意想不到的长尾幻觉。

面对这种不可避免的“非确定性”,最高级的测试手段不是在开发机上瞎猜,而是借助线上影子流量(Shadow Traffic)与金丝雀(Canary)进行受控对抗

1. 只读沙箱(Read-Only Guardrail / Dry-run Proxy)#

Agent 最大的风险在于“副作用”。查数据的只读工具哪怕查错了,危害也远小于一个错误的转账或发帖工具。
因此在影子测试中,所有注册工具必须实现双重实现或被代理包裹:

  • Read-Only 工具(查询订单、读文档、搜索):完全真实执行,确保新版 Agent 能够获取最新的真实生产数据;
  • Mutating / Side-Effect 工具(创建订单、扣款、调用 webhook、写库):必须被注入只读拦截屏障(Dry-run Wrap)。当 Agent 企图调用 refund(order_id, amount) 时,拦截器只把入参、调用顺序、预估影响写入影子审计日志,并直接向 Agent 返回一个假定的成功返回报文(模拟环境),绝不真正向生产数据库或外部支付网关发出写请求。

2. 流量镜像与决策比对(Diff Analysis)#

把线上真实进入网关的用户请求,通过异步队列(如 Kafka / Redis Stream)旁路分发出一份镜像流量,推给处于暗室(Shadow Mode)的新版 Agent:

  • 执行过程 Diff:对比线上主版本与影子版本在处理同一真实请求时,调用的工具序列是否一致?影子版本有没有出现多余的、未预期的工具调用?
  • 资源与延迟监控:影子版本的 Token 消耗是否暴增?推理轮次(Turns)有没有异常飙升?
  • 输出语义审查:通过自动化规则或低成本打分模型,监控影子版本是否输出敏感词、越权指令或明显的格式溃缩。

通过影子运行,系统可以在数千次真实线上流量的锤炼下暴露意料之外的长尾问题,同时完全不污染生产数据、不影响用户主流程。只有在影子流量中观察了数天、各项指标收敛稳定后,新版本才能正式切换放量。


🧭 宏观设计者的自我审视:如何衡量体系是否健康?#

站在全局规划的角度,设计完这套体系后,我经常会用一套标准来反推自己的项目是否健康:

flowchart LR Q1[1. 拔掉网线能否跑完核心测试?] --> Healthy{健康度评估} Q2[2. 状态机的异常边是否全部被点亮?] --> Healthy Q3[3. 写操作是否有物理隔离沙箱?] --> Healthy Q4[4. 测试成本是否与 Token 费用脱钩?] --> Healthy
  1. 断网运行能力:如果一个项目的核心单元测试必须挂梯子、填 API Key 才能跑,说明代码里的外部依赖耦合极其严重,架构尚未解耦。
  2. 看状态机边覆盖率,而不是传统代码行覆盖率:在 Agent 里,灾难往往发生在异常跳转上(比如挂起超期、并发取消)。每一个状态流转路径是否有单测用例覆盖,比代码写了多少行关键得多。
  3. 写副作用必须具备物理沙箱:无论单测还是集成测试,带有破坏性的工具在未显式 Mock 时,调用必须直接触发断言中断,绝不能依赖“模型应该不会乱调”的侥幸心理。
  4. 测试耗时与成本的解耦:当项目从 1 个工具扩充到 30 个工具时,测试套件的执行时间应该依然保持在几十秒内,账单不随代码量的膨胀而飙升。

总结:我的几点思考#

在学习和构建 Agent 的过程中,我越来越深地体会到:越是底层充满随机性的系统,其外部工程就越需要极致的确定性。

大模型给了我们一种全新的“概率计算单元”,但这绝不意味着软件工程的基本法失效了。相反:

  • 评估(Evals) 是我们在未知海域航行时的罗盘,告诉我们船在往哪个大致方向走、速度快不快;
  • 测试(Testing) 则是船舱底部的龙骨与防水隔舱,确保无论风浪怎么颠簸,船身绝对不会散架,发动机绝对不会熄火。

跳出单纯的“调参”和“打分”视角,用严谨的工程思维去构建确定性的沙盒、契约和状态防护网——这或许正是我们这一代年轻开发者,在 AI 浪潮中最该补上、也最硬核的一门系统设计必修课。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

写了半年 Agent,我才明白为什么「评估」不是「测试」
https://l1ngg.info/posts/tech/agent-testing-architecture/
作者
L1ngg
发布于
2026-09-10
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
腾讯 AI Agent 架构面经:拆解会话、执行与工具状态的三层设计
技术 从一道经典的 AI Agent 系统设计面试题出发,深度拆解 Session、Run 与 Tool Call 的三层状态模型,讲透并发控制、状态流转、用户插话、中断恢复与副作用失败边界。
2
GraphRAG 原理与实战精解:从多跳实体推理、社区摘要到工程落地边界
技术 深度解析 GraphRAG(图增强检索生成)的核心原理。从实体消歧与多跳检索切入,对比微软 GraphRAG 的 Local Search 与 Global Search 机制,探讨图谱无法自动解决的边界缺陷,并横向对比 Karpathy 的 LLM Wiki 范式。
3
给 Agent 团队装上记忆:腾讯 TencentDB Agent Memory 速通
技术 腾讯云数据库团队开源的 Agent 记忆中心:Chat Memory / Skill / Wiki / CodeGraph 四类记忆资产,配团队面板和代理接入,Claude Code、Codex 等主流 agent 都能用。本文讲清机制、装法和门槛。
4
一份 SKILL.md 就是一部事故法典:feat-lifecycle 精读
技术 精读 Clowder AI 项目的 feat-lifecycle skill:一个多 agent 团队如何用流程文件,制度性地对抗 AI 重复造轮子、自我锚定和谎报完成
5
读 Karpathy 的 LLM Wiki:从无状态 RAG 到持久累积的个人知识库(附全文精译)
技术 Andrej Karpathy 提出了一种基于 LLM Agent 维护个人 Wiki 的新范式:将 Obsidian 作为 IDE,LLM 作为程序员,Wiki 作为代码库,打破传统 RAG 的单次检索局限。本文包含背景介绍、全文翻译与深度技术思考。

目录