RAG系统评估:检索质量与生成质量的联合评测方法

cover

一、RAG评估的盲区:检索好≠生成好,生成好≠整体好

RAG(检索增强生成)系统的评估面临一个独特的挑战:系统由检索和生成两个子模块串联组成,单独评估任何一个都无法反映整体质量。检索模块召回了相关文档,但生成模块可能忽略关键信息或产生幻觉;生成模块产出了流畅文本,但可能完全基于模型内部知识而非检索到的文档。

一个典型的评估误区:只评估检索的 Recall@K,发现召回率 90% 就认为系统没问题。但实际用户反馈却很差——因为生成模型虽然看到了正确的文档,却在回答中混入了与文档矛盾的信息。反过来,如果只评估生成文本的流畅度和相关性,可能忽略了检索模块引入的噪声文档对生成质量的负面影响。

RAG 系统的评估必须是联合的:检索质量影响生成质量,生成质量又反过来反映检索的有效性。需要一套端到端的评测框架,同时量化检索精度、生成忠实度和整体答案质量。

二、RAG 联合评测框架设计

flowchart TB
    subgraph 评测输入["评测输入"]
        I1[问题集<br/>Questions]
        I2[参考答案<br/>Ground Truth]
        I3[知识库<br/>Corpus]
    end

    subgraph 检索评估["检索质量评估"]
        R1[召回率<br/>Recall@K]
        R2[精确率<br/>Precision@K]
        R3[MRR<br/>Mean Reciprocal Rank]
        R4[上下文相关性<br/>Context Relevance]
    end

    subgraph 生成评估["生成质量评估"]
        G1[忠实度<br/>Faithfulness]
        G2[答案相关性<br/>Answer Relevancy]
        G3[答案正确性<br/>Answer Correctness]
    end

    subgraph 联合评估["联合评估指标"]
        J1[端到端准确率<br/>E2E Accuracy]
        J2[幻觉率<br/>Hallucination Rate]
        J3[信息利用率<br/>Context Utilization]
        J4[检索-生成一致性<br/>Retrieval-Generation Consistency]
    end

    I1 --> R1
    I3 --> R1
    R1 --> G1
    R4 --> G1
    I2 --> G3
    G1 --> J2
    G2 --> J4
    G3 --> J1
    R4 --> J3

关键指标解析:

  1. 上下文相关性(Context Relevance):检索到的文档与问题的相关程度。高相关性意味着检索模块没有引入噪声。

  2. 忠实度(Faithfulness):生成答案是否忠实于检索到的文档,而非模型内部知识。这是 RAG 系统最核心的指标——如果答案不忠实于文档,RAG 就失去了意义。

  3. 答案相关性(Answer Relevancy):生成答案与问题的相关程度。一个忠实于文档但答非所问的答案同样没有价值。

  4. 信息利用率(Context Utilization):检索到的文档中有多少信息被生成答案实际利用。低利用率意味着检索了过多无关文档,浪费了 Token 预算。

三、RAG 评测框架的 Python 实现

3.1 忠实度评估

import json
from dataclasses import dataclass

@dataclass
class RAGEvalSample:
    """RAG评测样本"""
    question: str
    retrieved_docs: list[str]
    generated_answer: str
    reference_answer: str

class FaithfulnessEvaluator:
    """
    忠实度评估器
    评估生成答案是否忠实于检索文档
    方法:将答案拆分为声明,逐条验证是否可被文档支撑
    """

    def __init__(self, llm_client):
        self.llm = llm_client

    def evaluate(self, sample: RAGEvalSample) -> dict:
        """
        评估忠实度
        返回:忠实度分数(0-1)和每条声明的验证结果
        """
        # 第一步:将答案拆分为独立声明
        claims = self._extract_claims(sample.generated_answer)

        # 第二步:逐条验证声明是否可被文档支撑
        verification_results = []
        for claim in claims:
            supported = self._verify_claim(claim, sample.retrieved_docs)
            verification_results.append({
                "claim": claim,
                "supported": supported,
            })

        # 计算忠实度分数
        supported_count = sum(
            1 for r in verification_results if r["supported"])
        faithfulness = supported_count / max(len(claims), 1)

        return {
            "faithfulness": faithfulness,
            "total_claims": len(claims),
            "supported_claims": supported_count,
            "unsupported_claims": len(claims) - supported_count,
            "details": verification_results,
        }

    def _extract_claims(self, answer: str) -> list[str]:
        """将答案拆分为独立声明"""
        prompt = f"""
请将以下答案拆分为独立的原子声明。每个声明应是一个可验证的事实陈述。

答案:{answer}

请以JSON数组格式输出,例如:
["声明1", "声明2", "声明3"]
"""
        response = self.llm.chat(prompt)
        try:
            return json.loads(response)
        except json.JSONDecodeError:
            # 降级:按句号分割
            return [s.strip() for s in answer.split("。") if s.strip()]

    def _verify_claim(self, claim: str, docs: list[str]) -> bool:
        """验证声明是否可被文档支撑"""
        docs_text = "\n".join(f"[文档{i+1}]: {doc}"
                             for i, doc in enumerate(docs))
        prompt = f"""
请判断以下声明是否可被给定的文档支撑。

声明:{claim}

文档:
{docs_text}

请仅回答"是"或"否"。
"""
        response = self.llm.chat(prompt).strip()
        return response.startswith("是")

3.2 端到端评测流水线

class RAGEvaluationPipeline:
    """RAG端到端评测流水线"""

    def __init__(self, llm_client, rag_system):
        self.llm = llm_client
        self.rag = rag_system
        self.faithfulness_eval = FaithfulnessEvaluator(llm_client)

    def evaluate_dataset(
        self,
        dataset: list[dict],
        top_k: int = 5,
    ) -> dict:
        """
        对完整数据集执行评测
        dataset中每个样本包含:question, reference_answer
        """
        results = []

        for item in dataset:
            # 执行RAG推理
            rag_output = self.rag.query(
                item["question"], top_k=top_k)

            sample = RAGEvalSample(
                question=item["question"],
                retrieved_docs=rag_output.retrieved_docs,
                generated_answer=rag_output.answer,
                reference_answer=item["reference_answer"],
            )

            # 评估各项指标
            eval_result = self._evaluate_single(sample)
            results.append(eval_result)

        # 聚合结果
        return self._aggregate(results)

    def _evaluate_single(self, sample: RAGEvalSample) -> dict:
        """评估单个样本的所有指标"""
        # 检索质量
        context_relevance = self._compute_context_relevance(sample)

        # 生成质量
        faithfulness = self.faithfulness_eval.evaluate(sample)
        answer_relevancy = self._compute_answer_relevancy(sample)

        # 联合指标
        hallucination_rate = 1.0 - faithfulness["faithfulness"]

        return {
            "question": sample.question,
            "context_relevance": context_relevance,
            "faithfulness": faithfulness["faithfulness"],
            "answer_relevancy": answer_relevancy,
            "hallucination_rate": hallucination_rate,
        }

    def _compute_context_relevance(self, sample: RAGEvalSample) -> float:
        """计算上下文相关性"""
        prompt = f"""
请评估以下检索到的文档与问题的相关程度,给出0-1的分数。

问题:{sample.question}

文档:
{chr(10).join(sample.retrieved_docs[:3])}

请仅输出0到1之间的数字。
"""
        response = self.llm.chat(prompt).strip()
        try:
            return float(response)
        except ValueError:
            return 0.5

    def _compute_answer_relevancy(self, sample: RAGEvalSample) -> float:
        """计算答案相关性"""
        prompt = f"""
请评估以下答案与问题的相关程度,给出0-1的分数。

问题:{sample.question}
答案:{sample.generated_answer}

请仅输出0到1之间的数字。
"""
        response = self.llm.chat(prompt).strip()
        try:
            return float(response)
        except ValueError:
            return 0.5

    def _aggregate(self, results: list[dict]) -> dict:
        """聚合评测结果"""
        n = len(results)
        return {
            "total_samples": n,
            "avg_context_relevance": sum(
                r["context_relevance"] for r in results) / n,
            "avg_faithfulness": sum(
                r["faithfulness"] for r in results) / n,
            "avg_answer_relevancy": sum(
                r["answer_relevancy"] for r in results) / n,
            "avg_hallucination_rate": sum(
                r["hallucination_rate"] for r in results) / n,
        }

四、RAG 评测的架构权衡

LLM-as-Judge 的可靠性

使用 LLM 评估 LLM 的输出存在"同源偏差"——评估 LLM 可能对同类模型的输出更宽容。缓解方案是使用与生成模型不同的 LLM 作为评估器,并定期用人工评估校准。

评测成本

每个样本的忠实度评估需要多次 LLM 调用(声明提取 + 逐条验证),100 个样本可能需要 500+ 次调用。建议先在小样本上验证评测框架的有效性,再扩展到完整数据集。

参考答案的必要性

某些指标(如答案正确性)需要参考答案,但构建高质量参考答案的成本很高。忠实度和上下文相关性不需要参考答案,更适合日常自动化评测。

适用边界:RAG 联合评测适合知识库更新频繁、需要量化检索与生成协同效果的场景。对于简单的 FAQ 系统,关键词匹配评测即可。

五、总结

RAG 系统的评估必须同时关注检索质量和生成质量,以及两者的协同效果。落地路线建议:

  1. 忠实度优先:先建立忠实度评估,这是 RAG 系统最核心的质量指标。
  2. 检索-生成联合:不要单独评估检索或生成,关注端到端的答案质量。
  3. 幻觉监控:将幻觉率纳入日常监控,超过阈值触发知识库或检索策略的优化。
  4. 人工校准:定期用人工评估校准自动化指标,确保评测结果与用户感知一致。
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐