你大概率干过这件事:给 Agent 接上 Langfuse(或者别的 LLM 可观测平台),trace 画得漂漂亮亮,然后点开 “Dataset → Run Experiment”,跑出一个绿油油的通过率——92%。你松了口气:评估过了,可以上线。
结果上线没几天,投诉来了:该退款的没退、简单问题绕了五个工具、偶尔还答得驴唇不对马嘴。你回头盯着那个 92% 发懵:评估明明过了,怎么线上还这样?
问题就出在这儿——你点的那个"现成按钮",评的压根不是 Agent 的行为。
上一篇结尾我说过,坎 6 是比"接一个 Agent"本身更难的话题,值得单开一篇。就是这篇。读完你会想清楚三件事:
为什么现成评估对 Agent 会失效
——它错在哪个假设上;
Agent 到底该评什么
——而不是那段最终回答;
一套能进 CI 的分层评估怎么搭
——附关键代码骨架。
沿用上一篇的"订单助手(order-assistant)"设定,代码是可迁移的通用工程模式,不涉及具体业务。
- 先分清:你评的是"聊天",还是"Agent"
=========================
这是所有误会的根。
一个普通 chatbot 的执行模型很简单:输入一句话,输出一段话。你要评的就是这段话——答得准不准、口气好不好。终答质量,基本等于它的全部。
Agent 不是。Agent 的执行是多步循环:模型先想,决定调哪个工具,拿到结果,再想,可能再调下一个工具,循环往复,最后才吐出那段话。它真正的行为,是这中间一整条工具调用轨迹(trajectory)。
图 1:聊天是"输入 → 一段话"的单发式;Agent 是"想 → 调工具 → 看结果 → 再想 → 再调"的多步循环——它的行为是整条轨迹,不是末尾那段话。
只评最终那段话,等于只看考卷上的答案、不看解题过程。答案蒙对了、过程全是错的,你照样发现不了——而 Agent 恰恰特别擅长"用错误的过程,凑出一个看起来对的答案"。
一句话记住:聊天评"答得对不对",Agent 还得评"做得对不对"。下面 5 个坑,全是从这句话里长出来的。
坑 1 · 只评"最终那段话",不评"怎么走到的"
现象:数据集里放的是"输入 + 期望回答",评分器拿模型的最终输出跟期望答案比(或者用 LLM-as-judge 打个分)。分数挺高,线上却状况不断。
根因:这套评估默认了一个聊天时代的假设——“输出那段话 = Agent 的行为”。可对 Agent 来说,风险和价值都在轨迹里:它有没有调该调的工具、参数抽得对不对、有没有多调了不该调的(比如把"查询"走成了"退款")、有没有为了一件小事绕五轮。这些,终答里一个字都看不出来。一个把 refund 误调了、但话术圆回来的回答,终答评估会开心地给它打"通过"。
下面这张表最能说明问题——同一个用户问题,三条完全不同的轨迹,最终回答几乎一样,现成的终答评估会给它们全部打"通过":
| 轨迹 | 模型实际做了什么 | 最终回答 | 终答评估 | 真相 |
|---|---|---|---|---|
| A | queryOrder(ORD-123) | “已发货,预计明天送达” | ✅ 通过 | 干净、正确 |
| B | 绕三步:queryOrder → queryLogistics → queryOrder | “已发货,预计明天送达” | ✅ 通过 | 答对了,但烧了 3 倍 token |
| C | refund(ORD-123)→queryOrder | “已发货,预计明天送达” | ✅ 通过 | 误触发了退款,终答里一个字都看不出 |
C 是灾难,终答评估却和 A 打一样的分。这就是"只看答案不看过程"的代价。
解法:把评估对象从"终答"换成"轨迹",做轨迹级断言。跑完一条 case,把这一轮实际发生的工具调用抓出来,针对"调了什么、参数对不对、有没有碰红线"来断言:
// 跑完一条 case,拿到这一轮实际发生的工具调用序列 List<ToolCall> actual = runAndCaptureToolCalls(assistant, testCase.input()); // 断言"行为",而不是逐字匹配终答 assertThat(actual) .extracting(ToolCall::name) .containsExactly("queryOrder"); // 期望只调这一个工具 assertThat(actual.get(0).arguments()) .containsEntry("orderNo", "ORD-123"); // 关键参数必须抽对 assertThat(actual) .noneMatch(call -> call.name().equals("refund")); // 红线:退款工具绝不能被误调关键不是逐字匹配终答,而是断言行为。尤其 noneMatch 这类"红线断言"——危险工具绝不能被误调——比任何终答相似度都重要。至于怎么把工具调用抓出来:给模型挂一个 ChatModelListener,或者用下面坑 2 的工具包装类顺手记录,都行。
坑 2 · "重放"根本没复现
现象:同一条 case,今天跑通过、明天跑失败;换个时间点跑,通过率还不一样。你开始怀疑评估本身能不能信。
根因:你以为在"重放"历史 case,其实没有。现成按钮的"重跑",是拿旧的输入、重新真跑一遍你的 Agent——而 Agent 里的工具是真调下游的。可下游的库存、订单状态、当前时间,早就跟当初录 case 时不一样了。模型这一次看到的工具返回(observation),和当初那条 golden trace 根本不是一回事。输入相同、观测不同,轨迹自然就飘了。这不叫评估,这叫掷骰子。
说白了:你在同时测两样东西——模型 + 一直在变的下游环境。变量没锁死,结论就不可复现。
解法:冻结环境。把 golden case 里每个工具的"入参 → 出参"录下来,评估时不碰真实下游,直接把当初录好的结果确定性地喂回给模型(VCR / 磁带式回放)。这样模型每次面对的观测完全一致,轨迹才可复现、可回归。
图 2:重跑时工具真打下游、库存和时间都在变;回放时把录好的观测原样喂回,下游被钉死——评估才测得准。
public class ReplayableOrderTools { private final OrderService realService; // 真实下游 private final ToolCassette cassette; // 录制/回放的"磁带" private final Mode mode; // RECORD 或 REPLAY public ReplayableOrderTools(OrderService realService, ToolCassette cassette, Mode mode) { this.realService = realService; this.cassette = cassette; this.mode = mode; } @Tool("根据订单号查询订单的当前状态和物流信息") public String queryOrder(@P("订单号") String orderNo) { if (mode == Mode.REPLAY) { // 评估时:直接返回当初录下来的结果,完全不碰真实下游 return cassette.get("queryOrder", orderNo); } // 录制时:真调下游,并把 入参 → 出参 存进磁带 String result = realService.describeStatus(orderNo); cassette.put("queryOrder", orderNo, result); return result; } }录制一次(RECORD 模式打一条真实链路),之后所有回归都在 REPLAY 模式下进行。从此评估只测模型这一个变量,下游环境被钉死了。
坑 3 · 数据集从哪来、golden 怎么定
现象:道理都懂,可你手上根本没有"标准答案"。人肉去标注每条 case 该走什么轨迹,又贵又慢,标着标着就放弃了。
根因:Agent 的"对"往往不是唯一解。查订单可以先反问单号再查、也可以直接查;一件事有好几条都合理的轨迹。你要是按"唯一正确轨迹"去标,既标不出来,也不该那么标。
解法:两步走。
第一步,数据集从线上捞,别凭空造。你都接可观测了,真实 trace 就是最好的素材库——按场景采样(高频的、出过错的、边界的),人工挑出"这条走得对"的,连同当时的工具 I/O 一起,固化成一个 golden case。它天然自带坑 2 要的那盘"磁带"。
第二步,期望值写成"可接受的轨迹",不是逐字答案。用宽松匹配:关注该调的工具集合、关键参数、绝不能碰的红线,以及终答里必须出现的要点,而不是逐 token 对比。一条 golden case 大概长这样:
{ "id": "order-status-happy-path", "input": "帮我查下订单 ORD-123 到哪了", "toolOutputs": { "queryOrder|ORD-123": "已发货,预计明天送达" }, "expect": { "tools": ["queryOrder"], "mustNotCall": ["refund", "cancelOrder"], "answerContains": ["已发货"] } }toolOutputs 就是冻结环境用的磁带,expect 就是轨迹断言的依据。这么一条 JSON,坑 1、坑 2、坑 3 全串起来了。
坑 4 · 拿"跑一次"当结论
现象:同一条 case,你手动跑了一次、过了,就把它记成"通过"。可换个人跑、或者过两天再跑,结果变了。
根因:LLM 本身是非确定性的。温度、采样,决定了同一个输入可能走出不同轨迹。单次运行的方差很大,拿一次结果当结论,等于用一个样本估计整体——不靠谱。
解法:同一条 case 多跑几次,看通过率,而不是看一次的成败(pass@k 的思路)。比如跑 5 次,3 次以上走对才算"稳定通过";偶尔飘一次是概率问题,次次飘就是真有毛病。做严格回归时,再固定温度、固定随机种子,把非确定性尽量压到最低。你要的是"这个行为稳不稳定",不是"这次蒙没蒙对"。
坑 5 · 评估没进 CI,模型悄悄退化
现象:上线前热热闹闹评了一轮,过了。之后有人改了 prompt、有人把模型从大换成小省成本、有人顺手调了个工具描述——没人再评。直到线上出事,才发现某个能力早就退化了。
根因:把评估当成"上线前的一次性验收活动",而不是"持续的回归闸门"。可 Agent 的行为对 prompt、模型、工具描述极其敏感,任何一处改动都可能悄悄改变轨迹。一次性评估,保质期只有一次提交。
解法:把离线评估集接进 CI。prompt、模型版本、工具描述——任何一项变更,都触发这套 offline eval 跑一遍,设一条分数闸门(比如核心 case 通过率不得低于上次基线),不达标就卡住合并。因为坑 2 已经把环境冻结了,这套评估不依赖任何外部下游,纯离线、够快、够稳,进 CI 毫无障碍。
这一步才是把前四个坑的收益锁死的地方——评估不进流水线,写得再好也只是一次性的自我感动。
一张图收尾:分层评估长什么样
把前面 5 个坑收敛起来,就是一套按成本和频率分层的评估体系——越便宜的越靠前、跑得越勤,越贵的越靠后、跑得越省:
图 3:分层评估像一道层层放行的漏斗——顶部宽口最便宜、跑得最勤,越往下越贵、跑得越少,只有少数用例流到最下面那道最贵的评估。下面这张表是每一层的明细。
| 层 | 评什么 | 成本 | 跑的频率 |
|---|---|---|---|
| 1 · 单工具选择 | 给一句话,模型该不该调工具、调哪个、参数抽得对不对 | 便宜 | 每次提交 |
| 2 · 多步轨迹 | 冻结环境回放,断言整条工具调用轨迹是否可接受 | 中等 | 改 prompt、换模型时 |
| 3 · 端到端终答 | 真实(或高保真)环境,LLM-as-judge + 人工抽检看最终回答 | 贵 | 发版前 |
三层是层层放行的闸门:上一层通过率达标,才轮得到跑下一层——把最贵的评估留到最后,既省钱,又能在前面几层提前拦掉大部分问题。
对照开头的三个问题:为什么现成评估失效(坑 1、坑 2)、Agent 该评什么(轨迹,而非终答)、怎么搭一套进 CI 的分层评估(坑 3、坑 4、坑 5),这篇都给到了。
写在最后
Agent 的评估之所以难,不是因为工具不够花哨,而是因为它逼你先想清楚一件事:你的 Agent"做得对",到底是什么意思。想清楚了,评估只是把这个定义翻译成断言和数据集;想不清楚,再贵的平台也只是给你一个会骗人的绿色数字。
这也正好是后端工程师的主场——定义正确性、锁死变量、把验证塞进流水线,这些本来就是你写了很多年测试练出来的肌肉,只不过这次被测的对象换成了一个非确定性的模型。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~