知识库AgentRAG 检索增强RAG 检索增强RAG 评估与问题诊断RAG 评估与问题诊断
02 · RAG 检索增强RAG 检索增强
Roadmap 02进阶Markdown78 min

RAG 评估与问题诊断RAG 评估与问题诊断

分层评估召回、排序、上下文、忠实度与安全,掌握 Recall@K、MRR、nDCG 和 LLM-as-judge 的工程边界。分层评估召回、排序、上下文、忠实度与安全,掌握 Recall@K、MRR、nDCG 和 LLM-as-judge 的工程边界。

#EvaluationEvaluation#Information RetrievalInformation Retrieval#FaithfulnessFaithfulness#LLM-as-JudgeLLM-as-Judge更新于 2026-08-16

专题导读

RAG 的“回答不好”至少可能来自五类故障:证据未入库、检索未召回、排序或装配丢失、模型没有忠实使用证据、最终答案不满足用户任务。只给最终答案打一个总分,既无法定位,也容易把生成模型的语言流畅度误当成检索质量。

评估应建立可重放的分层协议:固定问题、相关证据、期望答案或断言、权限上下文和完整版本快照;分别度量初始召回、融合、重排、最终上下文、生成结果与安全边界。线上点击和点赞可补充真实行为信号,但不能替代事实、权限与忠实度审计。

知识地图

TEXT
评测样本:问题 + 权限 + 时间点 + 相关证据 + 答案/断言 + 期望行为
  └─ 索引快照:parser / chunker / embedding / indexVersion
      └─ 初始召回:Hit@K / Recall@K / Precision@K
          └─ 融合与重排:MRR / MAP / nDCG@K
              └─ 最终上下文:证据覆盖 / context precision / 引用映射
                  └─ 生成:faithfulness / relevance / completeness / correctness
                      └─ 安全与产品:ACL 泄漏 / 注入 / 拒答 / 延迟 / 成本
                          └─ 门禁:切片、成对比较、置信区间、人工复核

评估不是一次性跑分,而是版本管理、失败归因和发布决策系统。

面试题

Q1:为什么 RAG 必须分层评估?

核心回答: 最终回答是索引、检索、装配和生成共同作用的结果。一个总分不能说明正确证据在哪一步丢失,也不能给出下一步优化方向。

推荐至少保存四个候选快照:

  1. 初始召回:BM25、向量等各路 top-K;
  2. 融合后:RRF 或分数融合结果;
  3. 重排后:reranker 的候选与名次;
  4. 最终上下文:真正进入 prompt 的 chunk。

若初始 Recall@K 低,应检查解析、chunking、query、embedding 或过滤;初始高而重排后低,应检查 reranker;重排高而最终上下文缺证据,应检查去重、parent 扩展和 token 预算;上下文完整而答案出现无依据断言,才主要是生成忠实度问题。

此外,安全和成本是独立维度。回答看似正确但使用无权文档,属于严重失败;质量提高 0.2% 却使 P99 和成本翻倍,也未必可上线。

面试追问:可以只测端到端正确率吗?

可以作为最终产品指标,但不能作为唯一指标。端到端指标负责验收,分层指标负责归因,两者都需要。

常见错误回答: “让另一个大模型给答案打分即可。”这既没有检索诊断,也没有权限、版本和裁判可靠性保证。

Q2:如何构建可靠的 RAG 评测集?

核心回答: 每条样本至少包含问题、时间点、身份/租户、相关证据集合或判定规则、期望答案要点、可接受变体、是否应拒答,以及数据与系统版本。样本分布要贴近真实流量,同时覆盖风险边界和长尾。

来源可包括脱敏线上问题、客服/运维案例、领域专家编写、日志失败回放和受控合成。合成样本适合扩充格式和边界,但会继承生成模型偏差,不能完全替代真实问题与专家标注。相关性标注应说明粒度:相关的是文档、段落还是 chunk;是任一权威证据足够,还是多条证据都必须命中。

评测集应按高频、长尾、无答案、歧义、多跳、最新内容、精确标识符、多语言、权限边界和提示注入切片。开发集用于调参,隔离 holdout 用于最终报告;长期反复观察 holdout 也会产生过拟合,应定期轮换或增加盲测。

面试追问:文档更新后金标怎么办?

金标必须带有效时间和源修订。更新后重标、迁移到新证据 ID,或将旧样本标为失效;不能用过期答案惩罚正确的新系统。

常见错误回答: “从文档随机生成问题就代表真实用户。”随机生成通常偏向文档措辞、单跳问题和易检索片段,无法代表真实表达与失败分布。

Q3:Recall@K 与 Hit@K 有什么区别?

核心回答: 对问题 q,设所有相关证据集合为 Rel(q),前 K 个结果集合为 TopK(q)

Recall@K(q) = |Rel(q) ∩ TopK(q)| / |Rel(q)|

它衡量相关证据被覆盖的比例。若只判断前 K 是否至少命中一条相关证据,则是 Hit@K(也常称 Success@K):

Hit@K(q) = 1,若 Rel(q) ∩ TopK(q) ≠ ∅;否则为 0

例如一个问题有 4 条必要相关证据,top-5 命中 1 条:Recall@5 是 1/4=0.25,Hit@5 是 1。把“至少命中一条”叫 Recall@K 会掩盖多证据问题的覆盖不足。

当问题只需要任一等价权威证据时,Hit@K 更贴合成功定义;当回答必须组合多个事实时,应使用 Recall@K 或额外定义 all-required coverage。前提是 Rel(q) 尽可能完整,否则未标注但实际相关的结果会被误判。

面试追问:Recall@K 高是否表示排序好?

不表示。所有相关证据都可能挤在 K 的尾部。需要结合 MRR、nDCG 或其他排序指标。

常见错误回答: “Recall@K 就是 top-K 里有没有正确答案。”这实际描述的是 Hit@K。

Q4:Precision@K、Context Precision 与召回如何权衡?

核心回答: Precision@K = |Rel ∩ TopK| / K,表示前 K 中相关结果的比例。Recall 关注“别漏”,Precision 关注“别带太多噪声”。扩大 K 往往提高或不降低 Recall,却可能降低 Precision、增加 rerank 与生成成本。

在 RAG 中应区分检索候选的 Precision@K 与最终上下文的 context precision。候选阶段允许较宽,以给 reranker 留空间;最终上下文预算有限,应更强调必要证据密度。若相关性是分级的,简单二元 Precision 会损失“高度相关”和“略有帮助”的差异,可配合 nDCG。

Precision@K 的分母通常固定为 K,但当系统实际返回少于 K 条时,协议必须说明按 K 还是实际返回数计算。选择不同会影响无结果查询的解释,团队应固定实现并在报告中注明。

面试追问:为什么不能只追求 Recall@100?

Recall@100 只说明证据在大候选池中,可能完全进不了 rerank 或 prompt;还会掩盖排序差与高成本。

常见错误回答: “上下文越多,模型越容易找到答案。”噪声、冲突和长上下文会降低利用率并增加延迟。

Q5:MRR 衡量什么?它不适合哪些场景?

核心回答: MRR(Mean Reciprocal Rank)关注每个问题第一个相关结果的位置。对 N 个问题:

MRR = (1/N) × Σ 1/rankᵢ

若某问题没有相关结果,倒数排名记为 0。第一个相关结果在第 1、2、10 位时,贡献分别为 1、0.5、0.1。

MRR 适合“找到第一条正确答案就够”的导航、FAQ 或单答案检索。它完全忽略第一个相关结果之后的其他相关证据:对需要多个证据的比较、多跳或综述问题,MRR 不能反映覆盖和整体排序,应结合 Recall@K、MAP 或 nDCG。

MRR 还依赖二元相关性。如果第 1 位“略相关”、第 2 位“高度相关”,MRR 可能只看第 1 位;此时分级相关性的 nDCG 更合适。

面试追问:MRR 从 0.5 提升到 0.55 是否显著?

不能只看均值。应做逐问题成对比较,用 bootstrap 置信区间或合适的配对检验,并查看关键切片和回归样本。

常见错误回答: “MRR 会评估所有相关文档的平均排名。”它只使用第一个相关结果的排名。

Q6:nDCG@K 如何计算?为什么需要分级相关性?

核心回答: nDCG 用位置折损衡量前 K 个结果的分级相关性,并除以理想排序归一化。常见定义为:

DCG@K = Σᵢ₌₁ᴷ (2^relᵢ - 1) / log₂(i + 1)

nDCG@K = DCG@K / IDCG@K

relᵢ 是第 i 个结果的相关等级,IDCG@K 是把同一批相关等级按最优顺序排列得到的 DCG。若 IDCG@K=0,该问题没有已标注相关结果;应在协议中明确记 0、排除或单独归类,不能静默选择。

指数增益让高相关等级更有价值,对数折损让头部错误代价更大。例如相关等级 [3,0,2][0,3,2] 命中集合相同、Recall 相同,但前者把高度相关证据放在首位,nDCG 更高。

相关等级必须有标注规范,例如 3=直接完整支持,2=支持关键部分,1=背景相关,0=无关。若标注员对等级理解不一致,精密公式也不会产生可靠结论。

面试追问:为什么 nDCG 通常在 0 到 1?

在非负相关等级、按同一候选相关性构造理想排序的标准设定下,DCG 不超过 IDCG。自定义负增益或不一致 IDCG 会破坏该性质。

常见错误回答: “nDCG 就是给排名靠前的结果更大权重。”这只说了折损,漏掉分级增益和理想排序归一化。

Q7:如何判断问题出在召回、融合、重排还是上下文装配?

核心回答: 对同一金标证据,在每个阶段计算位置和留存状态,并建立淘汰漏斗。至少记录:retrievedBy、原始 rank/score、融合 rank、rerank rank、是否扩展/去重、是否进入 prompt、丢弃原因。

典型诊断矩阵:

现象 优先检查
各路 Recall@K 都低 解析、chunking、query、embedding、analyzer、ACL 误过滤
单路高、融合后低 RRF 身份映射、每路 K、权重/常数、重复候选
融合高、rerank 后低 reranker 输入截断、模型版本、文本拼接、超时降级
rerank 高、最终上下文低 parent 扩展、去重误杀、来源配额、token 预算
上下文证据完整、答案错 生成指令、faithfulness、推理、答案格式或模型能力

评估日志必须版本化,否则候选模型和基线可能读取不同索引或 ACL 快照,比较失效。还要区分“相关证据被安全过滤”与“ACL 规则错误”:前者是正确行为,后者才是召回缺陷。

面试追问:只存最终 prompt 能诊断吗?

只能判断模型看到了什么,无法知道正确 chunk 在上游何时、为何丢失。

常见错误回答: “Recall 低就调大 top-K。”这可能掩盖解析缺失、版本错误或 ACL 误配置,并增加成本。

Q8:Faithfulness(忠实度)准确衡量什么,不衡量什么?

核心回答: Faithfulness 衡量回答中的原子断言是否能由给定上下文支持。它不证明上下文本身是真实或权威的,也不等于答案相关、完整或符合用户任务。

一种可解释方法是先把回答拆成可验证原子断言,再逐条标注为 supported、contradicted、not-enough-information,最后计算支持断言比例。协议必须说明不同断言是否加权,以及引用只支持部分复合句时如何处理。

边界示例:上下文错误地写“Java 21 移除了 G1”,回答照抄该句,faithfulness 可能很高,但事实正确性很低;上下文正确介绍 G1,回答只复述无关背景,faithfulness 高但 answer relevance 低;回答遗漏关键限制,忠实但不完整。

因此至少分开报告:

  • Faithfulness/groundedness:断言是否有上下文依据;
  • Correctness:是否符合可信金标或现实事实;
  • Relevance:是否回答用户问题;
  • Completeness:必要要点是否覆盖;
  • Citation correctness:引用是否真的支持对应断言。

面试追问:模型没有引用的常识该算不忠实吗?

取决于产品契约。严格闭卷 RAG 应要求所有外部可验证断言有证据;允许常识的产品需明确豁免范围,不能在评分时临时决定。

常见错误回答: “Faithfulness 高就说明答案正确。”它只说明答案与给定上下文一致。

Q9:引用质量应该如何评估?

核心回答: 既评估引用与断言的对应关系,也评估应引用断言是否被覆盖。可以定义 citation precision 为“给出的引用中,真正支持相邻断言的比例”,citation recall 为“需要证据的断言中,有有效支持引用的比例”。

引用 URL 可打开不代表支持答案。评估应落到实际 sourceId/chunkId/sourceRevision,核对引用文本是否进入最终上下文、是否支持对应断言、是否来自用户有权访问且在该时间点有效的版本。多个引用堆在段尾会产生归属歧义,最好让服务端保存 assertion-to-source 映射。

引用评估还要识别:来源存在但内容不支持、只支持复合断言的一部分、引用过期版本、模型编造 sourceId、以及引用了召回但未进入 prompt 的文档。

面试追问:点击率能代表引用正确吗?

不能。点击受位置、标题和用户习惯影响,最多是行为信号;正确性仍需内容核验或人工抽样。

常见错误回答: “回答末尾列出三个真实链接就算有引用。”真实链接不等于与具体断言存在支持关系。

Q10:LLM-as-judge 适合做什么?边界和偏差是什么?

核心回答: LLM-as-judge 适合大规模初筛、开放文本 rubric 评分和候选间成对比较,但它是有误差、会漂移的测量工具,不是客观事实裁判。高风险结论必须用人工金标、规则或可执行验证校准。

主要风险包括:

  • 位置偏差:成对比较偏好先出现或后出现的候选;
  • 冗长/风格偏差:把长、流畅、自信误判为更正确;
  • 自家模型偏差:偏好与裁判相似的表述或模型族;
  • 提示敏感:rubric、示例与字段顺序改变结果;
  • 非确定性:同一样本重复评分不一致;
  • 知识与泄漏:裁判可能用自身先验而不是给定上下文,或评测样本进入训练数据;
  • 注入风险:候选答案或检索文档中的文本可能试图操纵裁判。

工程协议应固定 judgeModel/judgePrompt/rubric 版本,候选匿名化并随机交换顺序,要求结构化理由与逐项评分,重复抽样估计一致性,定期与人工标注计算一致率、混淆矩阵或相关性。分歧、高风险和边界样本升级给人审。

面试追问:裁判模型比被测模型更大就可靠吗?

不足以保证。更大模型仍有偏差和知识盲区;可靠性来自明确 rubric、校准集、盲测、一致性监控与人工复核。

常见错误回答: “temperature 设为 0 就完全可复现、无偏。”供应商实现和模型版本仍可能变化,确定性也不消除系统偏差。

Q11:离线发布门禁怎样避免被平均分误导?

核心回答: 对同一问题做基线与候选的成对比较,报告总体指标、关键切片、差值分布、回归案例和不确定性。平均值通过不代表每个高风险切片通过。

门禁可包括:核心 Recall@K/nDCG 不退化、无答案与拒答达到阈值、ACL 泄漏为零容忍、faithfulness 和引用指标达标、P95/P99 与单位请求成本不超预算。具体阈值来自业务风险与历史基线,不能照搬通用数字。

Bootstrap 可从问题集合有放回采样,多次计算“候选减基线”的指标差,得到经验置信区间。它不修复有偏样本,也不能把不显著解释为两者等价;样本独立性、分层流量和多重比较都需考虑。小样本或高风险发布应结合专家审查和线上渐进实验。

每次运行固定并保存:数据快照、索引/embedding/retriever/reranker/prompt/生成模型/judge 版本与参数。否则差异无法归因。

面试追问:候选总体 nDCG 提升,但无答案幻觉率上升怎么办?

若无答案是硬门禁,则拒绝发布或仅对安全切片灰度。不能用平均排序收益抵消关键安全回归。

常见错误回答: “新版本总分更高就全量上线。”聚合分会隐藏权限、语言、租户和长尾回归。

Q12:线上应该监控什么?如何把线上信号反馈到离线评测?

核心回答: 线上同时监控质量代理、系统指标、安全事件和版本分布,并把失败样本经脱敏和审核后沉淀为新的评测切片。

系统指标包括各阶段 P50/P95/P99、超时、降级率、候选数量、token、embedding/rerank/生成成本、缓存命中和拒答率。质量代理可包括来源点击、追问/改写、人工纠错、会话放弃和用户反馈,但这些都可能有选择偏差,不能直接当正确率。

安全指标包括无权候选、跨租户缓存命中、提示注入检测与工具越权尝试;关键是验证无权正文不出现在召回结果、rerank 输入、trace、缓存、最终 prompt 和引用任一层。日志与 trace 本身也要最小化敏感信息。

发现线上失败后保存可重放包:脱敏 query、身份类别、时间、各阶段候选 ID/名次、版本快照、最终证据和输出。经领域专家标注后加入开发集;有代表性的重大事故样本进入长期回归集,避免只修单个提示词。

面试追问:A/B 测试能替代离线评测吗?

不能。A/B 适合产品行为差异,但低频安全事故和事实错误可能难以从点击观察;上线前仍需离线门禁,上线后渐进放量和审计。

常见错误回答: “用户点赞率高就说明 RAG 正确。”用户可能偏好流畅错误答案,且只有部分用户会反馈。

Java 21 完整示例:实现 Recall@K、MRR 与 nDCG@K

下面示例使用 Java 21 标准库,实现单问题指标与宏平均。相关等级 grade > 0 视为 Recall/MRR 的相关;nDCG 保留等级差异。生产评测还应校验重复结果、样本版本和标注完整性。

JAVA
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.ToDoubleFunction;

public final class RagMetricsExample {

    public record EvaluationCase(
            String queryId,
            List<String> rankedChunkIds,
            Map<String, Integer> relevanceGrades) {
        public EvaluationCase {
            Objects.requireNonNull(queryId);
            rankedChunkIds = List.copyOf(rankedChunkIds);
            relevanceGrades = Map.copyOf(relevanceGrades);
            if (relevanceGrades.values().stream().anyMatch(grade -> grade < 0)) {
                throw new IllegalArgumentException("Relevance grade must be >= 0");
            }
            Set<String> unique = new HashSet<>(rankedChunkIds);
            if (unique.size() != rankedChunkIds.size()) {
                throw new IllegalArgumentException("Ranked results contain duplicate IDs");
            }
        }
    }

    public record Metrics(double hitAtK, double recallAtK,
                          double reciprocalRank, double ndcgAtK) {
    }

    public static Metrics evaluate(EvaluationCase c, int k) {
        if (k <= 0) throw new IllegalArgumentException("k must be positive");

        Set<String> relevant = new HashSet<>();
        c.relevanceGrades().forEach((id, grade) -> {
            if (grade > 0) relevant.add(id);
        });

        double reciprocalRank = 0.0;
        for (int i = 0; i < c.rankedChunkIds().size(); i++) {
            int grade = c.relevanceGrades().getOrDefault(
                    c.rankedChunkIds().get(i), 0);
            if (grade > 0) {
                reciprocalRank = 1.0 / (i + 1); // 未截断 MRR 的单题贡献
                break;
            }
        }

        int limit = Math.min(k, c.rankedChunkIds().size());
        int relevantRetrieved = 0;
        double dcg = 0.0;

        for (int i = 0; i < limit; i++) {
            String id = c.rankedChunkIds().get(i);
            int grade = c.relevanceGrades().getOrDefault(id, 0);
            if (grade > 0) {
                relevantRetrieved++;
            }
            dcg += gain(grade) / log2(i + 2.0);
        }

        double hitAtK = relevantRetrieved > 0 ? 1.0 : 0.0;
        double recallAtK = relevant.isEmpty()
                ? 0.0
                : (double) relevantRetrieved / relevant.size();

        List<Integer> idealGrades = c.relevanceGrades().values().stream()
                .filter(grade -> grade > 0)
                .sorted((left, right) -> Integer.compare(right, left))
                .limit(k)
                .toList();
        double idcg = 0.0;
        for (int i = 0; i < idealGrades.size(); i++) {
            idcg += gain(idealGrades.get(i)) / log2(i + 2.0);
        }
        double ndcgAtK = idcg == 0.0 ? 0.0 : dcg / idcg;

        return new Metrics(hitAtK, recallAtK, reciprocalRank, ndcgAtK);
    }

    public static Metrics macroAverage(List<EvaluationCase> cases, int k) {
        if (cases.isEmpty()) throw new IllegalArgumentException("cases must not be empty");
        List<Metrics> metrics = cases.stream().map(c -> evaluate(c, k)).toList();
        return new Metrics(
                average(metrics, Metrics::hitAtK),
                average(metrics, Metrics::recallAtK),
                average(metrics, Metrics::reciprocalRank),
                average(metrics, Metrics::ndcgAtK));
    }

    private static double average(List<Metrics> values,
                                  ToDoubleFunction<Metrics> extractor) {
        return values.stream().mapToDouble(extractor).average().orElseThrow();
    }

    private static double gain(int grade) {
        return Math.pow(2.0, grade) - 1.0;
    }

    private static double log2(double value) {
        return Math.log(value) / Math.log(2.0);
    }

    public static void main(String[] args) {
        EvaluationCase first = new EvaluationCase(
                "q-1",
                List.of("c-noise", "c-g1", "c-zgc", "c-other"),
                Map.of("c-g1", 3, "c-zgc", 2, "c-jdk21", 1));
        EvaluationCase second = new EvaluationCase(
                "q-2",
                List.of("c-virtual-thread", "c-noise"),
                Map.of("c-virtual-thread", 3));

        Metrics metrics = macroAverage(List.of(first, second), 3);
        System.out.printf(
                "Hit@3=%.3f Recall@3=%.3f MRR=%.3f nDCG@3=%.3f%n",
                metrics.hitAtK(), metrics.recallAtK(),
                metrics.reciprocalRank(), metrics.ndcgAtK());
    }
}

设问题数为 Q,每题完整排名长度为 Mᵢ、截断深度为 K,相关标注数为 Rᵢ

  • 示例计算未截断 MRR,因此单题最坏扫描完整排名 O(Mᵢ);Hit/Recall/DCG 只扫描 top-K,为 O(K)
  • 构造理想排序采用排序,单题为 O(Rᵢ log Rᵢ),可在评测集加载时预计算 IDCG;
  • 所有问题总时间约为 O(Σ(Mᵢ + K + Rᵢ log Rᵢ));单题临时空间为 O(Rᵢ),当前 macroAverage 物化全部结果,另占 O(Q) 空间;
  • 宏平均让每个问题权重相同;若按请求频率加权会更贴近流量,但可能掩盖长尾与高风险问题,应同时报告切片;
  • 示例把“无相关标注”的 Recall/nDCG 记为 0。实际协议也可将无答案样本从检索相关性指标中分离,单独评估拒答;关键是预先固定且透明报告。

场景设计题

场景:为企业知识库 RAG 建立从离线到线上的发布评估体系

背景: 系统支持 50 个租户,包含 Java 运维手册、内部 API、事故复盘和权限敏感文档。团队准备升级 chunking、embedding 和 reranker,希望判断是否上线。

一个完整设计应包括:

  1. 样本与标注:按真实流量分层抽样,补充错误码、比较、多跳、时效、无答案、ACL 和注入样本;每条保存身份类别、时间点、相关 chunk 等级、必要答案断言和期望拒答。
  2. 冻结版本:基线与候选分别保存数据快照、parser/chunker/embedding/index、检索参数、reranker、prompt、生成模型和 judge 版本;尽量在相同源数据与身份条件下成对运行。
  3. 分阶段指标:BM25/向量独立 Recall@K,融合后 Recall/MRR/nDCG,rerank 后 nDCG,最终上下文证据覆盖与 context precision,生成的 faithfulness/relevance/completeness/citation 指标。
  4. 安全门禁:无权文档不得出现在候选返回、rerank 输入、trace、缓存、prompt 和引用;注入文本不能改变工具权限;无答案样本评估误答与误拒。
  5. 统计与审查:报告逐题差值和 bootstrap 区间;按租户、语言、问题类型、文档新旧切片;人工复核所有严重回归、judge 分歧和高风险任务。
  6. 性能成本:比较构建时间、索引体积、P50/P95/P99、各模型调用次数、输入/输出 token 和每千请求成本;质量与成本用 Pareto 曲线讨论,而不是合成一个任意总分。
  7. 上线策略:先影子流量检查版本和安全,再小流量 A/B;设置自动回滚阈值和人工值班;线上失败脱敏后回流评测集。

可以提出一个示例门禁框架,但不能编造通用阈值:ACL 泄漏和跨租户缓存命中属于零容忍事件;质量、延迟和成本阈值根据当前基线、样本量与业务风险共同确定。

排障路径

遇到“候选版本最终回答指标下降”时按层定位:

  1. 先验证实验有效性:基线与候选是否使用同一问题、身份、数据时间点和评分协议;是否有缓存污染或 judge 版本变化?
  2. 查看索引覆盖:金标 chunk 是否仍存在,ID 是否因重切分失效,标注是否过期?
  3. 比较独立召回:BM25、向量各自 Recall@K 是否变化。仅向量下降时检查 embedding、归一化、ANN 与 query 模板;两路都下降时看 chunking、过滤和数据版本。
  4. 比较融合与重排:正确证据的 rank 在 RRF 与 rerank 前后如何变化;去重身份、候选 K、reranker 截断和超时是否改变?
  5. 检查最终上下文:证据是否因 parent 扩展、来源配额或 token 预算被丢弃;最终 prompt 与记录是否一致?
  6. 拆生成指标:是 faithfulness、relevance、completeness、correctness 还是 citation 下降;不要把所有失败归为“幻觉”。
  7. 审计裁判:抽取下降样本做人评,交换候选顺序并重复 judge;检查裁判是否偏爱长答案或受文档注入影响。
  8. 查看切片:总体下降可能集中在一个文档类型或语言;总体持平也可能掩盖权限和无答案回归。
  9. 核对性能降级:线上是否因超时跳过向量或 rerank;离线全功能结果不能代表线上 deadline 下的真实路径。

排障输出应是一条可验证结论,例如“正确证据在向量 top-50 中,但因 chunkId 版本变化未与 BM25 去重,RRF 后降到 43,超出 rerank top-40”,而不是“模型效果波动”。

速记总结

  • 端到端指标负责验收,分层指标负责定位;至少保存召回、融合、重排和最终上下文快照。
  • Recall@K = 命中的相关证据数 / 全部相关证据数;至少命中一个是 Hit@K,不要混淆。
  • MRR 只看第一个相关结果,适合单答案;多证据问题结合 Recall 与 nDCG。
  • nDCG 用分级增益、位置折损和理想排序归一化,适合判断头部排序质量。
  • Faithfulness 只回答“断言是否受给定上下文支持”,不证明来源真实,也不等于相关、完整或正确。
  • 引用要验证 assertion-to-source 支持关系,并绑定实际进入 prompt 的版本化 chunk。
  • LLM-as-judge 是需校准的测量工具:固定版本、匿名与随机顺序、重复评分、对齐人评、分歧升级。
  • 平均分会隐藏租户、语言、无答案和权限回归;发布门禁必须看关键切片。
  • 置信区间表达抽样不确定性,不修复有偏数据,也不证明“不显著即等价”。
  • 线上点击是行为代理,不是事实正确性;失败 trace 应脱敏、版本化并回流评测集。

参考资料