RAG设计之评估系统
文章目录
没有测试数据的性能优化就是放屁。靠脑补做的优化99%都是无效的。 这是我们开会时,大领导们的评价。我觉得说的没毛病🥹
最终沉淀的评估系统是:后端 + CLI、CSV/XLSX 金标集、自动匿名 A/B,不采用人工盲审系统
1 系统总览图
flowchart TD A["CSV/XLSX 金标集"] --> B["dataset_loader.py<br/>加载与校验"] CLI["CLI: scripts/run_eval.py"] --> B API["API: /eval/*"] --> SVC["service.py<br/>评估持久化服务"] SVC --> B B --> E["evaluator.py<br/>评估编排"] E --> R["retrieval_runner.py<br/>检索运行器"] R --> V2["baseline<br/>search_v2.Dealer"] R --> V3["current<br/>search_v3.Dealer"] V2 --> ES["Elasticsearch<br/>知识库索引"] V3 --> ES E --> ANS["answer_runner.py<br/>可选答案生成"] E --> M["metrics.py<br/>指标计算"] E --> AB["blind_ab.py<br/>匿名 A/B 映射"] M --> REP["report.py<br/>summary/detail 报告"] AB --> REP REP --> OUT["JSON / CSV / XLSX"] SVC --> PG["PostgreSQL<br/>eval_datasets / eval_runs / eval_results"] REP --> PG
第一版评估系统是这样设计的:
- 数据入口是 CSV/XLSX
- 运行入口是 CLI 或 API,核心编排集中在
evaluator.py - 检索复用现有 RAG 链路,报告由
report.py统一生成,API 模式下还会把数据集、运行记录和结果写入 PostgreSQL。
2.系统背景与目标
现有系统已经具备文档解析、分块入库、 ES 混合检索、可选 rerank、上下文增强生成、SSE 流式回答和引用展示能力。RAG 链路长,任何一层的小改动都可能影响最终回答:
- 分词词典调整可能提升游戏领域术语召回,也可能引入噪声。
- 同义词扩展可能解决玩家别名问题,也可能把无关实体扩进查询。
- 召回参数变化可能增加命中文档,也可能降低排序质量。
- rerank 模型或阈值变化可能改善首屏相关性,也可能误删 ES 已召回的有效片段。
- Prompt、上下文拼接和模型参数变化可能影响答案完整性、事实一致性和引用质量。
因此只靠人工体验、单条问题调试或线上主观反馈,无法稳定回答以下问题:
- 本次改动是否比上一个稳定版本更好。
- 变好的是召回、排序、答案,还是引用。
- 变差的问题集中在哪类查询、文档、术语或答案类型。
- 当前版本能否进入演示、压测、面试复刻或后续上线验证。
评估系统的第一阶段目标不是追求一次性覆盖所有线上复杂场景,而是建立一个小而稳的工程基线:
- 使用人工维护的 CSV/XLSX 金标集描述问题、标准答案、应命中文档或 chunk。
- 通过 CLI 调用后端评估服务,批量运行 baseline 与 current。
- 输出检索、rerank、答案、引用、成本和耗时等多层指标。
- 生成可读报告,定位每条 case 的通过、退化、改进和原因。
- 为后续人工盲审、线上日志采样、自动回归门禁预留数据结构。
2.1 RAG 的质量不等于模型质量
普通问答系统常把质量问题归因于大模型,但 RAG 系统的输出由多个环节共同决定:
- 文档是否被正确解析。
- chunk 是否保留了完整语义和页码坐标。
- 查询是否被正确分词、扩展和向量化。
- 检索是否能召回包含答案的片段。
- rerank 是否把正确片段排到前面。
- 上下文拼接是否覆盖答案依据且不过度冗长。
- 模型是否基于上下文回答,而不是自由发挥。
- 引用是否指向支撑答案的真实位置。
没有评估系统时,最终答案错误只能说明“系统错了”,无法知道错在哪一层。
评估系统要把长链路拆成可观测的短链路,让每次优化都有证据。
2.2 防止局部优化伤害整体质量
RAG 调优常见的问题是局部指标改善但整体体验下降。例如:
- Recall@20 提升,但 nDCG@10 下降,说明正确片段虽然进来了,却被排得太靠后。
- rerank 后 Top1 命中率提升,但引用覆盖率下降,说明答案看似更准,但引用证据不足。
- 同义词扩展让召回更多,但答案幻觉增加,说明噪声上下文干扰了生成。
评估系统需要同时看检索、排序、答案和引用,避免只优化一个数字。
2.3 支撑可复现的工程决策
当前项目包含词典、召回、rerank、parser 和流式问答等多条链路。未来改动会越来越多,如果没有固定数据集、固定配置和固定报告格式,团队只能依赖记忆和临时截图判断质量。
评估系统需要提供:
- 稳定数据集:同一批 case 可反复运行。
- 稳定配置:记录模型、参数、索引版本和代码版本。
- 稳定输出:每次运行都能比较整体指标和逐条 case。
- 稳定结论:支持“可上线”“需回滚”“只在某类问题上启用”等决策。
3. RAG 质量问题定位图
把所有的环节都拆开,就是这样的短链路

RAG 之所以复杂就是链路很容易做的很长,所以拆解成小链路,把“最终回答不好”拆成可定位的工程层,每层都有对应指标和修复动作,避免把所有问题都归因于大模型。
4. 为什么用CSV/XLSX 金标集、
第一版数据集我优先使用 CSV/XLSX 人工金标集,而不是直接用线上日志。

4.1 金标集有明确答案和证据
评估指标需要知道“什么是对的”。线上日志通常只有用户问题和系统回答,不一定有:
- 标准答案。
- 正确文档 ID。
- 正确 chunk ID。
- 应引用页码。
- 查询意图分类。
- 答案完整性要求。
- 不应回答或应拒答的边界。
没有这些标注,只能做粗略分析,不能稳定计算 Recall@K、MRR、nDCG、答案正确性和引用准确性。
4.2 金标集更适合和业务共建
项目中有游戏领域 RAG、文档问答:
- 产品、研发、运营、策划、市场都能编辑。
- 可以人工补充领域术语、别名、标准答案和证据位置。
- 可以按主题、难度、文档类型、问题类型做筛选。
- 可以随项目文档一起版本化。
这比直接解析线上日志更适合第一版快速建立可信测试集。
4.3 线上日志存在隐私、噪声和偏差
线上日志有价值,但直接作为第一版主数据源有风险:
- 可能包含用户隐私、账号、上传文件名或业务敏感信息。
- 用户问题质量不稳定,包含闲聊、误操作、重复输入和无答案问题。
- 线上流量分布受当天用户、场景影响,不稳定。
- 日志中的系统回答可能来自旧版本,不能天然当作标准答案。
第一版应先用金标集建立“能稳定测试的系统”,再把线上日志作为后续增量发现 case 的来源。
5. 如何进行测试
第一版采用的是程序化的自动匿名 A/B,而不是人工双盲测试。 对同一批 case 分别运行 baseline 与 current,并在报告中用 A/B 匿名标识展示结果。
5.1 自动 A/B 成本低、频率高
人工双盲适合主观质量判断,但成本高、周期长、样本有限。RAG 开发阶段需要高频验证:
- 改了词典后要马上知道召回是否退化。
- 改了 rerank 后要马上知道排序是否提升。
- 改了 prompt 后要马上知道引用和幻觉是否变差。
自动 A/B 可以在每次改动后运行,适合作为日常回归。
5.2 匿名标识降低版本偏见
报告中不直接展示“baseline/current 哪个更好”,而是先以 variant_a、variant_b 展示逐条结果,最后再揭示映射关系。这样做可以减少评审时的确认偏误:
- 不因为“新版本应该更好”而放过退化。
- 不因为“旧版本稳定”而忽视新版本提升。
- 便于后续人工评审复用同一套匿名样本。
5.3 程序自动化能覆盖更多工程问题
人工双盲很难高效判断 Recall@20、MRR、nDCG、引用页码和 chunk 命中等工程指标。自动 A/B 可以直接比较:
- 正确 chunk 是否进入 TopK。
- 排名是否前移。
- rerank 前后是否误删证据。
- 引用是否覆盖标准证据。
- 耗时和 token 成本是否变化。
这些指标是 RAG 调优的早期核心。
5.4 未来优化
人工双盲并不被否定。它适合评估:
- 答案表达是否自然。
- 多证据综合是否充分。
- 用户是否觉得可信。
- 两个正确答案中哪个更有帮助。
但人工盲审应该建立在自动评估已稳定的基础上,优先处理自动指标无法判断的主观体验问题。
5.5 自动匿名 A/B 流程图
flowchart TD D["同一份金标集"] --> B["baseline 运行"] D --> C["current 运行"] B --> RB["baseline 结果"] C --> RC["current 结果"] RB --> RAND["固定 seed 随机映射"] RC --> RAND RAND --> VA["variant_a"] RAND --> VB["variant_b"] VA --> J["匿名指标比较"] VB --> J J --> W["逐条 case 胜负或持平"] W --> OPEN["揭盲"] OPEN --> SUM["current_win_rate<br/>baseline_win_rate<br/>退化 case 列表"]
自动匿名 A/B 的关键是同一条 case 同时跑 baseline 和 current,再随机映射成 variant_a 和 variant_b。报告先基于匿名结果比较,最后再揭盲汇总,减少“新版本一定更好”或“旧版本更稳”的主观偏见。
6. 数据集设计
6.0 金标集构建流程图
flowchart TD Q["业务问题收集"] --> F["筛选高频/核心/边界问题"] F --> A["人工标注标准答案"] A --> E["绑定证据<br/>doc / chunk / page"] E --> T["补充标签<br/>tags / case_type"] T --> V["校验字段完整性"] V --> X["CSV/XLSX 固化"] X --> L["dataset_loader.py 加载"] L --> RUN["离线评估运行"] RUN --> BAD["低分 case 回流"] BAD --> A
金标集不是简单的问题列表,而是带标准答案、证据和标签的集合。
低召回、低引用、低答案覆盖的 case 会回流到标注阶段,用来修正证据、补标签或扩充难例集。
6.1 推荐字段
CSV/XLSX 金标集建议包含以下字段:
| 字段 | 必填 | 说明 |
|---|---|---|
case_id |
是 | 稳定唯一 ID,便于跨版本对比。 |
question |
是 | 用户问题。 |
expected_answer |
是 | 标准答案或答案要点。 |
expected_doc_ids |
否 | 期望命中的文档 ID,可多个。 |
expected_chunk_ids |
否 | 期望命中的 chunk ID,可多个。 |
expected_pages |
否 | 期望引用页码。 |
expected_terms |
否 | 期望分词命中的领域词。 |
expected_synonyms |
否 | 期望同义词或别名扩展。 |
question_type |
否 | 事实查询、步骤查询、对比查询、总结查询、无答案查询等。 |
domain |
否 | 业务域,例如通用文档、游戏资料、财报、药品资料。 |
difficulty |
否 | easy、medium、hard。 |
must_cite |
否 | 是否必须有引用。 |
notes |
否 | 标注说明。 |
6.2 数据集分层
建议把金标集分为三类:
smoke:少量核心 case,适合提交前快速运行。regression:稳定回归集,覆盖主要文档和问题类型。challenge:挑战集,包含别名、跨段答案、多文档综合、无答案、引用边界等。
这样可以让开发阶段快速反馈,同时保留更完整的质量评估。
标签分层统计流转图
flowchart LR CASE["case_001<br/>tags=职业|技能|高频"] --> DETAIL["detail row<br/>recall_at_k / mrr / ndcg"] DETAIL --> ALL["summary.metrics<br/>整体均值"] DETAIL --> T1["summary.by_tag.职业"] DETAIL --> T2["summary.by_tag.技能"] DETAIL --> T3["summary.by_tag.高频"] DETAIL --> CT["summary.by_case_type.retrieval"]
这里的标签不是互斥分类,而是多个观察维度。同一条样本可以同时进入多个 summary.by_tag 分组,这样总分稳定时也能看出某个业务主题或问题类型是否退化。
6.3 标注原则
金标集不应只写标准答案,还应尽量写证据:
- 检索评估依赖
expected_chunk_ids、expected_doc_ids或expected_pages。 - 答案评估依赖
expected_answer或答案要点。 - 引用评估依赖页码、坐标、chunk 或原文片段。
- 分词和同义词评估依赖领域词、别名和扩展期望。
如果某条 case 只有标准答案、没有证据,虽然可用于答案评估,但不能用于严格检索和引用评估。
7. 评估链路拆分
评估系统应把 RAG 拆成多个层级分别评估:
- 分词评估。
- 同义词评估。
- 召回评估。
- rerank 评估。
- 答案评估。
- 引用评估。
- 性能和成本评估。
7.0 单条 case 评估流程图
flowchart TD CASE["EvalCase<br/>query + gold_* + tags"] --> MAP["生成 A/B 映射"] CASE --> BASE["baseline: search_v2.Dealer"] CASE --> CUR["current: search_v3.Dealer"] BASE --> BR["baseline TopK chunks"] CUR --> CR["current TopK chunks"] BR --> MB["计算 baseline 指标"] CR --> MC["计算 current 指标"] BR --> AB["可选 answer_runner.py"] CR --> AC["可选 answer_runner.py"] AB --> MB AC --> MC MB --> DA["detail row: variant_a 或 variant_b"] MC --> DB["detail row: variant_a 或 variant_b"] DA --> REP["report.py 汇总"] DB --> REP
单条 case 的核心原则是“同题、同金标、同配置”对比两个版本。答案生成是可选步骤,默认可以只评检索和排序,避免每次离线回归都产生额外模型成本。
7.1 为什么要拆开评估
这样不是很麻烦吗?确实很😖
但是,错误的答案可能来自不同环节:
- 分词错:查询没有识别领域词,后续召回自然变差。
- 同义词错:用户别名没有扩展到官方名称。
- 召回错:正确 chunk 没进入候选集。
- rerank 错:正确 chunk 被排到后面或被过滤。
- 答案错:上下文已有答案,但模型没有正确生成。
- 引用错:答案正确,但引用指向错误页码或无关片段。
如果只看最终答案,就无法知道该修词典、检索、rerank、prompt 还是引用拼接。拆开评估能让每个指标对应明确工程动作。
7.2 分层报告示例
每条 case 的报告建议包含:
- 查询分析:分词结果、同义词扩展、向量查询文本。
- 召回结果:TopK chunk、分数、命中情况。
- rerank 结果:rerank 前后排名变化。
- 生成结果:答案文本、使用上下文、模型参数。
- 引用结果:引用 chunk、页码、坐标、证据覆盖。
- 指标结果:该 case 的各项分数和是否退化。
当前实现会在整体报告中输出三层汇总:
summary.metrics:所有 detail row 的整体均值。summary.by_tag:按tags分组后的指标均值。summary.by_case_type:按case_type分组后的指标均值。
多标签样本会同时进入多个标签分组。例如一条样本的 tags 为 职业|技能|高频,则它会同时参与 职业、技能、高频 三个分组统计。
这是特意设计的,原因上面也说了:标签表示多个观察维度,而不是互斥分类。
报告结构图
flowchart TD R["Evaluation Report"] --> S["summary"] R --> D["detail"] S --> M["metrics<br/>整体指标"] S --> BT["by_tag<br/>标签分层"] S --> BC["by_case_type<br/>类型分层"] S --> BA["blind_ab<br/>揭盲胜率"] D --> ROW["case detail row"] ROW --> Q["query / case_type / tags"] ROW --> RET["retrieval chunks"] ROW --> SCORE["recall_at_k / mrr / ndcg_at_k"] ROW --> ERR["retrieval_error / answer_error"]
读报告时先看 summary.metrics 判断整体是否达标,再看 summary.by_tag 和 summary.by_case_type 找局部短板。最后进入 detail 定位具体 case 的召回、排序、答案或引用问题。
示例:
|
|
这样可以避免总分掩盖局部短板。例如总 Recall@K 看起来稳定,但 活动、同义词、拒答 标签下明显下降,就能直接定位下一轮应优化文档更新、别名扩写或拒答 prompt。
8. 指标体系与设计依据
RAG 优化,在诡异的一点就是: 你以为优化了,实际效果不一定好,就很离谱…
所以要搞清楚不同的代码改动后,如何衡量效果好坏,为此搞了如下很多的指标评测:
8.1 分词指标
领域词命中率
决策依据:游戏术语、专有名词等领域词如果被切碎,BM25 和混合检索都会受影响。
解决的问题:判断领域词典是否覆盖关键问法。
适用场景:
- 新增或修改
rag/tokenizers/相关策略。 - 新增游戏领域词典或业务词典。
- 排查某类术语查不到的问题。
局限:
- 命中领域词不代表检索一定正确。
- 对纯向量召回的影响不如对词法召回直接。
- 需要金标集中维护
expected_terms。
查询切分稳定性
决策依据:同一类查询在不同版本中切分结果大幅变化,可能导致召回波动。
解决的问题:发现分词升级或词典调整带来的非预期变化。
适用场景:
- 比较 baseline/current 的分词差异。
- 判断某次词典更新是否过度影响通用问题。
局限:
- 稳定不等于正确。
- 合理的策略升级可能故意改变切分结果,需要结合召回指标判断。
8.2 同义词指标
同义词扩展命中率
决策依据:玩家常用口语、缩写和别名不一定出现在原文中,同义词扩展能把用户问法映射到文档术语。
解决的问题:判断别名表是否覆盖高频问法。
适用场景:
- 玩家叫法、系统简称、业务缩写较多的领域。
- 文档使用正式名称、用户使用口语名称的场景。
局限:
- 扩展过多会增加噪声。
- 同义词存在上下文歧义,同一个词在不同业务域可能含义不同。
- 需要结合召回和答案指标判断扩展是否真正有效。
错误扩展率
决策依据:同义词不只看召回提升,还要看是否引入无关实体。
解决的问题:发现“一扩就错”的高风险同义词。
适用场景:
- 多义词较多的领域。
- 新增大批自动挖掘同义词后。
局限:
- 需要人工维护负例或通过结果间接判断。
- 某些扩展是否错误依赖上下文,自动判断难度高。
8.3 召回指标
Recall@K
决策依据:RAG 的生成质量以检索候选为上限。正确证据没有进入 TopK,上游再强也无法基于正确上下文回答。
解决的问题:判断正确文档或 chunk 是否被召回。
适用场景:
- 调整 ES 查询、向量召回、混合检索权重。
- 比较不同 chunk 策略。
- 验证领域词典和同义词是否提升召回覆盖。
局限:
- 只看是否进入 TopK,不关心排第 1 还是第 K。
- K 取值不同结论可能不同。
- 依赖金标证据标注,若 expected chunk 不完整,会低估召回。
Hit@K
决策依据:某些 case 只需要至少一个正确证据进入候选集,Hit@K 比 Recall@K 更容易解释。
解决的问题:判断“有没有至少命中一个可回答片段”。
适用场景:
- 单答案事实查询。
- 早期粗粒度召回验收。
局限:
- 多证据问题只命中一个也算通过,可能高估质量。
- 不反映排序。
Precision@K
决策依据:召回越多不一定越好,TopK 中无关片段太多会污染上下文。
解决的问题:判断候选集噪声比例。
适用场景:
- 上下文窗口有限。
- 检索召回很宽、rerank 压力较大。
局限:
- RAG 更关心正确证据是否被包含,Precision@K 不能单独作为主指标。
- 对多标准答案和相关性等级的标注要求更高。
8.4 排序指标
MRR
决策依据:用户和生成模型更容易受前排证据影响。第一个正确证据越靠前,系统越可能生成正确答案。
解决的问题:衡量首个正确结果的位置。
适用场景:
- 单一核心证据问题。
- 比较 rerank 前后是否把正确片段前移。
- 检查 Top1 不稳定但 TopK 有命中的情况。
局限:
- 只关注第一个正确结果,不关心后续多个正确证据。
- 对需要多段综合的问题不够充分。
nDCG@K
决策依据:RAG 不只需要一个正确片段,很多问题需要多个证据,并且证据相关性有强弱。nDCG 能同时考虑相关性等级和排名位置。
解决的问题:衡量 TopK 排序整体质量。
适用场景:
- 多证据综合、对比、总结类问题。
- 有强相关、弱相关、无关等级标注的金标集。
- rerank 模型评估。
局限:
- 需要相关性等级标注,标注成本高于二分类命中。
- 对标注不完整的 case 较敏感。
Rerank Delta
决策依据:rerank 的价值不应只看最终结果,还要看它对 ES 初召回排序的改变。
解决的问题:判断 rerank 是提升排序、无明显作用,还是误伤有效结果。
适用场景:
- 引入或替换 rerank 服务。
- 调整 rerank 阈值、TopN、降级策略。
- 验证 rerank 服务不可用时的降级行为。
局限:
- rerank delta 是诊断指标,不是用户价值指标。
- 如果初召回本身很差,rerank 很难补救。
8.5 答案指标
答案正确性
决策依据:最终用户看到的是答案,检索命中只是前提。
解决的问题:判断回答是否覆盖标准答案要点。
适用场景:
- 事实问答、步骤问答、规则说明。
- Prompt 或模型参数变更后。
局限:
- 自动判断可能需要 LLM-as-judge,存在评审模型偏差。
- 对开放式总结问题,标准答案可能不唯一。
完整性
决策依据:答案可能部分正确但遗漏关键条件、例外或步骤。
解决的问题:区分“答到一点”和“完整回答”。
适用场景:
- 流程、政策、配置说明。
- 多点答案和多证据问题。
局限:
- 需要标准答案拆成要点。
- 自动评分需要较清晰的 rubrics。
忠实性
决策依据:RAG 系统应基于检索上下文回答,不能编造上下文不存在的信息。
解决的问题:发现幻觉和过度推断。
适用场景:
- 生成模型、prompt、上下文拼接策略调整。
- 无答案问题和低召回问题。
局限:
- 忠实于上下文不代表答案符合真实世界。
- 如果上下文本身错误,忠实性指标会误判。
拒答正确性
决策依据:当文档中没有答案时,系统应说明未找到依据,而不是编造。
解决的问题:评估无答案问题的边界控制。
适用场景:
- 恶意用户想搞事情,或者恶意聊天,注入等高风险场景。
- 检索低置信度策略验证。
局限:
- 需要金标集中明确哪些问题无答案。
- 过度拒答会损害可用性,需要与答案正确性平衡。
8.6 引用指标
引用命中率
决策依据:引用是用户信任 RAG 的关键。答案正确但引用错误,会降低可核验性。
解决的问题:判断引用是否指向标准文档、chunk 或页码。
适用场景:
- PDF 页码和坐标引用。
- 文档问答演示。
- 需要用户回查原文的场景。
局限:
- 标注成本较高。
- 某些答案可由多个位置支持,金标不完整会低估引用质量。
引用支撑率
决策依据:引用不仅要命中位置,还要真的支撑答案中的关键断言。
解决的问题:发现“挂了引用但引用不支持答案”的问题。
适用场景:
- 答案包含多个结论。
- 模型可能混合多个上下文片段生成。
局限:
- 自动判断较难,可能需要 LLM-as-judge 或人工复核。
- 对长答案需要拆分断言。
坐标准确性
决策依据:系统支持 PDF 解析和坐标引用,坐标错误会影响用户定位证据。
解决的问题:判断引用是否能落到正确页码和区域。
适用场景:
- 表格、图片、版面结构解析。
- PDF 原文定位展示。
局限:
- 需要标准坐标或人工可接受范围。
- 对纯文本、DOCX、TXT 不一定适用。
8.7 性能和成本指标
延迟
决策依据:RAG 质量不能只看准确率,用户也关心响应速度。
解决的问题:发现 rerank、LLM、长上下文或复杂解析导致的性能退化。
适用场景:
- baseline/current 对比。
- 模型、rerank、TopK、上下文长度调整。
局限:
- 本地环境和线上环境差异较大。
- 网络波动会影响模型调用耗时。
Token 和调用成本
决策依据:更长上下文和更强模型可能提升质量,但成本也会上升。
解决的问题:衡量质量提升是否值得成本增加。
适用场景:
- 比较不同模型或上下文策略。
- 评估是否需要压缩上下文。
局限:
- 成本指标必须和质量指标一起看,不能单独优化。
文章作者 沐桢