1. 从凭感觉调优到可量化度量RAG评测的痛点与转机做RAG落地最让人头疼的一件事不是文档切得不好不是Embedding模型选得不对而是——当用户反馈回答质量不稳定时你根本说不清问题出在哪一环。是检索召回了一堆无关片段还是大模型没忠实依据上下文回答还是用户的问题本身就不是一个适合RAG回答的问题我最早做RAG项目时评测方式是典型的人工抽样肉眼判断拉一批测试问题一个个跑一个个看然后凭感觉说好像还行。这套方法在小规模demo阶段勉强够用一旦进入生产环境维护期问题就暴露了你改了chunk大小效果是变好了还是变差了换了个Embedding模型检索准确率提升了多少Prompt里加了一句约束会不会反而导致忠实性下降这些问题如果没有一套标准化的评测机制根本无从回答。后来我开始接触Ragas和DeepEval这两个评测框架意识到一件事RAG评测这件事本身是可以工程化的。把评测指标固化下来把测试集管理起来把评测脚本跑进CI流水线里每次代码变更、参数调整、模型替换都能自动拉出一份质量报告——这时候RAG系统才算真正进入了可度量、可回归、可追踪的阶段。这篇文章就围绕我实际搭建的一套RAG质量评测流水线展开覆盖三个核心部分评测指标的底层原理与选型、Ragas与DeepEval双框架的实战脚本设计、以及如何把评测流程嵌进GitHub CI实现自动化回归。适合正在做RAG落地、被效果评估困扰的团队参考也适合想系统了解RAG评测体系的开发者阅读。2. 为什么同时用Ragas和DeepEval两套框架的评测哲学差异先说结论Ragas和DeepEval不是二选一的关系而是互补关系。我最终选择双框架并行不是因为哪个框架不够好而是因为它们各自擅长的层面确实不一样。2.1 Ragas以RAG链路为核心的指标设计RagasRAG Assessment的缩写词在设计之初就是冲着评估RAG系统整体质量去的。它的核心指标分两个维度一个是评测检索组件一个是评测生成组件。检索侧的指标是Context Precision上下文精确率和Context Recall上下文召回率。简单说精确率衡量的是检索出来的这些片段里有多少是真正相关的召回率衡量的是所有应该被检索到的相关片段到底被找回来了多少。这两个指标直接对标传统信息检索领域的Precision和Recall但Ragas把它适配到了RAG场景——通过LLM作为评判员判断每个检索片段与标准答案之间的相关性。生成侧的指标是Faithfulness忠实性和Answer Relevance答案相关性。Faithfulness检查的是生成的答案是否严格基于检索到的上下文有没有自己编造内容Answer Relevance检查的是答案是否真正回应了用户的问题而不是答非所问。这套指标体系的底层逻辑是把一个端到端的RAG系统拆开分别度量各个内部环节从而定位问题源头。如果Faithfulness低问题大概率出在Prompt设计或模型能力上如果Context Recall低问题大概率出在检索链路切分策略、Embedding模型、TopK设置上。2.2 DeepEval更像一个完整的LLM测试单元测试框架DeepEval的定位和Ragas有些不同。它的核心概念是Test Case和Metric用起来的感觉像是在给LLM写单元测试。你定义好测试输入、期望输出或者不定义让LLM自己评估、检索到的上下文然后选择相应的指标进行评估。DeepEval的指标体系更宽泛一些除了RAG常见的Answer Relevancy、Faithfulness、Contextual Precision、Contextual Recall还有Bias偏见检测、Toxicity毒性检测、RAGAS系列指标没错DeepEval里直接内置了Ragas指标的实现等。它还支持自定义指标这意味着你可以针对自己的业务定义专用评估标准。更关键的是DeepEval提供了Pytest集成功能。这意味着你不需要自己写一套评测调度逻辑只需要把每个测试用例写成Pytest参数化测试然后运行pytest命令就能把评测报告集成到JUnit XML里——这在CI/CD环境里简直不要太顺手。2.3 双框架并行的工作分工在实际流水线里我的分工是这样的职责使用框架原因生成侧指标评估忠实性、答案相关性RagasRagas对RAG场景的指标定义更成熟论文支撑完善Faithfulness评估的思路更适合RAG链路诊断检索侧指标评估上下文精确率/召回率Ragas指标定义清晰与LangChain集成方便CI中的单元测试式回归DeepEval原生Pytest集成失败阈值控制简单JUnit报告生成方便自定义业务指标如答案关键词覆盖率DeepEval自定义Metric的接口简洁Python装饰器风格上手快当然如果你只想用一个框架也完全跑得通。Ragas在生成侧指标上更专业DeepEval在工程集成上更顺手。双框架并行的代价是多维护一套依赖和配置对于评测体系刚起步的团队我建议先选一个跑通再做扩展——不需要一开始就上双框架。3. 指标选型的底层逻辑理解评测指标到底在评什么很多人在选RAG评测指标时是盲目的——看到别人用什么指标就跟着用什么。但指标选型本质上取决于一个更根本的问题你希望RAG系统在哪个环节上做到什么标准3.1 忠实性Faithfulness最不该被忽视的指标Faithfulness的评估逻辑是把生成的答案拆解成若干条独立陈述claim然后逐一检查每条陈述是否能从检索到的上下文中找到依据。找到依据的陈述占比越高Faithfulness分数越高。这个指标特别重要是因为RAG系统的核心价值就是基于知识库回答如果模型频繁脱离知识库自由发挥那RAG就退化成了一般的LLM聊天知识库的存在意义也就没了。我见过太多RAG项目答案看起来流畅自然但深入核对后发现大量内容根本不在知识库里——这时候Faithfulness必然低得离谱。Faithfulness低通常有几种原因一是Prompt里没有强约束仅基于上下文回答二是检索到的上下文太少模型被迫根据自己的知识来补全三是知识库本身内容与问题相关度不够模型接不住话就只能自圆其说。3.2 答案相关性Answer Relevancy评估是否答非所问Answer Relevancy的评估逻辑稍有不同它会基于生成的答案反向生成若干相关问题然后计算这些问题与原问题之间的相似度。如果答案能让LLM推断出用户可能问的问题和实际问题高度重合说明答案本身就是切题的。这个指标有两个特点需要注意。第一它的评估依赖LLM的理解能力所以评测用的LLM即评判模型质量很重要我一般会用GPT-4级别或Claude级别的模型来当评判者不太建议用太弱的模型。第二反向生成问题的思路在开放式问答里表现良好但在多选一或者事实型检索问答里偶尔会出现误判——因为模型可能用不同的措辞表达了正确的意思导致反向生成的问题与原问题文字相似度不高。3.3 上下文相关性定位检索环节的质量Contextual Precision和Contextual Recall解决的是检索到了没的问题。Contextual Precision的逻辑是把检索结果中与标准答案相关的片段排到越靠前的位置分数就越高。这其实在评估Rerank重排环节的效果——如果第一轮向量检索召回了10个片段其中5个相关但重排后这5个全被排到了后五位那Contextual Precision就会很低说明Rerank配置有问题。Contextual Recall的逻辑则是标准答案中涉及的每一个关键信息点是否都能在检索到的上下文中找到。这个指标衡量的是召回是否充分。如果一个问题的答案涉及三个事实但上下文只能支撑两个这个指标的分数就会打折扣。它是我排查切分粒度问题的第一抓手——如果Contextual Recall持续偏低大概率是文档切分粒度太粗或太细导致关键信息被切断或遗漏。3.4 别迷信单一指标需要建立指标组合的体检思维单一指标高不代表系统好。比如Answer Relevancy很高但Faithfulness低——这说明模型回答得很全面但内容很多是编的。再比如Contextual Recall高但Faithfulness低——说明信息都检索到了但Prompt约束或模型能力导致生成时偏离了上下文。我比较推荐的组合是检索侧看Contextual Recall Contextual Precision生成侧看Faithfulness Answer Relevancy。这样一套下来能覆盖召回是否充分—排序是否合理—生成是否忠实—回答是否切题的完整RAG链路。再多加指标比如噪声敏感度、完整性等会让每次评测耗时成倍增加实际维护起来很累。评测不是指标越多越好而是够用且能定位问题就好。4. 评测集设计与管理把测试数据当成一等公民对待评测流水线里最容易被低估的环节是测试集。没有一套高质量的评测集再好的评测框架也是白搭——这就像写单元测试却没有测试用例框架做得再好也没意义。4.1 测试集的三种来源与配比我建议测试集由三部分构成真实用户问题沉淀从线上日志或人工整理中抽取真实用户的提问这是最宝贵的评测数据。配比建议占总测试集的50%以上。专家构造的标准问题针对知识库中的核心知识点由业务专家或领域专家构造一批必须回答正确的问题配比约30%。边界与异常问题包括模糊问题、多轮隐含指代、无答案问题、知识库覆盖不到的问题配比约20%。这类问题主要用来测试系统的拒答能力和防幻觉能力——知道什么不知道其实比什么都敢答更难得。4.2 标准答案与参考答案的标注规范有了原始问题还不够还需要为标准问题标注参考答案或者关键信息点。Ragas有一种模式叫TestsetGenerator可以从文档中自动合成测试集——让LLM基于文档内容生成问题和对应的答案。这个方式适合冷启动但生成的测试集质量良莠不齐我一般只会在没有真实用户数据时使用它做底料不会用它替代人工标注。在标注参考答案时有一个技巧值得分享不要只写一个标准答案文本而是建议拆成若干个关键信息点key points。比如针对什么是Grouped Query Attention这个问题参考答案可以拆成GQA是一种注意力机制优化方案它将Key和Value头分组共享目的是减少KV Cache显存占用是MHA和MQA的折中方案这四个信息点。这样标注的好处是在评测Contextual Recall时可以精确到哪个信息点没被召回定位问题的粒度更细。4.3 评测集的版本管理与增量更新测试集不是一次性工作它需要伴随系统的演进而持续更新。我采用的方案是把测试集以JSON或YAML格式放在代码仓库里由专门的testset/目录管理。字段结构大致如下- question: 什么是Grouped Query Attention expected_key_points: - GQA是一种注意力机制优化方案 - 它将Key和Value头分组共享 - 目的是减少KV Cache显存占用 - 是MHA和MQA的折中方案 source_docs: - docs/llm/attention.md tag: core-knowledge每次知识库文档更新对应领域的测试问题也需要顺带review一遍看是否有原本有答案文档更新后答案信息点变了的情况。这一步不跟上评测集就会慢慢失真到后面评测分数再高也说明不了问题。5. 实战脚本编写Ragas DeepEval 双引擎评测的完整实现下面是我实际跑通的一套评测脚本设计。整个流程分四步准备测试集、运行Ragas评测、运行DeepEval评测、聚合报告。5.1 环境依赖安装# Python 3.10 环境 pip install ragas deepeval langchain langchain-openai datasets # 如果使用LLM作为评判模型还需要配置环境变量 export OPENAI_API_KEYyour-api-key特别提一句Ragas和DeepEval的版本更新很快接口变化也挺频繁。我最初写这套脚本时Ragas用的还是0.1.x版本接口和现在0.2.x就有不少差异。建议安装时锁定版本避免流水线突然跑挂。我的requirements.txt里会写死具体版本号ragas0.2.14 deepeval2.4.6 langchain0.3.215.2 测试集加载与格式化先把评测集从YAML或JSON加载成Python对象转成Ragas和DeepEval各自需要的格式。import json from datasets import Dataset from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall from ragas import evaluate # 假设testset.json是评测集文件每项包含question、answer、contexts等字段 with open(testset.json, r, encodingutf-8) as f: testset json.load(f) # Ragas需要HuggingFace Dataset格式 ragas_dataset Dataset.from_list([ { question: item[question], answer: item[answer], # RAG系统生成的回答 contexts: item[contexts], # RAG系统检索到的上下文片段列表 ground_truth: item[ground_truth], # 参考答案 } for item in testset ])这里有个设计细节值得说明评测集文件里的answer和contexts不应该预先存好而应该是在每次评测时实时从RAG系统获取。也就是说评测脚本的流程是加载测试问题 → 调用RAG系统生成回答和检索上下文 → 把回答和上下文交给评测框架打分。这样才能保证评测的是当前代码状态下的RAG系统而不是上一次评测的缓存数据。5.3 Ragas核心评测脚本from ragas.metrics import ( faithfulness, answer_relevancy, context_precision, context_recall, ) metrics [ faithfulness, answer_relevancy, context_precision, context_recall, ] result evaluate( ragas_dataset, metricsmetrics, llmcritic_llm, # 评判模型建议用强模型 embeddingsembedding_model, # 用于部分指标间相似度计算 ) # 打印总分 print(Ragas Scores:) print(result) # 查看单条明细定位问题问题 df result.to_pandas() df.to_csv(ragas_scores_detail.csv, indexFalse)一个非常容易踩坑的地方Ragas内部调LLM时默认可能使用OPENAI_API_KEY环境变量而且LLM评判时并发有限制跑大批量测试集会非常慢。我的处理方法是给评判环节单独指定一个更强的模型同时控制并发参数from langchain_openai import ChatOpenAI critic_llm ChatOpenAI(modelgpt-4o, temperature0) result evaluate( ragas_dataset, metricsmetrics, llmcritic_llm, embeddingsembeddings, raise_exceptionsFalse, column_map{ question: question, answer: answer, contexts: contexts, ground_truth: ground_truth, }, )注意temperature0评判模型必须用确定性较高的配置否则同样一批数据两次评测分数会漂移得很厉害。这是评测可复现性的前提。5.4 DeepEval核心评测脚本DeepEval部分走的是Pytest风格。我会为每一类指标写一个独立的测试文件# test_eval_rag.py import pytest from deepeval import assert_test from deepeval.metrics import ( FaithfulnessMetric, AnswerRelevancyMetric, ContextualPrecisionMetric, ContextualRecallMetric, ) from deepeval.test_case import LLMTestCase def build_test_cases(): 从testset构建DeepEval测试用例并调用RAG系统获取实时响应 test_cases [] for item in load_testset(): answer, contexts run_rag_system(item[question]) test_case LLMTestCase( inputitem[question], actual_outputanswer, retrieval_contextcontexts, expected_outputitem[ground_truth], ) test_cases.append((item[question], test_case)) return test_cases pytest.mark.parametrize(question,test_case, build_test_cases()) def test_faithfulness(question, test_case): metric FaithfulnessMetric(threshold0.8) assert_test(test_case, [metric])这样每条测试问题都会作为一条独立的Pytest用例执行用例失败时可以看到是哪一个问题导致的指标不达标和常规代码测试的工作流完全一致。阈值设置这块我建议这么定先把当前系统的评测分数基线跑出来然后浮动5%-10%作为阈值。一开始不用卡得太死因为测试集质量本身也在迭代中。我最初把Faithfulness的阈值设在0.9结果天天跑挂后来仔细看了失败用例才发现有些测试问题本身标注的质量就有问题——参考信息点不全导致模型回答其实是对的但和标注对不上。先把测试集本身的质量打磨好再提高阈值顺序不能反。5.5 双框架结果聚合与报告输出Ragas输出的是Pandas DataFrameDeepEval输出的是Pytest报告和JSON格式的明细。为了在CI里统一查看我会写一个聚合脚本把两边的结果合并成一份总报告import json import pandas as pd def merge_reports(ragas_csv_path, deepeval_json_path): ragas_df pd.read_csv(ragas_csv_path) with open(deepeval_json_path, r, encodingutf-8) as f: deepeval_data json.load(f) # 每个问题的DeelEval指标分数 deepeval_df pd.DataFrame([ { question: item[input], deepeval_faithfulness: item[metrics_data][Faithfulness][score], deepeval_answer_relevancy: item[metrics_data][Answer Relevancy][score], } for item in deepeval_data[test_runs] ]) merged ragas_df.merge(deepeval_df, onquestion, howouter) merged.to_csv(merged_eval_report.csv, indexFalse) return merged合并报告的意义在于同一个测试问题上Ragas和DeepEval给出的Faithfulness分数可能会不同。当两边分数差异很大时比如一边0.9一边0.5这本身就是一个信号——说明这个问题处在评估不确定性较高的区域值得人工介入检查。我在实际项目中就用这个方法挖出过几个标注有争议的测试问题。6. 嵌入CI流水线让质量回归成为提交代码的必过关卡脚本能跑只是第一步真正让评测体系发挥威力的是把它嵌进CI/CD流程。我的目标是任何涉及RAG系统配置或代码的改动每次合并之前都要自动跑一遍评测只有全部指标达到阈值才允许合并进主干分支。6.1 GitHub Actions工作流设计我用的CI是GitHub Actions工作流定义大概长这样name: rag-eval-ci on: pull_request: paths: - rag/** - configs/** - testset/** - prompts/** push: branches: [main] paths: - rag/** - configs/** - testset/** jobs: evaluate: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkoutv4 - name: Setup Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install dependencies run: | pip install -r requirements.txt - name: Run Ragas Evaluation env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | python scripts/run_ragas_eval.py - name: Run DeepEval Pytest env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | pytest test_eval_rag.py --junit-xmldeepeval-report.xml - name: Upload Evaluation Reports uses: actions/upload-artifactv4 with: name: eval-reports path: | ragas_scores_detail.csv merged_eval_report.csv deepeval-report.xmlpaths字段的过滤很重要。不是每次代码提交都要跑完整评测RAG评测耗时较长且消耗LLM API额度只在与RAG相关的文件变更时才触发能省下大量时间和成本。6.2 并发与成本控制RAG评测是典型的LLM密集型任务。比如测试集有50个问题每个问题要跑4个指标每个指标内部还涉及多次LLM调用总调用次数经常达到几百上千次。在CI里跑一次光API开销可能就要几美元甚至更多。所以成本控制是评测流水线设计里不能回避的问题。我的做法是分三级PR级快速评测从测试集中抽取10-15个冒烟问题只跑Faithfulness和Answer Relevancy两个核心指标5-10分钟内出结果只拦截明显劣化。主干合并级完整评测推送main分支时跑全量测试集和全部指标产出完整报告。定时深度评测每天凌晨跑一次更重度的评测包括多组参数对比不同chunk size、不同TopK、不同Embedding模型输出详细对比报告。通过分级设计既保证了CI反馈的及时性又把成本控制在了可接受范围内。6.3 评测报告的可视化与追踪CI里产生的评测报告如果只是放在Artifacts里供人下载价值会大打折扣。我现在的做法是把每次评测结果写入一个集中的追踪服务比如Langfuse或Arize Phoenix这类LLM可观测性平台它们能把评测分数、追踪链路、检索到的上下文、模型输出、Token消耗关联起来。以Langfuse为例我可以在评测脚本里为每次评测创建一条Trace把问题、检索上下文、模型回答、各项指标分数全部挂到这条Trace上。后续要排查某次线上回答质量差的时候直接按问题关键词搜索Trace一眼就能看到当时检索到了哪些文档、模型是怎么生成的、各项评分是多少。这个排查效率比翻代码日志高了一个量级。用Arize Phoenix的路径也类似它的评估结果展示面板更适合横对比不同实验版本。我一般是这样分配的Langfuse用于线上单条问题的追踪诊断Arize Phoenix用于评测集的批量离线分析Promptfoo则适合在Prompt迭代时做快速A/B评测。三套工具有重叠但定位不同核心思路是——评测数据一定要有归处不能跑完就丢弃。7. 实际踩过的坑LLM评测的不确定性与针对性解法评测流水线搭建过程中我踩过的坑不少其中有些属于不上手就根本预料不到的类型这里挑几个最有代表性的分享。7.1 评测分数漂移同一份代码两次评测结果差很多有段时间我发现CI里同一份代码跑两次评测Faithfulness居然能从0.85漂到0.62。最开始怀疑是代码有随机性排查了很久最后定位到根源是评判LLM的temperature没有设为0。Ragas和DeepEval底层评判都依赖LLM而LLM在非零温度下生成的判断本身就有随机性。同一个答案LLM评判的思路可能略有不同导致分数波动。解决方式很简单所有评判模型的temperature统一设为0追求确定性。另外尽量用版本固定的评判模型不要用那种自动升级的模型别名否则哪天模型悄悄更新了你很难区分分数变化是RAG系统导致的还是评判模型导致的。7.2 评判模型能力不足弱模型评不出细微差异我犯过的另一个错误是为了省成本初始阶段用一个中等规模的开源模型充当评判模型。结果所有指标分数都在0.7-0.8之间几乎没有区分度——差的回答和好的回答分数差不多评测等于白跑。后来对照试验发现GPT-4级别的评判模型能明显识别出回答中的逻辑跳跃和事实张冠李戴而弱模型往往只做表面措辞判断。评测这件事上的投入不能省评判模型至少要比被评测的生成模型强一个档次。如果预算实在紧张可以适当减少测试集规模但不要压缩评判模型的档次。7.3 多轮对话与Agentic RAG场景的评测盲区标准的Ragas和DeepEval指标主要针对单轮问答。但现在的RAG系统越来越多地引入多轮记忆、甚至Agentic RAG自主规划多步检索这些场景下现成指标就有些力不从心了。比如多轮对话里用户第二轮问那它的性能对比呢——这里的它指代第一轮提到的某个模型。如果评测时单独拿这个问题去测上下文信息不足指标自然很难看。针对这个盲区我的做法是在评测集里标注对话历史字段DeepEval的测试模型也支持带上对话历史。脚本里把前序轮次作为conversation_history传入至少能让评测更贴近真实使用场景。Agentic RAG的评测则更复杂不仅看最终答案质量还要看中间检索规划和工具调用是否合理。现阶段我在这块的思路是不追求自动化全面覆盖而是把Agent的每步中间动作记录成结构化日志再针对关键步骤单独写一批过程校验指标和DeepEval的自定义指标结合做半自动化评估。这部分还在演进中目前没有完美解法但至少比纯人工翻日志强不少。8. 评测流水线的下一步演化方向整套流水线搭起来之后我最大的感受是评测这件事本身也在不断内卷——指标会过时、测试集会老化、评判模型的局限会暴露。所以流水线设计之初就要留出持续迭代的空间。一个我目前正在探索的方向是自动化测试集生成与去重。Ragas提供的TestsetGenerator可以基于知识库自动合成问题但直接生成的测试集冗余度很高——同一知识点可能被生成20个句式不同的问法跑评测时既浪费API额度又拉低指标区分度。我的思路是先用合成器生成候选问题再用Embedding做聚类去重每个簇只保留最有代表性的问题人工抽验后并入测试集。这样测试集能以较低成本持续扩充覆盖新上线的知识库内容。另一个方向是把评测分数与线上真实反馈关联起来。评测集毕竟是离线构造的和真实用户问题的分布存在偏差。理想状态是在线上埋点采集用户的隐式反馈追问、复制答案、反馈按钮等然后把线上问题自动回流到评测集里。这样一来评测体系就形成了一个闭环线上发现问题 → 流入评测集 → 触发CI回归 → 定位根因 → 修复验证 → 发布上线。RAG系统的质量就在这个循环里持续螺旋上升。这套流水线上线后我团队对RAG系统的改动频率明显提高了——以前改个chunk参数要商量半天因为没人敢保证觉得效果更好了是真的更好。现在每次改动都有评测分数背书有理有据返工和争论都少了很多。如果你也在做RAG实战且正在为效果说不清发愁建议从最小的单指标评测先跑起来再逐步扩展这个投入的性价比绝对比反复人工抽测要高得多。