第二部分
在此基础上,我们只需要补充安装Ragas评估框架相关的额外依赖即可,本课件依旧使用 uv 作为包管理工具,在你的项目根目录下运行:
python
# 安装 ragas 核心评估框架
uv pip install ragas
# 安装 Ragas 运行所需的额外依赖
uv pip install openai pandas
# 用于格式化展示评估结果的表格工具(可选但推荐)
uv pip install tabulate
安装完之后验证一下 Ragas 是否安装成功:
python
uv pip show ragas
# 应该输出类似:
# Name: ragas
# Version: 0.4.x
# ...
python
# .env 文件内容
DASHSCOPE_API_KEY=sk-你的key
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
DASHSCOPE_MODEL_NAME=qwen-plus
第三部分 四大核心指标
3.1 Faithfulness(忠实度)------ 解决「LLM 幻觉」问题(最核心)
3.1.3 分步代码实战
第一步:导入基础依赖(重点!新版本 API 的导入方式)
python
# 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
第二步:初始化 "评判用的 LLM" 和 Embedding(核心步骤)
python
# 定义异步主函数(因为Ragas的评估方法是异步的,必须写在async函数里)
async def main():
# Step 1:初始化异步OpenAI客户端(对接DashScope API)
client = AsyncOpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
)
# Step 2:初始化LLM和Embedding(新版本AnswerRelevancy需要这两个)
llm = llm_factory("qwen-plus", provider="openai", client=client)
# 初始化Embedding模型,用于计算回答相关性的向量相似度
embeddings = embedding_factory(
"openai",
model="text-embedding-v3", # DashScope的Embedding模型,兼容OpenAI接口
client=client,
interface="modern"
)
print("✅ 评判用LLM和Embedding初始化成功")
第三步:构造测试样本(新手最容易出错的地方,重点看)
python
# 继续在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("✅ 忠实度测试样本构造成功(好样本+坏样本)")
第四步:计算忠实度分数(核心操作,一行代码搞定)
python
# 继续在main()函数里写(接上面的代码)
# Step 4:初始化忠实度评估器,传入评判用的LLM
scorer = Faithfulness(llm=llm)
# 新版本API:ascore要直接传入三个参数,不是传入SingleTurnSample对象!
score_good = await scorer.ascore(
user_input=sample_good.user_input,
response=sample_good.response,
retrieved_contexts=sample_good.retrieved_contexts
)
score_bad = await scorer.ascore(
user_input=sample_bad.user_input,
response=sample_bad.response,
retrieved_contexts=sample_bad.retrieved_contexts
)
第五步:完善结果解读(理解分数含义)
python
# 继续在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回答全是编造的,存在严重幻觉。")
完整代码
python
# 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_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
)
# Step 2:初始化LLM和Embedding(新版本AnswerRelevancy需要这两个)
llm = llm_factory("qwen-plus", provider="openai", client=client)
# 初始化Embedding模型,用于计算回答相关性的向量相似度
embeddings = embedding_factory(
"openai",
model="text-embedding-v3", # DashScope的Embedding模型,兼容OpenAI接口
client=client,
interface="modern"
)
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(llm=llm)
# 新版本API:ascore要直接传入三个参数,不是传入SingleTurnSample对象!
score_good = await scorer.ascore(
user_input=sample_good.user_input,
response=sample_good.response,
retrieved_contexts=sample_good.retrieved_contexts
)
score_bad = await scorer.ascore(
user_input=sample_bad.user_input,
response=sample_bad.response,
retrieved_contexts=sample_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())
3.1.4 运行验证
python
uv run python 01_ragas_basics.py
3.2 Answer Relevancy(回答相关性)------ 解决「答非所问」问题
3.2.3 分步代码实战(基于上一个文件,逐步追加)
第一步:导入 AnswerRelevancy 指标(新增依赖)
python
# 我们已经在第一步的导入里加了,如果你是单独写的话,要确保导入了:
# from ragas.metrics.collections import AnswerRelevancy
第二步:构造 3 个相关性测试样本(切题、完全跑题、部分跑题)
python
# 继续在main()函数里写(接忠实度结果的代码)
# Step 7:构造回答相关性的测试样本(3个,覆盖切题、完全跑题、部分跑题)
# 样本C:切题------回答直接针对了"退款政策"这个问题
sample_on_topic = SingleTurnSample(
user_input="退款政策是什么?",
response="30天内可无理由退款,需提供订单号。",
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("✅ 相关性测试样本构造成功(切题+完全跑题+部分跑题)")
第三步:计算相关性分数
python
# 继续在main()函数里写(接上面的代码)
# Step 8:初始化相关性评估器,新版本需要传入llm和embeddings两个参数!
relevancy_scorer = AnswerRelevancy(llm=llm, embeddings=embeddings)
# 新版本API:ascore只需要user_input和response两个参数
score_c = await relevancy_scorer.ascore(
user_input=sample_on_topic.user_input,
response=sample_on_topic.response
)
score_d = await relevancy_scorer.ascore(
user_input=sample_off_topic.user_input,
response=sample_off_topic.response
)
score_e = await relevancy_scorer.ascore(
user_input=sample_partial.user_input,
response=sample_partial.response
)
第四步:打印结果并解读(重点看不同样本的分数差异)
python
# 继续在main()函数里写(接上面的代码)
# 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 本身的"话多"特性,添加了无关的补充内容")
完整代码
python
# 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_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
)
# Step 2:初始化LLM和Embedding(新版本AnswerRelevancy需要这两个)
llm = llm_factory("qwen-plus", provider="openai", client=client)
# 初始化Embedding模型,用于计算回答相关性的向量相似度
embeddings = embedding_factory(
"openai",
model="text-embedding-v3", # DashScope的Embedding模型,兼容OpenAI接口
client=client,
interface="modern"
)
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(llm=llm)
# 新版本API:ascore要直接传入三个参数,不是传入SingleTurnSample对象!
score_good = await scorer.ascore(
user_input=sample_good.user_input,
response=sample_good.response,
retrieved_contexts=sample_good.retrieved_contexts
)
score_bad = await scorer.ascore(
user_input=sample_bad.user_input,
response=sample_bad.response,
retrieved_contexts=sample_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="退款政策是什么?",
response="30天内可无理由退款,需提供订单号。",
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(llm=llm, embeddings=embeddings)
# 新版本API:ascore只需要user_input和response两个参数
score_c = await relevancy_scorer.ascore(
user_input=sample_on_topic.user_input,
response=sample_on_topic.response
)
score_d = await relevancy_scorer.ascore(
user_input=sample_off_topic.user_input,
response=sample_off_topic.response
)
score_e = await relevancy_scorer.ascore(
user_input=sample_partial.user_input,
response=sample_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())
3.2.4 运行验证
python
uv run python 01_ragas_basics.py
3.3 Context Precision(上下文精确率)------ 解决「检索噪音」问题
3.3.3 分步代码实战
第一步:导入依赖
python
# 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
第二步:初始化 LLM(和之前一样,复用配置)
python
async def main():
# 初始化异步OpenAI客户端
client = AsyncOpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
)
# 初始化LLM
llm = llm_factory("qwen-plus", provider="openai", client=client)
print("✅ 评判用LLM初始化成功")
第三步:构造测试样本(重点!这里要加 reference 字段,也就是标准答案)
# 构造精确率的测试样本
# 注意:这里必须加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(llm=llm)
# 计算分数
score_f = await precision_scorer.ascore(
user_input=sample_noisy.user_input,
reference=sample_noisy.reference,
retrieved_contexts=sample_noisy.retrieved_contexts
)
score_g = await precision_scorer.ascore(
user_input=sample_clean.user_input,
reference=sample_clean.reference,
retrieved_contexts=sample_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 = 部分有用,部分噪音,需要优化检索器")
完整代码
# 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_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
)
# 初始化LLM
llm = llm_factory("qwen-plus", provider="openai", client=client)
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(llm=llm)
# 计算分数
score_f = await precision_scorer.ascore(
user_input=sample_noisy.user_input,
reference=sample_noisy.reference,
retrieved_contexts=sample_noisy.retrieved_contexts
)
score_g = await precision_scorer.ascore(
user_input=sample_clean.user_input,
reference=sample_clean.reference,
retrieved_contexts=sample_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())
3.3.4 运行验证
uv run python 02_ragas_metrics.py
3.4 Context Recall(上下文召回率)------ 解决「检索漏检」问题
3.4.3 分步代码实战
第一步:构造召回率的测试样本
python
# 继续在main()函数里写
# 构造召回率的测试样本
# 样本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 天内申请全额退款,无需说明原因,提供订单号即可。"
]
)
第二步:计算召回率分数
python
# 初始化召回率评估器
recall_scorer = ContextRecall(llm=llm)
# 计算分数
score_h = await recall_scorer.ascore(
user_input=sample_low_recall.user_input,
reference=sample_low_recall.reference,
retrieved_contexts=sample_low_recall.retrieved_contexts
)
score_i = await recall_scorer.ascore(
user_input=sample_full_recall.user_input,
reference=sample_full_recall.reference,
retrieved_contexts=sample_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 = 部分召回,部分遗漏,需要优化检索器")
完整的代码
python
# 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_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
)
# 初始化LLM
llm = llm_factory("qwen-plus", provider="openai", client=client)
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(llm=llm)
# 计算分数
score_f = await precision_scorer.ascore(
user_input=sample_noisy.user_input,
reference=sample_noisy.reference,
retrieved_contexts=sample_noisy.retrieved_contexts
)
score_g = await precision_scorer.ascore(
user_input=sample_clean.user_input,
reference=sample_clean.reference,
retrieved_contexts=sample_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(llm=llm)
# 计算分数
score_h = await recall_scorer.ascore(
user_input=sample_low_recall.user_input,
reference=sample_low_recall.reference,
retrieved_contexts=sample_low_recall.retrieved_contexts
)
score_i = await recall_scorer.ascore(
user_input=sample_full_recall.user_input,
reference=sample_full_recall.reference,
retrieved_contexts=sample_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())
3.4.4 运行验证
python
uv run python 02_ragas_metrics.py
第四部分:评估器封装(可复用代码,一键调用)
4.2 完整封装代码(新建rag_evaluator.py)
python
# rag_evaluator.py
# 目标:封装RAG评估器,复用四大核心指标的评估逻辑,支持单样本/批量样本测试
# 适配Ragas 0.4+新版本API
import asyncio
import os
from dotenv import load_dotenv
from ragas import SingleTurnSample
# 新版本:所有指标都从collections导入
from ragas.metrics.collections import (
Faithfulness,
AnswerRelevancy,
ContextPrecision,
ContextRecall
)
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory
from openai import AsyncOpenAI
# 加载环境变量(封装在类初始化中,无需外部单独调用)
load_dotenv()
class RAGEvaluator:
"""RAG系统评估器,封装四大核心指标,支持单样本和批量样本评估"""
def __init__(self, model: str = "qwen-plus"):
"""
初始化评估器(自动完成LLM和评估器初始化)
:param model: 评估用的LLM模型,默认qwen-plus,和.env文件中保持一致
"""
# 1. 初始化异步OpenAI客户端(对接DashScope API)
self.client = self._init_client()
# 2. 初始化Ragas可识别的LLM和Embedding
self.llm = self._init_llm(model)
self.embeddings = self._init_embeddings()
# 3. 初始化四大核心指标的评估器(一次性初始化,后续可重复调用)
self.evaluators = self._init_evaluators()
print("✅ RAG评估器初始化完成,可直接调用评估方法!")
def _init_client(self) -> AsyncOpenAI:
"""私有方法:初始化异步OpenAI客户端(内部调用,外部无需关注)"""
# 从.env文件中获取配置,兜底地址防止配置缺失
api_key = os.getenv("DASHSCOPE_API_KEY")
base_url = os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 校验API Key是否存在(避免后续报错)
if not api_key:
raise ValueError("❌ 未找到DASHSCOPE_API_KEY,请检查.env文件配置!")
return AsyncOpenAI(
api_key=api_key,
base_url=base_url
)
def _init_llm(self, model: str) -> object:
"""私有方法:初始化Ragas可识别的LLM(内部调用,外部无需关注)"""
return llm_factory(
model=model,
provider="openai", # 固定写法,对接DashScope兼容OpenAI接口
client=self.client
)
def _init_embeddings(self) -> object:
"""私有方法:初始化Ragas可识别的Embedding(内部调用,外部无需关注)"""
return embedding_factory(
"openai",
model="text-embedding-v3",
client=self.client,
interface="modern"
)
def _init_evaluators(self) -> dict:
"""私有方法:初始化四大指标评估器,返回字典便于调用(内部调用)"""
return {
"faithfulness": Faithfulness(llm=self.llm), # 忠实度
"answer_relevancy": AnswerRelevancy(llm=self.llm, embeddings=self.embeddings), # 回答相关性
"context_precision": ContextPrecision(llm=self.llm), # 上下文精确率
"context_recall": ContextRecall(llm=self.llm) # 上下文召回率
}
async def evaluate_single_sample(self, sample: SingleTurnSample) -> dict:
"""
评估单一样本,返回该样本的四大指标分数
:param sample: SingleTurnSample对象,包含user_input、response、retrieved_contexts,需标准答案的指标需加reference
:return: 字典格式,key为指标名,value为分数(保留3位小数)
"""
# 存储单个样本的评估结果
result = {}
# 1. 计算忠实度
score = await self.evaluators["faithfulness"].ascore(
user_input=sample.user_input,
response=sample.response,
retrieved_contexts=sample.retrieved_contexts
)
result["faithfulness"] = round(score.value, 3)
# 2. 计算回答相关性
score = await self.evaluators["answer_relevancy"].ascore(
user_input=sample.user_input,
response=sample.response
)
result["answer_relevancy"] = round(score.value, 3)
# 3. 计算上下文精确率(如果有reference的话)
if sample.reference:
score = await self.evaluators["context_precision"].ascore(
user_input=sample.user_input,
reference=sample.reference,
retrieved_contexts=sample.retrieved_contexts
)
result["context_precision"] = round(score.value, 3)
# 4. 计算上下文召回率(如果有reference的话)
if sample.reference:
score = await self.evaluators["context_recall"].ascore(
user_input=sample.user_input,
reference=sample.reference,
retrieved_contexts=sample.retrieved_contexts
)
result["context_recall"] = round(score.value, 3)
return result
async def evaluate_batch_samples(self, samples: list[SingleTurnSample]) -> list[dict]:
"""
批量评估多个样本,返回每个样本的四大指标分数
:param samples: SingleTurnSample对象列表,可包含多个样本
:return: 列表格式,每个元素是单个样本的评估结果字典
"""
# 用asyncio.gather批量执行异步评估,提升效率
batch_results = await asyncio.gather(
*[self.evaluate_single_sample(sample) for sample in samples]
)
return batch_results
def format_result(self, sample: SingleTurnSample, result: dict) -> str:
"""
格式化评估结果,让输出更直观(可选调用,新手推荐使用)
:param sample: 被评估的样本
:param result: 该样本的评估结果字典
:return: 格式化后的字符串,便于打印查看
"""
# 拼接格式化内容
format_str = f"\n{'=' * 60}\n"
format_str += f"📋 测试样本:{sample.user_input}\n"
format_str += f"{'=' * 60}\n"
# 拼接各个指标的分数
if "faithfulness" in result:
format_str += f"忠实度(无幻觉): {result['faithfulness']:.3f} | "
if "answer_relevancy" in result:
format_str += f"回答相关性(不跑题): {result['answer_relevancy']:.3f}\n"
if "context_precision" in result:
format_str += f"上下文精确率(无噪音): {result['context_precision']:.3f} | "
if "context_recall" in result:
format_str += f"上下文召回率(无漏检): {result['context_recall']:.3f}\n"
format_str += f"{'=' * 60}\n"
return format_str
# 测试代码(可选,运行该文件可验证封装是否成功)
async def test_evaluator():
# 1. 创建评估器实例(自动初始化LLM和评估器)
evaluator = RAGEvaluator(model="qwen-plus")
# 2. 构造测试样本(包含reference,适配所有指标)
test_sample = SingleTurnSample(
user_input="退款政策是什么?",
response="购买后30天内可申请无理由全额退款,只需提供订单号。",
reference="用户可在购买后30天内申请全额退款,无需说明原因,提供订单号即可。",
retrieved_contexts=[
"退款政策:所有用户可在购买后 30 天内申请全额退款,无需说明原因,提供订单号即可。",
"配送政策:全国包邮,3-5天送达。" # 增加噪音,测试精确率
]
)
# 3. 评估单一样本
result = await evaluator.evaluate_single_sample(test_sample)
# 4. 格式化并打印结果
print(evaluator.format_result(test_sample, result))
# 运行测试代码(直接运行rag_evaluator.py即可验证)
if __name__ == "__main__":
asyncio.run(test_evaluator())
4.4 运行验证(新手必做)
python
uv run python rag_evaluator.py
第五部分:对接你的 PDF 阅读助手,跑出第一份真实评估报告
5.2 改造 main.py:新增 ask_with_contexts() 方法
python
# main.py(在 ask() 方法后追加,紧接着 ask() 方法,其余代码完全不变)
def ask_with_contexts(self, question: str) -> tuple[str, list[str]]:
"""
向助手提问,同时返回 LLM 的回答 和 检索到的上下文片段列表
专门为 Ragas 评估设计,原有的 ask() 方法完全不受影响。
Args:
question: 用户问题(和 ask() 一样传字符串就行)
Returns:
tuple:
- answer (str): LLM 生成的回答
- retrieved_contexts (list[str]): 检索到的文档片段纯文本列表
"""
# Step 1:直接调用 self.retriever(就是 __init__ 里初始化的那个检索器)
# 使用 invoke() 而不是 get_relevant_documents(),后者在新版 LangChain 里已废弃
# 这里的检索结果和 rag_chain 内部的检索结果完全一致,不会"作弊"
retrieved_docs = self.retriever.invoke(question)
# Step 2:把 LangChain 的 Document 对象列表,转成 Ragas 需要的纯文本字符串列表
# Ragas 的 SingleTurnSample.retrieved_contexts 要求是 list[str],不是 list[Document]
# doc.page_content 就是文档片段的纯文本内容
retrieved_contexts = [doc.page_content for doc in retrieved_docs]
# Step 3:调用原有的 ask() 方法,拿到 LLM 生成的回答
# 复用 ask() 的好处:对话历史维护、历史截断等逻辑完全不需要重写
answer = self.ask(question)
# Step 4:把回答和检索片段一起返回,供 Ragas 使用
return answer, retrieved_contexts
为什么测试数据集要基于你的 PDF?
python
[
{
"user_input": "什么是机器学习?它有哪些类型?",
"reference": "机器学习是人工智能的核心分支,它让计算机从数据中学习规律并进行预测,而无需显式编程。常见的机器学习类型分为监督学习、无监督学习和强化学习。"
},
{
"user_input": "大语言模型的训练过程分为哪几个阶段?",
"reference": "主流大模型训练过程包含预训练、微调、对齐三个阶段,对齐阶段通常使用人类反馈强化学习(RLHF)提升模型安全性与实用性。"
},
{
"user_input": "大语言模型是基于什么架构的?它如何理解文本中的长距离依赖?",
"reference": "大语言模型(LLM)基于 Transformer 架构,通过海量文本数据进行预训练,学习语言的语法、语义、知识和逻辑。模型通过自注意力机制捕捉文本中的长距离依赖关系,从而实现理解、生成、翻译、摘要等任务。"
},
{
"user_input": "RAG 的完整工作流程是什么?",
"reference": "RAG 工作流程主要分为:文档加载与解析、文本分块(Chunking)、向量化与向量数据库存储、用户问题向量化、相似度检索召回相关片段、将检索内容拼接为上下文输入大模型生成答案。"
},
{
"user_input": "向量数据库是做什么用的?常见的有哪些?",
"reference": "向量数据库专门用于存储和检索高维向量数据,通过近似最近邻搜索(ANN)快速找到与查询向量最相似的文本片段。常见向量数据库包括:Chroma、FAISS、Milvus、Pinecone、Qdrant 等。"
},
{
"user_input": "RAG 有哪些常见的优化手段?",
"reference": "RAG 常见优化手段包括:优化分块策略调整块大小与重叠长度、使用更优质的嵌入模型、多路召回与重排序(Rerank)提升相关性、加入关键词检索弥补纯向量检索不足、对检索结果进行过滤去重精简、优化提示词让模型更好利用检索信息。"
},
{
"user_input": "RAG 技术解决了大模型的哪些问题?",
"reference": "RAG 是一种结合检索与生成的技术,用于解决大模型知识过时、幻觉、隐私数据无法访问等问题。"
},
{
"user_input": "向量数据库的性能对 RAG 系统有什么影响?",
"reference": "向量数据库的性能直接影响 RAG 系统的召回准确率与响应速度。"
}
]
5.4 完整评估入口脚本 run_evaluation.py
第一步:导入依赖(关键!需要从你的 main.py 导入助手类)
python
# run_evaluation.py
# 目标:对接 PDFReadingAssistant,批量跑出评估报告
# 前提:已完成 5.2 中 main.py 的改造(新增了 ask_with_contexts 方法)
# 适配 Ragas 0.4+ 新版本 API
import asyncio # Ragas 评估方法是异步的,必须用 asyncio 运行
import json # 读取 test_dataset.json 测试数据集
import os # 读取环境变量(API Key 等)
from dotenv import load_dotenv
# 从你的 main.py 导入 PDFReadingAssistant 类
# 注意:确保 main.py 和 run_evaluation.py 在同一个目录下
from main import PDFReadingAssistant
# 从第四部分封装好的评估器导入 RAGEvaluator 类
from rag_evaluator import RAGEvaluator
# Ragas 的样本格式
from ragas import SingleTurnSample
# 加载 .env 文件里的环境变量(DASHSCOPE_API_KEY 等)
load_dotenv()
第二步:初始化 PDF 助手和评估器
python
async def run_full_evaluation():
"""完整评估流程主函数"""
print("=" * 70)
print("🚀 PDF 阅读助手 × Ragas 完整评估开始")
print("=" * 70)
# -----------------------------------------------
# Step 1:初始化你的 PDF 阅读助手
# 这里用 documents 目录,和你 main.py 里的 main() 函数保持一致
# 如果你的 PDF 在其他目录,修改这里的路径即可
# -----------------------------------------------
print("\n📚 Step 1:初始化 PDF 阅读助手...")
print("(第一次运行会加载 PDF、分割文档、创建向量库,稍等片刻)")
assistant = PDFReadingAssistant(
pdf_directory="documents" # 修改为你的 PDF 文件目录
)
print("✅ PDF 阅读助手初始化完成!")
# -----------------------------------------------
# Step 2:初始化 Ragas 评估器
# 复用第四部分封装好的 RAGEvaluator,不需要重复写初始化代码
# -----------------------------------------------
print("\n🔧 Step 2:初始化 Ragas 评估器...")
evaluator = RAGEvaluator(model="qwen-plus")
print("✅ Ragas 评估器初始化完成!")
第三步:加载测试数据集
python
# -----------------------------------------------
# Step 3:加载测试数据集
# 从 test_dataset.json 读取所有测试问题和标准答案
# -----------------------------------------------
print("\n📋 Step 3:加载测试数据集...")
with open("test_dataset.json", "r", encoding="utf-8") as f:
test_data = json.load(f)
print(f"✅ 共加载 {len(test_data)} 个测试案例")
第四步:调用助手,批量生成问答数据(核心!)
python
# -----------------------------------------------
# Step 4:逐个问题调用 ask_with_contexts(),收集真实的问答数据
# 注意:这里用的是我们在 5.2 中新增的 ask_with_contexts(),不是 ask()
# -----------------------------------------------
print("\n🔍 Step 4:调用 PDF 助手,收集真实问答数据...")
print("(每个问题会触发一次检索 + 一次 LLM 推理,请稍等)")
samples = []
for i, item in enumerate(test_data):
question = item["user_input"]
reference = item["reference"] # 人工标注的标准答案
print(f" [{i+1}/{len(test_data)}] 正在处理:{question[:30]}...")
# 调用 ask_with_contexts(),同时拿到:
# - answer: LLM 生成的回答
# - retrieved_contexts: 检索到的文档片段列表(list[str])
# 注意:ask_with_contexts() 是同步方法,不需要 await
answer, retrieved_contexts = assistant.ask_with_contexts(question)
# 打印检索到了几个片段(你的检索器配置了 k=4,通常返回 4 个)
print(f" → 检索到 {len(retrieved_contexts)} 个片段,LLM 已生成回答")
# 构造 Ragas 的 SingleTurnSample 对象
# 包含四大指标需要的所有字段:user_input、response、reference、retrieved_contexts
sample = SingleTurnSample(
user_input=question,
response=answer,
reference=reference, # 标准答案(Context Precision/Recall 需要)
retrieved_contexts=retrieved_contexts # 检索片段(Faithfulness 等需要)
)
samples.append(sample)
print(f"\n✅ 问答数据收集完成,共 {len(samples)} 个样本")
# 清除对话历史,避免多轮历史影响评估结果
# 因为评估时每个问题是独立的,不应该有上下文关联
assistant.clear_history()
第五步:调用评估器,批量计算四大指标分数
python
# -----------------------------------------------
# Step 5:调用 RAGEvaluator,批量评估所有样本
# evaluate_batch_samples() 内部用 asyncio.gather 并发执行,速度更快
# -----------------------------------------------
print("\n📊 Step 5:Ragas 评估中(这一步要调用 LLM 判卷,会花一点时间)...")
results = await evaluator.evaluate_batch_samples(samples)
print("✅ 评估完成!")
第六步:生成评估报告,保存到文件
python
# -----------------------------------------------
# Step 6:生成并打印完整评估报告
# -----------------------------------------------
print("\n" + "=" * 70)
print("📈 完整评估报告")
print("=" * 70)
# 计算四大指标的平均分数(所有样本分数的算术平均值)
faithfulness_scores = [r["faithfulness"] for r in results]
relevancy_scores = [r["answer_relevancy"] for r in results]
precision_scores = [r.get("context_precision", 0) for r in results]
recall_scores = [r.get("context_recall", 0) for r in results]
avg_faithfulness = sum(faithfulness_scores) / len(faithfulness_scores)
avg_relevancy = sum(relevancy_scores) / len(relevancy_scores)
avg_precision = sum(precision_scores) / len(precision_scores)
avg_recall = sum(recall_scores) / len(recall_scores)
# 打印总体平均分数(一眼看出系统的整体水平)
print("\n📌 系统整体平均分数:")
print(f" 忠实度(Faithfulness) : {avg_faithfulness:.3f} {'✅ 优秀' if avg_faithfulness >= 0.9 else '⚠️ 需优化' if avg_faithfulness >= 0.7 else '❌ 严重问题'}")
print(f" 回答相关性(Answer Relevancy): {avg_relevancy:.3f} {'✅ 优秀' if avg_relevancy >= 0.9 else '⚠️ 需优化' if avg_relevancy >= 0.7 else '❌ 严重问题'}")
print(f" 上下文精确率(Context Prec.) : {avg_precision:.3f} {'✅ 优秀' if avg_precision >= 0.9 else '⚠️ 需优化' if avg_precision >= 0.7 else '❌ 严重问题'}")
print(f" 上下文召回率(Context Rec.) : {avg_recall:.3f} {'✅ 优秀' if avg_recall >= 0.9 else '⚠️ 需优化' if avg_recall >= 0.7 else '❌ 严重问题'}")
# 打印每个样本的详细结果
print("\n📋 逐样本详细结果:")
for i, (sample, result) in enumerate(zip(samples, results)):
print(evaluator.format_result(sample, result))
# -----------------------------------------------
# 把完整报告保存到 JSON 文件,方便后续分析
# -----------------------------------------------
report = {
"summary": {
"total_samples": len(samples),
"average_scores": {
"faithfulness": round(avg_faithfulness, 3),
"answer_relevancy": round(avg_relevancy, 3),
"context_precision": round(avg_precision, 3),
"context_recall": round(avg_recall, 3)
}
},
"detailed_results": [
{
"question": samples[i].user_input,
"response": samples[i].response,
"reference": samples[i].reference,
"retrieved_contexts": samples[i].retrieved_contexts,
"scores": results[i]
}
for i in range(len(samples))
]
}
with open("evaluation_report.json", "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print("\n✅ 完整报告已保存到 evaluation_report.json")
print("=" * 70)
# 程序入口
if __name__ == "__main__":
asyncio.run(run_full_evaluation())
5.5 完整代码
python
# run_evaluation.py
# 目标:对接 PDFReadingAssistant,批量跑出完整评估报告
# 前提:已完成 5.2 中 main.py 的改造(新增了 ask_with_contexts 方法)
# 适配 Ragas 0.4+ 新版本 API
import asyncio
import json
import os
from dotenv import load_dotenv
from main import PDFReadingAssistant
from rag_evaluator import RAGEvaluator
from ragas import SingleTurnSample
load_dotenv()
async def run_full_evaluation():
"""完整评估流程主函数"""
print("=" * 70)
print("🚀 PDF 阅读助手 × Ragas 完整评估开始")
print("=" * 70)
# Step 1:初始化 PDF 阅读助手
print("\n📚 Step 1:初始化 PDF 阅读助手...")
assistant = PDFReadingAssistant(pdf_directory="documents")
print("✅ PDF 阅读助手初始化完成!")
# Step 2:初始化 Ragas 评估器
print("\n🔧 Step 2:初始化 Ragas 评估器...")
evaluator = RAGEvaluator(model="qwen-plus")
print("✅ Ragas 评估器初始化完成!")
# Step 3:加载测试数据集
print("\n📋 Step 3:加载测试数据集...")
with open("test_dataset.json", "r", encoding="utf-8") as f:
test_data = json.load(f)
print(f"✅ 共加载 {len(test_data)} 个测试案例")
# Step 4:逐个问题调用助手,收集真实问答数据
print("\n🔍 Step 4:调用 PDF 助手,收集真实问答数据...")
samples = []
for i, item in enumerate(test_data):
question = item["user_input"]
reference = item["reference"]
print(f" [{i+1}/{len(test_data)}] 正在处理:{question[:30]}...")
answer, retrieved_contexts = assistant.ask_with_contexts(question)
print(f" → 检索到 {len(retrieved_contexts)} 个片段,LLM 已生成回答")
sample = SingleTurnSample(
user_input=question,
response=answer,
reference=reference,
retrieved_contexts=retrieved_contexts
)
samples.append(sample)
print(f"\n✅ 问答数据收集完成,共 {len(samples)} 个样本")
assistant.clear_history()
# Step 5:Ragas 批量评估
print("\n📊 Step 5:Ragas 评估中(调用 LLM 判卷,稍等)...")
results = await evaluator.evaluate_batch_samples(samples)
print("✅ 评估完成!")
# Step 6:生成完整评估报告
print("\n" + "=" * 70)
print("📈 完整评估报告")
print("=" * 70)
faithfulness_scores = [r["faithfulness"] for r in results]
relevancy_scores = [r["answer_relevancy"] for r in results]
precision_scores = [r.get("context_precision", 0) for r in results]
recall_scores = [r.get("context_recall", 0) for r in results]
avg_faithfulness = sum(faithfulness_scores) / len(faithfulness_scores)
avg_relevancy = sum(relevancy_scores) / len(relevancy_scores)
avg_precision = sum(precision_scores) / len(precision_scores)
avg_recall = sum(recall_scores) / len(recall_scores)
print("\n📌 系统整体平均分数:")
print(f" 忠实度(Faithfulness) : {avg_faithfulness:.3f} {'✅ 优秀' if avg_faithfulness >= 0.9 else '⚠️ 需优化' if avg_faithfulness >= 0.7 else '❌ 严重问题'}")
print(f" 回答相关性(Answer Relevancy): {avg_relevancy:.3f} {'✅ 优秀' if avg_relevancy >= 0.9 else '⚠️ 需优化' if avg_relevancy >= 0.7 else '❌ 严重问题'}")
print(f" 上下文精确率(Context Prec.) : {avg_precision:.3f} {'✅ 优秀' if avg_precision >= 0.9 else '⚠️ 需优化' if avg_precision >= 0.7 else '❌ 严重问题'}")
print(f" 上下文召回率(Context Rec.) : {avg_recall:.3f} {'✅ 优秀' if avg_recall >= 0.9 else '⚠️ 需优化' if avg_recall >= 0.7 else '❌ 严重问题'}")
print("\n📋 逐样本详细结果:")
for sample, result in zip(samples, results):
print(evaluator.format_result(sample, result))
report = {
"summary": {
"total_samples": len(samples),
"average_scores": {
"faithfulness": round(avg_faithfulness, 3),
"answer_relevancy": round(avg_relevancy, 3),
"context_precision": round(avg_precision, 3),
"context_recall": round(avg_recall, 3)
}
},
"detailed_results": [
{
"question": samples[i].user_input,
"response": samples[i].response,
"reference": samples[i].reference,
"retrieved_contexts": samples[i].retrieved_contexts,
"scores": results[i]
}
for i in range(len(samples))
]
}
with open("evaluation_report.json", "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print("\n✅ 完整报告已保存到 evaluation_report.json")
print("=" * 70)
if __name__ == "__main__":
asyncio.run(run_full_evaluation())
5.6 运行验证
python
uv run python run_evaluation.py
第六部分:报告解读与针对性优化(完全基于你的代码参数)
6.2 忠实度(Faithfulness)低 → 怎么改你的代码
python
# main.py 当前的 Prompt(位于 _create_rag_chain() 方法中)
prompt = ChatPromptTemplate.from_messages([
("system", """你是一个专业的 PDF 文档阅读助手。你的任务是根据提供的文档内容回答用户问题。
规则:
1. 只使用提供的文档内容回答问题
2. 如果文档中没有相关信息,请诚实告知
3. 在回答时,请指出信息来源(如"根据第X页...")
4. 回答要准确、简洁、有条理
参考文档:
{context}"""),
...
])
优化方案:把 Prompt 改成更强约束的版本
python
# main.py → _create_rag_chain() 方法(优化后的 Prompt)
prompt = ChatPromptTemplate.from_messages([
("system", """你是一个严格基于文档内容回答问题的 PDF 阅读助手。
【严格规则】
1. 你的回答必须 100% 基于下方【参考文档】中的内容,不得引入文档之外的任何知识。
2. 如果参考文档中没有与问题相关的内容,你必须明确回答:"根据提供的文档,无法找到关于该问题的相关信息。"
3. 不允许进行任何推断、扩展或补全------文档里没有写的,你也不能说。
4. 引用文档内容时,请标注来源,格式:(来源:文件名 第X页)
【参考文档】
{context}"""),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}")
])
优化逻辑(为什么这样改可以提升忠实度):
python
# main.py → __init__ 方法(调整 temperature)
self.llm = ChatOpenAI(
model=os.getenv("DASHSCOPE_MODEL_NAME", "qwen-plus"),
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL"),
temperature=0.1, # 从 0.3 降低到 0.1,让 LLM 更保守、更不容易发散
)
6.3 上下文召回率(Context Recall)低 → 怎么改你的代码
6.3.1 优化方向一:增加 Top-K 数量
python
# main.py → __init__ 方法(当前配置)
self.retriever = self.vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "fetch_k": 10} # 当前:返回 4 个,候选池 10 个
)
如果召回率低,把 k 从 4 改成 6,把候选池从 10 改成 20:
python
# main.py → __init__ 方法(召回率优化版)
self.retriever = self.vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 6, # 从 4 增加到 6,给 LLM 更多的参考片段
"fetch_k": 20 # 候选池从 10 增加到 20,候选越多,最终选出的片段越多样
}
)
6.3.2 优化方向二:调整分块策略
python
# main.py → _split_documents() 方法(当前配置)
splitter = RecursiveCharacterTextSplitter(
chunk_size=400, # 每个片段最大 400 个字符
chunk_overlap=50, # 相邻片段重叠 50 个字符
separators=["\n\n", "\n", "。", ".", "!", "?", " ", ""]
)
优化方案:增大 chunk_size ,并增加 chunk_overlap :
python
# main.py → _split_documents() 方法(召回率优化版)
splitter = RecursiveCharacterTextSplitter(
chunk_size=600, # 从 400 增大到 600,让更完整的语义单元在同一个片段里
chunk_overlap=100, # 从 50 增大到 100,更多的重叠可以减少关键信息被切断的概率
separators=["\n\n", "\n", "。", ".", "!", "?", " ", ""]
)
改完 chunk_size 之后,必须删除旧的向量库,让它重新创建:
python
# 删除旧的向量库目录(Windows 用 rd /s /q pdf_assistant_db)
rm -rf pdf_assistant_db/
# 重新运行评估,会自动重建向量库
uv run python run_evaluation.py
6.4 上下文精确率(Context Precision)低 → 怎么改你的代码
优化方向一:减小 k 的数量
python
# main.py → __init__ 方法(精确率优化版)
self.retriever = self.vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 3, # 从 4 减少到 3,更严格地过滤噪音
"fetch_k": 10
}
)
优化方向二:在 MMR 参数里增加 lambda_mult (相关性权重)
python
# main.py → __init__ 方法(增加 lambda_mult 提升精确率)
self.retriever = self.vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 4,
"fetch_k": 10,
"lambda_mult": 0.7 # 从默认 0.5 提升到 0.7,更偏向相关性,减少噪音
}
)
6.5 回答相关性(Answer Relevancy)低 → 怎么改你的代码
python
# main.py → _create_rag_chain() 方法(忠实度 + 相关性双优化版 Prompt)
prompt = ChatPromptTemplate.from_messages([
("system", """你是一个严格基于文档内容回答问题的 PDF 阅读助手。
【严格规则】
1. 你的回答必须 100% 基于下方【参考文档】中的内容,不得引入文档之外的任何知识。
2. 如果参考文档中没有与问题相关的内容,你必须明确回答:"根据提供的文档,无法找到关于该问题的相关信息。"
3. 不允许进行任何推断、扩展或补全------文档里没有写的,你也不能说。
4. 引用文档内容时,请标注来源,格式:(来源:文件名 第X页)
5. 【直接回答】不要在回答前加任何背景介绍、铺垫或无关的上下文,直接回答用户的问题。
【参考文档】
{context}"""),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}")
])
第七部分:实践作业(自己练习即可!)
7.2 阶段一:跑通基础评估
python
# 新建 test_ask_with_contexts.py,运行这个文件验证改造
from main import PDFReadingAssistant
# 初始化助手
assistant = PDFReadingAssistant(pdf_directory="documents")
# 调用新方法
answer, contexts = assistant.ask_with_contexts("什么是机器学习?")
print(f"回答:{answer[:100]}...")
print(f"检索到 {len(contexts)} 个片段")
print(f"第一个片段:{contexts[0][:80]}...")
7.4 阶段三:进阶挑战(选做)
挑战 1:给评估报告增加可视化
python
# 在 run_evaluation.py 末尾追加(需要 pip install pandas tabulate)
import pandas as pd
# 构造评估结果 DataFrame
rows = []
for i, (sample, result) in enumerate(zip(samples, results)):
rows.append({
"编号": i + 1,
"问题": sample.user_input[:20] + "...",
"忠实度": result["faithfulness"],
"相关性": result["answer_relevancy"],
"精确率": result.get("context_precision", "-"),
"召回率": result.get("context_recall", "-")
})
df = pd.DataFrame(rows)
print("\n📊 评估结果汇总表:")
print(df.to_string(index=False))
print(df.to_string(index=False))
挑战 2:自动识别问题样本
python
def find_worst_samples(samples, results, top_n=3):
"""找出分数最低的 N 个样本,帮助快速定位问题"""
# 找忠实度最低的样本
sorted_by_faith = sorted(
zip(samples, results),
key=lambda x: x[1]["faithfulness"]
)
print(f"\n⚠️ 忠实度最低的 {top_n} 个样本(可能存在幻觉):")
for sample, result in sorted_by_faith[:top_n]:
print(f" [{result['faithfulness']:.3f}] {sample.user_input}")
print(f" 助手回答:{sample.response[:60]}...")
# 找召回率最低的样本
sorted_by_recall = sorted(
zip(samples, results),
key=lambda x: x[1].get("context_recall", 1)
)
print(f"\n⚠️ 召回率最低的 {top_n} 个样本(可能漏检了关键信息):")
for sample, result in sorted_by_recall[:top_n]:
print(f" [{result.get('context_recall', '-'):.3f}] {sample.user_input}")