1. 为什么你的 Agent 总是“偶尔成功”
1.1 从一次让人崩溃的演示说起
上个月我帮一个朋友看他做的客服 Agent,演示的时候一切正常,用户问退货政策,它准确调用了知识库检索,回答得滴水不漏。结果第二天上线,第一个真实用户问了个稍微绕一点的问题——“我上周买的东西,包装拆了还能退吗”,Agent 直接开始胡言乱语,把退货政策编了一半,还自信满满地给了个根本不存在的退货地址。
这不是个例。我接触过十几个做 AI Agent 的团队,几乎所有人都经历过这个阶段:Demo 惊艳,上线翻车。你调了十次,成功七八次,觉得“差不多了”,但剩下那两三次失败,在真实场景里就是灾难。因为用户不会给你十次机会,一次错误就足以让他们关掉对话框。
问题的根源在于,大多数人做 Agent 的方式是“手工作坊式”的——改改提示词,跑几个例子,觉得行了就上。没有系统化的评测,没有可重复的验证流程,更没有从失败中自动学习的机制。Agent 的表现就像抛硬币,你永远不知道下一次是正面还是反面。
1.2 评测闭环到底在解决什么问题
所谓“评测闭环”,说白了就是给 Agent 装上一套自动化的体检系统。它要回答三个核心问题:第一,Agent 现在到底行不行?第二,不行的话,是哪个环节出了问题?第三,改完之后,是真的变好了还是只是碰巧?
这三个问题对应着评测闭环的三个关键能力:可量化的评估指标、可定位的失败归因、可对比的迭代验证。没有这三样,你的优化就是盲人摸象——今天觉得提示词加一句“请仔细思考”好像好点了,明天又发现换个模型好像也行,但到底哪个改动真正起了作用,你根本说不清楚。
我见过最离谱的案例是,一个团队花了两个月调 Agent,每次改完就找几个同事“感觉一下”,觉得不错就上线。结果两个月后,他们连自己最初版本的性能基线是多少都忘了。这种“凭感觉优化”的模式,注定走不远。
1.3 谁最需要这套东西
如果你只是做个玩具项目,跑几个例子乐呵乐呵,那确实不需要。但只要你满足以下任意一条,评测闭环就是刚需:
- Agent 要面对真实用户:用户不会容忍随机失败,你需要知道成功率到底是多少。
- 团队多人协作:A 改了提示词,B 换了工具调用逻辑,没有统一评测,根本不知道谁的改动是正向的。
- 需要持续迭代:今天加个新功能,明天换个模型,每次改动都可能引入回归问题。
- 要向老板或客户汇报:你需要拿出数据证明“这周比上周好了 15%”,而不是“我感觉好多了”。
我个人的经验是,当你的 Agent 需要连续运行超过一周,或者需要处理超过 20 种不同类型的用户输入时,就必须上评测闭环了。再晚,你欠下的技术债就会像滚雪球一样,压得你喘不过气。
2. 评测闭环的核心组件拆解
2.1 测试用例集:你的“考卷”决定了 Agent 的上限
评测闭环的第一步,是准备一套高质量的测试用例集。这东西就像给 Agent 出的考卷——考卷出得烂,考出来的分数就没有意义。
我见过很多团队的做法是,随便找几十个问题,跑一遍,看正确率。这有两个致命问题:第一,问题分布不均匀,可能 80% 都是简单问题,Agent 随便答答就 90% 正确率,但真实场景里用户问的全是那 20% 的难题。第二,没有标准答案,或者标准答案太死板,Agent 换个说法就算错,这也不合理。
我的建议是,测试用例集要满足三个条件:
- 覆盖真实分布:从真实用户日志里采样,或者至少模拟真实场景的问题分布。简单、中等、困难的问题比例要合理,一般建议 3:5:2 或者 2:5:3。
- 有明确的判定标准:不是简单的字符串匹配,而是要有语义级别的判定。比如用户问“退货要多久”,Agent 回答“通常 3-5 个工作日”和“大概三到五天”都应该算对。
- 持续更新:每次发现新的失败案例,就把它加进测试集。这样你的“考卷”会越来越贴近真实战场。
我自己的习惯是,维护一个“黄金测试集”,大概 200-500 条,覆盖核心场景和边界情况。每次改动后都跑一遍,确保没有回归。另外还有一个“探索测试集”,专门放那些新发现的、还没搞定的难题,用来指导下一步优化方向。
2.2 评估指标:别只看“对”和“错”
很多人评测 Agent 就只看一个指标:准确率。这太粗糙了。一个 Agent 回答对了,可能是因为它真的理解了,也可能是因为它蒙对了。一个 Agent 回答错了,可能是检索错了,可能是推理错了,也可能是格式错了。你只知道它错了,但不知道错在哪,就没法改。
我通常会把评估拆成几个维度:
| 指标 | 说明 | 为什么重要 |
|---|---|---|
| 任务完成率 | 用户的问题是否被真正解决 | 最核心的指标,但粒度太粗 |
| 工具调用准确率 | 该调工具时调了没,调的对不对 | Agent 的核心能力之一 |
| 检索相关性 | 检索到的内容是否真的有用 | 决定回答质量的上限 |
| 推理链正确性 | 中间推理步骤是否合理 | 影响可解释性和稳定性 |
| 格式合规率 | 输出是否符合预期格式 | 影响下游系统对接 |
| 响应延迟 | 从输入到输出的时间 | 影响用户体验 |
这些指标不是孤立的。比如工具调用准确率低,任务完成率肯定也高不了。但分开看,你就能定位到具体是哪个环节拖了后腿。我一般会先看任务完成率,如果低于预期,再逐个维度排查,找到瓶颈。
2.3 自动化评测流水线:让机器替你干活
手工评测只能应付小规模,一旦测试集超过 100 条,手工跑一遍就要大半天,根本不可持续。你需要一套自动化流水线。
这套流水线的基本流程是:加载测试集 → 逐条运行 Agent → 收集输出 → 自动判定 → 生成报告。听起来简单,但有几个坑要注意:
- 判定逻辑要足够智能:简单的字符串匹配肯定不行。我一般会用“规则判定 + 模型判定”的混合策略。规则判定处理那些有明确答案的,比如“是否调用了搜索工具”;模型判定处理那些开放性的,比如“回答是否合理”。
- 运行环境要隔离:每次评测都要在干净的环境里跑,避免缓存、状态残留影响结果。我习惯用容器化方案,每次评测启动一个全新的实例。
- 报告要可追溯:不仅要看总体分数,还要能下钻到每一条用例。哪条失败了,失败原因是什么,当时的输入输出是什么,都要能查到。
我自己的流水线大概长这样:一个 Python 脚本,读取 YAML 格式的测试集,调用 Agent 的 API,收集结果,然后用一个判定模块打分,最后输出一个 HTML 报告。整个流程跑一遍 200 条用例,大概 10-15 分钟,完全可以接受。
2.4 失败归因:从“错了”到“为什么错”
自动化评测告诉你“错了 15 条”,但不会告诉你“为什么错”。失败归因就是要把这 15 条失败案例,归类到具体的失败模式上。
常见的失败模式有:
- 检索失败:知识库里明明有答案,但没检索到,或者检索到了不相关的内容。
- 推理错误:检索对了,但推理过程中跑偏了,比如把“退货”理解成了“换货”。
- 工具调用错误:该调工具时没调,或者调错了工具,或者参数传错了。
- 格式错误:内容对了,但输出格式不符合要求,比如该输出 JSON 却输出了纯文本。
- 幻觉:完全编造信息,知识库里根本没有。
我一般会先人工看一遍失败案例,给它们打上标签,然后统计各类失败模式的占比。如果检索失败占大头,那就去优化检索策略;如果推理错误多,那就去调提示词或者换更强的模型。归因的粒度越细,优化的方向就越明确。
3. 从零搭建评测闭环的实操步骤
3.1 第一步:定义你的“成功标准”
在写任何代码之前,先想清楚:什么样的 Agent 才算“好”?这个问题听起来简单,但很多人答不上来。
我的做法是,先跟业务方或者自己对齐三个问题:
- 核心任务是什么?比如客服 Agent 的核心任务是“准确回答用户关于退换货、物流、支付的问题”。
- 可接受的失败率是多少?比如“核心任务完成率不低于 90%,工具调用准确率不低于 95%”。
- 哪些错误是绝对不能接受的?比如“绝对不能编造退货地址”或者“绝对不能泄露用户隐私”。
把这些写下来,变成可量化的目标。这样你后续的评测才有方向,优化才有终点。
3.2 第二步:构建测试集(附具体方法)
测试集的来源主要有三个:
- 真实日志:如果 Agent 已经上线,直接从日志里采样。注意要脱敏,去掉用户隐私信息。
- 人工构造:根据业务场景,人工编写各种类型的问题。我一般会按照“正常问题、边界问题、对抗问题”三类来构造。
- 失败案例回流:每次发现新的失败案例,就把它加进测试集。这是最宝贵的来源。
构造测试集时,每条用例至少包含以下字段:
- id: "case_001" input: "我上周买的东西,包装拆了还能退吗?" expected_tools: ["search_knowledge_base"] expected_keywords: ["拆封", "退货", "条件"] category: "退货政策" difficulty: "medium" notes: "测试拆封后的退货政策,注意区分'拆封'和'使用'"这里的关键是expected_tools和expected_keywords。前者用来评测工具调用,后者用来做初步的语义判定。当然,更复杂的判定可以交给模型来做。
我一般会先构造 50-100 条,跑起来,看看效果。然后根据失败情况,有针对性地补充用例。测试集不是一次性的,而是持续演进的。
3.3 第三步:搭建自动化评测脚本
下面是一个简化版的评测脚本框架,用 Python 写的,你可以直接参考:
import yaml import json from agent_client import AgentClient from evaluator import Evaluator def load_test_cases(path): with open(path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) def run_evaluation(test_cases, agent, evaluator): results = [] for case in test_cases: # 运行 Agent output = agent.run(case['input']) # 评测 score = evaluator.evaluate( output=output, expected_tools=case.get('expected_tools', []), expected_keywords=case.get('expected_keywords', []), input_text=case['input'] ) results.append({ 'id': case['id'], 'input': case['input'], 'output': output, 'score': score, 'category': case.get('category', 'unknown') }) return results def generate_report(results): total = len(results) passed = sum(1 for r in results if r['score']['overall'] >= 0.8) print(f"总用例数: {total}") print(f"通过数: {passed}") print(f"通过率: {passed/total*100:.1f}%") # 按类别统计 categories = {} for r in results: cat = r['category'] if cat not in categories: categories[cat] = {'total': 0, 'passed': 0} categories[cat]['total'] += 1 if r['score']['overall'] >= 0.8: categories[cat]['passed'] += 1 print("\n分类通过率:") for cat, stats in categories.items(): rate = stats['passed'] / stats['total'] * 100 print(f" {cat}: {rate:.1f}% ({stats['passed']}/{stats['total']})") # 输出失败案例 print("\n失败案例:") for r in results: if r['score']['overall'] < 0.8: print(f" [{r['id']}] {r['input']}") print(f" 输出: {r['output'][:100]}...") print(f" 得分: {r['score']}") if __name__ == '__main__': test_cases = load_test_cases('test_cases.yaml') agent = AgentClient() evaluator = Evaluator() results = run_evaluation(test_cases, agent, evaluator) generate_report(results) # 保存详细结果 with open('evaluation_results.json', 'w', encoding='utf-8') as f: json.dump(results, f, ensure_ascii=False, indent=2)这个脚本的核心是Evaluator类,它负责判定每条用例的得分。我一般会实现一个多维度打分的 Evaluator,比如:
class Evaluator: def evaluate(self, output, expected_tools, expected_keywords, input_text): scores = {} # 工具调用得分 actual_tools = output.get('tools_called', []) if expected_tools: tool_score = len(set(actual_tools) & set(expected_tools)) / len(expected_tools) else: tool_score = 1.0 if not actual_tools else 0.5 scores['tool'] = tool_score # 关键词得分 if expected_keywords: text = output.get('text', '') keyword_hits = sum(1 for kw in expected_keywords if kw in text) keyword_score = keyword_hits / len(expected_keywords) else: keyword_score = 1.0 scores['keyword'] = keyword_score # 模型判定得分(可选) # 调用一个轻量级模型,判断回答是否合理 # scores['model'] = self.model_judge(input_text, output.get('text', '')) # 综合得分 scores['overall'] = (scores['tool'] * 0.4 + scores['keyword'] * 0.6) return scores这个框架跑起来后,你就能得到一个初步的评测报告。然后根据报告,定位问题,优化 Agent,再跑一遍,对比分数变化。这就是闭环。
3.4 第四步:建立迭代节奏
评测闭环不是跑一次就完了,而是要融入日常开发流程。我的习惯是:
- 每次改动后必跑:不管是改提示词、换模型、还是调工具,只要动了 Agent 的逻辑,就跑一遍完整评测。
- 每周一次全量评测:即使没改动,也跑一次,确保没有环境变化导致的性能波动。
- 每月一次测试集更新:根据最近的失败案例和用户反馈,补充新的测试用例。
这样坚持下来,你会有一个清晰的性能曲线,知道每次改动到底带来了多少提升。数据不会骗人,感觉会。
4. 常见问题与排查技巧实录
4.1 评测结果波动大,今天 85% 明天 75%
这是最常见的问题。原因通常有三个:
- 模型本身的随机性:大模型输出有随机性,同样的输入可能得到不同的输出。解决办法是固定随机种子(如果 API 支持),或者多次运行取平均。我一般会跑 3 次,取平均分。
- 测试集分布不均匀:如果测试集里难题和简单题混在一起,每次随机采样可能导致分布变化。解决办法是全量跑,不要采样。
- 环境不一致:比如检索库更新了,或者工具 API 版本变了。解决办法是锁定环境版本,每次评测前检查依赖。
我踩过最坑的一次是,检索库的索引在后台自动重建,导致评测时检索结果和之前不一样。后来我养成了习惯,评测前先跑一个“环境检查”脚本,确认所有依赖都是预期状态。
4.2 模型判定太贵,跑一次评测要花几百块
用大模型做判定确实贵,尤其是测试集大的时候。我的优化策略是:
- 规则优先:能用规则判定的,绝不用模型。比如工具调用、格式检查、关键词匹配,这些都是确定性的。
- 小模型判定:对于需要语义理解的判定,用一个小模型(比如 7B 级别的)就够了,不需要用最大的模型。
- 抽样判定:如果测试集很大,可以只对“规则判定不确定”的用例调用模型判定。比如规则判定得分在 0.5-0.8 之间的,才需要模型介入。
- 缓存结果:同样的输入和输出,判定结果可以缓存。我一般用 Redis 或者本地文件缓存,避免重复调用。
这样优化下来,一次 200 条用例的评测,成本可以控制在几块钱以内。
4.3 Agent 在评测集上表现很好,但上线后还是翻车
这说明你的测试集没有覆盖真实场景。我见过太多团队,测试集是自己人拍的脑袋想的,跟真实用户问的完全不是一回事。
解决办法只有一个:从真实日志里采样。如果还没上线,那就找一些真实用户来试用,收集他们的输入。如果连真实用户都没有,那就找几个不了解这个 Agent 的人,让他们随便问,你在旁边记录。你会发现,他们问问题的方式跟你想象的完全不一样。
另外,对抗测试也很重要。专门构造一些“刁钻”的问题,比如带错别字的、带歧义的、带多重意图的。这些在真实场景里很常见,但人工构造测试集时很容易忽略。
4.4 失败归因太耗时,人工看不过来
失败归因确实是个体力活,尤其是失败案例多的时候。我的做法是半自动化:
- 自动聚类:用文本聚类算法,把相似的失败案例聚在一起。这样你只需要看每个簇的代表案例,就能了解一类问题。
- 自动打标:训练一个简单的分类器,根据失败案例的输入输出,自动预测失败模式(检索失败、推理错误、格式错误等)。准确率不用太高,70% 就行,剩下的人工复核。
- 优先级排序:根据失败模式的影响面和修复难度,排个优先级。先修那些“影响大、修复容易”的。
我自己的经验是,前 20% 的失败模式,往往导致了 80% 的失败案例。所以不要试图一次性解决所有问题,抓住主要矛盾就行。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 评测分数波动大 | 模型随机性、环境不一致 | 固定种子、多次运行、环境检查 | 锁定版本、取平均分 |
| 评测成本太高 | 模型判定调用过多 | 统计模型判定占比 | 规则优先、小模型、缓存 |
| 评测好但上线差 | 测试集不真实 | 对比测试集和真实日志分布 | 从真实日志采样、对抗测试 |
| 归因耗时 | 失败案例太多 | 统计失败模式分布 | 自动聚类、自动打标、优先级排序 |
| 优化后分数没涨 | 改动没生效或评测不准 | 检查改动是否部署、评测逻辑是否正确 | 确认部署、复核评测逻辑 |
| 某些类别始终差 | 该类问题本身难或测试集偏差 | 分析该类失败案例 | 针对性优化、补充训练数据 |
5. 让闭环真正“闭”起来的关键细节
5.1 评测集要“活”,不能是死水
我见过最可惜的情况是,团队花大力气建了一个测试集,然后就不管了。三个月后,测试集还是那 100 条,但 Agent 已经迭代了十几个版本,真实用户的问题分布早就变了。这样的评测,测出来的分数再高也没意义。
测试集必须持续更新。我的做法是,每次发现线上失败案例,就把它脱敏后加进测试集。同时,每个月 review 一次测试集,把那些已经“太简单”的用例降权或者移除,补充新的难题。这样测试集才能跟上 Agent 的成长。
另外,测试集要分版本管理。每次大改动前,先冻结一个测试集版本,改动后用同一个版本评测,确保对比公平。我一般用 Git 管理测试集,每次评测报告里都记录对应的测试集 commit hash。
5.2 评测要“快”,不能等半天
评测速度直接影响迭代效率。如果跑一次评测要一个小时,那你一天最多迭代两三次。如果跑一次只要五分钟,那你一天可以迭代十几次。
提速的关键在于并行化。测试用例之间是独立的,完全可以并行跑。我一般用 Python 的concurrent.futures或者asyncio来并行调用 Agent。注意控制并发数,别把 API 打挂了。一般 5-10 个并发就够了。
另外,增量评测也很重要。如果只改了检索模块,那就只跑跟检索相关的用例,不用全量跑。我一般会给测试用例打上标签,比如retrieval、reasoning、format,然后根据改动范围选择性地跑。
5.3 评测报告要“可行动”
评测报告不是给老板看的 PPT,而是给你自己看的行动指南。所以报告里最重要的不是总分,而是失败案例的详细信息。
我自己的报告模板大概长这样:
评测时间: 2024-XX-XX 测试集版本: v1.2.3 Agent 版本: v0.8.1 总体通过率: 82.5% (165/200) 较上次: +3.5% 分类通过率: 退货政策: 90% (45/50) 物流查询: 85% (34/40) 支付问题: 75% (30/40) 其他: 80% (56/70) 失败案例 Top 5: 1. [case_023] "我买的东西还没发货,能改地址吗?" 失败原因: 检索到了退货政策,但没检索到改地址流程 建议: 补充改地址相关的知识库条目 2. [case_067] "退款到账要多久?" 失败原因: 工具调用正确,但回答中编造了具体天数 建议: 在提示词中强调"不确定的信息不要编造" ...这样的报告,你一看就知道下一步该干什么。好的评测报告,本身就是一份优化任务清单。
5.4 把评测融入 CI/CD
如果你的 Agent 是持续部署的,那评测应该融入 CI/CD 流程。每次代码提交,自动触发评测,如果通过率低于阈值,就阻止合并。
我一般会在 CI 里配置两个阶段:
- 快速评测:只跑核心的 50 条用例,5 分钟内出结果。如果通过率低于基线,直接 fail。
- 全量评测:跑完整的 200 条用例,15 分钟内出结果。生成详细报告,供人工 review。
这样既能保证快速反馈,又能保证全面覆盖。阈值怎么定?我一般设成“不低于上次全量评测的 95%”。比如上次是 85%,那这次快速评测至少要 80.75% 才通过。这样既能容忍正常波动,又能及时发现严重退化。
5.5 一个容易被忽略的细节:评测数据的版本管理
评测数据本身也需要版本管理。我见过团队因为测试集被误改,导致评测结果不可比,白白浪费了一周的优化。
我的做法是:
- 测试集用 Git 管理:每次修改都有 commit 记录,可以追溯。
- 评测结果存档:每次评测的详细结果都保存下来,跟测试集版本和 Agent 版本关联。
- 基线管理:维护一个“基线版本”,每次大改动前,先跑一遍基线,确认当前性能。改动后,再跑一遍,对比差异。
这样即使过了几个月,你也能清楚地知道,某个版本的 Agent 在某个版本的测试集上,表现到底如何。可追溯性,是工程化的基础。
6. 从“偶尔成功”到“稳定可用”的实战心得
6.1 先修“致命错误”,再修“体验问题”
评测报告里,失败案例分两种:一种是“致命错误”,比如编造信息、泄露隐私、完全答非所问;另一种是“体验问题”,比如回答不够详细、语气不够友好、格式不够美观。
我的原则是,先修致命错误,再修体验问题。因为致命错误会导致用户直接流失,而体验问题只是让用户不爽。资源有限的情况下,优先解决致命错误。
怎么判断哪些是致命错误?我一般会问自己:如果这个错误发生在真实用户身上,他会不会直接关掉对话框?如果答案是“会”,那就是致命错误。
6.2 提示词优化要“小步快跑”
很多人优化提示词,喜欢一次性改一大段,然后跑评测,看分数涨没涨。涨了还好,跌了根本不知道是哪句话改坏了。
我的做法是,每次只改一个点。比如这次只加一句“不确定的信息不要编造”,跑评测,看幻觉率有没有下降。下次只改检索的 top_k 参数,看检索相关性有没有提升。这样每次改动的影响都是可归因的。
另外,保留每次改动的版本。我一般用 Git 管理提示词,每次改动都有 commit message,说明改了什么、为什么改、评测结果如何。这样即使改坏了,也能快速回滚。
6.3 不要迷信“更大更强的模型”
我见过团队一遇到问题就想换模型,觉得换个更大的模型就能解决。但实测下来,很多问题不是模型能力不够,而是提示词没写好、检索没做好、工具没调对。
换模型确实能带来提升,但成本也高。我的建议是,先把提示词、检索、工具调用这些“免费”的优化做到极致,再考虑换模型。很多时候,你把检索的 top_k 从 3 调到 5,或者把提示词里的示例从 2 个加到 5 个,效果比换个贵十倍的模型还好。
当然,如果评测数据显示,失败案例主要集中在“推理能力不足”上,而且提示词优化已经到瓶颈了,那该换模型就换。但一定要用数据说话,不要凭感觉。
6.4 建立“失败案例库”,让每次失败都有价值
每次失败都是宝贵的。我习惯把每个失败案例都记录下来,包括输入、输出、失败原因、修复方案。时间长了,这就成了一个“失败案例库”。
这个库有三个用途:第一,作为测试集的补充来源;第二,作为新人的培训材料;第三,作为优化方向的参考。我经常翻这个库,看看哪些失败模式反复出现,哪些修复方案真正有效。
失败不可怕,可怕的是同样的失败反复发生。有了失败案例库,你就能确保每次失败都让你离“稳定可用”更近一步。
6.5 最后的体会
做 Agent 评测闭环这件事,最大的挑战不是技术,而是耐心。它不像调提示词那样立竿见影,也不像换模型那样简单粗暴。它需要你静下心来,一条一条看失败案例,一个一个定位问题,一次一次验证改动。
但只要你坚持下来,你会发现,Agent 的表现从“偶尔成功”变成了“稳定可用”。你不再需要祈祷这次演示别翻车,因为你知道,评测数据已经证明了它的可靠性。
我自己的经验是,从开始搭建评测闭环,到 Agent 真正稳定可用,大概需要 2-3 个月的持续迭代。这期间,你会经历分数波动、优化瓶颈、甚至偶尔的倒退。但只要方向对了,每一步都算数。
最后分享一个小技巧:每次评测后,把失败案例的截图或者日志发到团队群里。不是为了追责,而是为了让所有人都能看到 Agent 的真实表现。当整个团队都对失败案例有感知时,优化就不再是你一个人的事,而是所有人的共同目标。这种“全员评测”的文化,比任何工具都管用。