多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Ragas-----从零开始为你的RAG项目打分

Ragas-----从零开始为你的RAG项目打分 1. 为什么需要 RagasRAG 系统的隐藏问题RAG 能大幅降低幻觉但不是银弹手动测几个问题根本发现不了隐患问题具体表现检索器不靠谱① 把大量无关噪音片段塞给 LLM干扰判断② 漏掉真正关键的内容LLM 无内容可用生成器依然幻觉LLM 对检索内容 视而不见偷用通用知识库编造答案尤其是内容不全时会自作主张补全回答答非所问绕圈铺垫无关背景甚至问东答西→ 只有量化系统评估才能真正知道系统好不好、问题出在哪这正是 Ragas 解决的。2. 核心痛点幻觉定义LLM 以非常自信的语气说出文档中根本不存在的信息肉眼很难发现尤其编造得合理时例子文档写 违约金为合同总额的 10%LLM 却回答 20%听起来正确实际是错的Ragas 的价值自动检测 输出量化 体检报告幻觉比例、检索准确度、有无遗漏关键信息精准定位问题不靠感觉3. Ragas 定位1.2全称Retrieval Augmented Generation Assessment检索增强生成评估背景2023 年发布的开源评估框架论文 2024 年发表于 EACL 会议RAG 领域最常用的评估工具之一最大特点大部分指标不需要人工标注的标准答案直接让 LLM 当 判卷老师 自动给分 → 大幅降低评估成本使用流程把你的 RAG 系统跑出的问答数据喂给 Ragas → 自动完成打分4.四大核心指标Ragas 有很多评估指标但最核心、最常用的是这四个也是我们后续评估 PDF 阅读助手的核心依据。这四个指标分为两类新手可以先掌握第一类无需标准答案再学习第二类需要标准答案循序渐进无需标准答案先掌握Faithfulness忠实度、Answer Relevancy回答相关性—— 只需要用户问题、LLM 回答、检索到的文档片段就能评估不需要你手动写标准答案。需要标准答案进阶掌握Context Precision上下文精确率、Context Recall上下文召回率—— 需手动写每个测试题的标准答案才能评估检索器的好坏。4.1Faithfulness忠实度---解决LLM幻觉问题的核心它的核心作用就是检查LLM生成的回答是不是凭空捏造的简单来说它的流程它会先把LLM的回答拆成一句句的“独立声名”然后逐句对比检索到的文档片段检查每一句声明是否能在文档里找到依据有依据的算合格没有依据的不合格最后生成一个0-1的分数# 1. 导入基础依赖加载环境变量、异步运行相关 import asyncio import os from dotenv import load_dotenv # 2. 加载.env文件里的环境变量关键步骤否则拿不到API Key load_dotenv() # 3. 导入Ragas核心模块新版本要从collections导入 from ragas import SingleTurnSample # 新版本所有指标都从 ragas.metrics.collections 导入 from ragas.metrics.collections import Faithfulness, AnswerRelevancy from ragas.llms import llm_factory from ragas.embeddings.base import embedding_factory # 新增AnswerRelevancy需要Embedding模型 from openai import AsyncOpenAI # 定义异步主函数因为Ragas的评估方法是异步的必须写在async函数里 async def main(): # Step 1初始化异步OpenAI客户端对接DashScope API client AsyncOpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlos.getenv( DASHSCOPE_BASE_URL, https://dashscope.aliyuncs.com/compatible-mode/v1 ) ) # Step 2初始化LLM和Embedding新版本AnswerRelevancy需要这两个 llm llm_factory(qwen-plus, provideropenai, clientclient) # 初始化Embedding模型用于计算回答相关性的向量相似度 embeddings embedding_factory( openai, modeltext-embedding-v3, # DashScope的Embedding模型兼容OpenAI接口 clientclient, interfacemodern ) print(✅ 评判用LLM和Embedding初始化成功) # 继续在main()函数里写接上面的代码 # Step 3构造忠实度测试样本2个样本对比无幻觉和有幻觉的区别 # 样本A好的回答——每句话都能在文档里找到依据无幻觉 sample_good SingleTurnSample( user_input退款政策是什么, response购买后 30 天内可申请无理由全额退款只需提供订单号。, retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 ] ) # 样本B有幻觉的回答——2年质保和上门维修在文档里没有有幻觉 sample_bad SingleTurnSample( user_input产品保修期是多久, response产品享有 2 年全面质保包含免费上门维修服务。, retrieved_contexts[ 质保条款本产品享有自购买之日起 1 年的质量保证不包含人为损坏。 ] ) # 打印样本确认构造成功可选调试用 print(✅ 忠实度测试样本构造成功好样本坏样本) # 继续在main()函数里写接上面的代码 # Step 4初始化忠实度评估器传入评判用的LLM scorer Faithfulness(llmllm) # 新版本APIascore要直接传入三个参数不是传入SingleTurnSample对象 score_good await scorer.ascore( user_inputsample_good.user_input, responsesample_good.response, retrieved_contextssample_good.retrieved_contexts ) score_bad await scorer.ascore( user_inputsample_bad.user_input, responsesample_bad.response, retrieved_contextssample_bad.retrieved_contexts ) # 继续在main()函数里写接上面的代码 # Step 5打印忠实度结果 print(\n * 50) print( Faithfulness忠实度测试结果) print( * 50) # 新版本要取.value才能拿到分数值 print(f样本A好回答无幻觉: {score_good.value:.3f}) print(f样本B坏回答有幻觉: {score_bad.value:.3f}) # Step 6结果解读帮新手理解分数含义 print(\n 分数解读) print( 1.0 回答的每句话都有文档依据无幻觉最优) print( 0.0 回答完全是凭空编造全是幻觉最差) print( 0.5-0.9 部分内容有依据部分内容是幻觉需要优化) print(\n 本次测试解读) print(f 好样本分数接近1.0说明LLM回答完全基于文档没有幻觉) print(f 坏样本分数接近0.0说明LLM回答全是编造的存在严重幻觉。) if __name__ __main__: asyncio.run(main())4.2Answer Relevancy回答相关性---解决LLM答非所问的问题它的核心作用就是去检测LLM生成的回答有没有切题跟用户的请求是不是相关联的有没有答非所问注意它不关注LLM回答的对不对这是忠诚度干的事情它只关注LLM回答的是不是相关的内容# 01_ragas_basics.py # 目标从0到1学会构造测试样本跑通Ragas两大核心指标忠实度回答相关性 # 新手注意每一步代码不要跳过复制粘贴到文件中逐段运行验证 # 适配Ragas 0.4新版本API # 1. 导入基础依赖加载环境变量、异步运行相关 import asyncio import os from dotenv import load_dotenv # 2. 加载.env文件里的环境变量关键步骤否则拿不到API Key load_dotenv() # 3. 导入Ragas核心模块新版本要从collections导入 from ragas import SingleTurnSample # 新版本所有指标都从 ragas.metrics.collections 导入 from ragas.metrics.collections import Faithfulness, AnswerRelevancy from ragas.llms import llm_factory from ragas.embeddings.base import embedding_factory # 新增AnswerRelevancy需要Embedding模型 from openai import AsyncOpenAI # 定义异步主函数因为Ragas的评估方法是异步的必须写在async函数里 async def main(): # Step 1初始化异步OpenAI客户端对接DashScope API client AsyncOpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlos.getenv( DASHSCOPE_BASE_URL, https://dashscope.aliyuncs.com/compatible-mode/v1 ) ) # Step 2初始化LLM和Embedding新版本AnswerRelevancy需要这两个 llm llm_factory(qwen-plus, provideropenai, clientclient) # 初始化Embedding模型用于计算回答相关性的向量相似度 embeddings embedding_factory( openai, modeltext-embedding-v3, # DashScope的Embedding模型兼容OpenAI接口 clientclient, interfacemodern ) print(✅ 评判用LLM和Embedding初始化成功) # Step 3构造忠实度测试样本2个样本对比无幻觉和有幻觉的区别 # 样本A好的回答——每句话都能在文档里找到依据无幻觉 sample_good SingleTurnSample( user_input退款政策是什么, response购买后 30 天内可申请无理由全额退款只需提供订单号。, retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 ] ) # 样本B有幻觉的回答——2年质保和上门维修在文档里没有有幻觉 sample_bad SingleTurnSample( user_input产品保修期是多久, response产品享有 2 年全面质保包含免费上门维修服务。, retrieved_contexts[ 质保条款本产品享有自购买之日起 1 年的质量保证不包含人为损坏。 ] ) print(✅ 忠实度测试样本构造成功好样本坏样本) # Step 4初始化忠实度评估器传入评判用的LLM scorer Faithfulness(llmllm) # 新版本APIascore要直接传入三个参数不是传入SingleTurnSample对象 score_good await scorer.ascore( user_inputsample_good.user_input, responsesample_good.response, retrieved_contextssample_good.retrieved_contexts ) score_bad await scorer.ascore( user_inputsample_bad.user_input, responsesample_bad.response, retrieved_contextssample_bad.retrieved_contexts ) # Step 5打印忠实度结果 print(\n *50) print( Faithfulness忠实度测试结果) print(*50) # 新版本要取.value才能拿到分数值 print(f样本A好回答无幻觉: {score_good.value:.3f}) print(f样本B坏回答有幻觉: {score_bad.value:.3f}) # Step 6结果解读帮新手理解分数含义 print(\n 分数解读) print( 1.0 回答的每句话都有文档依据无幻觉最优) print( 0.0 回答完全是凭空编造全是幻觉最差) print( 0.5-0.9 部分内容有依据部分内容是幻觉需要优化) print(\n 本次测试解读) print(f 好样本分数接近1.0说明LLM回答完全基于文档没有幻觉) print(f 坏样本分数接近0.0说明LLM回答全是编造的存在严重幻觉。) # -------------------------- # 接下来是回答相关性的测试 # -------------------------- # Step 7构造回答相关性的测试样本3个覆盖切题、完全跑题、部分跑题 # 样本C切题——回答直接针对了退款政策这个问题 sample_on_topic SingleTurnSample( user_input退款政策是什么, response30天内可无理由退款需提供订单号。, retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 ] ) # 样本D完全跑题——回答的是配送政策和退款完全无关 sample_off_topic SingleTurnSample( user_input退款政策是什么, response我们支持全国包邮下单后3-5天即可送达。, retrieved_contexts[ 配送政策全国大部分地区支持包邮下单后3-5个工作日送达。 ] ) # 样本E部分跑题——前面加了一堆无关的公司背景后面才是正确的回答 sample_partial SingleTurnSample( user_input退款政策是什么, response我们公司成立于2010年主营电子产品深耕行业15年积累了大量的用户口碑。退款政策是30天内无理由退款。, retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 ] ) print(\n✅ 相关性测试样本构造成功切题完全跑题部分跑题) # Step 8初始化相关性评估器新版本需要传入llm和embeddings两个参数 relevancy_scorer AnswerRelevancy(llmllm, embeddingsembeddings) # 新版本APIascore只需要user_input和response两个参数 score_c await relevancy_scorer.ascore( user_inputsample_on_topic.user_input, responsesample_on_topic.response ) score_d await relevancy_scorer.ascore( user_inputsample_off_topic.user_input, responsesample_off_topic.response ) score_e await relevancy_scorer.ascore( user_inputsample_partial.user_input, responsesample_partial.response ) # Step 9打印相关性测试结果 print(\n *50) print( Answer Relevancy回答相关性测试结果) print(*50) # 新版本要取.value才能拿到分数值 print(f样本C切题: {score_c.value:.3f}) print(f样本D完全跑题: {score_d.value:.3f}) print(f样本E部分跑题: {score_e.value:.3f}) print(\n 分数解读) print( 1.0 回答完全切题没有任何无关内容最优) print( 0.0 回答完全跑题和问题毫无关系最差) print( 0.5-0.9 部分内容切题部分内容无关需要优化) print(\n 本次测试解读) print(f 切题样本分数接近1.0说明LLM回答完全针对问题) print(f 跑题样本分数接近0.0说明LLM回答完全偏离了问题) print(f 部分跑题样本分数在0.6左右说明回答里有部分无关的背景内容。) print(\n⚠️ 相关性低的常见原因) print( 1. 检索到了相关但不精确的文档LLM 顺着无关内容展开) print( 2. Prompt 没有约束 LLM 直接回答导致 LLM 铺垫大量背景) print( 3. LLM 本身的“话多”特性添加了无关的补充内容) # 运行异步主函数 if __name__ __main__: asyncio.run(main())4.3Context Precision上下文精确率—— 解决「检索噪音」问题这个是用来评估检索器的指标核心作用就是检测从检索器找回来的文档片段里有多少是有用的有多少是无关的噪音简单来说检索器每次会给你返回 Top-K 个文档片段比如 Top5精确率就是看这 5 个片段里真正能回答用户问题的片段占了多少比例 —— 比例越高说明检索器越精准没有把太多无关的噪音塞给 LLM。# 02_ragas_metrics.py # 目标测试检索器相关的两个进阶指标Context Precision和 Context Recall # 适配Ragas 0.4新版本API import asyncio import os from dotenv import load_dotenv load_dotenv() from ragas import SingleTurnSample # 新版本所有指标都从collections导入 from ragas.metrics.collections import ContextPrecision, ContextRecall from ragas.llms import llm_factory from openai import AsyncOpenAI async def main(): # 初始化异步OpenAI客户端 client AsyncOpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlos.getenv( DASHSCOPE_BASE_URL, https://dashscope.aliyuncs.com/compatible-mode/v1 ) ) # 初始化LLM llm llm_factory(qwen-plus, provideropenai, clientclient) print(✅ 评判用LLM初始化成功) # 构造精确率的测试样本 # 注意这里必须加reference字段这就是人工标注的标准答案 # 样本F检索结果有噪音的情况 sample_noisy SingleTurnSample( user_input退款政策是什么, reference用户可在购买后30天内申请全额退款无需说明原因提供订单号即可。, # 标准答案 retrieved_contexts[ 配送政策全国大部分地区支持包邮下单后3-5个工作日送达。, # 噪音1 会员政策会员可享9折优惠生日月双倍积分。, # 噪音2 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 # 有用的 ] ) # 样本G检索结果无噪音的情况 sample_clean SingleTurnSample( user_input退款政策是什么, reference用户可在购买后30天内申请全额退款无需说明原因提供订单号即可。, retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 ] ) # 初始化精确率评估器 precision_scorer ContextPrecision(llmllm) # 计算分数 score_f await precision_scorer.ascore( user_inputsample_noisy.user_input, referencesample_noisy.reference, retrieved_contextssample_noisy.retrieved_contexts ) score_g await precision_scorer.ascore( user_inputsample_clean.user_input, referencesample_clean.reference, retrieved_contextssample_clean.retrieved_contexts ) # 打印结果 print(\n * 50) print( Context Precision上下文精确率测试结果) print( * 50) print(f样本F有噪音: {score_f.value:.3f}) print(f样本G无噪音: {score_g.value:.3f}) print(\n 分数解读) print( 1.0 检索结果全是有用内容没有任何噪音最优) print( 0.0 检索结果全是无关内容完全没找到有用的最差) print( 0.5-0.9 部分有用部分噪音需要优化检索器) if __name__ __main__: asyncio.run(main())4.4Context Recall上下文召回率—— 解决「检索漏检」问题这个也是评价检索器的指标它的核心作用就是看看检索器有没有漏掉真正有用的内容也就是有没有把能回答问题的段落都找出来# 02_ragas_metrics.py # 目标测试检索器相关的两个进阶指标Context Precision和Context Recall # 适配Ragas 0.4新版本API import asyncio import os from dotenv import load_dotenv load_dotenv() from ragas import SingleTurnSample # 新版本所有指标都从collections导入 from ragas.metrics.collections import ContextPrecision, ContextRecall from ragas.llms import llm_factory from openai import AsyncOpenAI async def main(): # 初始化异步OpenAI客户端 client AsyncOpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlos.getenv( DASHSCOPE_BASE_URL, https://dashscope.aliyuncs.com/compatible-mode/v1 ) ) # 初始化LLM llm llm_factory(qwen-plus, provideropenai, clientclient) print(✅ 评判用LLM初始化成功) # -------------------------- # 1. 上下文精确率测试 # -------------------------- # 构造精确率的测试样本 # 注意这里必须加reference字段这就是人工标注的标准答案 # 样本F检索结果有噪音的情况 sample_noisy SingleTurnSample( user_input退款政策是什么, reference用户可在购买后30天内申请全额退款无需说明原因提供订单号即可。, # 标准答案 retrieved_contexts[ 配送政策全国大部分地区支持包邮下单后3-5个工作日送达。, # 噪音1 会员政策会员可享9折优惠生日月双倍积分。, # 噪音2 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 # 有用的 ] ) # 样本G检索结果无噪音的情况 sample_clean SingleTurnSample( user_input退款政策是什么, reference用户可在购买后30天内申请全额退款无需说明原因提供订单号即可。, retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 ] ) # 初始化精确率评估器 precision_scorer ContextPrecision(llmllm) # 计算分数 score_f await precision_scorer.ascore( user_inputsample_noisy.user_input, referencesample_noisy.reference, retrieved_contextssample_noisy.retrieved_contexts ) score_g await precision_scorer.ascore( user_inputsample_clean.user_input, referencesample_clean.reference, retrieved_contextssample_clean.retrieved_contexts ) # 打印精确率结果 print(\n *50) print( Context Precision上下文精确率测试结果) print(*50) print(f样本F有噪音: {score_f.value:.3f}) print(f样本G无噪音: {score_g.value:.3f}) print(\n 分数解读) print( 1.0 检索结果全是有用内容没有任何噪音最优) print( 0.0 检索结果全是无关内容完全没找到有用的最差) print( 0.5-0.9 部分有用部分噪音需要优化检索器) # -------------------------- # 2. 上下文召回率测试 # -------------------------- # 构造召回率的测试样本 # 样本H召回不全的情况 sample_low_recall SingleTurnSample( user_input退款政策是什么, reference用户可在购买后30天内申请全额退款无需说明原因提供订单号即可。, # 标准答案有3个信息点 retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请退款。 # 只找回了1个信息点 ] ) # 样本I召回完全的情况 sample_full_recall SingleTurnSample( user_input退款政策是什么, reference用户可在购买后30天内申请全额退款无需说明原因提供订单号即可。, retrieved_contexts[ 退款政策所有用户可在购买后 30 天内申请全额退款无需说明原因提供订单号即可。 ] ) # 初始化召回率评估器 recall_scorer ContextRecall(llmllm) # 计算分数 score_h await recall_scorer.ascore( user_inputsample_low_recall.user_input, referencesample_low_recall.reference, retrieved_contextssample_low_recall.retrieved_contexts ) score_i await recall_scorer.ascore( user_inputsample_full_recall.user_input, referencesample_full_recall.reference, retrieved_contextssample_full_recall.retrieved_contexts ) # 打印召回率结果 print(\n *50) print( Context Recall上下文召回率测试结果) print(*50) print(f样本H召回不全: {score_h.value:.3f}) print(f样本I召回完全: {score_i.value:.3f}) print(\n 分数解读) print( 1.0 所有关键信息都找回来了没有任何遗漏最优) print( 0.0 所有关键信息都没找回来完全漏检了最差) print( 0.5-0.9 部分召回部分遗漏需要优化检索器) # 运行异步主函数 if __name__ __main__: asyncio.run(main())5.优化策略优化优先级记住这个顺序 忠实度 召回率 精确率 相关性原因忠实度低意味着 LLM 在编造内容最容易误导用户危害最大召回率低意味着关键信息被遗漏回答不完整精确率低只是多了噪音影响稍小相关性最后优化。如果忠诚度低----改system prompt 让LLM生成内容去遵循更加严格的标准或者减低temperature参数让LLM输出的内容更加稳定不易发散如果上下文召回率低----这说明返回的有效文档少了所以我们可以增加Top k这个参数的数值让LLM多返回一些文档或者调整文档的分块策略--增大 chunk_size并增加 chunk_overlap# main.py → _split_documents() 方法召回率优化版 splitter RecursiveCharacterTextSplitter( chunk_size600, # 从 400 增大到 600让更完整的语义单元在同一个片段里 chunk_overlap100, # 从 50 增大到 100更多的重叠可以减少关键信息被切断的概率 separators[\n\n, \n, 。, ., , , , ] )上下文精确率Context Precision低----说明返回的文档中噪音太多了无效文档远远大于有效文档的数量所以我们可以考虑降低topk的数量或者在MMR中增大lambda_mult参数的大小你当前用的是 MMRMaximum Marginal Relevance检索MMR 在相关性和多样性之间做平衡。lambda_mult 参数控制这个平衡0最大多样性1最大相关性默认值是 0.5。回答相关性Answer Relevancy低----说明LLM答非所问了我们还是先采取改system prompt的策略
返回列表