字节面试:你的 Agent 单次成功率 60%,敢上线吗?
先说一个数字60%。那是我们客服 Agent 在内测集上的单次任务成功率。当时我觉得账算得过来——六成能自己办成剩下四成转人工兜底。直到有位做风控的同事问我一句那同一个用户连着来八次是不是至少有一次要翻车我拿计算器按了一下0.6 的 8 次方约等于1.7%。也就是说一个60 分及格的 Agent用户只要用满八次几乎必然撞上一次失败。而我们当时的监控看板上一片绿色——接口全是 200平均响应 800ms错误率 0.3%。这道题上周一位学员在字节三面被问住了。面试官的原话是你的 Agent 上线了怎么证明它没在瞎搞他答了准确率、答了人工抽检、答了看日志。面试官摇摇头追了三句同一个任务跑八次它八次都能做对吗它回复说退款已处理你怎么知道系统里真的只退了一笔、金额没退错下个月模型换版本你靠什么发现它悄悄变笨了三问一问比一问狠。答案分别是三个词pass^k、最终状态断言、回归评测门禁。这三个词就是这一期要讲的全部。台阶一先别谈过程你的结果可能压根没测对先破一个流传很广的误区。网上讲 Agent 评测的文章第一句基本都是不能只看结果要看过程。这话对但只对了半句——大部分团队连结果这一项都没测明白。他们怎么测结果拿 Agent 的最后一句回复去做文本匹配或者让模型打个分。这就出问题了。因为 Agent 跟传统大模型的本质区别不是它会思考、会调工具、会规划步骤——这些是能力的量变。真正的质变是它有行动权会产生副作用。传统大模型评测Agent 评测产物一段文本一段文本 一系列被改变的世界状态判分对象回答对不对任务办成没有 有没有办砸别的事判分方式标准答案比对 / 模型打分终端状态断言 轨迹评估失败代价答错了用户划走多退一笔钱、改错一条数据、发错一封邮件稳定性同一题答案基本稳定同一任务跑八次结果可能各不相同所以 Agent 的结果判定标准做法是最终状态断言final state assertion不读它说了什么去查它到底把世界改成了什么样。举个例子。任务是给 4812 号订单退款并通知客户断言应该长这样refund.count(order4812) 1 # 只能退一次 refund.amount approved_amount # 金额必须等于审批金额 order.status refunded # 订单状态要流转 ledger.net_change -approved_amount # 账要平 email.count(templaterefund) 1 # 通知只能发一封 unrelated_records.changed 0 # 没动到不该动的东西看最后一条。没动到不该动的东西也要断言——这才是 Agent 评测和普通接口测试的分界线。接口测试问的是退款接口返回 200 了吗brAgent 评测问的是这笔退款是不是只发生了一次金额是不是审批金额订单状态流转了吗账平不平邮件是不是只发了一封有没有误改别人的记录一句顶一万句的话Agent 可以留下一地鸡毛之后写出一段非常漂亮的总结陈词。只看它说了什么你测的是它的文笔不是它的工作。台阶二八次都对吗—— passk 和 pass^k状态断言解决了结果对不对紧接着就是同事问我的那个问题它对一次不算本事能不能次次都对这里有一对指标一个上标之差语义完全相反。我见过太多人在面试里把它们混为一谈。指标定义衡量什么k 增大时passkk 次里至少成功 1 次的概率能力上限它能不能做到单调上升趋近 100%pass^kk 次全部成功的概率可靠性它是不是次次都行单调下降趋近 0%passk最早用在代码生成HumanEval、Codex里因为那个场景你可以生成 5 个候选自己挑一个能用的——成一次就有价值所以用 pass5 合理。但 Agent 不是。客服 Agent 替用户改签机票用户没有从五个结果里挑一个的机会他只经历一次错了就是错了。所以 Agent 场景看的是pass^k。Anthropic 在评测指南里给过一个心算就能验证的例子单次成功率 75% 的 Agent连续三次全对的概率是 0.75³ ≈ 42%。Sierra 的 τ-bench读 tau-benchτ tool-agent-user把这件事量化到了扎心的程度。它在零售、航空两个领域里让 Agent 一边跟 LLM 扮演的用户多轮对话一边调用能读能写数据库的领域工具还要全程遵守一份业务规则文档判定不看对话只看最终数据库状态是否等于标注的目标状态。论文首发版报告的数字是GPT-4o 在零售域 pass^1 约 61%而 pass^8 跌破 25%官方仓库后续复跑数字有波动量级结论不变。翻成大白话一个单次成功率刚过六成的 Agent用户用满八次能八次全对的概率不到四分之一。这就是demo 很惊艳、上线就翻车的数学解释。而且请注意这些失败不是集中在它不会做的难题上而是散布在它会做、也做对过的任务上——方差在任务内部不在任务之间。所以这一层的工程结论只有一句选指标的判断依据用户能挑的场景看 passk用户只能接受一次的场景看 pass^k。几乎所有 Agent 上线场景都是后者。台阶三把过程拆开打分——四层金字塔结果测对了稳定性也有了指标现在才轮到过程。我把 Agent 评测拆成四层。你往上走一层成本高一档但漏测的故障少一类。层级回答什么问题具体指标常见漏测结果层事办成了没有任务成功率、pass^k、终端状态断言、部分得分重复退款、金额错误、越权改动过程层每一步走没走对工具选择准确率、参数正确率、上下文一致性、失败自愈中期失忆、参数幻觉、无意义重试约束层有没有越线、值不值安全拒答、业务规则合规、步数/token/时延/金钱成本工具调对了但违反业务政策基建层分数是怎么算出来的可重置沙箱、裁判与人工对齐率、CI 回归门禁用例互相污染、裁判本身不准四层里最容易被忽略的其实是过程层的两个细分项我单独拎出来说。第一工具调用必须拆成两个独立指标。工具选对和参数填对是两种完全不同的故障混在一起测出问题你不知道该调 prompt 还是该改 schema。还有个高阶指标叫 abstention rate——没有合适工具时它能不能正确拒绝调用而不是硬编一个。第二上下文一致性。这是长任务最典型的故障中期失忆。第 2 步用户提的关键约束到第 8 步被彻底忘掉于是它很努力地完成了一个稍微不同的目标。用实习生比喻特别好讲他记得要去订酒店但忘了你们有 8 个人订了间 4 人房。每一步单看都自洽只有跨步断言抓得出来。业内公开复盘里有个典型案例某 Agent 反复调用一个返回 Tool not found 的工具毫无进展最后直接转人工。每一次调用都返回干净的状态码只看接口监控完全发现不了——全 200也能整件事办砸。三种判分器别让裁判模型干它不该干的活这一层最容易犯的错是什么都丢给大模型当裁判。正确做法是三层分工原则只有一句能用代码判定的绝不用模型判。判分器判什么成本特点规则判分死循环、禁用工具、步数/成本超上限、JSON schema 校验零确定性每次结果一致状态断言数据库/外部系统的终端状态、副作用边界低客观不受措辞影响裁判模型推理是否合理、是否忠于检索到的上下文、语气是否合适高主观必须校准裁判模型有三个坑面试里说出来就是分位置偏差偏爱排在第一个的候选答案。长度偏差偏爱更啰嗦的那个答案哪怕它更空。自恋偏差偏爱自己家族模型生成的输出。所以裁判模型必须配详细 rubric必须跟人类标注算对齐率agreement rate。未经校准的裁判不能直接当质量门禁用——我见过团队用裁判模型卡发布结果它给一堆错误答案打了满分因为那些答案写得很长很自信。台阶四把评测从报告变成门禁到这一步很多人以为完事了。其实还差最后、也是最容易被面试官追问的一层。同一个 Agent你今天测 80 分下个月换个模型版本可能只剩 50 分。Agent 的行为会随模型更新、prompt 改动、工具 schema 调整、流量分布偏移而漂移。所以评测集不是一次性用品它是要挂进 CI 的每次改 prompt、加工具、换模型都跑一遍评测集pass^1 或 pass^8 掉出基线就卡住合并。这是会评测和工程化落地的分水岭也是面试官判断你真做过还是背过的地方。沙箱本身还有两条硬性要求缺一条分数就不可信可重置每次跑完必须回到已知初始状态。否则用例之间互相污染这一次的分和上一次的分根本不可比。边界隔离沙箱要在架构上访问不到生产。靠一个TEST_MODE变量、而进程里握着生产支付密钥那不叫隔离。Java 实战一个能直接跑的评测执行器光说不练假把式。下面这份骨架是纯 JavaJDK 17不依赖任何 AI 框架你能直接抄走改造。代码里Json.parse(...)是 JacksonObjectMapper.readValue的简写占位换成你项目里的 JSON 工具即可。package com.fox.agent.eval; import java.time.Duration; import java.util.*; import java.util.function.*; import java.util.stream.*; // ══════════ 0. 轨迹每一步都必须是可检索的 span ══════════ public record Trajectory( String traceId, ListStep steps, String finalAnswer, long tokensUsed, long costCents, Duration wallClock ) { public record Step(String thought, String toolName, MapString, Object args, String observation) {} public int toolCallCount() { return (int) steps.stream().filter(s - s.toolName() ! null).count(); } /** 死循环检测同一工具 同一组参数重复出现 3 次以上 */ public boolean hasToolLoop() { MapString, Long counter new HashMap(); for (Step s : steps) { if (s.toolName() null) continue; String sig s.toolName() | s.args(); if (counter.merge(sig, 1L, Long::sum) 3L) return true; } return false; } } // ══════════ 1. 世界状态Agent 能改的一切都必须可快照 ══════════ public final class WorldState { private final MapString, ListMapString, Object tables; private WorldState(MapString, ListMapString, Object tables) { this.tables new LinkedHashMap(tables); } public static WorldState of(MapString, ListMapString, Object t) { return new WorldState(t); } public ListMapString, Object select(String table) { return tables.getOrDefault(table, List.of()); } public long count(String table, String field, Object expected) { return select(table).stream() .filter(row - Objects.equals(row.get(field), expected)).count(); } public OptionalObject field(String table, String id, String field) { return select(table).stream() .filter(row - Objects.equals(row.get(id), id)) .map(row - row.get(field)).findFirst(); } public ListString tables() { return List.copyOf(tables.keySet()); } } // ══════════ 2. 状态断言代码能判的绝不用模型判 ══════════ FunctionalInterface public interface StateAssertion { AssertionResult test(WorldState before, WorldState after); } public record AssertionResult(String name, boolean passed, String detail) { public static AssertionResult ok(String n, String d) { return new AssertionResult(n, true, d); } public static AssertionResult fail(String n, String d) { return new AssertionResult(n, false, d); } } public final class Assertions { /** 条数断言抓多退了一笔多发了封邮件 */ public static StateAssertion countIs(String table, String field, Object value, long expected) { return (before, after) - { long actual after.count(table, field, value); String detail expected expected , actual actual; return actual expected ? AssertionResult.ok(table .count, detail) : AssertionResult.fail(table .count, detail); }; } /** 字段断言抓金额退错了状态没流转 */ public static StateAssertion fieldIs(String table, String id, String field, Object expected) { return (before, after) - { Object actual after.field(table, id, field).orElse(null); String detail expected expected , actual actual; return Objects.equals(actual, expected) ? AssertionResult.ok(table . field, detail) : AssertionResult.fail(table . field, detail); }; } /** 副作用断言抓顺手动了不该动的表 */ public static StateAssertion noSideEffect(SetString allowedTables) { return (before, after) - { ListString touched after.tables().stream() .filter(t - !allowedTables.contains(t)) .filter(t - !Objects.equals(before.select(t), after.select(t))) .toList(); return touched.isEmpty() ? AssertionResult.ok(noSideEffect, 未触碰白名单外的表) : AssertionResult.fail(noSideEffect, 越界修改: touched); }; } } // ══════════ 3. 评测用例初始状态 指令 期望终态 ══════════ public record EvalCase( String id, String instruction, String sandboxFixture, ListString mustCallTools, ListString forbiddenTools, ListStateAssertion assertions, int maxSteps, long maxCostCents ) {} // ══════════ 4. 三种判分器规则 / 断言 / 裁判 ══════════ public record Grade(String grader, boolean passed, double score, String reason) {} public sealed interface Grader permits RuleGrader, StateGrader, LlmJudgeGrader { Grade grade(EvalCase kase, Trajectory traj, WorldState before, WorldState after); } /** 规则判分器零成本、确定性负责一票否决 */ public final class RuleGrader implements Grader { Override public Grade grade(EvalCase k, Trajectory t, WorldState b, WorldState a) { if (t.hasToolLoop()) return new Grade(rule, false, 0.0, 检测到工具死循环); SetString used t.steps().stream() .map(Trajectory.Step::toolName).filter(Objects::nonNull) .collect(Collectors.toSet()); ListString hit k.forbiddenTools().stream().filter(used::contains).toList(); if (!hit.isEmpty()) return new Grade(rule, false, 0.0, 调用了禁用工具: hit); if (t.steps().size() k.maxSteps()) return new Grade(rule, false, 0.0, 步数超限: t.steps().size()); if (t.costCents() k.maxCostCents()) return new Grade(rule, false, 0.0, 成本超限: t.costCents()); return new Grade(rule, true, 1.0, 规则层通过); } } /** 状态判分器真正的结果层不看它说了什么看世界变成什么样 */ public final class StateGrader implements Grader { Override public Grade grade(EvalCase k, Trajectory t, WorldState b, WorldState a) { ListAssertionResult all k.assertions().stream() .map(x - x.test(b, a)).toList(); ListAssertionResult failed all.stream().filter(r - !r.passed()).toList(); double score all.isEmpty() ? 1.0 : (double) (all.size() - failed.size()) / all.size(); return failed.isEmpty() ? new Grade(state, true, score, 全部状态断言通过) : new Grade(state, false, score, 失败断言: failed); } } /** 裁判模型只判代码判不了的语义部分且必须与人工标注对齐后才可当门禁 */ public final class LlmJudgeGrader implements Grader { public record JudgeVerdict(boolean passed, double score, String reason) {} private final BiFunctionString, String, String llm; // (rubric, 待评内容) - JSON private final String rubric; public LlmJudgeGrader(BiFunctionString, String, String llm, String rubric) { this.llm llm; this.rubric rubric; } Override public Grade grade(EvalCase k, Trajectory t, WorldState b, WorldState a) { String payload t.steps().stream() .map(s - s.thought() | s.toolName() s.args()) .collect(Collectors.joining(\n)); try { JudgeVerdict v Json.parse(llm.apply(rubric, payload), JudgeVerdict.class); return new Grade(llm-judge, v.passed(), v.score(), v.reason()); } catch (Exception e) { // 裁判自己出错绝不能默认判通过——这是最容易埋雷的地方 return new Grade(llm-judge, false, 0.0, 裁判输出解析失败降级为不通过: e.getMessage()); } } } // ══════════ 5. pass^k 无偏估计C(c,k) / C(n,k) ══════════ public final class PassK { public static double passHatK(int trials, int successes, int k) { if (k 1) throw new IllegalArgumentException(k 必须 1); if (trials k) throw new IllegalArgumentException(试验次数必须 k); if (successes k) return 0.0; return comb(successes, k) / comb(trials, k); } static double comb(int n, int k) { if (k 0 || k n) return 0.0; double r 1.0; for (int i 1; i k; i) r r * (n - k i) / i; return r; } } // ══════════ 6. 执行器沙箱必须可重置异常不能中断整轮 ══════════ public final class EvalRunner { public interface Agent { Trajectory run(String instruction); } public interface Sandbox { void reset(String fixture); WorldState snapshot(); } public record CaseReport(String caseId, int successes, int trials, double pass1, double pass8) {} public record EvalReport(ListCaseReport cases) { public double meanPass1() { return cases.stream().mapToDouble(CaseReport::pass1).average().orElse(0.0); } public double meanPass8() { return cases.stream().mapToDouble(CaseReport::pass8).average().orElse(0.0); } } private final SupplierAgent agentFactory; private final Sandbox sandbox; private final ListGrader graders; public EvalRunner(SupplierAgent f, Sandbox s, ListGrader g) { this.agentFactory f; this.sandbox s; this.graders List.copyOf(g); } public EvalReport run(ListEvalCase cases, int repeats) { ListCaseReport out new ArrayList(); for (EvalCase kase : cases) { int successes 0; for (int i 0; i repeats; i) { if (runOnce(kase)) successes; } int k8 Math.min(8, repeats); out.add(new CaseReport(kase.id(), successes, repeats, PassK.passHatK(repeats, successes, 1), PassK.passHatK(repeats, successes, k8))); } return new EvalReport(out); } private boolean runOnce(EvalCase kase) { try { sandbox.reset(kase.sandboxFixture()); // 每次都回到已知初始状态 WorldState before sandbox.snapshot(); Trajectory traj agentFactory.get().run(kase.instruction()); WorldState after sandbox.snapshot(); return graders.stream() .allMatch(g - g.grade(kase, traj, before, after).passed()); } catch (Exception e) { return false; // 异常即失败但绝不能中断整轮评测 } } }把上面这套挂成 CI 门禁就差一个测试class AgentEvalGateTest { Test DisplayName(回归门禁通过率不得低于基线) void regressionGateMustNotDropBelowBaseline() { EvalReport report new EvalRunner(AgentFactory::new, sandbox, List.of(new RuleGrader(), new StateGrader(), judge)) .run(suite.cases(), 8); assertThat(report.meanPass1()).isGreaterThanOrEqualTo(0.60); assertThat(report.meanPass8()).isGreaterThanOrEqualTo(0.25); } }五个工程细节面试里说出来就是加分项断言写状态不写措辞Agent 换种说法不算失败世界状态错了才算。顺序约束只加在真有依赖的地方先读后写必须有序其他用存在性断言。写成固定动作序列会误杀换了条同样正确的路的 Agent。裁判解析失败降级为不通过默认判过是最隐蔽的埋雷方式。异常吞掉记为失败但不中断整轮一个用例崩了不应该让整晚的评测白跑。每次跑之前 reset 沙箱不 reset你的分数就没有可比性趋势图全是噪声。⚠️ 五个避坑清单坑一拿最终回复做文本匹配。Agent 可以留下一地鸡毛后写出漂亮总结。改状态断言。坑二把评测集写成固定动作序列。会误杀走另一条正路的 Agent。只对有真实依赖的步骤加顺序约束。坑三只看接口返回码。全 200 也可能整件事办砸——那个反复调 Tool not found 的案例就是。坑四裁判模型直接当门禁。先跟人工标注算对齐率。位置偏差、长度偏差、自恋偏差都会让它的分数系统性失真。坑五评测跑一次就归档。模型会更新、prompt 会改、流量会漂移。不挂 CI你的评测集三个月后就是废纸。追问连环炮Q1passk 和 pass^k 到底选哪个br看用户有没有重试/挑选的机会。代码补全给你 5 个候选你挑一个pass5 合理客服 Agent 替用户改签机票用户只经历一次必须看 pass^k。一句话能挑看 passk不能挑看 pass^k。Q2为什么不能让裁判模型判所有东西br三个理由成本高、有系统性偏差位置/长度/自恋、结果不稳定。规则能判的死循环、成本超限、schema 校验用代码判零成本且每次一致。裁判只留给真正需要语义理解的部分。Q3Agent 输出自由怎么保证评测能覆盖所有情况br不要试图覆盖所有输出去覆盖所有故障模式。从线上事故和人工复核记录里反向沉淀用例比正向脑补有效得多。业界常见的起点是 30~50 条覆盖关键分支的用例再随事故增长。Q4沙箱和全量复制生产哪个对br都不要全量复制。建最小忠实沙箱保留影响 Agent 决策和副作用的部分——带固定数据的库、会记录意图但不能真转账的支付适配器、能收信的邮件汇、可控的时钟、身份夹具加一个 reset 函数。生产要访问不到。Q5工具调用对了但结果还是错的怎么抓br这正是状态断言存在的意义。工具调用正确不等于业务效果正确——中间可能因为工具实现 bug、环境差异导致结果偏离。必须查终端状态。答题骨架2 分钟版面试官问怎么评测一个 Agent按这四拍说第一拍 · 点破差异Agent 和传统大模型的本质区别不是会思考会调工具是它有行动权、会产生副作用。所以不能用标准化试卷考要像考核一个能出去办事的人一样全程跟踪。第二拍 · 分四层答结果层看任务成功率和终端状态断言可靠性层看 pass^k不是 passk过程层拆规划、工具选择、参数正确率、上下文一致性、失败自愈约束层看安全、业务规则合规和成本效率。第三拍 · 说清怎么判分三种判分器分工——规则判一票否决项状态断言判业务结果裁判模型只判语义且必须与人工标注对齐。沙箱要可重置、要隔离。第四拍 · 落到工程评测集挂 CI 做回归门禁每次改 prompt、加工具、换模型都跑一遍pass^1 / pass^8 掉出基线就卡住合并。加分收尾如果只让我留一句话——单次成功率 60% 的 Agent连做八次全对的概率只有 1.7%。Agent 评测测的不是它能不能做到是它是不是次次都做到。资料展示下面是我整理的AI大模型 学习资料和工具包预览适合收藏后按主题逐步学习