Ragas 课件课程代码

第二部分

在此基础上,我们只需要补充安装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}")
相关推荐
艾斯特_38 分钟前
工作流与多Agent协作:LangGraph、MCP和A2A的应用分层
人工智能·python·ai
IT小盘1 小时前
04-大模型流式输出原理-SSE与Python实现
开发语言·网络·人工智能·python
我叫黑大帅1 小时前
add()和 __add__() 写法哪个更好呢?
后端·python·面试
不如语冰1 小时前
AI大模型入门-Python进阶-上下文管理与with语句
开发语言·数据结构·数据库·人工智能·pytorch·redis·python
柠檬味的Cat1 小时前
GEO优化系统哪个渠道商好
大数据·人工智能·python
青春不败 177-3266-05202 小时前
基于Python实现的深度学习技术在水文水质领域应用
python·深度学习·机器学习·水文水资源·水质模型
阿童木写作2 小时前
Python批量翻译亚马逊商品图实战教程
开发语言·python·xcode
砚底藏山河2 小时前
多家股票数据接口对比、企业级股票数据API
java·python·金融·maven
Dxy12393102162 小时前
Python Socket入门学习
python
会飞的大鱼人3 小时前
一文搞懂 Java HashSet:把它想成游乐园里只允许一次入场的盖章名单
java·开发语言·windows