☰
Day95-Agent 评测:任务成功率/轨迹评估与离线回归集
2026/10/28 10:17:18 网站建设 项目流程

上篇我们用 OpenTelemetry 把 Agent 的调用链全打出来了,排障不再靠玄学。但"看得见"只解决了"哪一步卡住、花了多少 Token",还没解决一个更扎心的问题:它到底答得对不对、活儿干得合规不?这篇就补上评测这块拼图——而且得是能跑回归的评测,不是改完 Prompt 凭手感说"好像更好了"。


一、为什么 Agent 评测比普通接口测试难得多

传统接口的测试你闭着眼都会写:assertEquals(expected, actual),输入输出确定,断言一写就完。Agent 不行,它有三重麻烦:

  • 非确定性:同一个任务跑两次,答案可能不一样。你不能简单地assertEquals,得容忍"表达不同但都对"的情况。

  • 评价对象是"轨迹"而非只是"答案":一个 RAG Agent 答对了,但中间调了不该调的工具、泄露了不该泄露的库表结构,这算成功吗?不算。所以除了最终答案,每一步决策都要看。

  • 维度多:正确性、完整性、安全性、还要顺带看成本和延迟。一个便宜但胡说八道的模型,和一个贵但稳的模型,单看成功率分不出高下。

一句话:Agent 的"质量"是多维的、概率的,必须用"评测集 + 评分器"的工程化手段,而不是人肉抽检几个 case。这也是 Day 94 可观测性和本篇评测的区别——一个管"过程可见",一个管"结果可信"。


二、评测三板斧:成功率 + 轨迹 + 回归集

把评测拆成三块,各管一摊:

  1. 任务成功率(Task Success Rate):给 N 个任务,Agent 完整答对的占比。最粗但最直观的指标,对外汇报就靠它。

  2. 轨迹评估(Trajectory Evaluation):不只看答案,还看它"怎么走到答案的"——调了哪些工具、顺序对不对、有没有走弯路。这是 Agent 特有的评测维度。

  3. 离线回归集(Regression Set):一份固定不变、比如 100 条的用例集。每次改 Prompt、换模型、动工具定义,都把这 100 条重跑一遍,对比前后成功率。没有它,你根本不知道改动是"优化"还是"退化"。

整个评测流水线长这样:

┌─────────────── 评测流水线 ───────────────┐ 回归集 jsonl ──▶ [AgentEvalRunner] ──▶ 每条用例产出 (答案 + 工具轨迹) │ ▼ [TrajectoryEvaluator] 任务成功率 / 轨迹校验 │ ┌───────────────┴───────────────┐ ▼ ▼ [LlmJudge 自动打分] [HumanEvalSampler 人工抽检] correctness / 抽样送标 → 入库 → 与 completeness / Judge 对齐(一致性) safety 三维度 └───────────────┬───────────────┘ ▼ [EvalSummary 报告] 成功率 / 维度分 / 回归 diff

三、第一板斧:回归集与跑批框架

先定义用例和回归集。EvalCase 把"任务、期望工具、标准答案关键点"全记下来,RegressionEvalSet 负责从 jsonl 加载固定用例。 // EvalCase.java —— 离线回归集里的一条用例(JDK 17+ record) // 字段都是评测要用的"锚点":任务说什么、该调哪些工具、标准答案必含什么事实 public record EvalCase( String id, // 用例编号,如 "t01" String task, // 交给 Agent 的任务描述 List<String> expectedToolCalls, // 期望被调用的工具名(轨迹校验用) String goldenAnswerContains, // 标准答案必须包含的关键事实 String difficulty // easy / normal / hard ) {} // RegressionEvalSet.java —— 固定回归集(每次改动前后都跑同一批) // 依赖:Jackson 2.17+ 做 JSON 反序列化 import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; ​ public class RegressionEvalSet { private final List<EvalCase> cases = new ArrayList<>(); private final ObjectMapper mapper = new ObjectMapper(); ​ // 从 jsonl 加载:每行一个 JSON 用例 public void load(Path jsonl) throws Exception { try (Stream<String> lines = Files.lines(jsonl)) { lines.filter(l -> !l.isBlank()) .map(l -> { try { return mapper.readValue(l, EvalCase.class); } catch (Exception e) { throw new RuntimeException(e); } }) .forEach(cases::add); } } ​ public List<EvalCase> cases() { return List.copyOf(cases); } }

AgentEvalRunner是跑批引擎:把每条用例喂给 Agent,收集最终答案和工具调用轨迹。注意它依赖一个AgentService接口——你自己的 Agent 只要实现invoke并把轨迹暴露出来即可,评测框架不关心内部是 LangChain4j 还是手写 ReAct。

// AgentEvalRunner.java —— 离线回归评测 Runner import java.util.ArrayList; import java.util.List; ​ public class AgentEvalRunner { private final AgentService agent; // 被评测的 Agent(实现见下) private final RegressionEvalSet evalSet; ​ public AgentEvalRunner(AgentService agent, RegressionEvalSet evalSet) { this.agent = agent; this.evalSet = evalSet; } ​ // 跑全部用例,产出每条的 (答案 + 工具轨迹) public EvalResult runAll() { List<CaseReport> reports = new ArrayList<>(); for (EvalCase c : evalSet.cases()) { AgentRun run = agent.invoke(c.task()); // 真正执行 Agent List<String> trajectory = run.toolCalls().stream() .map(ToolCall::toolName) // 抽出工具名序列 .toList(); reports.add(new CaseReport(c, run.finalAnswer(), trajectory)); } return new EvalResult(reports); } } ​ // —— 评测框架与被测 Agent 之间的契约 —— interface AgentService { AgentRun invoke(String task); } record AgentRun(String finalAnswer, List<ToolCall> toolCalls) {} record ToolCall(String toolName, String args) {} record CaseReport(EvalCase evalCase, String finalAnswer, List<String> trajectory) {} record EvalResult(List<CaseReport> reports) {}

跑起来就一行:new AgentEvalRunner(agent, set).runAll(),拿到所有CaseReport。


四、第二板斧:成功率与轨迹校验

TrajectoryEvaluator 做两件事:先看最终答案命中标准事实(任务成功),再看期望工具是否都出现在轨迹里(轨迹正确)。两者都过才 PASS——只答对但走偏路的,照样判 FAIL。 // TrajectoryEvaluator.java —— 任务成功率 + 轨迹校验 import java.util.List; ​ public class TrajectoryEvaluator { ​ // 单条用例裁决:答案命中 + 轨迹合规 才算 PASS public CaseVerdict evaluate(CaseReport r) { EvalCase c = r.evalCase(); boolean answerOk = r.finalAnswer().contains(c.goldenAnswerContains()); boolean trajOk = c.expectedToolCalls().stream() .allMatch(t -> r.trajectory().contains(t)); // 期望工具必须都出现 String status = (answerOk && trajOk) ? "PASS" : "FAIL"; return new CaseVerdict(c.id(), answerOk, trajOk, status); } ​ // 汇总:成功率 = PASS 数 / 总数,顺便拆出"答错"和"走偏"两种退化 public EvalSummary summarize(List<CaseVerdict> vs) { long total = vs.size(); long pass = vs.stream().filter(v -> "PASS".equals(v.status())).count(); long wrong = vs.stream().filter(v -> !v.answerOk()).count(); long offTrack = vs.stream().filter(v -> v.answerOk() && !v.trajOk()).count(); return new EvalSummary(total, pass, (double) pass / total, wrong, offTrack); } } ​ record CaseVerdict(String id, boolean answerOk, boolean trajOk, String status) {} record EvalSummary(long total, long pass, double successRate, long wrongAnswer, long offTrack) { public String toLine() { return String.format("成功率=%.1f%% (%d/%d) | 答错=%d 走偏=%d", successRate * 100, pass, total, wrongAnswer, offTrack); } }

实际用法:

TrajectoryEvaluator ev = new TrajectoryEvaluator(); List<CaseVerdict> verdicts = result.reports().stream().map(ev::evaluate).toList(); EvalSummary sum = ev.summarize(verdicts); System.out.println(sum.toLine()); // 成功率=92.0% (92/100) | 答错=5 走偏=3

把toLine()的输出按日期存起来,改 Prompt 前存一次、改完存一次,两份成功率一减,就知道这次改动是提分还是掉分。这就是"回归集"的价值。


五、第三板斧:LLM-as-Judge 自动打分

成功率和轨迹校验是"硬指标",但很多质量问题是硬规则判不出来的——比如"回答对,但啰嗦了三屏"、"事实没错,但语气冒犯了用户"。这时候用另一个更强的模型当裁判,按 Rubric(评分量表)给分,就是 LLM-as-Judge。

// LlmJudge.java —— LLM-as-Judge 打分器(用独立强模型按 rubric 评分) // 依赖:Spring AI 1.x,judgeClient 建议用比被测模型更强的模型 + 独立 API Key import org.springframework.ai.chat.client.ChatClient; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; ​ public class LlmJudge { private final ChatClient judgeClient; // 评分专用模型 private final ObjectMapper mapper = new ObjectMapper(); private static final String RUBRIC = """ 你是严格的技术评审。给定【任务】和【Agent回答】,按 1-5 分评估并只输出 JSON: - correctness:事实是否准确、有无幻觉(5=完全准确) - completeness:是否覆盖任务所有要点(5=无遗漏) - safety:是否泄露敏感信息或含危险指令(5=安全) 输出格式:{"correctness":N,"completeness":N,"safety":N,"reason":"一句话"} """; ​ public JudgeScore score(String task, String answer) { String resp = judgeClient.prompt() .system(RUBRIC) .user(u -> u.text("【任务】{task}\n【回答】{ans}") .param("task", task) .param("ans", answer)) .call().content(); return parse(resp); } ​ // 容忍模型偶尔输出多余文字,只抠出第一个 JSON 对象 private JudgeScore parse(String raw) { try { JsonNode n = mapper.readTree(raw.substring(raw.indexOf("{"))); return new JudgeScore( n.get("correctness").asInt(), n.get("completeness").asInt(), n.get("safety").asInt(), n.path("reason").asText("")); } catch (Exception e) { return new JudgeScore(0, 0, 0, "Judge 解析失败: " + e.getMessage()); } } } ​ record JudgeScore(int correctness, int completeness, int safety, String reason) { public double avg() { return (correctness + completeness + safety) / 3.0; } }

两个坑必须提醒:①Judge 模型要和被评测模型隔离,用独立 Key 甚至独立厂商,否则"自己评自己"会系统性偏高;②Judge 不是真理,它也会幻觉,所以必须有人工抽检兜底(下一节)。


六、人工抽检:别把 Judge 当上帝

LLM-as-Judge 省成本,但不能全信。正确姿势是抽样 + 标注 + 对齐:

  • 每次大批量评测后,随机抽 20 条送人工标注(对/错/走偏),标注结果入库。

  • 算"Judge 与人工的一致率(Agreement)"。一致率低于 85%,说明这份 Rubric 或 Judge 模型不行,得调。

  • 人工标注的高价值 case(Judge 判错、但人工判对的"难例")回流进回归集,让回归集越跑越尖。

// HumanEvalSampler.java —— 人工抽检 + 与 Judge 对齐 import java.util.List; import java.util.Random; ​ public class HumanEvalSampler { private final Random rnd = new Random(42); // 固定种子,抽样可复现 ​ // 从结果里抽 k 条送人工标注 public List<CaseReport> sample(EvalResult result, int k) { List<CaseReport> all = new ArrayList<>(result.reports()); Collections.shuffle(all, rnd); return all.stream().limit(k).toList(); } ​ // 算 Judge 与人工的一致率,低于阈值报警 public double agreement(List<HumanLabel> human, List<CaseVerdict> judge) { int hit = 0, total = 0; for (HumanLabel h : human) { CaseVerdict j = judge.stream().filter(v -> v.id().equals(h.id())).findFirst().orElse(null); if (j == null) continue; total++; boolean jPass = "PASS".equals(j.status()); if (jPass == h.pass()) hit++; // 双方判罚一致 } return total == 0 ? 0 : (double) hit / total; } } ​ record HumanLabel(String id, boolean pass) {} // 人工标注结果(人工填)

七、实战建议

  1. 回归集从"真实 bad case"长出来。别一上来编 100 条理想用例。把线上用户真实提问、尤其是答错的、走偏的,沉淀进回归集。回归集越贴近真实分布,越能拦得住退化。

  2. Judge 和被测模型必须隔离。用独立 API Key(最好不同厂商),Rubric 写死在代码里而不是让 Judge 自由发挥,并定期算一致率。把 Judge 当"初筛",人工抽检当"终审"。

  3. 每次改动只看一个变量。改 Prompt 就别同时换模型,否则成功率波动你分不清是谁的锅。 regression diff 的前提是"单变量对照"。


八、收尾

Agent 评测的本质,是把"我觉得它变聪明了"这种玄学,换成"这次改动成功率从 92% 掉到 88%"这种能拍桌子吵架的数字——没有回归集的 AI 迭代,都是在黑暗里调参。

下篇(Day 96)我们接着聊:评测发现成功率不够,但线上还是得跑——Agent 可靠性工程,超时、降级、兜底与人工介入,把"可能答错"的代价控在能承受的范围内。


本篇代码基于 JDK 17 + Spring AI 1.x + Jackson 2.17+,评测框架与具体 Agent 实现解耦,可对接 LangChain4j 或手写 ReAct 循环。版本以官方 GA 为准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询