ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

AI Agent评测的主动式验证与可信度工程实践

2026/9/9 1:28:01 拓冰建站 浏览量
AI Agent评测的主动式验证与可信度工程实践 我参与的最近一个AI Agent项目在灰度评估时出了大问题内部评测集上93.6%的准确率ROUGE分数也足够漂亮可上线后用户满意度反而下跌了。翻看对话日志才发现测试集里全是如何申请退款这种标准问法真实用户说的是我上周买的东西有点问题想退了但是找不到订单里面的按钮。这件事让我开始怀疑整个评测体系的根基——不是模型不够强而是评测方法本身一直在被动接题。于是我把重点从怎么把分数刷高转向怎么让评测结果本身可信做了几个月的主动式验证与评测可信度工程实践。这篇文章没有教科书式的理论推导全是我在这条路上踩出来的方法、代码和教训适合正在做大模型应用评测、Agent评测或者对评测结果可复现性有要求的同行参考。1. 从一次高分翻车说起为什么被动式验证不够用1.1 那次让我怀疑评测体系的灰度事故当时我们做的是一个智能客服Agent内部有一套看起来很完善的离线评测机制3124条FAQ问答对按意图分类均匀采样覆盖了售前、售中、售后三个大场景。评测指标走的是准确率ROUGE-L双线每周跑一次连续三个月分数稳定在93%上下当时的结论是模型表现优秀可以放量。结果灰度第一周就出了事故。未解决率比旧版系统高出8个百分点用户满意度1.9分5分制质检人工抽检发现大量答非所问——不是模型不懂问题而是用户的问题方式和测试集差得太远了。比如测试集里的问题是问发货后多久能到 答普通地区3-5个工作日偏远地区7-10个工作日。真实用户会这样问问我上周四拍的东西现在都一周了还没到你们是不是忘记发货了订单号是20241108xxxx。比较发现真实对话里至少有三种情况是测试集完全覆盖不到的一是长句夹杂着情绪和无关信息二是用户会在多轮对话里忽然回到之前的问题三是用户频繁出现错别字和口语缩略表达。这些在标准测试集里几乎不存在所以模型在离线环境下看起来很强一上真实场景就崩了。1.2 被动式验证的三个先天盲区我把传统评测方式梳理了一遍发现它的问题不是样本量不够而是被动这两个字决定了上限。所谓的被动式验证核心模式是评测方准备一批固定的题然后一次性抛给被测系统等系统给出答案后逐题打分。这套模式有三个天生的盲区。第一个盲区是测试集静态化。测试用例一旦固定团队就会在迭代中不断针对这套题目调优模型对这套题的表现会越来越好但这个好是拟合测试集的结果不是真实能力的提升。我在团队里做过一个实验把同一批测试集连续跑六周每周记录模型分数模型没有任何代码改动但分数从91.2%涨到了93.8%。原因很简单——评测链路里用的few-shot示例被调整过几次调试过程本身就是对测试集的隐性拟合。第二个盲区是逐题独立测不到状态管理能力。Agent类系统最大的特点是有状态多轮对话里的记忆、工具调用后的结果缓存、任务中断后的恢复这些都是动态过程。传统评测把每条case当成独立实验每条之间完全隔离模型每次都是失忆状态开始根本测不出长期记忆和上下文管理的问题。第三个盲区是只看输出不看过程。一个答对的case可能是因为模型真的理解也可能是因为候选答案里的关键词恰好命中了模板。两种路径结果一样但可靠性天差地别。被动式评测只有最终答案没有过程记录自然也无法区分这两种情况。1.3 我理解的主动式验证是什么从那次灰度事故之后我开始调整评测思路评测系统不能只做发卷子、收卷子的考务角色它应该像面试官一样根据被测系统的回答、状态和行为动态地调整问题。这就是我理解的主动式验证——评测系统主动构建任务场景、主动注入干扰变量、主动追溯系统内部状态而不是坐在那里等着被测系统给出答案。主动式验证和被动式评测的分水岭在于谁来驱动评测被动式评测里驱动方是固定的测试集主动式验证里驱动方是评测系统本身它会根据被测系统的历史表现、状态变化和实时反馈决定下一步探测什么。后面我详细展开这三个驱动方向——任务构造、扰动注入、过程追溯每一步都有对应的落地方法和代码。2. 主动式验证的三板斧任务构造、扰动注入、过程追溯2.1 第一板斧基于弱点反向构造任务主动式验证的第一步是不再满足于我有什么题就测什么而是被测系统哪里弱我就专门造什么题去试它。这需要一套反馈链路从真实用户对话、线上日志、历史失败case中提取弱点信号然后把它们转化成评测任务。我们当时设计了一套弱点驱动的测试集扩张机制。具体来说每周从线上日志里挑出200条人工标记为不满意的真实对话用大模型把其中用户的提问改写成不同的口语变体改掉敏感信息后再人工审核一遍格式和标签合入测试集。这里有个关键点不是简单地把真实对话丢进测试集而是要改写——因为真实对话里包含时间、订单号等动态信息直接使用会造成输入分布偏移模型可能记住这些具体值。改写策略我在代码里做成了一套模板REWRITE_PROMPT 你是数据增强工程师。请将下面的用户问题改写为5种口语化变体保留原意图不得省略任何关键约束条件。 要求 1. 至少2个变体包含口语化表达如咋、搞半天、整 2. 至少1个变体包含无关的抱怨情绪 3. 至少1个变体包含错别字 4. 不改变任务类型和约束条件 输入{question} 输出JSON数组每个元素包含question和variant_type两个字段。 这套机制跑了一个月后测试集从原来的3124条扩到了接近6400条新增部分几乎全是真实场景里能难住模型的问法。团队明显感觉到同样的模型在扩大的测试集上分数从93.6%降到了87.9%但这个87.9%比原来那个93.6%可信得多。2.2 第二板斧执行过程中的主动扰动注入任务构造解决的是测什么的问题但Agent系统的很多缺陷是在执行过程中暴露的——对话进行到一半突然插入无关信息、工具调用迟迟不返回、用户忽然改口。这些动态干扰在传统评测里无法模拟因为传统评测全程只有提问-回答两步。为此我在评测框架里加了一层扰动注入器Perturbation Injector负责在Agent执行任务的过程中按照预设的时序向会话里注入事件。常见的扰动类型有以下几类扰动类型具体做法考察能力上下文污染在关键问题前插入3-5条无关闲聊上下文过滤与关键信息提取信息遮蔽把核心信息订单号、日期藏在长段废话中间长文本关键信息定位变卦/改口让用户中途改变需求再追问新的信息状态更新与记忆覆盖工具异常注入工具返回超时、空数据、格式错误异常处理与恢复策略静默期让用户长时间不回复后重新出现会话状态保持以信息遮蔽为例我们的Agent会收到类似这样的注入用户你们客服电话打了三次没人接我那个订单啊是双十一那天下的当时有满减活动 我买了两件毛衣和一条围巾后来觉得围巾颜色不合适想换但是页面上的售后入口一直 点不了订单号你等一下我看一下啊……是20241111开头的那笔等等我找找…… 嗯JD2024111138271对就是这个帮我看看现在能退吗这种输入在传统测试集里几乎不会出现但在真实场景中非常常见。加入扰动注入后Agent的判断能力问题暴露得非常快它经常被前面的情绪和抱怨带偏给出安抚性的话术而不是执行查询操作。2.3 第三板斧从只看答案到追踪全过程主动式验证的第三板斧是评测系统必须有能力记录和分析被测Agent的完整执行轨迹。我在设计评测输出格式时把每条评测case的记录从输入-输出-分数三元组扩展成了一棵结构化的执行树包括用户输入序列、Agent每一步推理摘要、工具调用参数与返回结果、中间状态快照、最终回复、思考延迟等。为什么要做这么重因为只有拿到过程数据你才能定位结果错了到底是哪一步出的问题。举一个实际案例有一次Agent在评测中答错了任务完成度的问题只看最终输出会觉得是模型理解能力不行但调出执行轨迹后发现模型其实理解对了是工具调用时写错了参数名导致查询接口返回了错误数据Agent拿错误数据硬着头皮继续编了个答案。这两类问题一个是NLU层面的一个是工具调用层的修复方式完全不同。dataclass class ExecutionRecord: case_id: str input_seq: list[dict] # 每次用户输入 tool_calls: list[dict] # 每次工具调用name, args, result, latency state_snapshots: list[dict] # 每轮执行后的关键状态 final_answer: str latency_ms: int token_count: int这套记录结构看似简单实际落地时最大的坑是记录本身改变了被测系统的行为。如果在被测代码里插入记录逻辑等于加了额外逻辑跑出来的行为和线上不一致。我的解决方案是把记录逻辑放在网关层通过日志采集的方式异步记录而不是在推理过程中同步写库。3. 可信度工程的三根柱子数据可信、过程可信、结论可信3.1 数据可信评测集也需要代码评审和版本管理很多团队对评测集的态度非常随意——谁有空谁加几条格式不统一标签口径模糊。这个问题在评测体系规模小的时候不明显一旦评测集涨到几千上万条数据的可信度就直接决定了评测结果的可信度。我的定义是如果一条测试数据被污染了那么基于它算出来的所有指标都是不可信的。数据可信的第一个问题是数据泄露。大模型的训练数据里很可能包含公开的评测集如果你用一个已经传遍全网的评测集来评估新模型分数虚高是一定的。我在评测环境里专门做了一次泄露检测用模型生成的结果跟评测集里的标准答案做相似度比对发现确实有少量case的生成内容几乎原文命中标准答案这种case直接踢出测试集。第二个问题是标签漂移。同一类问题三个月前标注为投诉分类三个月后新标注的人标成了售后咨询。对策是把测试集当成代码一样管理每条case必须有id、版本号、创建人、修改记录测试集的变更必须走PR评审任何人不允许直接往master分支上加case。下面是我用的数据schemaTEST_CASE_SCHEMA { case_id: TC-2024-00123, version: 3, task_type: multi_turn_refund, input_seq: [], expected: {...}, difficulty: hard, source: online_log_20241108, labels: [口语化, 长文本, 信息遮蔽], created_by: zhang.li, reviewed_by: wang.fang, changelog: [2024-11-10 修正期望答案中的订单格式说明] }3.2 过程可信让评测结果能100%复现评测结果不可复现是另一个常见问题同一个模型昨天跑是87分今天跑变成84分没人知道是模型变了还是评测环境变了。做可信度工程的过程中我发现影响评测结果的因素远比你想象得多。这些变量包括但不限于模型版本大模型服务商经常悄无声息地升级、temperature和top_p参数、提示词内容哪怕多一个空格、few-shot示例的排列顺序、向量数据库的版本和分块参数、随机种子、系统时间有些提示词里带了日期、甚至并发线程数对输出顺序的影响。我的解法是给每次评测生成一个环境指纹Environment Fingerprint跑评测之前先把这些关键信息全部hash进一个字符串存到评测报告里。再次跑的时候先比指纹指纹不一致时先不要看分数先查环境差异。def gen_env_fingerprint(): env_info { model: client.model_version, # 模型版本 prompt_hash: sha256(prompt_text), # 提示词全文hash temperature: config.temperature, max_tokens: config.max_tokens, vector_db_hash: get_collection_hash(), code_version: git_head_sha(), } return sha256(json.dumps(env_info, sort_keysTrue))这招最大的价值不是技术层面的而是管理层面的有了环境指纹你可以在团队内部立一条规矩——没有指纹的报告不参与讨论。这是让评测结论可信的第一道门槛。3.3 结论可信不要用单次分数做决策即使数据可信、过程可复现还有一个统计陷阱绕不过去单次评测的分数本身有波动性。大模型生成有随机性哪怕temperature设为0不同批次跑的分数也会有1-2个点的差异。拿单次分数做回归决策很容易被噪声带偏。我在评测报告里引入了几条硬性规则指标至少跑3次报告展示的是均值和95%置信区间不是单次值。对比两个版本模型时使用配对样本的Wilcoxon符号秩检验p值小于0.05才认为有显著差异。不做全量平均的单点决策而是先分桶看每个桶内部的指标变化。分桶这一点极其重要。有一次我们的总指标涨了0.4个点团队准备上线分桶一看才发现单轮对话桶涨了1.2%多轮对话桶反而跌了1.5%。总分是涨了没错但多轮能力实际在退步全被单轮桶的涨幅掩盖了。如果不是分桶分析这次回归就要被当成稳定提升上线了。4. 用pytest搭一套带探针的评测流水线4.1 为什么选pytest而不是专用评测框架当时市面上已经有一些评测框架比如LangSmith、Promptfoo、DeepEval等。我都试过最后选了pytest作为评测框架的底座原因很简单团队对pytest最熟悉它天然支持fixture、参数化、并发执行、断言失败收集而且能直接接CI。评测和测试本质上是同一件事——都是断言系统在某种条件下应该达到某个预期状态。pytest的fixture机制很适合管理Agent会话的生命周期参数化机制适合做大批量的case驱动而它的插件生态可以让我们拿到结构化的测试报告。至于那些评测框架它们的问题在于绑定太深换被测系统时要改很多东西。4.2 核心抽象探针Probe、会话Session和状态快照Snapshot我的评测流水线围绕三个核心抽象设计Probe探针一类评测意图的封装比如测多轮记忆是一个探针测上下文污染恢复是另一个探针。每个探针负责准备输入、执行评测、上报结果。Session会话一次与被测Agent的完整交互过程。Session管理消息序列、状态缓存、工具调用记录是所有探针执行的基础上下文。Snapshot状态快照在指定的时间点抓取被测系统的内部状态用于前后对比判断系统是否做出了预期的状态更新。目录结构大概是这样的eval_project/ ├── conftest.py # fixture注册 ├── probes/ │ ├── memory_probe.py │ ├── context_pollution_probe.py │ └── tool_recovery_probe.py ├── cases/ │ ├── test_multi_turn_memory.py │ ├── test_context_robustness.py │ └── test_tool_error_recovery.py ├── scoring/ │ ├── rule_based.py │ └── llm_judge.py └── report/ ├── collect_results.py └── gen_env_fingerprint.py4.3 关键代码探针注入、状态快照和评分聚合下面是我实际在用的核心代码骨架做了简化。先看conftest里的fixtureimport pytest import asyncio pytest.fixture def agent_session(): 创建一个被测Agent会话并挂载状态快照采集器 session AgentSession( endpointhttp://localhost:8001/chat, modelgpt-4o-mini, temperature0, ) # 插入快照采集钩子 snapshotter StateSnapshotter(session) yield session asyncio.run(session.close())再看一个具体的探针用例——上下文污染恢复def build_pollution_prompt(core_question, distraction_sentences): 构造带污染信息的用户输入 return .join(distraction_sentences) core_question def test_context_pollution_recovery(agent_session): 在关键问题前注入无关信息验证Agent能否准确提取核心意图 core 我的订单JD2024111138271需要退款 pollutions [ 你们那个机器人是不是真的客服啊, 我刚才打了电话没人接, 天气越来越冷了, 对了还有件事我想起来了, ] polluted_input build_pollution_prompt(core, pollutions) response agent_session.send(polluted_input) # 断言1回复中必须包含订单号 assert JD2024111138271 in response.answer, Agent未能捕获关键订单号 # 断言2回复意图必须是退款处理而不是闲聊安抚 assert response.intent refund, f意图识别错误: {response.intent} # 断言3不能使用请提供订单号这类话术说明没找到订单号 assert 请提供订单号 not in response.answer, Agent未从污染上下文中提取到已有订单号再来看状态快照对比的用法。这个探针专门测多轮对话中用户修改需求后Agent是否正确地更新了记忆状态def test_state_update_after_requirement_change(agent_session): snapshotter agent_session.snapshotter # 第一轮用户要求查物流 r1 agent_session.send(帮我看看JD2024111138271这个订单到哪了) snap1 snapshotter.capture() assert snap1.get(order_id) JD2024111138271 assert query in snap1.get(last_tool, []) # 第二轮用户改口说要退款不要查物流 r2 agent_session.send(算了不查了直接帮我退款吧) snap2 snapshotter.capture() assert snap2.get(order_id) JD2024111138271 assert snap2.get(intent) refund assert snap2.get(last_tool) refund_apply如果没有快照机制你只能看到最终回答无法判断Agent在第二轮是否真的执行了退款操作还是只是嘴上报了好的已为您退款但实际上啥也没干。快照能确认工具调用是真的发生过。最后是评分聚合。我的原则是规则优先规则覆盖不到的地方才用LLM-as-judgedef score_response(response, expected, use_llm_judgeFalse): 混合评分能用规则判定的不交给LLM score 0.0 # 规则线关键信息覆盖度 for required_info in expected.required_fields: if required_info in response.answer: score 0.5 # 规则线禁止话术检查 for forbidden_phrase in expected.forbidden_phrases: if forbidden_phrase in response.answer: score - 1.0 # LLM线语义是否答非所问仅当规则线分数在中间区间才启用 if use_llm_judge and -0.5 score 1.5: judge_score llm_judge(response.answer, expected.criteria) score 0.5 * score 0.5 * judge_score return max(0.0, min(1.0, score))4.4 pytest跑批的几个实操细节跑批时有几个细节值得提一下。第一千万要控制并发度。Agent系统通常有速率限制并发太高会导致大量429错误然后你会误以为模型变笨了。我在pytest里调整了并发策略同一时间只跑8个测试线程超出部分排队。第二fixture的作用域要注意。Agent会话如果设计成function级每条case都会新建会话多轮状态类测试就没法做如果设计成session级case之间又会互相污染。我的解法是设计两个维度的fixture短生命周期会话给单轮测试用长生命周期会话给多轮状态测试用互相隔离。第三超时处理必须严格。Agent系统有时会卡住不设超时的话一个case能把整个评测流程拖死。我给每条测试设了60秒超时超时的case记录为超时错误而不是答案错误两类问题分开统计因为修复方式完全不同。5. 指标不能照搬Agent评测要重新定义好标准5.1 单轮评测指标用在Agent身上会出什么问题传统大模型评测的核心指标是准确率、BLEU、ROUGE最多加一个基于嵌入的语义相似度。这些指标设计的前提是一轮问答一个标准答案对Agent这种多轮、带工具调用、带状态变体的系统照搬这些指标会出现系统性偏差。我用一个客服Agent的case举例。用户问我的订单被退回了我该怎么办标准答案是您的订单因地址不完整被退回请联系客服修改地址。模型生成的回答是您的订单被退回了请联系客服。——按ROUGE-L算这两个句子的分数可能只有0.3因为漏了关键细节但按是否完成了用户诉求来算这个回答勉强算部分完成。反过来一个长答案把标准答案的所有词都嵌进去了ROUGE-L可能高达0.8但人类判断会觉得它冗余、重点不突出。评价维度单轮问答评测Agent评测核心问题答案是否正确任务是否完成过程不关心工具调用是否有效状态无状态状态是否更新正确恢复能力不关心出错后能否恢复效率不关心是否用最少的步骤完成5.2 Agent评测我实际在用的指标集跑通主动式验证之后我沉淀了一套针对Agent场景的指标集目前团队每周都在跑。这套指标不追求面面俱到只求每项都能反向定位到具体问题。任务完成率Task Completion Rate核心指标判断Agent是否达成了用户原始诉求由规则人工抽检混合判定。工具调用有效率Tool Call Effectiveness有效工具调用次数/总工具调用次数衡量Agent是否在做无用功。无效调用率Useless Call Rate调用了一个工具但结果完全没用上常见于先查了一堆数据但回答时没有引用。失败恢复率Failure Recovery Rate工具超时或返回错误后Agent能否在后续步骤中纠正并完成任务。关键信息覆盖率KII Coverage回复中是否包含订单号、时间、金额等所有关键信息。多轮记忆保持率Memory Retention Rate第N轮提问时Agent是否正确记得第N-1轮提供的约束条件。其中多轮记忆保持率这个指标是最有价值的它验证了主动式验证的威力——没有多轮探针设计这个指标根本测不出来。5.3 用LLM当裁判先给裁判做校准LLM-as-judge是个能省人力的方案但直接拿一个LLM给另一个LLM打分水很深。我在实践中发现三个典型问题。第一是位置偏好。同一个回答放在第一个参评位置和第二个参评位置得分可能差5-8分。对策是打分时把候选回答的顺序随机打乱每个case跑两次取平均。第二是自夸偏好。用GPT-4当裁判去评判GPT-4的队友它会倾向于给它认为说得更顺的答案高分而这个顺和对经常不是一回事。我在评测集里手工标了一批带错误但语言华丽的反例专门用来测裁判是不是真的在判断正确性。第三是裁判对不做不该做的事不敏感。客服Agent如果自动给用户发了一堆营销信息语言很流畅LLM裁判可能给高分但业务规则上这是严重违规。对策是规则线先行先把禁止行为用程序断言拦下来拦截不到的再交给LLM。6. 让评测结论被团队信任量化证据与人工抽检的配合6.1 一份有说服力的评测报告应该包含什么评测报告写不好再好的评测体系也没人信。我发现最容易被团队质疑的不是分数本身而是这个分数为什么不一致以及这个分数到底代不代表线上效果。所以我现在每份评测报告强制包含以下区块环境指纹明确记录模型版本、提示词hash、代码版本、依赖版本。总体指标分桶指标分桶维度按任务类型、对话轮次、输入长度三个维度交叉。失败case清单按严重程度排序每条附上执行轨迹和失败截图。人工抽检结果随机抽50条case标注人机一致或人机不一致抽检一致率本身作为报告的一部分。与上一版本对比的显著性检验结果。这份模板看起来繁重但它的价值在于任何一个结论都能被追溯。当有人问这个分数为什么比上周低了1.2%时你不需要拍脑袋解释只需要翻报告里的分桶数据就能定位变化来自哪个桶然后从失败case清单里找到具体案例。6.2 用留存集防止团队把评测集练熟任何固定测试集都会被团队隐性拟合。除了前面提到的定期扩充测试集之外我还在评测体系里加了一个留存集机制从每月的测试集更新中随机抽取10%的case放进一个独立的留存集任何人不允许查看留存集的标注答案只能拿它做最终的盲测。留存集的使用场景有两个一个是新模型上线前的最终把关用留存集跑一遍如果留存集上表现明显差于主测试集说明主测试集已经被练熟了。另一个是评测体系本身的健康度监控——如果主测试集分数持续上涨但留存集分数原地踏步这不是进步是过拟合。这个机制在我们团队里起过实际作用有一次主测试集分数涨了3.6%大家都在庆祝留存集一跑只涨了0.4%最后结论是模型能力没有明显提升只是拟合了测试集。这个案例直接让团队建立了对留存集的信任。6.3 环境依赖管理是最后一道防线最后想分享一个经常被忽略但极其影响可信度的细节评测环境的依赖管理。做过AI评测的人应该都有过这种经历——同样一份代码换台机器跑分数就不一样。我遇到的真实案例是同事A的机器上评测某个Agent工具调用成功率的得分是0.86换到同事B的机器上变成了0.73。查了半天发现两台机器上的向量数据库版本不同一个是v0.4.2一个是v0.5.1分块算法和默认的embedding模型都变了导致Agent检索到的上下文完全不同。这类问题如果不用环境指纹固定所有依赖版本差距也会被误判成模型效果退步。我的做法是评测环境完全容器化依赖锁文件lock file纳入版本控制向量数据库用固定镜像版本运行。评测流水线在CI和本地的唯一差异只剩下硬件性能而性能对结果的影响通过固定随机种子和温度参数基本可控。做到这一步之后同一个评测跑两次的分数波动基本稳定在1个百分点以内这个置信度已经足够支撑日常决策。从最开始那个高分翻车的灰度事故到现在我最大的体会是评测体系本身也是一种工程产品需要维护它的数据质量、过程可复现性和结论的可解释性。主动式验证解决了测什么和怎么测的问题可信度工程解决了结果是否值得信的问题两者缺一不可。这套体系跑了大半年虽然维护成本不低但每次模型上线前的决策都变得有底气得多。如果你也在跟Agent评测较劲我建议第一步不是去选一个更花哨的评测框架而是先给现有的评测流程画一张可信度清单——哪些环节可能让结果失真哪些环节可能让结果无法复现把这些问题一个个补上评测结果会自己变可信。