【大模型应用开发 4.RAG高级技术与实践】
目录
一、RAG技术树
二、RAFT方法
三、RAG高效召回方法
1.合理设置 Top-k
2.改进索引算法
3.引入重排序(Reranking)
4.优化查询扩展
Ⅰ、初始化大语言模型(阿里百炼)
Ⅱ、创建嵌入模型
Ⅲ、加载Faiss向量数据库
Ⅳ、创建多路查询检索器
Ⅴ、完整代码
5.双向改写
Ⅰ、Query2Doc:将查询改写成文档
Ⅱ、Doc2Query:为文档生成关联查询
6.索引拓展
Ⅰ、离散索引
① 关键词抽取
② 实体识别
Ⅱ、混合索引召回
Ⅲ、Small-to-Big
四、Qwen-Agent构建RAG
1.基本介绍
2.使用Qwen-Agent将上下文记忆扩展到百万量级
3.构建智能体
Ⅰ、Level1 检索
Ⅱ、Level2 分块检索
Ⅲ、Level3 逐步推理
4.RAG评测结果
5.Qwen-Agent使用
Ⅰ、安装
Ⅱ、日志捕捉器
Ⅲ、模型配置
Ⅳ、创建Agent智能体
Ⅴ、作为聊天机器人运行智能体
Ⅵ、分析观察日志并打印
Ⅶ、完整代码
6.Qwen-Agent加载docs多文档知识库
Ⅰ、加载解析文件夹
Ⅱ、模型配置
Ⅲ、创建Agent智能体
Ⅳ、作为聊天机器人运行智能体【流式输出】
Ⅴ、完整代码
五、RAG质量评估
1.RAG 三元组
2.RAGAs评估
Ⅰ、什么是Ragas评估
Ⅱ、评估指标
① 忠实度(faithfulness)
② 答案相关性(Answer relevancy)
③ 上下文精度(Context precision)
④ 上下文召回率(Context recall)
⑤ 上下文相关性
5.Ragas评估实操
Ⅰ、父文档检索器
Ⅱ、检索完整文档
Ⅲ、检索较大文档块
Ⅳ、代码实操
① 导入依赖库(工具准备)
② 数据输入(加载文档)
③ 模型配置
④ 文档预处理与检索器构建
⑤ 构建问答链测试功能
⑥ 批量查询与数据收集
⑦ RAG效果评测
完整代码
六、商业落地实施RAG工程的核心步骤
1.数据集的准备(语料)
2.测试集的准备(QA对)
3.技术选型
⭐核心差异及应用场景对比表格
4.构建知识库
5.测试和优化
6.最终效果评估
7.生产环境部署
不断成长,直到你无法联系的人与你联系
—— 25.8.12
一、RAG技术树
-
RAG研究的技术树主要涉及预训练(Pre-training)、微调(Fine-tuning)和推理(Inference)等阶段。
-
随着LLM的出现,RAG的研究最初侧重于利用LLMs强大的上下文学习能力,主要集中在推理阶段(Inference)。
-
随后的研究进一步深入,逐渐与LLMs的微调阶段更加融合。研究人员也在探索通过检索增强技术来提升预训练阶段的语言模型性能。
参考文章:Retrieval Augmented Generation (RAG) for LLMs | Prompt Engineering Guide
二、RAFT方法
RAFT方法(Retrieval Augmented Fine Tuning)
RAFT:Adapting Language Model to Domain Specific RAG, 2024 https://arxiv.org/pdf/2403.10131
- 基于微调的方法通过“学习”来实现“记忆”输入文档或回答练习题而不参考文档。
- 或者,基于上下文检索的方法未能利用固定领域所提供的学习机会,相当于参加开卷考试但没有事先复习。
- 相比之下,我们的方法RAFT利用了微调与问答对,并在一个模拟的不完美检索环境中参考文档 —— 从而有效地为开卷考试环境做准备。
让LLMs从一组正面和干扰文档中读取解决方案,这与标准的RAG设置形成对比,因为在标准的RAG设置中,模型是基于检索器输出进行训练的,这包含了记忆和阅读的混合体。在测试时,所有方法都遵循标准的RAG设置,即提供上下文中排名前k的检索文档。
微调数据集样例:
RAFT在所有专业领域的RAG性能上有所提升(在PubMed、HotPot、HuggingFace、Torch Hub和TensorflowHub等多个领域),领域特定的微调提高了基础模型的性能,RAFT无论是在有RAG的情况下还是没有RAG的情况下,都持续优于现有的领域特定微调方法。这表明了需要在上下文中训练模型。
RAFT(Retrieval-Augmented Fine-Tuning)的训练数据集通过 三类数据样本 构建,核心是让模型同时学习 基础问答能力、零样本泛化能力 和 检索增强推理能力
① 问题 - 答案对(Question-Answer Pairs)
每个样本包含一个问题(Q)和基于领域知识生成的标准答案(A*)。答案不仅包含最终结论,还需包含思维链(Chain-of-Thought),即详细的推理过程,并明确引用文档中的具体内容(如使用##begin_quote##
和##end_quote##
标记)。这种设计迫使模型学习如何从文档中提取信息并构建逻辑链条。
② 相关文档(黄金文档,Golden Docs)
每个问题关联一个或多个黄金文档(D*),这些文档包含回答问题所需的全部关键信息。例如,在医疗领域中,黄金文档可能是权威医学指南或研究论文。训练时,模型需从黄金文档中定位并引用相关片段,确保答案的准确性和可追溯性。
③ 干扰文档(Distractor Docs)
除黄金文档外,训练数据还包含多个干扰文档(Di),这些文档与问题无关或仅包含冗余信息。例如,在法律领域中,干扰文档可能是其他领域的判决书或过时的法规。通过混合干扰文档,模型需学会区分有用和无用信息,提升对检索噪声的鲁棒性。
总结:
RAFT方法(Retrieval Augmented Fine Tuning):
-
适应特定领域的LLMs对于许多新兴应用至关重要,但如何有效融入信息仍是一个开放问题。
-
RAFT结合了检索增强生成(RAG)和监督微调(SFT),从而提高模型在特定领域内回答问题的能力。
-
训练模型识别并忽略那些不能帮助回答问题的干扰文档,只关注和引用相关的文档。
-
通过在训练中引入干扰文档,提高模型对干扰信息的鲁棒性,使其在测试时能更好地处理检索到的文档。
三、RAG高效召回方法
1.合理设置 Top-k
RAG检索中 top-k 的选择需平衡召回率与噪音:优先看检索模型精度(高精度选5-10,低精度可10-20,并配合重排序),结合文档特性(短文档 / 密集信息用3-5,长文档 / 分散信息用10-15),适配模型上下文窗口避免截断,同时按需调整(精准场景小k,复杂场景适当放大)。
向量数据库对象.similarity_search():是LangChain封装的向量数据库中用于向量相似性检索的核心方法。它通过输入一个查询向量,在数据库的向量集合中计算其与其他向量的相似度(如余弦相似度、欧氏距离等),最终返回最相似的 top-k 条结果(通常包含向量对应的原始数据、元信息及相似度分数)。
参数名 | 是否必填 | 作用 |
---|---|---|
query_vector | 是 | 用于检索的查询向量(与数据库中存储的向量维度需一致)。 |
k | 否(默认值通常为 10) | 指定返回最相似结果的数量(如 k=5 表示返回 top5 最相似结果)。 |
filter | 否 | 过滤条件(通常为字典),用于在检索时筛选符合条件的结果(如按元数据 {"type": "document"} 过滤)。 |
score_threshold | 否 | 相似度阈值(如 0.7 ),仅返回相似度高于该值的结果,低于阈值的将被过滤。 |
docs = knowledgeBase.similarity_search(query, k=10)
2.改进索引算法
知识图谱: 利用知识图谱中的语义信息和实体关系,增强对查询和文档的理解,提升召回的相关性
使用 GraphRAG 基于知识图谱构建知识库
3.引入重排序(Reranking)
重排序模型: 对召回结果进行重排,提升问题和文档的相关性。常见的重排序模型有 BGE-Rerank
和 Cohere Rerank
。
-
场景:用户查询“如何提高深度学习模型的训练效率?”
-
召回结果:初步召回10篇文档,其中包含与“深度学习”、“训练效率”相关的文章。
-
重排序:BGE-Rerank对召回的10篇文档进行重新排序,将与“训练效率”最相关的文档(如“优化深度学习训练的技巧”)排在最前面,而将相关性较低的文档(如“深度学习基础理论”)排在后面。
混合检索: 结合向量检索和关键词检索的优势,通过重排序模型对结果进行归一化处理,提升召回质量。
4.优化查询扩展
相似语义改写: 使用大模型将用户查询改写成多个语义相近的查询,提升召回多样性。
- 例如,LangChain的
MultiQueryRetriever
支持多查询召回,再进行回答问题。
Ⅰ、初始化大语言模型(阿里百炼)
DASHSCOPE_API_KEY:用于访问阿里云 DashScope 服务的 API 密钥(API Key)。DashScope 是阿里云提供的大模型服务平台,支持调用通义千问(Qwen)等系列大模型。使用该平台的服务时,需要通过 API 密钥进行身份验证和权限校验,以确保只有授权用户能使用相应服务。
os.getenv():Python 标准库 os
模块中的一个函数,核心作用是读取系统环境变量的值。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
key | str | 必需参数,要获取的环境变量的名称(字符串)。 | 无 |
default | 任意类型 | 可选参数,当指定的环境变量不存在时,返回该默认值(未指定时默认返回 None )。 |
llm:大语言模型(Large Language Model,LLM)的实例对象,具体是基于阿里云通义千问(Tongyi Qwen)模型创建的实例。
Tongyi():用于初始化通义千问大模型实例的类(常见于 langchain
等大模型集成库中),核心作用是创建一个可直接调用通义千问(阿里云研发的大语言模型)功能的对象。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
model_name | str | 可选参数,指定要使用的通义千问模型版本(如 "qwen-max"、"qwen-plus" 等)。 | 通常为 "qwen" |
dashscope_api_key | str | 必需参数,访问阿里云 DashScope 服务的 API 密钥(用于身份验证)。 | 无 |
temperature | float | 可选参数,控制生成文本的随机性(范围 0~1,值越高随机性越强)。 | 0.7 |
max_tokens | int | 可选参数,限制生成文本的最大 token 数量(控制输出长度)。 | 不同模型有默认值 |
top_p | float | 可选参数,核采样参数(范围 0~1,控制生成时考虑的候选词范围)。 | 0.95 |
stop | list | 可选参数,生成文本时遇到这些字符串则停止生成(如 ["\n", "。"] )。 | None |
# 初始化大语言模型
DASHSCOPE_API_KEY = os.getenv("BAILIAN_API_KEY")
llm = Tongyi(model_name="qwen-max",dashscope_api_key=DASHSCOPE_API_KEY
)
Ⅱ、创建嵌入模型
embeddings:通过 DashScopeEmbeddings
类创建的嵌入模型实例对象。
DashScopeEmbeddings():用于初始化阿里云 DashScope 平台嵌入模型的类(常见于 langchain
等大模型集成库),其核心作用是创建一个可调用 DashScope 平台提供的文本嵌入模型的实例,封装了底层的 API 调用逻辑(如身份验证、参数处理、网络请求等)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
model | str | 可选参数,指定要使用的 DashScope 嵌入模型版本(如 "text-embedding-v2" 是阿里云提供的常用文本嵌入模型)。 | 通常为 "text-embedding-v1" 或平台默认模型 |
dashscope_api_key | str | 必需参数,访问阿里云 DashScope 服务的 API 密钥(用于身份验证,确保有权限调用嵌入模型服务)。 | 无 |
timeout | int | 可选参数,API 请求的超时时间(单位:秒),超过该时间未收到响应则终止请求。 | 通常为 60 秒 |
batch_size | int | 可选参数,批量处理文本时每次输入的最大文本数量(控制一次 API 调用处理的文本条数)。 | 通常为 32 或 64 |
**kwargs | 任意类型 | 可选参数,用于传递其他额外的 API 配置参数(如请求重试次数、代理设置等,具体取决于底层实现)。 | 无 |
# 创建嵌入模型
embeddings = DashScopeEmbeddings(model="text-embedding-v2",dashscope_api_key=DASHSCOPE_API_KEY
)
Ⅲ、加载Faiss向量数据库
vectorstore:通过 FAISS.load_local()
加载的本地向量数据库实例。
FAISS.load_local():FAISS(Facebook AI Research 开发的向量相似性搜索库)提供的一个工具函数,主要用于从本地文件系统加载之前保存的 FAISS 向量数据库。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
folder_path | str | 必需参数,本地向量数据库文件所在的文件夹路径(如代码中的 r"F:\AI_BigModel\..." )。 | 无 |
embeddings | 嵌入模型实例(如 DashScopeEmbeddings ) | 必需参数,用于加载向量时匹配的嵌入模型(需与存储时使用的嵌入模型一致,否则可能导致向量不兼容)。 | 无 |
allow_dangerous_deserialization | bool | 可选参数,是否允许反序列化操作。由于向量数据库文件可能包含序列化数据,反序列化存在潜在安全风险(如恶意代码),因此默认禁用;需显式设为 True 才能加载本地存储的数据库。 | False |
index_name | str | 可选参数,指定要加载的索引名称(若文件夹中存在多个索引文件时使用,默认加载文件夹中的默认索引)。 | "index" |
# 加载向量数据库,添加allow_dangerous_deserialization=True参数以允许反序列化
vectorstore = FAISS.load_local(r"F:\AI_BigModel\appTest2\day2_RagBase\vector_db", embeddings, allow_dangerous_deserialization=True)
Ⅳ、创建多路查询检索器
retriever:通过 MultiQueryRetriever.from_llm()
创建的多查询检索器实例。
MultiQueryRetriever.from_llm():接收一个基础检索器(如从向量数据库转换的检索器)和一个大语言模型(LLM),通过 LLM 对原始查询进行 “多样化改写” 生成多个查询,再调用基础检索器用这些查询检索文档,最终返回合并后的结果。该方法封装了 “多查询生成” 和 “结果合并” 的逻辑,简化了增强检索的实现流程。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
retriever | 检索器实例(如 VectorStoreRetriever ) | 必需参数,基础检索器,用于实际执行查询(通常由向量数据库转换而来,如 vectorstore.as_retriever() 的返回值)。 | 无 |
llm | 大语言模型实例(如 Tongyi ) | 必需参数,用于生成多个查询变体的大语言模型,需具备文本生成能力。 | 无 |
query_generator | 生成器对象(可选) | 可选参数,自定义的查询生成器(若不指定,默认使用 LLM 生成查询)。 | None |
number_of_queries | int | 可选参数,指定生成的查询变体数量(数量越多,检索越全面,但效率可能降低)。 | 3 |
verbose | bool | 可选参数,是否打印检索过程中的日志(如生成的查询、检索结果等)。 | False |
vectorstore.as_retriver():向量数据库(vectorstore
)的方法,用于将向量数据库转换为一个检索器(Retriever)对象。为向量数据库提供标准化的检索接口,使其能被集成到 RAG 流程中作为基础检索工具。
# 创建MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(retriever=vectorstore.as_retriever(),llm=llm
)
Ⅴ、完整代码
query:用户输入的查询字符串,代表需要检索相关信息的问题或需求。
results:检索器执行查询后返回的相关文档集合,通常是一个包含多个文档对象的列表。
retriever.get_relevant_documents():检索器(retriever
)的核心方法,用于根据查询字符串从向量数据库中获取相关文档。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
query | str | 必需参数,用户的查询字符串(如代码中的 query 变量),作为检索的依据。 | 无 |
k | int | 可选参数,指定返回的相关文档数量(默认返回最相关的前 N 条)。 | 5 |
filter | dict | 可选参数,检索时的过滤条件(如按文档来源、时间等筛选,具体支持取决于向量数据库类型)。 | None |
**kwargs | 任意类型 | 可选参数,传递其他检索配置(如相似度阈值、排序方式等,因检索器类型而异)。 |
enumerate():Python 内置函数,用于遍历可迭代对象(如列表、元组)时,同时获取元素的索引和值。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
iterable | 可迭代对象 | 必需参数,需要遍历的对象(如代码中的 results 列表)。 | 无 |
start | int | 可选参数,指定索引的起始值(默认从 0 开始,代码中通过 i+1 转为从 1 开始计数)。 |
doc.pagecontent:检索结果中单个文档对象(doc
)的属性,用于存储该文档的具体文本内容。
import os
from langchain.retrievers import MultiQueryRetriever
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.llms import Tongyi# 初始化大语言模型
DASHSCOPE_API_KEY = os.getenv("BAILIAN_API_KEY")
llm = Tongyi(model_name="qwen-max",dashscope_api_key=DASHSCOPE_API_KEY
)# 创建嵌入模型
embeddings = DashScopeEmbeddings(model="text-embedding-v2",dashscope_api_key=DASHSCOPE_API_KEY
)# 加载向量数据库,添加allow_dangerous_deserialization=True参数以允许反序列化
vectorstore = FAISS.load_local(r"F:\AI_BigModel\appTest2\day2_RagBase\vector_db", embeddings, allow_dangerous_deserialization=True)# 创建MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(retriever=vectorstore.as_retriever(),llm=llm
)# 示例查询
query = "客户经理的考核标准是什么?"
# 执行查询
results = retriever.get_relevant_documents(query)# 打印结果
print(f"查询: {query}")
print(f"找到 {len(results)} 个相关文档:")
for i, doc in enumerate(results):print(f"\n文档 {i+1}:")print(doc.page_content[:300] + "..." if len(doc.page_content) > 200 else doc.page_content)
5.双向改写
将查询改写成文档(Query2Doc)或为文档生成查询(Doc2Query),缓解短文本向量化效果差的问题
Ⅰ、Query2Doc:将查询改写成文档
-
用户查询:“如何提高深度学习模型的训练效率?”
-
Query2Doc 改写:
-
原始查询较短,可能无法充分表达用户意图。
-
通过 Query2Doc 生成一段扩展文档:
-
提高深度学习模型的训练效率可以从以下几个方面入手:
-
使用更高效的优化算法,如AdamW或LAMB。
-
采用混合精度训练(Mixed Precision Training),减少显存占用并加速计算。
-
调整学习率调度策略,避免训练过程中的震荡。
-
对数据进行预处理和增强,减少训练时的冗余计算。
-
使用分布式训练技术,如数据并行或模型并行。
-
-
Ⅱ、Doc2Query:为文档生成关联查询
-
文档内容:
本文介绍了深度学习模型训练中的优化技巧,包括:
-
使用AdamW优化器替代传统的SGD。
-
采用混合精度训练,减少显存占用。
-
使用分布式训练技术加速大规模模型的训练……
-
-
通过 Doc2Query 生成一组可能的查询:
-
如何选择深度学习模型的优化器?
-
混合精度训练有哪些优势?
-
分布式训练技术如何加速深度学习?
-
如何减少深度学习训练中的显存占用?
-
深度学习模型训练的最佳实践是什么?
-
6.索引拓展
-
离散索引扩展: 使用关键词抽取、实体识别等技术生成离散索引,与向量检索互补,提升召回准确性。
-
连续索引扩展: 结合多种向量模型(如OpenAI的Ada、智源的BGE)进行多路召回,取长补短。
-
混合索引召回: 将BM25等离散索引与向量索引结合,通过Ensemble Retriever实现混合召回,提升召回多样性
Ⅰ、离散索引
使用关键词抽取、实体识别等技术生成离散索引,与向量检索互补,提升召回准确性。
① 关键词抽取
从文档中提取出重要的关键词,作为离散索引的一部分,用于补充向量检索的不足
例:
文档内容:
本文介绍了深度学习模型训练中的优化技巧,包括:
1. 使用AdamW优化器替代传统的SGD。
2. 采用混合精度训练,减少显存占用。
3. 使用分布式训练技术加速大规模模型的训练。通过关键词抽取技术(如TF-IDF、TextRank)提取出以下关键词:
["深度学习", "模型训练", "优化技巧", "AdamW", "混合精度训练", "分布式训练"]
当用户查询“如何优化深度学习模型训练?”时,离散索引中的关键词能够快速匹配到相关文档。
② 实体识别
从文档中识别出命名实体(如人名、地点、组织等),作为离散索引的一部分,增强检索的精确性。
例:
文档内容:
2023年诺贝尔物理学奖授予了三位科学家,以表彰他们在量子纠缠领域的研究成果。
通过实体识别技术(如SpaCy、BERT-based NER)提取出以下实体:
["2023年", "诺贝尔物理学奖", "量子纠缠"]
当用户查询“2023年诺贝尔物理学奖的获奖者是谁?”时,离散索引中的实体能够快速匹配到相关文档。
Ⅱ、混合索引召回
将离散索引(如关键词、实体)与向量索引结合,通过混合召回策略提升检索效果。
例:
文档内容:
本文介绍了人工智能在医疗领域的应用,包括:
1. 使用深度学习技术进行医学影像分析。
2. 利用自然语言处理技术提取电子病历中的关键信息。
3. 开发智能诊断系统辅助医生决策。关键词抽取:["人工智能", "医疗领域", "深度学习", "医学影像分析", "自然语言处理", "电子病历", "智能诊断系统"]
实体识别:["人工智能", "医疗领域", "深度学习", "自然语言处理"]
当用户查询“人工智能在医疗领域的应用有哪些?”时, 离散索引通过关键词和实体匹配到相关文档。 向量索引通过语义相似度匹配到相关文档。 综合两种召回结果,提升检索的准确性和覆盖率。
Ⅲ、Small-to-Big
Small-to-Big 索引策略: 一种高效的检索方法,特别适用于处理长文档或多文档场景。核心思想是通过小规模内容(如摘要、关键句或段落)建立索引,并链接到大规模内容主体中。这种策略的优势在于能够快速定位相关的小规模内容,并通过链接获取更详细的上下文信息,从而提高检索效率和答案的逻辑连贯性。
Small-to-Big机制:
① 小规模内容检索: 用户输入查询后,系统首先在小规模内容(如摘要、关键句或段落)中检索匹配的内容。小规模内容通常是通过摘要生成、关键句提取等技术从大规模内容中提取的,并建立索引。
② 链接到大规模内容: 当小规模内容匹配到用户的查询后,系统会通过预定义的链接(如文档ID、URL 或指针)找到对应的大规模内容(如完整的文档、文章)。大规模内容包含更详细的上下文信息,为RAG 提供丰富的背景知识。
③ 上下文补充: 将大规模内容作为RAG 系统的上下文输入,结合用户查询和小规模内容,生成更准确和连贯的答案。
例:
小规模内容(索引部分):
摘要:从每篇论文中提取摘要作为索引内容。
摘要1:本文介绍了Transformer 模型在机器翻译任务中的应用,并提出了改进的注意力机制。
摘要2:本文探讨了Transformer 模型在文本生成任务中的性能,并与RNN 模型进行了对比。
关键句:从论文中提取与查询相关的关键句。
关键句1:Transformer 模型通过自注意力机制实现了高效的并行计算。
关键句2:BERT 是基于Transformer 的预训练模型,在多项NLP 任务中取得了显著效果。
大规模内容(链接部分):
每篇论文的完整内容作为大规模内容,通过链接与小规模内容关联。
论文1:链接到完整的PDF 文档,包含详细的实验和结果。
论文2:链接到完整的PDF 文档,包含模型架构和性能分析。
四、Qwen-Agent构建RAG
1.基本介绍
Qwen-Agent是一个开发框架。充分利用基于通义千问模型(Qwen)的指令遵循、工具使用、规划、记忆能力。
Qwen-Agent支持的模型形式:
-
DashScope服务提供的Qwen模型服务
-
支持通过OpenAI API方式接入开源的Qwen模型服务
Github:https://github.com/QwenLM/Qwen-Agent
2.使用Qwen-Agent将上下文记忆扩展到百万量级
现在能够原生处理数百万字输入的大型语言模型(LLMs)成为了一种趋势。如何让一个上下文长度为8K的模型,能处理1M的上下文?
可以采取以下方法准备数据:
① 利用一个较弱的8k上下文聊天模型构建一个相对强大的智能体,能够处理1M的上下文。
② 随后,使用该智能体合成微调数据,并应用自动化过滤确保数据质量。
③ 最终,使用合成数据对预训练模型进行微调,得到一个强大的1M上下文聊天模型
3.构建智能体
Qwen-Agent构建的智能体包含三个复杂度级别,每一层都建立在前一层的基础上
Ⅰ、Level1 检索
处理100万字上下文的一种朴素方法是简单采用增强检索生成(RAG)。 RAG将上下文分割成较短的块,每块不超过512个字,然后仅保留最相关的块在8k字的上下文中。 挑战在于如何精准定位最相关的块。经过多次尝试,我们提出了一种基于关键词的解决方案:
步骤:
① 指导聊天模型将用户查询中的指令信息与非指令信息分开。
例如,将用户查询"回答时请用2000字详尽阐述,我的问题是,自行车是什么时候发明的?请用英文回复。" 转化为:{"信息": ["自行车是什么时候发明的"], "指令": ["回答时用2000字", "尽量详尽", "用英文回复"]}。
② 要求聊天模型从查询的信息部分推导出多语言关键词。
例如,短语"自行车是什么时候发明的" 会转换为:{"关键词_英文": ["bicycles", "invented", "when"], "关键词_中文": ["自行车", "发明", "时间"]}。
③ 运用BM25这一传统的基于关键词的检索方法,找出与提取关键词最相关的块。
Ⅱ、Level2 分块检索
上述RAG方法很快速,但常在相关块与用户查询关键词重叠程度不足时失效,导致这些相关的块未被检索到、没有提供给模型。尽管理论上向量检索可以缓解这一问题,但实际上效果有限。 为了解决这个局限,我们采用了一种暴力策略来减少错过相关上下文的几率:
步骤:
① 对于每个512字块,让聊天模型评估其与用户查询的相关性,如果认为不相关则输出”无“,如果相关则输出相关句子。这些快会被并行处理以避免长时间等待
② 然后,取那些非不相关的输出(即相关句),用它们作为搜索查询词,通过BM25算法检索出最相关的块(总的检索长度控制在8k上下文限制内)
③ 最后,基于检索到的上下文生成最终答案,这一步骤的实现与通常的RAG相同
Ⅲ、Level3 逐步推理
在基于文档的问题回答中,一个典型的挑战就是多跳推理
-
例如,考虑回答问题:“与第五交响曲创作于同一世纪的交通工具是什么?
-
”模型首先需要确定子问题的答案,“第五交响曲是在哪个世纪创作的?”即19世纪。
-
然后,它才可以意识到包含“自行车于19世纪发明”的信息块实际上与原始问题相关的。
-
工具调用(也称为函数调用)智能体或ReAct智能体是经典的解决方案,它们内置了问题分解和逐步推理的能力。因此,我们将前述级别二的智能体(分块检索智能体)封装为一个工具,由工具调用智能体(逐步推理智能体)调用。工具调用智能体进行多跳推理的流程如下:
向Lv3-智能体提出一个问题。
while(Lv3-智能体无法根据其记忆回答问题){
Lv3-智能体提出一个新的子问题待解答。
Lv3-智能体向Lv2-智能体提问这个子问题。
将Lv2-智能体的回应添加到Lv3-智能体的记忆中。
}Lv3-智能体提供原始问题的最终答案。
4.RAG评测结果
评测基准:
-
NeedleBench,一个测试模型是否能在充满大量无关句子的语境中找到最相关句子的基准,类似于“大海捞针”。回答一个问题可能需要同时找到多根“针”,并进行多跳逐步推理。
-
LV-Eval 是一个要求同时理解众多证据片段的基准测试。我们对LV-Eval原始版本中的评估指标进行了调整,因为其匹配规则过于严苛,导致了许多假阴性结果。
模型方法:
-
32k-模型:这是一个7B对话模型,主要在8k上下文样本上进行微调,并辅以少量32k上下文样本。为了扩展到256k上下文,我们采用了无需额外训练的方法,如基于RoPE的外推。
-
4k-RAG:使用与32k-模型相同的模型,但采取了Lv1-智能体的RAG策略。它仅检索并处理最相关的4k上下文。
-
4k-智能体:同样使用32k-模型的模型,但采用前文描述的更复杂的智能体策略。该智能体策略会并行处理多个片段,但每次请求模型时至多只使用模型的4k上下文。
5.Qwen-Agent使用
Ⅰ、安装
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -U qwen agent[rag, code_interpreter, gui, mcp]
[gui]:用于提供基于 Gradio 的 GUI 支持;
[rag]:用于支持 RAG;
[code_interpreter]:用于提供代码解释器相关支持;
[mcp]:用于支持 MCP。
Ⅱ、日志捕捉器
self.log_capture_string:类型为 io.StringIO
,是一个内存中的字符串缓冲区,用于临时存储捕获到的日志内容
io.StringIO():Python io
模块中的类,用于创建内存中的字符串缓冲区(类似文件对象),可像操作文件一样读写字符串(无需实际创建磁盘文件)。常用于临时存储文本数据(如捕获日志输出)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
initial_value | str | 可选参数,初始化缓冲区的字符串内容(默认空字符串)。 | "" |
newline | str | 可选参数,换行符处理方式(如 "\n" "\r\n" ,默认 None 表示自动转换)。 | None |
self.log_handler:类型为 logging.StreamHandler
,是日志处理器,绑定到 log_capture_string
,负责将日志输出定向到内存缓冲区(而非默认的控制台)。其作用是 “拦截” 日志并写入 log_capture_string
。
.StreamHandler():Python logging
模块的日志处理器类,用于将日志输出到指定的流对象(如控制台 sys.stdout
、内存缓冲区 StringIO
等),控制日志的输出目标。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
stream | 流对象 | 可选参数,日志输出的目标流(如 sys.stdout 控制台、StringIO 内存缓冲区),默认输出到控制台。 | sys.stderr |
.setLevel():日志器(Logger
)或日志处理器(Handler
)的方法,用于设置日志级别阈值,仅处理级别大于或等于该阈值的日志(过滤低级日志)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
level | int /str | 必需参数,日志级别(如 logging.INFO 、logging.DEBUG ,或字符串 "INFO" "DEBUG" )。 | 无 |
logging.INFO:日志级别常量,表示 “信息性日志” 级别。日志级别用于区分日志的重要程度,INFO
级别通常用于记录程序正常运行过程中的关键信息(如 “服务启动成功”“数据加载完成” 等),确认程序按预期执行。
self.log_formatter:logging.Formatter
类的实例,日志格式器,定义了日志的输出格式。代码中格式为 '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
,包含日志时间、日志器名称、日志级别和日志消息,确保日志结构化易读。
.Formatter():logging
模块的格式器类,用于定义日志的输出格式(如包含时间、日志器名称、级别、消息等),使日志结构化、易读。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
fmt | str | 可选参数,日志格式字符串(含占位符,如 '%(asctime)s - %(message)s' ),默认使用内置格式。 | None |
datefmt | str | 可选参数,时间格式字符串(如 '%Y-%m-%d %H:%M:%S' ),默认使用 ISO8601 格式。 | None |
asctime:logging.Formatter
格式化日志时使用的占位符,用于表示日志记录产生的时间戳(人类可读的时间格式)。
name:logging.Formatter
格式化日志时使用的占位符,表示日志记录器(Logger)的名称。
levelname:logging.Formatter
格式化日志时使用的占位符,表示日志的级别名称(如 INFO
、WARNING
、ERROR
等)。
message:logging.Formatter
格式化日志时使用的占位符,表示日志的具体内容(即用户传入的日志消息)。
.setFormatter():日志处理器(Handler
)的方法,用于为处理器绑定格式器(Formatter),使该处理器输出的日志按指定格式显示。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
formatter | Formatter 实例 | 必需参数,已初始化的格式器对象(如 logging.Formatter(...) 创建的实例)。 | 无 |
self.logger:通过 logging.getLogger('qwen_agent_logger')
获取的命名日志记录器,专门用于捕获 'qwen_agent_logger'
名称空间下的日志。设置其日志级别为 INFO
,并添加 log_handler
处理器,使该日志器的输出被捕获到内存。
.getLogger():logging
模块的函数,用于获取或创建日志记录器(Logger)。日志器是日志系统的入口,负责接收日志请求并转发给处理器(Handler)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
name | str | 可选参数,日志器名称(如 "qwen_agent_logger" ),相同名称返回同一实例;默认空字符串表示根日志器。 | "" |
.addHandler():logging
模块中日志记录器(Logger) 的方法,用于向日志器添加日志处理器(Handler)。日志处理器负责定义日志的输出目标(如控制台、文件、内存缓冲区等),添加后,日志器会将接收到的日志消息转发给该处理器,由处理器完成实际输出。一个日志器可以添加多个处理器,实现日志的多目标输出(如同时输出到控制台和文件)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
handler | logging.Handler 实例 | 必需参数,要添加到日志器的处理器对象(如 StreamHandler FileHandler 等,必须是 Handler 的子类实例)。 | 无 |
self.root_logger:通过 logging.getLogger()
获取的根日志记录器,负责捕获系统中所有未被特定日志器处理的日志(全局日志)。同样设置级别为 INFO
并添加 log_handler
,确保全局日志也能被捕获到内存。
.getvalue():io.StringIO
实例的方法,用于获取缓冲区中存储的所有字符串内容(从开头到当前位置),常用于读取内存中的临时文本数据(如捕获的日志)。
.truncate():io.StringIO
实例的方法,用于截断缓冲区内容,保留从开头到指定位置的内容(超出部分被删除),常用于清空缓冲区。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
size | int | 可选参数,截断后的长度(字节数);默认 None 表示截断到当前指针位置,传 0 则清空缓冲区。 | None |
.seek():io.StringIO
实例的方法,用于移动缓冲区的读写指针位置,控制后续读写操作的起始位置(类似文件指针)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
offset | int | 必需参数,指针移动的偏移量(字节数,正数向前移,负数向后移)。 | 无 |
whence | int | 可选参数,参考位置:0 从开头(默认),1 从当前位置,2 从结尾。 | 0 |
log_capture:LogCapture
类的实例,自定义日志捕获器的实例对象,通过调用其方法(get_log()
、clear_log()
)可获取或清空捕获的日志。
LogCapture():自定义的日志捕获器类,用于创建日志捕获实例,封装日志缓冲区、处理器、格式器的初始化逻辑,实现对指定日志器(如 qwen_agent_logger
)和根日志器的日志捕获。
# 创建一个自定义的日志处理器来捕获日志输出
class LogCapture:def __init__(self):# 初始化内存日志缓冲区self.log_capture_string = io.StringIO()# 创建日志处理器并绑定缓冲区self.log_handler = logging.StreamHandler(self.log_capture_string)# 设置日志处理器的级别self.log_handler.setLevel(logging.INFO)# 创建日志格式器并绑定到处理器self.log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')self.log_handler.setFormatter(self.log_formatter)# 配置 qwen_agent 命名日志器self.logger = logging.getLogger('qwen_agent_logger')self.logger.setLevel(logging.INFO)self.logger.addHandler(self.log_handler)# 配置根日志器(全局日志捕获)self.root_logger = logging.getLogger()self.root_logger.setLevel(logging.INFO)self.root_logger.addHandler(self.log_handler)# 获取日志内容def get_log(self):return self.log_capture_string.getvalue()# 清空日志内容def clear_log(self):self.log_capture_string.truncate(0) # 截断缓冲区内容self.log_capture_string.seek(0) # 重置缓冲区指针到开头# 初始化日志捕获器
log_capture = LogCapture()
Ⅲ、模型配置
model:指要使用的具体大语言模型名称。
model_server:指提供模型服务的平台或服务器类型。
api_key:指访问模型服务的API 密钥,用于身份验证和权限校验。
generate_cfg:字典,用于配置模型生成文本时的参数(生成策略),控制输出内容的风格、随机性、长度等。
top_p:一个 0~1 之间的概率阈值,模型在生成下一个词时,会从概率最高的候选词开始依次累加概率,直到累积概率之和刚好超过 top_p
,最终只从这个 “累积概率达标” 的候选词集合中随机选择下一个词。
os.getenv():Python 标准库 os
模块中的一个函数,核心作用是读取系统环境变量的值。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
key | str | 必需参数,要获取的环境变量的名称(字符串)。 | 无 |
default | 任意类型 | 可选参数,当指定的环境变量不存在时,返回该默认值(未指定时默认返回 None )。 |
# 步骤 1:配置您所使用的 LLM。
llm_cfg = {# 使用 DashScope 提供的模型服务:'model': 'qwen-max','model_server': 'dashscope','api_key': os.getenv("BAILIAN_API_KEY"),'generate_cfg': {'top_p': 0.8}
}
Ⅳ、创建Agent智能体
system_instruction:定义智能体行为准则和背景信息的系统指令字符串。系统指令通常会告诉智能体其角色(如 “你是一名金融顾问”)、工作目标(如 “基于提供的文件回答用户关于考核办法的问题”)、回答风格(如 “简洁明了”)等。
tools:存储智能体可使用工具的列表。“工具” 通常指智能体可以调用的函数、API、插件等(如计算器、网页搜索工具、数据库查询工具等),用于增强智能体的能力(如处理复杂计算、获取实时信息)。
files:存储供智能体读取的文件路径的列表。智能体可以加载并解析这些文件中的内容,作为回答用户问题的依据(类似 RAG 中的 “知识库”)。
log_capture:LogCapture
类的实例,自定义日志捕获器的实例对象,通过调用其方法(get_log()
、clear_log()
)可获取或清空捕获的日志。
log_capture.clear_log():LogCapture
类的实例方法,用于清空之前捕获的日志内容。
其作用是重置日志缓冲区,确保后续操作产生的日志从空白开始记录,避免与之前的日志混淆。
bot:通过Assistant
类创建的智能体实例对象。
Assistant():创建智能体(Assistant)实例的类,核心作用是封装大语言模型、工具、文件处理等能力,构建一个可与用户交互的智能体。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
llm | 字典(如 llm_cfg ) | 必需参数,大语言模型的配置信息,包含模型名称、服务平台、API 密钥、生成参数等(如之前定义的 llm_cfg )。 | 无 |
system_message | str | 可选参数,智能体的系统指令(即 system_instruction ),用于定义智能体的角色和行为准则。 | 空字符串 |
function_list | 列表(工具集合) | 可选参数,智能体可调用的工具列表(即 tools ),每个工具通常是一个包含函数信息的字典。 | 空列表 |
files | 列表(文件路径集合) | 可选参数,智能体可读取的文件路径列表(即 files ),智能体将解析这些文件内容作为知识来源。 | 空列表 |
# 步骤 2:创建一个智能体。这里我们以 `Assistant` 智能体为例,它能够使用工具并读取文件。
system_instruction = ''
tools = []
files = [r"F:\AI_BigModel\L1课件\第3章_RAG高级技术\浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf"] # 给智能体一个 PDF 文件阅读。# 清除之前的日志
log_capture.clear_log()# 创建智能体
bot = Assistant(llm=llm_cfg,system_message=system_instruction,function_list=tools,files=files)
Ⅴ、作为聊天机器人运行智能体
messages:存储聊天历史的列表,用于保存对话过程中所有消息(包括用户输入和智能体回复)。
每个元素是一个字典,包含 'role'
(角色,如 'user'
表示用户、'assistant'
表示智能体)和 'content'
(消息内容)。
query:用户当前的查询字符串,即需要智能体回答的问题。
列表.append():向列表的末尾添加单个元素,直接修改原列表(不返回新列表)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
element | 任意类型 | 必需参数,要添加到列表中的单个元素(可是字符串、数字、字典等任意类型)。 |
response:初始为空列表,后续在循环中接收智能体 bot
生成的响应结果。
current_index:跟踪智能体响应内容输出进度的整数,用于记录已打印的响应字符位置。
bot:通过Assistant
类创建的智能体实例对象。
bot.run():智能体(bot
)的核心方法,用于处理聊天历史并生成响应,支持流式输出(逐步返回内容),通常结合大语言模型、检索结果等生成回答。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
messages | 列表 | 必需参数,聊天历史列表,格式为 [{'role': 角色, 'content': 内容}, ...] (role 通常为 'user' 或 'assistant' )。 | 无 |
stream | bool | 可选参数,是否启用流式输出(True 表示分批次返回响应,False 表示一次性返回完整响应)。 | True |
**kwargs | 任意类型 | 可选参数,其他配置参数(如超时时间、生成参数等,具体取决于智能体实现)。 |
log_content:通过 log_capture.get_log()
获取的日志内容字符串,包含智能体运行过程中捕获的所有日志信息(如检索文档、调用工具、模型输出等)。
log_capture.get_log():LogCapture
类的实例方法,用于获取内存缓冲区中捕获的所有日志内容,返回字符串形式的日志文本。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
self | 实例本身 | 类方法默认参数,指向当前 LogCapture 实例。 | 无 |
retrieval_logs:筛选出的与 “检索” 相关的日志行列表。
通过列表推导式从 log_content
中提取包含 'retriev'
、'search'
、'document'
等关键词的日志行,用于查看智能体检索文档的过程(如是否成功召回相关文档)。
line:遍历原始日志的每一行,用于筛选符合条件的日志行。
str.split():按指定分隔符将字符串分割为列表,原字符串不变。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
sep | str | 可选参数,分割符(如 '\n' 表示按换行分割,',' 表示按逗号分割)。 | None (默认按任意空白字符(空格、换行等)分割) |
maxsplit | int | 可选参数,最大分割次数(-1 表示无限制,n 表示只分割 n 次)。 | -1 |
any():Python 内置函数,判断可迭代对象中是否至少有一个元素为 True
,返回布尔值(True
或 False
)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
iterable | 可迭代对象 | 必需参数,包含布尔值的可迭代对象(如列表、生成器等,元素会被自动转换为布尔值判断)。 |
line.lower():字符串方法,将字符串中所有大写字母转换为小写字母,不改变原字符串,返回新的小写字符串。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
self | 字符串本身 | 方法默认参数,指向当前字符串(如代码中的 line )。 | 无 |
log_line:遍历筛选后日志列表的每一行,用于打印筛选后的日志内容。
content_logs:筛选出的可能包含 “文档内容” 的日志行列表。通过筛选包含 'content'
、'text'
、'chunk'
等关键词的日志行,用于查看智能体实际使用的文档片段(如从 PDF 中提取的考核办法条款)。
current_response:当前需要输出的智能体响应片段,即从 current_index
位置开始到响应内容末尾的部分。
列表.extend():将可迭代对象(如列表、元组、字符串等)中的所有元素添加到列表末尾,直接修改原列表(不返回新列表)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
iterable | 可迭代对象 | 必需参数,包含多个元素的可迭代对象(如 [1,2,3] 、(4,5) 等),其元素会被逐个添加到列表中。 |
# 步骤 3:作为聊天机器人运行智能体。
messages = [] # 这里储存聊天历史。
query = "客户经理被客户投诉一次,扣多少分?"
# 将用户请求添加到聊天历史。
messages.append({'role': 'user', 'content': query})
response = []
current_index = 0# 运行智能体
for response in bot.run(messages=messages):# 在第一次响应时,分析日志以查找召回的文档内容if current_index == 0:# 获取日志内容log_content = log_capture.get_log()print("\n===== 从日志中提取的检索信息 =====")# 查找与检索相关的日志行retrieval_logs = [line for line in log_content.split('\n')if any(keyword in line.lower() for keyword in['retriev', 'search', 'chunk', 'document', 'ref', 'token'])]# 打印检索相关的日志for log_line in retrieval_logs:print(log_line)# 尝试从日志中提取文档内容# 通常在日志中会有类似 "retrieved document: ..." 或 "content: ..." 的行content_logs = [line for line in log_content.split('\n')if any(keyword in line.lower() for keyword in['content', 'text', 'document', 'chunk'])]print("\n===== 可能包含文档内容的日志 =====")for log_line in content_logs:print(log_line)print("===========================\n")current_response = response[0]['content'][current_index:]current_index = len(response[0]['content'])print(current_response, end='')# 将机器人的回应添加到聊天历史。
messages.extend(response)
Ⅵ、分析观察日志并打印
log_content:通过 log_capture.get_log()
获取的日志内容字符串,包含智能体运行过程中捕获的所有日志信息(如检索文档、调用工具、模型输出等)。
log_capture:LogCapture
类的实例,自定义日志捕获器的实例对象,通过调用其方法(get_log()
、clear_log()
)可获取或清空捕获的日志。
.getLogger():logging
模块的函数,用于获取或创建日志记录器(Logger)。日志器是日志系统的入口,负责接收日志请求并转发给处理器(Handler)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
name | str | 可选参数,日志器名称(如 | "" |
keyword_logs:存储日志中所有与 “关键词提取” 相关的日志行,用于聚焦分析关键词处理环节的信息。
doc_logs:存储日志中所有与 “文档处理” 相关的日志行,用于分析文档加载、分割等预处理环节的信息。
retrieval_logs:存储日志中所有与 “检索环节” 相关的日志行,用于分析检索过程的执行情况。
content_logs:存储日志中所有可能包含 “文档实际内容” 的日志行,用于查看日志中记录的具体文本信息。
str.split():Python 字符串的内置方法,用于将字符串按照指定分隔符分割成子字符串列表。若未指定分隔符,默认按空白字符(空格、换行、制表符等)分割,且会忽略字符串开头和结尾的空白,连续的空白会被视为单个分隔符。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
sep | str /None | 可选参数,用于分割字符串的分隔符。若为 None (默认),则按任意空白字符(空格、\n 、\t 等)分割,且忽略开头 / 结尾空白。 | None |
maxsplit | int | 可选参数,最大分割次数。若为正数 n ,则最多分割 n 次,返回 n+1 个子字符串;若为 -1 (默认),则无限制,分割所有可能的位置。 | -1 |
str.lower():Python 字符串的内置方法,用于将字符串中所有大写字母转换为小写字母,并返回转换后的新字符串(原字符串不变)。非字母字符不受影响。
# 运行结束后,分析完整的日志
print("\n\n===== 运行结束后的完整日志分析 =====")
log_content = log_capture.get_log()# 尝试从日志中提取更多信息
print("\n1. 关键词提取:")
keyword_logs = [line for line in log_content.split('\n') if 'keywords' in line.lower()]
for log_line in keyword_logs:print(log_line)print("\n2. 文档处理:")
doc_logs = [line for line in log_content.split('\n') if 'doc' in line.lower() or 'chunk' in line.lower()]
for log_line in doc_logs:print(log_line)print("\n3. 检索相关:")
retrieval_logs = [line for line in log_content.split('\n') if'retriev' in line.lower() or 'search' in line.lower() or 'ref' in line.lower()]
for log_line in retrieval_logs:print(log_line)print("\n4. 可能包含文档内容的日志:")
content_logs = [line for line in log_content.split('\n') if 'content:' in line.lower() or 'text:' in line.lower()]
for log_line in content_logs:print(log_line)
Ⅶ、完整代码
import logging
import io
from qwen_agent.agents import Assistant
import os# 创建一个自定义的日志处理器来捕获日志输出
class LogCapture:def __init__(self):self.log_capture_string = io.StringIO()self.log_handler = logging.StreamHandler(self.log_capture_string)self.log_handler.setLevel(logging.INFO)self.log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')self.log_handler.setFormatter(self.log_formatter)# 获取 qwen_agent 的日志记录器self.logger = logging.getLogger('qwen_agent_logger')self.logger.setLevel(logging.INFO)self.logger.addHandler(self.log_handler)# 也可以捕获根日志记录器的输出self.root_logger = logging.getLogger()self.root_logger.setLevel(logging.INFO)self.root_logger.addHandler(self.log_handler)def get_log(self):return self.log_capture_string.getvalue()def clear_log(self):self.log_capture_string.truncate(0)self.log_capture_string.seek(0)# 初始化日志捕获器
log_capture = LogCapture()# 步骤 1:配置您所使用的 LLM。
llm_cfg = {# 使用 DashScope 提供的模型服务:'model': 'qwen-max','model_server': 'dashscope','api_key': os.getenv("BAILIAN_API_KEY"),'generate_cfg': {'top_p': 0.8}
}# 步骤 2:创建一个智能体。这里我们以 `Assistant` 智能体为例,它能够使用工具并读取文件。
system_instruction = ''
tools = []
files = [r"F:\AI_BigModel\L1课件\第3章_RAG高级技术\浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf"] # 给智能体一个 PDF 文件阅读。# 清除之前的日志
log_capture.clear_log()# 创建智能体
bot = Assistant(llm=llm_cfg,system_message=system_instruction,function_list=tools,files=files)# 步骤 3:作为聊天机器人运行智能体。
messages = [] # 这里储存聊天历史。
query = "客户经理被客户投诉一次,扣多少分?"
# 将用户请求添加到聊天历史。
messages.append({'role': 'user', 'content': query})
response = []
current_index = 0# 运行智能体
for response in bot.run(messages=messages):# 在第一次响应时,分析日志以查找召回的文档内容if current_index == 0:# 获取日志内容log_content = log_capture.get_log()print("\n===== 从日志中提取的检索信息 =====")# 查找与检索相关的日志行retrieval_logs = [line for line in log_content.split('\n')if any(keyword in line.lower() for keyword in['retriev', 'search', 'chunk', 'document', 'ref', 'token'])]# 打印检索相关的日志for log_line in retrieval_logs:print(log_line)# 尝试从日志中提取文档内容# 通常在日志中会有类似 "retrieved document: ..." 或 "content: ..." 的行content_logs = [line for line in log_content.split('\n')if any(keyword in line.lower() for keyword in['content', 'text', 'document', 'chunk'])]print("\n===== 可能包含文档内容的日志 =====")for log_line in content_logs:print(log_line)print("===========================\n")current_response = response[0]['content'][current_index:]current_index = len(response[0]['content'])print(current_response, end='')# 将机器人的回应添加到聊天历史。
messages.extend(response)# 运行结束后,分析完整的日志
print("\n\n===== 运行结束后的完整日志分析 =====")
log_content = log_capture.get_log()# 尝试从日志中提取更多信息
print("\n1. 关键词提取:")
keyword_logs = [line for line in log_content.split('\n') if 'keywords' in line.lower()]
for log_line in keyword_logs:print(log_line)print("\n2. 文档处理:")
doc_logs = [line for line in log_content.split('\n') if 'doc' in line.lower() or 'chunk' in line.lower()]
for log_line in doc_logs:print(log_line)print("\n3. 检索相关:")
retrieval_logs = [line for line in log_content.split('\n') if'retriev' in line.lower() or 'search' in line.lower() or 'ref' in line.lower()]
for log_line in retrieval_logs:print(log_line)print("\n4. 可能包含文档内容的日志:")
content_logs = [line for line in log_content.split('\n') if 'content:' in line.lower() or 'text:' in line.lower()]
for log_line in content_logs:print(log_line)print("===========================\n")
6.Qwen-Agent加载docs多文档知识库
Ⅰ、加载解析文件夹
folder_path:get_file_list
的参数,表示需要遍历的根文件夹路径(字符串类型)。
file_list:一个存储文件完整路径的列表,在函数 get_file_list
中初始化并返回。
root:os.walk(folder_path)
遍历过程中返回的当前目录路径(字符串类型)。
在循环 for root, dirs, files in os.walk(folder_path)
中,root
表示当前正在遍历的目录的完整路径(如初始遍历根目录时,root
等于 folder_path
;遍历子目录时,root
为子目录的完整路径)。
dirs: os.walk(folder_path)
遍历过程中返回的当前目录下的子目录列表(列表类型,元素为子目录名称字符串)。
files: os.walk(folder_path)
遍历过程中返回的当前目录下的文件列表(列表类型,元素为文件名字符串)。
os.walk():Python 标准库 os
模块的函数,用于递归遍历指定文件夹及其所有子目录,生成目录树中每个目录的路径、子目录列表和文件列表。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
top | str | 必需参数,要遍历的根文件夹路径(如代码中的 folder_path )。 | 无 |
topdown | bool | 可选参数,若为 True 则先遍历根目录,再遍历子目录(自上而下);False 则自下而上。 | True |
onerror | 函数 | 可选参数,用于处理遍历过程中出现的错误(如权限不足),需传入一个错误处理函数。 | None |
followlinks | bool | 可选参数,若为 True 则遍历符号链接指向的目录(Windows 下通常为快捷方式)。 | False |
file:遍历 files
列表时的循环变量,表示当前目录下的单个文件名(字符串类型)。
file_path:通过 os.path.join(root, file)
生成的文件完整路径(字符串类型)。
os.path.join(): Python 标准库 os.path
模块的函数,用于拼接多个路径组件为一个完整路径,自动处理不同操作系统的路径分隔符(如 Windows 的 \
和 Linux 的 /
)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
path1 | str | 必需参数,第一个路径组件(如目录路径 root )。 | 无 |
*path2 | str | 可变参数,后续的路径组件(如文件名 file ,可传入多个组件,如 os.path.join("a", "b", "c.txt") )。 | 无 |
# RAG助手
import os
def get_file_list(folder_path):# 初始化文件列表file_list = []# 遍历文件夹for root, dirs, files in os.walk(folder_path):for file in files:# 获取文件的完整路径file_path = os.path.join(root, file)# 将文件路径添加到列表中file_list.append(file_path)return file_list# 获取指定知识库文件列表
file_list = get_file_list(r"F:\AI_BigModel\appTest3\docs")
print(file_list)
Ⅱ、模型配置
model:指要使用的具体大语言模型名称。
model_server:指提供模型服务的平台或服务器类型。
api_key:指访问模型服务的API 密钥,用于身份验证和权限校验。
generate_cfg:字典,用于配置模型生成文本时的参数(生成策略),控制输出内容的风格、随机性、长度等。
top_p:一个 0~1 之间的概率阈值,模型在生成下一个词时,会从概率最高的候选词开始依次累加概率,直到累积概率之和刚好超过 top_p
,最终只从这个 “累积概率达标” 的候选词集合中随机选择下一个词。
os.getenv():Python 标准库 os
模块中的一个函数,核心作用是读取系统环境变量的值。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
key | str | 必需参数,要获取的环境变量的名称(字符串)。 | 无 |
default | 任意类型 | 可选参数,当指定的环境变量不存在时,返回该默认值(未指定时默认返回 None )。 |
# 步骤 1:配置您所使用的 LLM
llm_cfg = {# 使用 DashScope 提供的模型服务:'model': 'qwen-max','model_server': 'dashscope','api_key': os.getenv('BAILIAN_API_KEY'),# 如果这里没有设置 'api_key',它将读取 `DASHSCOPE_API_KEY` 环境变量。# 使用与 OpenAI API 兼容的模型服务,例如 vLLM 或 Ollama:# 'model': 'Qwen2-7B-Chat',# 'model_server': 'http://localhost:8000/v1', # base_url,也称为 api_base# 'api_key': 'EMPTY',# (可选) LLM 的超参数:'generate_cfg': {'top_p': 0.8}
}
Ⅲ、创建Agent智能体
system_instruction:定义智能体行为准则和背景信息的系统指令字符串。系统指令通常会告诉智能体其角色(如 “你是一名金融顾问”)、工作目标(如 “基于提供的文件回答用户关于考核办法的问题”)、回答风格(如 “简洁明了”)等。
tools:存储智能体可使用工具的列表。“工具” 通常指智能体可以调用的函数、API、插件等(如计算器、网页搜索工具、数据库查询工具等),用于增强智能体的能力(如处理复杂计算、获取实时信息)。
files:存储供智能体读取的文件路径的列表。智能体可以加载并解析这些文件中的内容,作为回答用户问题的依据(类似 RAG 中的 “知识库”)。
Assistant():创建智能体(Assistant)实例的类,核心作用是封装大语言模型、工具、文件处理等能力,构建一个可与用户交互的智能体。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
llm | 字典(如 llm_cfg ) | 必需参数,大语言模型的配置信息,包含模型名称、服务平台、API 密钥、生成参数等(如之前定义的 llm_cfg )。 | 无 |
system_message | str | 可选参数,智能体的系统指令(即 system_instruction ),用于定义智能体的角色和行为准则。 | 空字符串 |
function_list | 列表(工具集合) | 可选参数,智能体可调用的工具列表(即 tools ),每个工具通常是一个包含函数信息的字典。 | 空列表 |
files | 列表(文件路径集合) | 可选参数,智能体可读取的文件路径列表(即 files ),智能体将解析这些文件内容作为知识来源。 | 空列表 |
# 步骤 2:创建一个智能体。这里我们以 `Assistant` 智能体为例,它能够使用工具并读取文件。
system_instruction = '你是一位保险专家,根据你的经验来精准的回答用户提出的问题'tools = [] # `code_interpreter` 是框架自带的工具,用于执行代码。bot = Assistant(llm=llm_cfg,system_message=system_instruction,function_list=tools,files=file_list)
Ⅳ、作为聊天机器人运行智能体【流式输出】
messages:存储聊天历史的列表,用于保存对话过程中所有消息(包括用户输入和智能体回复)。
每个元素是一个字典,包含 'role'
(角色,如 'user'
表示用户、'assistant'
表示智能体)和 'content'
(消息内容)。
query:用户当前的查询字符串,即需要智能体回答的问题。
input():Python 的内置函数,用于从标准输入设备(通常是键盘)获取用户输入。其核心逻辑是:暂停程序执行,等待用户输入文本并按下回车键后,将用户输入的内容以字符串类型返回。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
prompt | str | 可选参数,用于在获取输入前显示的提示文本(如 "请输入你的问题:" ),若不指定则无提示信息。 | None |
列表.append():向列表的末尾添加单个元素,直接修改原列表(不返回新列表)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
element | 任意类型 | 必需参数,要添加到列表中的单个元素(可是字符串、数字、字典等任意类型)。 |
response:初始为空列表,后续在循环中接收智能体 bot
生成的响应结果。
current_index:跟踪智能体响应内容输出进度的整数,用于记录已打印的响应字符位置。
bot:通过Assistant
类创建的智能体实例对象。
bot.run():智能体(bot
)的核心方法,用于处理聊天历史并生成响应,支持流式输出(逐步返回内容),通常结合大语言模型、检索结果等生成回答。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
messages | 列表 | 必需参数,聊天历史列表,格式为 [{'role': 角色, 'content': 内容}, ...] (role 通常为 'user' 或 'assistant' )。 | 无 |
stream | bool | 可选参数,是否启用流式输出(True 表示分批次返回响应,False 表示一次性返回完整响应)。 | True |
**kwargs | 任意类型 | 可选参数,其他配置参数(如超时时间、生成参数等,具体取决于智能体实现)。 |
current_response:当前需要输出的智能体响应片段,即从 current_index
位置开始到响应内容末尾的部分。
列表.extend():将可迭代对象(如列表、元组、字符串等)中的所有元素添加到列表末尾,直接修改原列表(不返回新列表)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
iterable | 可迭代对象 | 必需参数,包含多个元素的可迭代对象(如 [1,2,3] 、(4,5) 等),其元素会被逐个添加到列表中。 |
# 步骤 3:作为聊天机器人运行智能体。
messages = [] # 这里储存聊天历史。
while True:query = input('用户请求: ')if query == '-1':break# 将用户请求添加到聊天历史。messages.append({'role': 'user', 'content': query})response = []current_index = 0for response in bot.run(messages=messages):# 流式输出current_response = response[0]['content'][current_index:]current_index = len(response[0]['content'])print(current_response, end='')# 将机器人的回应添加到聊天历史。messages.extend(response)
Ⅴ、完整代码
# RAG助手
import os
def get_file_list(folder_path):# 初始化文件列表file_list = []# 遍历文件夹for root, dirs, files in os.walk(folder_path):for file in files:# 获取文件的完整路径file_path = os.path.join(root, file)# 将文件路径添加到列表中file_list.append(file_path)return file_list# 获取指定知识库文件列表
file_list = get_file_list(r"F:\AI_BigModel\appTest3\docs")
print(file_list)from qwen_agent.agents import Assistant# 步骤 1:配置您所使用的 LLM
llm_cfg = {# 使用 DashScope 提供的模型服务:'model': 'qwen-max','model_server': 'dashscope','api_key': os.getenv('BAILIAN_API_KEY'),# 如果这里没有设置 'api_key',它将读取 `DASHSCOPE_API_KEY` 环境变量。# 使用与 OpenAI API 兼容的模型服务,例如 vLLM 或 Ollama:# 'model': 'Qwen2-7B-Chat',# 'model_server': 'http://localhost:8000/v1', # base_url,也称为 api_base# 'api_key': 'EMPTY',# (可选) LLM 的超参数:'generate_cfg': {'top_p': 0.8}
}# 步骤 2:创建一个智能体。这里我们以 `Assistant` 智能体为例,它能够使用工具并读取文件。
system_instruction = '你是一位保险专家,根据你的经验来精准的回答用户提出的问题'tools = [] # `code_interpreter` 是框架自带的工具,用于执行代码。bot = Assistant(llm=llm_cfg,system_message=system_instruction,function_list=tools,files=file_list)# 步骤 3:作为聊天机器人运行智能体。
messages = [] # 这里储存聊天历史。
while True:query = input('用户请求: ')if query == '-1':break# 将用户请求添加到聊天历史。messages.append({'role': 'user', 'content': query})response = []current_index = 0for response in bot.run(messages=messages):# 流式输出current_response = response[0]['content'][current_index:]current_index = len(response[0]['content'])print(current_response, end='')# 将机器人的回应添加到聊天历史。messages.extend(response)
五、RAG质量评估
当我们完成了一个RAG系统的开发工作以后,我们还需要对RAG系统的性能进行评估,那如何来对RAG系统的性能进行评估呢?我们可以仔细分析一下RAG系统的产出成果,比如检索器组件它产出的是检索出来的相关文档即Context,而生成器组件它产出的是最终的答案即Response,除此之外还有我们最初的用户问题即Question。
因此RAG系统的评估应该是将question、context、answer结合在一起进行评估。
1.RAG 三元组
标准的 RAG 流程就是用户提出 Query 问题,RAG 应用去召回 Context,然后 LLM 将 Context 组装,生成满足 Query 的 Response 回答。那么在这里出现的三元组:—— Query、Context 和 Response 就是 RAG 整个过程中最重要的三元组,它们之间两两相互牵制。我们可以通过检测三元组之间两两元素的相关度,来评估这个 RAG 应用的效果:
-
Context Relevance: 衡量召回的 Context 能够支持 Query 的程度。如果该得分低,反应出了召回了太多与Query 问题无关的内容,这些错误的召回知识会对 LLM 的最终回答造成一定影响。
-
Groundedness: 衡量 LLM 的 Response 遵从召回的 Context 的程度。如果该得分低,反应出了 LLM 的回答不遵从召回的知识,那么回答出现幻觉的可能就越大。
-
Answer Relevance: 衡量最终的 Response 回答对 Query 提问的相关度。如果该得分低,反应出了可能答不对题。
2.RAGAs评估
Ⅰ、什么是Ragas评估
官网地址:Ragas
Ragas (Retrieval-Augmented Generation Assessment) 它是一个框架,它可以帮助我们来快速评估RAG系统的性能,为了评估RAG系统,Ragas需要以下信息:
-
question:用户输入的问题。
-
answer:从 RAG 系统生成的答案(由LLM给出)。
-
contexts:根据用户的问题从外部知识源检索的上下文即与问题相关的文档。
-
ground_truths: 人类提供的基于问题的真实(正确)答案。 这是唯一的需要人类提供的信息。
Ⅱ、评估指标
Ragas提供了五种评估指标包括:
-
忠实度(faithfulness)
-
答案相关性(Answer relevancy)
-
上下文精度(Context precision)
-
上下文召回率(Context recall)
-
上下文相关性(Context relevancy)
① 忠实度(faithfulness)
忠实度(faithfulness)衡量了生成的答案(answer)与给定上下文(context)的事实一致性。它是根据answer和检索到的context计算得出的。并将计算结果缩放到 (0,1) 范围且越高越好。
如果答案(answer)中提出的所有基本事实(claims)都可以从给定的上下文(context)中推断出来,则生成的答案被认为是忠实的。为了计算这一点,首先从生成的答案中识别一组claims。然后,将这些claims中的每一项与给定的context进行交叉检查,以确定是否可以从给定的context中推断出它。忠实度分数由以下公式得出:
示例:
② 答案相关性(Answer relevancy)
评估指标“答案相关性”重点评估生成的答案(answer)与用户问题(question)之间相关程度。不完整或包含冗余信息的答案将获得较低分数。该指标是通过计算question和answer获得的,它的取值范围在 0 到 1 之间,其中分数越高表示相关性越好。
当答案直接且适当地解决原始问题时,该答案被视为相关。重要的是,我们对答案相关性的评估不考虑真实情况,而是对答案缺乏完整性或包含冗余细节的情况进行惩罚。为了计算这个分数,LLM会被提示多次为生成的答案生成适当的问题,并测量这些生成的问题与原始问题之间的平均余弦相似度。基本思想是,如果生成的答案准确地解决了最初的问题,LLM应该能够从答案中生成与原始问题相符的问题。
示例:
③ 上下文精度(Context precision)
上下文精度(Context precision)是一种衡量标准,它评估所有在上下文(contexts)中呈现的与基本事实(ground-truth)相关的条目是否排名较高。理想情况下,所有相关文档块(chunks)必须出现在顶层。该指标使用question和contexts计算,值范围在 0 到 1 之间,其中分数越高表示精度越高。
④ 上下文召回率(Context recall)
上下文召回率(Context recall)衡量检索到的上下文(Context)与人类提供的真实答案(ground truth)的一致程度。它是根据ground truth和检索到的Context计算出来的,取值范围在 0 到 1 之间,值越高表示性能越好。
为了根据真实答案(ground truth)估算上下文召回率(Context recall),分析真实答案中的每个句子以确定它是否可以归因于检索到的Context。 在理想情况下,真实答案中的所有句子都应归因于检索到的Context。
示例:
⑤ 上下文相关性
该指标衡量检索到的上下文(Context)的相关性,根据用户问题(question)和上下文(Context)计算得到,并且取值范围在 (0, 1)之间,值越高表示相关性越好。理想情况下,检索到的Context应只包含解答question的信息。 我们首先通过识别检索到的Context中与回答question相关的句子数量来估计 |S| 的值。 最终分数由以下公式确定:
说明:这里的|S|是指Context中存在的与解答question相关的句子数量
示例:
5.Ragas评估实操
Ⅰ、父文档检索器
文档切割时传统的做法是使用像CharacterTextSplitter,RecursiveCharacterTextSplitter这样的文档分割器将文档按指定的块大小(chunk_size)来均匀的切割文档,然后将每个文档块做向量化处理(Embedding)后将其保存到向量数据库中,而当我们在做文档检索时,会将用户的问题转换成的向量与向量数据库中的文档块的向量做相似度计算,并从中获取k个与用户问题向量相似度最高的文档块(也就是和用户问题相关的文档块),然后我们会把用户的问题以及相关的文档块一起发送给LLM,最后LLM会给出一个对用户友好的回复。这就是一般的传统文档检索的方法。
传统检索方法其实存在一定的局限性,这是因为文档块的大小会影响和用户问题的匹配度,也就是说当我们切割的文档块越大时,它与用户问题的匹配度就会越低,当文档块越小时,它与用户问题的匹配度会越高,这是因为较大的文档块可能会包含较多的内容,当它被转换成一个固定维度的向量时,该向量可能不能够准确反应出该文档块中的所有内容,因而对用户问题的匹配度就会降低,而小的文档块包含的内容较少,当它被转换成一个固定维度的向量时,该向量基本能够准确反应出该文档块中的内容,因此它与用户问题的匹配度会教高,但是较小的文档块可能因为所包含的信息量较少,因而它可能不是一个全面且正确的答案。为了解决这些问题今天来介绍Langchain中的父文档检索器,它能够有效的解决文档块大小与用户问题匹配的问题。
由于我们在利用大模型进行文档检索的时候,常常会有相互矛盾的需求,比如:
- 希望得到较小的文档块,以便它们Embedding以后能够最准确地反映出文档的含义,如果文档块太大,Embedding就失去了意义。
- 希望得到较大的文档块以保留教多的内容,然后将它们发送给LLM以便得到全面且正确的答案。
面对这样矛盾的需求,Langchain的父文档检索器为我们提供了两种有效的解决方案:
- 检索完整文档
- 检索较大的文档块
Ⅱ、检索完整文档
所谓检索完整文档是指将原始文档均匀的切割成若干个较小的文档块,然后将它们与用户的问题进行匹配,最后将匹配到的文档块所在原始文档和用户问题一起发送给llm后,由llm生成最终答案,如下图所示:
Ⅲ、检索较大文档块
当原始文档比较大时,我们需要将原始文档按照两个层级进行切割,即切割成主文档块和子文档块,而用户的问题会与所有的子文档块进行匹配(相似度比较) ,当匹配到特定的子文档块后,将该子文档块所属的主文档块的全部内容以及用户问题发送给llm,最后由llm来生成答案。
Ⅳ、代码实操
① 导入依赖库(工具准备)
# 文档加载工具
from langchain.document_loaders import PyPDFLoader
# 系统操作库
import os
# 嵌入模型(向量生成)
from langchain_community.embeddings import DashScopeEmbeddings
# 大语言模型(LLM)
from langchain_community.llms import Tongyi
# 文本分割工具
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 父文档检索器(高级检索逻辑)
from langchain.retrievers import ParentDocumentRetriever
# 内存存储(用于保存主文档)
from langchain.storage import InMemoryStore
# 向量数据库(存储子文档向量)
from langchain_chroma import Chroma
# 数据集处理工具
from datasets import Dataset
# 提示词模板
from langchain.prompts import ChatPromptTemplate
# LangChain表达式语言(构建问答链)
from langchain.schema.runnable import RunnableMap
# 输出解析器(处理LLM输出)
from langchain.schema.output_parser import StrOutputParser
# 评测工具
import pandas as pd
# 从 ragas 库中导入核心评估函数 evaluate,该函数是 RAGAS 评估流程的 “入口”,用于触发对 RAG 系统的完整评估。
from ragas import evaluate
# 从 ragas.metrics 模块(RAGAS 的指标集合)中导入 4 个核心评估指标,这些指标从不同维度量化 RAG 系统的性能,是评估的 “具体维度标准”。
from ragas.metrics import faithfulness, answer_relevancy, context_recall, context_precision
② 数据输入(加载文档)
docs:存储加载后的 PDF 文档内容的列表,列表中的每个元素是 LangChain 框架中的 Document
对象。
每个 Document
对象包含两个核心属性:
page_content
:PDF 对应页面的文本内容(字符串);metadata
:文档元数据(字典),通常包含页码(page
)、文件路径(source
)等信息。
PyPDFLoader():LangChain 框架 langchain.document_loaders
模块中的一个PDF 文档加载器类,用于初始化一个针对特定 PDF 文件的加载器实例。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
file_path | str | 必需参数,PDF 文件的本地路径,指定要加载的 PDF 文件。 | 无 |
PyPDFLoader().load():是 PyPDFLoader
实例的核心方法,用于实际读取并解析 PDF 文件内容,将 PDF 中的每一页转换为 Document
对象,并返回包含这些对象的列表。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
self | 实例本身 | 方法默认参数,指向当前 PyPDFLoader 实例(已关联目标 PDF 文件路径)。 | 无 |
extract_images | bool | 可选参数,是否提取 PDF 中的图像(若 PDF 包含图像,设为 True 可尝试提取,默认不提取)。 | False |
# 文档加载工具
from langchain.document_loaders import PyPDFLoaderdocs = PyPDFLoader(r"F:\AI_BigModel\L1课件\第3章_RAG高级技术\浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf").load()
③ 模型配置
DASHSCOPE_API_KEY:用于访问阿里云 DashScope 服务的 API 密钥(字符串类型)。
DashScope 是阿里云提供的大模型服务平台,调用通义千问(Qwen)等模型需通过该密钥进行身份验证。代码中通过 os.getenv("BAILIAN_API_KEY")
从环境变量 BAILIAN_API_KEY
中读取该密钥,避免硬编码敏感信息,提升安全性。
os.getenv():Python 标准库 os
模块的函数,用于读取系统环境变量的值。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
key | str | 必需参数,要获取的环境变量名称(如代码中的 "BAILIAN_API_KEY" )。 | 无 |
default | 任意类型 | 可选参数,环境变量不存在时返回的默认值(未指定时默认返回 None )。 | None |
llm:通过 Tongyi
类创建的大语言模型(LLM)实例对象。该对象封装了调用阿里云通义千问模型的能力,用于后续的文本生成(如基于检索到的上下文回答用户问题)。
Tongyi():LangChain 框架中用于初始化通义千问大模型的类,用于创建可调用通义千问模型的实例。封装了模型调用的底层逻辑(如 API 请求、参数处理、身份验证),简化开发者调用通义千问模型的流程。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
model_name | str | 可选参数,指定通义千问模型版本(如代码中的 "qwen-max" ,其他版本如 "qwen-plus" )。 | "qwen" |
dashscope_api_key | str | 必需参数,访问 DashScope 服务的 API 密钥(即 DASHSCOPE_API_KEY ),用于身份验证。 | 无 |
temperature | float | 可选参数,控制生成文本的随机性(0~1,值越高随机性越强)。 | 0.7 |
max_tokens | int | 可选参数,限制生成文本的最大 token 数量(控制输出长度)。 | 模型默认值 |
embeddings:通过 DashScopeEmbeddings
类创建的嵌入模型实例对象。嵌入模型的核心作用是将文本(如句子、段落)转换为固定维度的数值向量(向量嵌入),这些向量能反映文本的语义信息,是向量数据库存储和相似性检索的基础。
DashScopeEmbeddings():LangChain 框架中用于初始化阿里云 DashScope 嵌入模型的类,用于创建可生成文本向量的实例。封装了调用 DashScope 嵌入模型的底层逻辑,将文本转换为语义向量,支持后续的向量存储和相似性检索。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
model | str | 可选参数,指定嵌入模型版本(如代码中的 "text-embedding-v3" ,阿里云提供的文本嵌入模型)。 | "text-embedding-v1" |
dashscope_api_key | str | 必需参数,访问 DashScope 服务的 API 密钥(即 DASHSCOPE_API_KEY ),用于身份验证。 | 无 |
timeout | int | 可选参数,API 请求超时时间(单位:秒),超过时间未响应则终止请求。 | 60 |
batch_size | int | 可选参数,批量处理文本时每次输入的最大文本数量。 | 32 |
# 系统操作库
import os
# 嵌入模型(向量生成)
from langchain_community.embeddings import DashScopeEmbeddings
# 大语言模型(LLM)
from langchain_community.llms import Tongyi# 初始化大语言模型
DASHSCOPE_API_KEY = os.getenv("BAILIAN_API_KEY")llm = Tongyi(model_name="qwen-max",dashscope_api_key=DASHSCOPE_API_KEY
)# 创建嵌入模型
embeddings = DashScopeEmbeddings(model="text-embedding-v3",dashscope_api_key=DASHSCOPE_API_KEY
)
④ 文档预处理与检索器构建
parent_splitter:主文档分割器实例,通过 RecursiveCharacterTextSplitter
初始化,用于将原始文档分割为 “主文档块”(较大的文本块)。
child_splitter:子文档分割器实例,同样通过 RecursiveCharacterTextSplitter
初始化,用于将主文档块进一步分割为 “子文档块”(较小的文本块)。
RecursiveCharacterTextSplitter(): LangChain 中用于文本分割的核心类,通过递归方式按字符分割文本,优先使用自然分隔符(如换行、空格),避免破坏语义完整性。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
chunk_size | int | 必需参数,每个文本块的最大长度(单位:字符,部分场景下为 token)。 | 无(需显式指定,如代码中的 512 或 256) |
chunk_overlap | int | 可选参数,相邻文本块的重叠长度(用于保留上下文连贯性)。 | 20 |
separators | list | 可选参数,分割文本的分隔符列表(按优先级排序,如 ["\n\n", "\n", " "] )。 | ["\n\n", "\n", " ", ""] |
length_function | callable | 可选参数,计算文本长度的函数(默认按字符数,可自定义为 token 计数)。 | len (默认按字符长度计算) |
vectorstore:Chroma 向量数据库实例,用于存储子文档块的向量嵌入,并支持基于向量的相似性搜索。与嵌入模型(embeddings
)关联,负责将子文档块转换为向量并存储,后续检索时通过查询向量匹配最相关的子文档向量。
Chroma():LangChain 中用于初始化 Chroma 向量数据库的类,Chroma 是轻量级开源向量数据库,支持高效存储和相似性检索向量。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
collection_name | str | 可选参数,向量集合的名称(用于区分不同文档集,如代码中的 "split_parents" )。 | "langchain" |
embedding_function | 嵌入模型实例 | 必需参数,用于将文本转换为向量的嵌入函数(如代码中的 embeddings )。 | 无 |
persist_directory | str | 可选参数,向量数据持久化路径(未指定则仅在内存中存储,不持久化)。 | None |
client_settings | dict | 可选参数,Chroma 客户端配置(如连接参数等)。 | None |
store:内存存储实例,通过 InMemoryStore
初始化,用于存储主文档块的完整内容(非向量形式)。作为临时存储(不持久化到磁盘),它保存主文档块的原始文本,供检索时返回完整上下文(子文档用于检索,主文档用于提供完整语义)。
InMemoryStore():LangChain 中用于在内存中存储文档的类,属于轻量级存储方案,不将数据持久化到磁盘,仅在程序运行时临时存储。
retriever:父文档检索器实例,通过 ParentDocumentRetriever
初始化,是连接子文档向量检索与主文档完整内容的核心组件。
ParentDocumentRetriever():LangChain 中用于实现 “父文档 - 子文档” 检索逻辑的类,结合子文档向量检索和主文档完整内容返回,提升检索的准确性和语义完整性。用子文档向量在向量数据库中检索相似片段,再从主文档存储中返回对应的完整主文档块。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
vectorstore | 向量数据库实例 | 必需参数,存储子文档向量的向量数据库(如代码中的 vectorstore )。 | 无 |
docstore | 存储实例 | 必需参数,存储主文档块的文档存储(如代码中的 store )。 | 无 |
child_splitter | 文本分割器实例 | 必需参数,用于分割主文档为子文档的分割器(如代码中的 child_splitter )。 | 无 |
parent_splitter | 文本分割器实例 | 必需参数,用于分割原始文档为主文档的分割器(如代码中的 parent_splitter )。 | 无 |
search_kwargs | dict | 可选参数,检索配置(如 {"k": 2} 表示返回 top 2 相关结果)。 | {"k": 4} |
retriever.add_documents():ParentDocumentRetriever
的方法,用于将原始文档添加到检索器,内部自动完成文档分割、子文档向量存储和主文档存储。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
documents | 文档列表 | 必需参数,原始文档列表(如 docs ,每个元素为 LangChain 的 Document 对象)。 | 无 |
ids | list | 可选参数,文档的唯一标识列表(未指定则自动生成)。 | None |
list():Python 内置函数,用于将可迭代对象转换为列表。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
iterable | 可迭代对象 | 必需参数,需转换为列表的可迭代对象(如 store.yield_keys() 返回的迭代器)。 | 无 |
store.yield_keys():InMemoryStore
的方法,用于生成存储中所有主文档块的唯一标识(键)的迭代器。通过迭代器返回主文档的键,结合 list()
可获取所有键的列表,进而通过 len()
统计主文档的数量(验证文档是否正确分割和存储)。
# 文本分割工具
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 父文档检索器(高级检索逻辑)
from langchain.retrievers import ParentDocumentRetriever
# 内存存储(用于保存主文档)
from langchain.storage import InMemoryStore
# 向量数据库(存储子文档向量)
from langchain_chroma import Chroma# 创建主文档分割器
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=512)# 创建子文档分割器
child_splitter = RecursiveCharacterTextSplitter(chunk_size=256)# 创建向量数据库对象
vectorstore = Chroma(collection_name="split_parents", embedding_function=embeddings
)
# 创建内存存储对象
store = InMemoryStore()
# 创建父文档检索器
retriever = ParentDocumentRetriever(vectorstore=vectorstore,docstore=store,child_splitter=child_splitter,parent_splitter=parent_splitter,search_kwargs={"k": 2}
)# 添加文档集
retriever.add_documents(docs)# 切割出来主文档的数量
print(len(list(store.yield_keys())))
⑤ 构建问答链测试功能
template:一个字符串格式的提示词模板,定义了大语言模型(LLM)回答问题的规则和格式。
模板中包含固定文本(如角色定义、回答要求)和占位符({question}
和 {context}
),占位符将在运行时被实际的用户问题和检索到的上下文替换,引导 LLM 生成符合要求的回答。
prompt:通过 ChatPromptTemplate.from_template()
生成的提示词模板实例,是可被 LangChain 链调用的结构化对象。它封装了 template
中的逻辑,能够接收输入参数(如 question
和 context
)并动态生成完整的提示词文本,供 LLM 处理。
ChatPromptTemplate.from_template():ChatPromptTemplate
类的类方法,用于从字符串模板创建提示词模板实例。核心作用是将原始字符串模板(template
)转换为 LangChain 可处理的结构化对象,自动识别模板中的占位符(如 {question}
)作为输入参数,便于后续动态填充内容。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
template | str | 必需参数,字符串格式的提示词模板(如代码中的 template 变量),包含占位符 {key} 。 | 无 |
input_variables | list | 可选参数,指定模板中的占位符名称列表(未指定时自动从模板中提取,如 ["question", "context"] )。 | 自动提取 |
chain:通过 LangChain 表达式语言(LCEL)构建的问答流程链,串联了检索、提示词生成、LLM 调用和输出解析的完整逻辑。
ICEL(LangChain Expression Language):整条链通过管道符 |
串联多个组件,数据从左到右依次传递,最终输出用户问题的回答。结构如下:
RunnableMap(...) | prompt | llm | StrOutputParser()
这段代码是使用 LangChain 表达式语言(LCEL,LangChain Expression Language)构建的问答流程链(chain),它串联了从“获取上下文”到“生成最终回答”的完整逻辑。我们可以按组件和数据流向拆解其作用:核心逻辑:组件串联与数据流动
整条链通过管道符 `|` 串联多个组件,数据从左到右依次传递,最终输出用户问题的回答。结构如下:
`RunnableMap(...) | prompt | llm | StrOutputParser()` 逐组件解释 1. RunnableMap(...):输入映射与上下文获取
RunnableMap 是 LCEL 中的“输入映射组件”,作用是将原始输入转换为后续步骤所需的结构化数据。它接收一个字典作为参数,字典的键是“输出变量名”,值是生成该变量的函数。 代码中:
输入:一个包含用户问题的字典(如 `{"question": "客户经理被投诉一次扣多少分?"}`)。
处理逻辑: 键 "context":通过 lambda 函数 lambda x: retriever.get_relevant_documents(x["question"]) 实现 从输入 x 中提取 x["question"](用户问题),调用检索器 retriever 的 get_relevant_documents 方法,获取与问题相关的文档片段(上下文)。 键 "question":通过 lambda 函数 lambda x: x["question"] 直接提取输入中的用户问题,保持原样传递。 输出:一个字典 {"context": 检索到的上下文, "question": 用户问题},为后续步骤提供所需的核心输入。 2. | prompt:生成完整提示词
prompt 是之前通过 ChatPromptTemplate.from_template(template) 创建的提示词模板实例。管道符 "|" 表示将 RunnableMap 的输出 {"context": ..., "question": ...} 传递给 prompt。 作用:ChatPromptTemplate 会用 RunnableMap 输出的 context 和 question 填充模板中的占位符{question} 和 {context},生成完整的提示词文本。 3. | llm:调用大语言模型生成回答
llm 是之前初始化的大语言模型实例(如通义千问 qwen-max)。管道符 | 表示将 prompt 生成的完整提示词传递给 llm。 作用:llm 接收提示词文本,基于提示词中的问题和上下文生成回答内容。此时 llm 的输出通常是一个包含生成文本的对象。 4. | StrOutputParser():解析输出为纯字符串
StrOutputParser` 是 LangChain 的输出解析器,作用是将 llm 生成的复杂结果(如 LLMResult 对象)解析为简洁的纯字符串。 作用:提取 llm 生成的核心回答文本,去除多余的元数据,最终返回一个可直接使用的字符串(如“每投诉一次扣2分”)。 整体流程总结
这条链的完整数据流向是:
用户输入(问题)
→ 经 RunnableMap 生成 {"context": 检索上下文, "question": 问题}
→ 经 prompt 填充为完整提示词
→ 经 llm 生成回答内容
→ 经 StrOutputParser 解析为纯字符串回答
RunnableMap():LangChain 中的输入映射组件,用于将输入数据映射到多个输出键(键值对形式),为后续流程提供结构化输入。LCEL 中的 “输入映射组件”,作用是将原始输入转换为后续步骤所需的结构化数据。它接收一个字典作为参数,字典的键是 “输出变量名”,值是生成该变量的函数。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
mapping | dict | 必需参数,键值对字典,键为输出变量名(如 "context" ),值为生成该变量的函数(如 lambda x: ... )。 | 无 |
StrOutputPaser():LangChain 框架中用于解析大语言模型(LLM)输出结果的类,核心作用是将 LLM 生成的原始输出(通常是包含文本内容和元数据的复杂对象)转换为纯字符串格式,提取其中的核心文本内容。
LLM 的原始输出可能包含额外信息(如生成过程中的元数据、token 统计等),StrOutputParser
会过滤这些冗余信息,仅保留实际的生成文本,方便直接使用或展示(例如作为最终回答返回给用户)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
self | 实例本身 | 类方法的默认参数,指向当前 StrOutputParser 实例。该类初始化时无需额外参数,因为其功能仅为提取文本,逻辑固定。 | 无 |
retriever.get_relevant_documents():检索器(retriever
)的核心方法,用于根据用户问题从向量数据库中获取相关文档。核心逻辑:将问题转换为向量,在向量数据库中搜索相似子文档,再返回对应的主文档完整内容作为上下文,为 LLM 回答提供知识依据。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
query | str | 必需参数,用户的问题字符串(如代码中的 x["question"] ),作为检索的依据。 | 无 |
k | int | 可选参数,指定返回的相关文档数量(默认由检索器初始化时的 search_kwargs 决定,如代码中 k=2 )。 | 检索器默认值 |
query:用户的具体问题字符串,即需要 LLM 回答的查询内容。
response:chain.invoke()
执行后返回的LLM 回答结果(字符串类型)。它是 LLM 基于检索到的上下文和提示词模板生成的最终回答,包含对用户问题的具体响应(如 “每投诉一次扣 2 分”)。
chain.invoke():LangChain 链的执行方法,用于触发整个问答流程并返回结果。它接收输入参数(通常为包含用户问题的字典),按链定义的逻辑(检索→提示词→LLM→解析)执行,最终返回处理后的输出(即 response
)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
input | dict | 必需参数,输入数据字典,键为链所需的变量名(如代码中的 {"question": query} )。 | 无 |
config | dict | 可选参数,链的配置选项(如超时时间、日志级别等)。 | None |
# 提示词模板
from langchain.prompts import ChatPromptTemplate
# LangChain表达式语言(构建问答链)
from langchain.schema.runnable import RunnableMap
# 输出解析器(处理LLM输出)
from langchain.schema.output_parser import StrOutputParser# 创建prompt模板
template = """You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, just say that you don't know.
Use two sentences maximum and keep the answer concise.
Question: {question}
Context: {context}
Answer:
"""# 由模板生成prompt
prompt = ChatPromptTemplate.from_template(template)# 创建 chain(LCEL langchain 表达式语言)
chain = RunnableMap({"context": lambda x: retriever.get_relevant_documents(x["question"]),"question": lambda x: x["question"]
}) | prompt | llm | StrOutputParser()query = "客户经理被投诉了,投诉一次扣多少分?"
response = chain.invoke({"question": query})
print(response)
⑥ 批量查询与数据收集
questions:是一个存储测试问题的列表,包含多个用户可能提出的查询,用于批量测试 RAG 系统的回答能力。每个元素是一个字符串(如 “客户经理被投诉一次扣多少分?”),覆盖了知识库中的核心问题(考核标准、评聘时间、职位设置等),用于验证系统在不同场景下的表现。
ground_truths:是一个存储标准答案的列表,与 questions
一一对应,作为评测模型回答准确性的参考依据。每个元素是问题的 “正确答案”(如 “每投诉一次扣 2 分”),通常来自知识库原文,用于后续通过评测工具(如 RAGAS)对比模型回答与标准答案的一致性。
answers:是一个存储模型回答结果的空列表,后续通过循环调用问答链 chain
填充内容。每次循环中,将 chain.invoke()
生成的回答添加到列表中,最终 answers
与 questions
长度相同,记录每个问题的模型输出。
contexts:是一个存储检索上下文的空列表,后续通过循环调用检索器 retriever
填充内容。每次循环中,将 retriever.get_relevant_documents()
返回的相关文档内容添加到列表中,每个元素是一个子列表(包含 1 个或多个文档片段的文本),用于分析检索到的上下文是否准确支持回答。
chain.invoke():LangChain 中 “链(chain)” 的核心执行方法,用于触发整个问答流程(从输入问题到生成回答),接收输入参数并返回最终结果。它按链定义的逻辑(如检索上下文→生成提示词→调用 LLM→解析输出)执行,将用户问题转换为模型回答。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
input | dict | 必需参数,输入数据字典,包含链所需的变量(通常为 {"question": 用户问题} ,键需与链定义的输入变量匹配)。 | 无 |
config | dict | 可选参数,链的配置选项(如超时时间、日志级别、回调函数等,用于控制链的执行细节)。 | None |
列表.append():Python 列表的内置方法,用于在列表的末尾添加单个元素,直接修改原列表(不返回新列表)。常用于动态收集数据(如批量存储模型回答、检索结果等)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
element | 任意类型 | 必需参数,要添加到列表中的元素(可以是字符串、字典、列表等任意类型,如模型回答、文档对象)。 | 无 |
retriever.get_relevant_documents():检索器(Retriever)的核心方法,用于根据输入的查询字符串,从向量数据库或文档存储中获取与查询语义相关的文档片段,为模型生成回答提供上下文支持。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
query | str | 必需参数,用户的查询字符串(如问题文本),作为检索的依据(会转换为向量进行相似性匹配)。 | 无 |
k | int | 可选参数,指定返回的相关文档数量(即 “Top-k” 检索,默认值由检索器初始化时的 search_kwargs 决定,如 k=2 )。 | 检索器默认值 |
filter | dict | 可选参数,检索时的过滤条件(如按文档来源、时间等筛选,具体支持取决于底层向量数据库)。 | None |
data:是一个整合测试数据的字典,将问题、模型回答、检索上下文和标准答案关联起来,为生成评测数据集做准备。包含 4 个键:"user_input"
(问题)、"response"
(模型回答)、"retrieved_contexts"
(检索上下文)、"reference"
(标准答案),键值均为列表且长度一致。
dataset:通过 Dataset.from_dict(data)
生成的评测数据集对象(来自 datasets
库),用于兼容 RAG 评测工具(如 RAGAS)的输入格式。数据集将 data
中的字典结构转换为结构化表格,方便后续调用 evaluate()
函数进行量化评估。
Dataset.from_dict():datasets
库中的方法,用于将字典格式的数据转换为结构化的 Dataset
对象(一种用于存储和处理数据的容器),适配机器学习或评测工具(如 RAGAS)的输入格式,方便后续数据处理和评估。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
data | dict | 必需参数,包含数据的字典,键为字段名(如 "user_input" "response" ),值为列表(每个列表元素对应一条数据,且所有列表长度需一致)。 | 无 |
split | str | 可选参数,指定数据集的拆分名称(如 "train" 表示训练集、"test" 表示测试集,用于区分不同数据子集)。 | None |
verify_integrity | bool | 可选参数,是否验证数据完整性(检查所有列表长度是否一致)。 | True |
# 数据集处理工具
from datasets import Datasetquestions = ["客户经理被投诉了,投诉一次扣多少分?","客户经理每年评聘申报时间是怎样的?","客户经理在工作中有不廉洁自律情况的,发现一次扣多少分?","客户经理不服从支行工作安排,每次扣多少分?","客户经理需要什么学历和工作经验才能入职?","个金客户经理职位设置有哪些?"
]ground_truths = ["每投诉一次扣2分","每年一月份为客户经理评聘的申报时间","在工作中有不廉洁自律情况的每发现一次扣50分","不服从支行工作安排,每次扣2分","须具备大专以上学历,至少二年以上银行工作经验","个金客户经理职位设置为:客户经理助理、客户经理、高级客户经理、资深客户经理"
]answers = []
contexts = []# Inference
for query in questions:answers.append(chain.invoke({"question": query}))contexts.append([docs.page_content for docs in retriever.get_relevant_documents(query)])# To dict
data = {"user_input": questions,"response": answers,"retrieved_contexts": contexts,"reference": ground_truths
}# Convert dict to dataset
dataset = Dataset.from_dict(data)
print(dataset)
⑦ RAG效果评测
pd.set_option():配置 pandas 的全局显示参数,调整 DataFrame、Series 等对象的输出格式,优化可读性。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
option_name | str | 必需参数,要设置的选项名称(如 'display.max_colwidth' 控制列内容最大宽度)。 | 无 |
value | 任意类型 | 必需参数,选项的取值(如 None 表示不限制列宽,2000 表示设置显示宽度为 2000)。 | 无 |
result:是 evaluate()
函数返回的RAG 评测结果对象(来自 ragas 库),包含各项评测指标的计算结果。
metrics:是 evaluate()
函数的参数,指需要计算的评测指标列表(来自 ragas.metrics),用于指定评估 RAG 系统的维度。代码中包含 4 个核心指标:
context_precision
(上下文精确率):检索到的上下文是否均与问题相关;context_recall
(上下文召回率):检索到的上下文是否包含回答所需的所有关键信息;faithfulness
(回答忠实度):回答是否完全基于检索到的上下文(无幻觉);answer_relevancy
(回答相关性):回答是否紧扣问题,不包含无关内容。
evaluate():ragas 库的核心函数,用于量化评估 RAG 系统的性能,基于输入的数据集和指定指标,计算每个样本的得分并返回汇总结果。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
dataset | Dataset 对象 | 必需参数,包含测试数据的数据集(来自 datasets 库),需包含 user_input (问题)、response (回答)、retrieved_contexts (检索上下文)、reference (标准答案)等字段。 | 无 |
metrics | 指标列表 | 必需参数,要计算的评测指标(如 [context_precision, faithfulness] ,需从 ragas.metrics 导入)。 | 无 |
embeddings | 嵌入模型实例 | 必需参数,用于计算文本相似度的嵌入模型(如代码中的 embeddings ,用于指标如 answer_relevancy 的计算)。 | 无 |
llm | 大语言模型实例 | 必需参数,用于推理的大语言模型(如代码中的 llm ,用于指标如 faithfulness 的逻辑判断)。 | 无 |
raise_exceptions | bool | 可选参数,是否在计算失败时抛出异常(False 表示忽略错误并继续计算)。 | False |
df:通过 result.to_pandas()
转换得到的pandas DataFrame 对象,以表格形式存储评测结果。
result.to_pandas():将 ragas 评测结果对象(result
)转换为pandas DataFrame,以结构化表格形式展示每个样本的各项指标得分,方便查看和后续分析(如排序、筛选)。
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
self | 评测结果对象 | 方法默认参数,指向当前 result 对象(ragas 的评测结果实例)。 | 无 |
flatten | bool | 可选参数,是否将嵌套的指标数据展平(如将整体得分和样本得分合并为单表)。 | True |
# 5.显示评测结果
# 评测结果# 评测工具
# 从 ragas 库中导入核心评估函数 evaluate,该函数是 RAGAS 评估流程的 “入口”,用于触发对 RAG 系统的完整评估。
from ragas import evaluate
# 从 ragas.metrics 模块(RAGAS 的指标集合)中导入 4 个核心评估指标,这些指标从不同维度量化 RAG 系统的性能,是评估的 “具体维度标准”。
from ragas.metrics import faithfulness, answer_relevancy, context_recall, context_precision'''
# 设置列宽:None 表示不限制(或设为足够大的数值,如 1000)
pd.set_option('display.max_colwidth', None)# 设置显示所有列(避免列折叠)
pd.set_option('display.max_columns', None)# 设置整体显示宽度(适配终端/Notebook 宽度,如设为 2000)
pd.set_option('display.width', 2000)
'''result = evaluate(dataset=dataset,metrics=[context_precision,context_recall,faithfulness,answer_relevancy,],embeddings=embeddings,llm=llm,
)df = result.to_pandas()
print(df)
完整代码
# 1. 加载PDF文档
from langchain.document_loaders import PyPDFLoader# 2. 准备文档集,初始化llm
import os
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.llms import Tongyi
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_chroma import Chroma# 3. 调用chain进行推理
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnableMap
from langchain.schema.output_parser import StrOutputParser# 4. 准备评估的QA数据集
from datasets import Dataset# 5. 评估结果
from ragas import evaluate
from ragas.metrics import (faithfulness,answer_relevancy,context_recall,context_precision,
)# ——————————————————————————————————————————————————————————————————# 1.加载PDF文档
docs = PyPDFLoader(r"F:\AI_BigModel\L1课件\第3章_RAG高级技术\浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf").load()print("——————————————————————————————————-——————————")# 2. 准备文档集,初始化llm
# 初始化大语言模型
DASHSCOPE_API_KEY = os.getenv("BAILIAN_API_KEY")llm = Tongyi(model_name="qwen-max",dashscope_api_key=DASHSCOPE_API_KEY
)# 创建嵌入模型
embeddings = DashScopeEmbeddings(model="text-embedding-v3",dashscope_api_key=DASHSCOPE_API_KEY
)# 创建主文档分割器
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=512)# 创建子文档分割器
child_splitter = RecursiveCharacterTextSplitter(chunk_size=256)# 创建向量数据库对象
vectorstore = Chroma(collection_name="split_parents", embedding_function=embeddings
)
# 创建内存存储对象
store = InMemoryStore()
# 创建父文档检索器
retriever = ParentDocumentRetriever(vectorstore=vectorstore,docstore=store,child_splitter=child_splitter,parent_splitter=parent_splitter,search_kwargs={"k": 2}
)# 添加文档集
retriever.add_documents(docs)# 切割出来主文档的数量
print(len(list(store.yield_keys())))print("——————————————————————————————————-——————————")# 3. 调用chain进行推理
# 创建prompt模板
template = """You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, just say that you don't know.
Use two sentences maximum and keep the answer concise.
Question: {question}
Context: {context}
Answer:
"""# 由模板生成prompt
prompt = ChatPromptTemplate.from_template(template)# 创建 chain(LCEL langchain 表达式语言)
chain = RunnableMap({"context": lambda x: retriever.get_relevant_documents(x["question"]),"question": lambda x: x["question"]
}) | prompt | llm | StrOutputParser()query = "客户经理被投诉了,投诉一次扣多少分?"
response = chain.invoke({"question": query})
print(response)print("——————————————————————————————————-——————————————————————————")# 4. 准备评估的QA数据集
questions = ["客户经理被投诉了,投诉一次扣多少分?","客户经理每年评聘申报时间是怎样的?","客户经理在工作中有不廉洁自律情况的,发现一次扣多少分?","客户经理不服从支行工作安排,每次扣多少分?","客户经理需要什么学历和工作经验才能入职?","个金客户经理职位设置有哪些?"
]ground_truths = ["每投诉一次扣2分","每年一月份为客户经理评聘的申报时间","在工作中有不廉洁自律情况的每发现一次扣50分","不服从支行工作安排,每次扣2分","须具备大专以上学历,至少二年以上银行工作经验","个金客户经理职位设置为:客户经理助理、客户经理、高级客户经理、资深客户经理"
]answers = []
contexts = []# Inference
for query in questions:answers.append(chain.invoke({"question": query}))contexts.append([docs.page_content for docs in retriever.get_relevant_documents(query)])# To dict
data = {"user_input": questions,"response": answers,"retrieved_contexts": contexts,"reference": ground_truths
}# Convert dict to dataset
dataset = Dataset.from_dict(data)
print(dataset)print("——————————————————————————————————-——————————————————————————")# 5.显示评测结果
# 评测结果'''
# 设置列宽:None 表示不限制(或设为足够大的数值,如 1000)
pd.set_option('display.max_colwidth', None)# 设置显示所有列(避免列折叠)
pd.set_option('display.max_columns', None)# 设置整体显示宽度(适配终端/Notebook 宽度,如设为 2000)
pd.set_option('display.width', 2000)
'''result = evaluate(dataset=dataset,metrics=[context_precision,context_recall,faithfulness,answer_relevancy,],embeddings=embeddings,llm=llm,
)df = result.to_pandas()
print(df)
六、商业落地实施RAG工程的核心步骤
1.数据集的准备(语料)
文档结构化处理:采用现代的智能文档技术
2.测试集的准备(QA对)
使用主流的LLM模型来根据文档来生成QA对
3.技术选型
不同场景选择不同的RAG技术
NativeRAG:RAG 技术的基础形态,核心逻辑是 “检索 - 增强 - 生成” 的线性流程 ,其核心特点是依赖文本片段的向量相似度匹配,强调 “即插即用” 的便捷性和较低的技术复杂度。
- 文档加载与分割:将原始文档(如 PDF、文本)按固定策略(如字符长度、语义边界)分割为可处理的文本片段(Chunks);
- 向量存储与检索:通过嵌入模型(Embedding Model)将文本片段转化为向量,存储于向量数据库(如 Chroma、Milvus),检索时通过向量相似度匹配获取相关片段;
- 生成式问答:将检索到的文本片段与用户问题拼接,通过大语言模型(LLM)生成回答。
GraphRAG:基于知识图谱(Knowledge Graph)的拓扑结构组织信息,核心逻辑是 “实体建模 - 关系推理 - 关联检索”,其核心特点是捕捉实体间的语义关联,支持多跳推理和复杂关系查询。
- 实体与关系抽取:通过命名实体识别(NER)和关系抽取模型,从文本中提取核心实体(如人物、机构)和实体间关系(如隶属、因果);
- 知识图谱构建:将实体作为节点、关系作为边,构建结构化知识图谱(如 Neo4j、Amazon Neptune);
- 图检索与推理:基于用户问题中的实体,在知识图谱中通过路径查询(如 Cypher)获取关联实体和关系,结合 LLM 生成基于关系链的回答。
AgenticRAG:AgenticRAG 引入 “智能体(Agent)” 机制,核心逻辑是 “任务规划 - 动态检索 - 反思优化” 的闭环流程,其核心特点是具备自主决策能力,支持复杂问题的分步解决和动态优化。
- 任务解析模块:将用户问题拆解为子任务(如 “需要检索 XX 数据”“需验证 XX 信息”);
- 工具调用能力:通过工具接口(Tools)动态调用检索引擎、计算器、API 等外部能力;
- 记忆与反思机制:记录检索历史和中间结果,通过 “自我反思” 优化检索策略(如调整关键词、扩大检索范围);
- 多轮决策逻辑:根据中间结果判断是否需要进一步检索或直接生成回答。
⭐核心差异及应用场景对比表格
对比维度 | NativeRAG(原生 RAG) | GraphRAG(图结构 RAG) | AgenticRAG(智能体 RAG) |
---|---|---|---|
核心定义 | 基于文本片段向量检索的基础 RAG 架构,实现 “检索 - 增强 - 生成” 线性流程 | 基于知识图谱组织实体与关系的结构化 RAG,支持实体关联推理 | 引入智能体决策机制的动态 RAG,通过任务规划与工具调用实现复杂问题解决 |
知识组织形式 | 非结构化文本片段(Chunks),按语义或长度分割存储 | 结构化知识图谱(实体为节点、关系为边),显性存储实体间关联 | 动态整合多源知识(文本片段 + 知识图谱 + 实时数据),按需调用不同类型资源 |
检索逻辑 | 向量相似度匹配(单轮静态检索),通过嵌入模型计算问题与文本片段的向量距离 | 实体关系路径查询(多跳推理),基于实体在图谱中遍历关联关系链 | 任务驱动的动态检索(多轮优化),根据子任务目标调整检索策略和范围 |
技术核心模块 | 文档分割器、向量数据库、LLM 生成器 | 实体关系抽取器、知识图谱数据库(如 Neo4j)、图查询引擎 | 任务解析器、工具调用接口、记忆与反思模块、多轮决策逻辑 |
推理能力 | 依赖 LLM 对检索文本的直接理解,支持基础事实性推理 | 基于实体关系链的逻辑推理,支持多实体关联和因果关系推导 | 基于任务拆解的分步推理,支持复杂逻辑验证和动态修正结论 |
技术复杂度 | 低,无需复杂预处理,可通过 LangChain 等框架快速部署 | 中,需实体关系抽取和知识图谱构建,依赖图谱存储与查询技术 | 高,需设计智能体决策逻辑、工具接口和反思机制,技术栈复杂 |
响应速度 | 快(检索 - 生成链路短,毫秒至秒级) | 中(图查询增加额外耗时,秒级) | 慢(多轮交互和决策耗时,秒至分钟级) |
优势 | 1. 部署成本低,易上手 2. 对简单问题响应速度快 3. 适配各类非结构化文档 | 1. 支持多跳关系查询,推理可解释性强 2. 实体关联清晰,适合关系密集型知识 3. 减少文本歧义 | 1. 可处理复杂、模糊或多步骤问题 2. 支持动态调用外部工具和实时数据 3. 具备自主优化能力 |
局限性 | 1. 难以处理跨文档关联问题 2. 对长文本上下文理解有限 3. 推理可解释性弱 | 1. 知识图谱构建成本高,依赖高质量实体关系抽取 2. 对非结构化文本细节覆盖不足 | 1. 响应速度慢,资源消耗高 2. 决策逻辑设计复杂,易出现无效循环 |
典型应用场景 | 1. 企业内部文档检索(如员工手册、产品说明书查询) 2. 客服标准化问答(如套餐说明、报销流程) 3. 开源工具快速集成(中小团队轻量部署) | 1. 金融风控分析(企业关联关系、供应链风险传导) 2. 医疗知识问答(疾病 - 症状 - 药物关联) 3. 历史 / 社科研究(人物关系、事件因果链分析) | 1. 科研文献分析(领域核心突破梳理、论文关联检索) 2. 商业决策支持(市场份额分析、竞品调研) 3. 专业服务场景(法律咨询、政策合规验证) |
核心价值 | 快速实现基础问答能力,解决简单事实性查询需求 | 挖掘知识间隐藏关联,提供可追溯的结构化推理结果 | 突破静态检索限制,实现类人类的复杂任务处理能力 |
4.构建知识库
5.测试和优化
- 根据不同的阶段来进行优化处理(数据准备、知识检索、答案生成)
- 数据预处理,结构化处理
- 切片策略
- 召回策略
- 重排序
- RAFT
6.最终效果评估
- Ragas 来进行 RAG 系统性能的评估
7.生产环境部署
- 本地模型部署 vLLM