04-RAG高级技术与实践
知识图谱

第一步:交朋友(基本原理)
想象一下,你想认识一个班级里的所有人。你首先会知道每个人的名字(比如“小明”、“小红”),然后慢慢了解他们之间的关系(比如“小明是小红的朋友”、“小红是班长”)。
知识图谱干的就是这个事:
-
名字,在知识图谱里叫 “实体” (比如“苹果公司”、“史蒂夫·乔布斯”、“iPhone”)。
-
关系,就叫 “关系” (比如“乔布斯 创立了 苹果公司”、“苹果公司 发布了 iPhone”)。
把无数个这样的“谁-是什么关系-谁”连接起来,就形成了一张巨大的、描述世界万事万物关系的“图谱”。
第二步:建圈子(构建和应用)
-
收集信息:从各种地方(文档、网页、图片)找资料,就像收集同学们的自我介绍和八卦。
-
整理归类:
-
定规矩:先想好我们要记录哪些“人”(实体)和“关系”(本体构建)。比如,一个“公司”实体,会有“创始人”、“产品”这些属性。
-
认人:从资料里自动找出“人名”——实体(知识抽取)。
-
对号入座:发现资料里“Apple Inc.”和“苹果公司”指的是同一个东西,就统一起来(知识融合)。
-
记在本子上:把这些整理好的关系,用一种结构化的方式(比如图数据库)存起来(知识存储)。
-
-
形成圈子:整理完之后,你就得到了一个清晰的、关于某个领域(比如科技圈、娱乐圈)的“社交关系图”,这就是领域知识图谱。
第三步:做事情(它能干什么)
建好了这个“关系网”,机器就能像人一样“理解”事物背后的联系了。于是它可以帮你做很多事情,如图二所示:
-
智能问答:你可以问:“乔布斯创办了哪些公司?” 它不像搜索引擎给你一堆网页,而是能直接回答“他创办了苹果公司和NeXT公司”,因为它“知道”这个关系。
-
深度推荐:在购物网站,它不仅知道你买了手机,还能“理解”手机需要贴膜、耳机,因为这些物品在知识图谱里是强相关的。这比“买了手机的人还买了…”这种统计更智能。
-
关系可视化:可以把复杂的关系(比如一家公司的股权结构、人物的社会关系)用图表直观地画出来,一目了然。
最后,知识图谱 vs. 大模型
你可以这么理解它们的区别和配合:
-
大模型(如ChatGPT) 像是一个博览群书、文笔极好的作家。它根据学过的海量文本模式来“生成”答案,非常灵活,但有时会“编造”或记不清细节(幻觉)。
-
知识图谱 则像是一个严谨的档案管理员或关系专家。它记下的每一条关系都是经过核实的、结构化的“事实”,准确可靠,但不够灵活,只能回答它“知道”的事情。
最佳拍档:现在常常把两者结合,让大模型(作家)去查阅知识图谱(档案库)里的准确事实,再组织语言回答你。这样既准确又有文采,这就是“检索增强生成(RAG)”的一种高级形式。
总结一下:知识图谱就是把世界变成一张机器能看懂的关系大网,让它能“理解”事物之间的联系,从而提供更精准、更智能的服务。
1. RAG技术树

-
RAG研究的技术树主要涉及预训练(Pre-training)、微调(Fine-tuning)和推理(Inference)等阶段。
-
随着LLM的出现,RAG的研究最初侧重于利用LLMs强大的上下文学习能力,主要集中在推理阶段。
-
随后的研究进一步深入,逐渐与LLMs的微调阶段更加融合。研究人员也在探索通过检索增强技术来提升预训练阶段的语言模型性能。
参考文章:https://www.promptingguide.ai/research/rag
2. 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):
-
适应特定领域的LLMs对于许多新兴应用至关重要,但如何有效融入信息仍是一个开放问题。
-
RAFT结合了检索增强生成(RAG)和监督微调(SFT),从而提高模型在特定领域内回答问题的能力。
-
训练模型识别并忽略那些不能帮助回答问题的干扰文档,只关注和引用相关的文档。
-
通过在训练中引入干扰文档,提高模型对干扰信息的鲁棒性,使其在测试时能更好地处理检索到的文档。
训练示例:https://github.com/lumpenspace/raft
3. RAG高效召回方法
Thinking:都有哪些RAG召回的策略,提升召回的质量?
3.1. 合理设置TOP_K
# docs = knowledgeBase.similarity_search(query, k=10) # !pip install pypdf2 # !pip install dashscope # !pip install langchain # !pip install langchain-openai # !pip install langchain-community # !pip install faiss-cpu这里代码存在版本问题,请参看03
import os import logging import pickle from PyPDF2 import PdfReader from langchain.chains.question_answering import load_qa_chain from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.callbacks.manager import get_openai_callback from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import FAISS from typing import List, Tuple def extract_text_with_page_numbers(pdf) -> Tuple[str, List[int]]: """ 从PDF中提取文本并记录每行文本对应的页码 参数: pdf: PDF文件对象 (需支持.pages属性迭代) 返回: text: 提取的文本内容 (行之间用\\n分隔) page_numbers: 每行文本对应的页码列表 (与text.split('\\n')严格对应) """ all_lines = [] # 存储所有文本行 page_numbers = [] # 存储每行对应的页码 for page_number, page in enumerate(pdf.pages, start=1): extracted_text = page.extract_text() if not extracted_text or extracted_text.isspace(): logging.warning(f"No text found on page {page_number}.") continue # 跳过空页面 # 按行分割并保留空行 (关键: keepends=False 确保不保留换行符) lines = extracted_text.splitlines(keepends=False) # 过滤掉纯空白行 (可选,根据需求决定是否保留空行) # lines = [line for line in lines if line.strip() != ""] # 记录有效行及其页码 all_lines.extend(lines) page_numbers.extend([page_number] * len(lines)) # 重建完整文本 (行之间用\\n连接) text = "\n".join(all_lines) return text, page_numbers def process_text_with_splitter(text: str, page_numbers: List[int], save_path: str = None) -> FAISS: """ 处理文本并创建向量存储 参数: text: 提取的文本内容 page_numbers: 每行文本对应的页码列表 save_path: 可选,保存向量数据库的路径 返回: knowledgeBase: 基于FAISS的向量存储对象 """ # 创建文本分割器(递归字符文本分割器),用于将长文本分割成小块 text_splitter = RecursiveCharacterTextSplitter( separators = ["\n\n", "\n", ".", " ", ""], chunk_size = 512, chunk_overlap = 128, length_function = len, ) # 分割文本 chunks = text_splitter.split_text(text) # logging.debug(f"Text split into {len(chunks)} chunks.") print(f"文本被分割成 {len(chunks)} 个块。") # 创建嵌入模型,OpenAI嵌入模型,配置环境变量 OPENAI_API_KEY # embeddings = OpenAIEmbeddings() # 调用阿里百炼平台文本嵌入模型,配置环境变量 DASHSCOPE_API_KEY embeddings = DashScopeEmbeddings( model = "text-embedding-v4" ) # 从文本块创建知识库 knowledgeBase = FAISS.from_texts(chunks, embeddings) print("已从文本块创建知识库...") # 存储每个文本块对应的页码信息 page_info = {chunk: page_numbers[i] for i, chunk in enumerate(chunks)} knowledgeBase.page_info = page_info # 如果提供了保存路径,则保存向量数据库和页码信息 if save_path: # 确保目录存在 os.makedirs(save_path, exist_ok=True) # 保存FAISS向量数据库 knowledgeBase.save_local(save_path) print(f"向量数据库已保存到: {save_path}") # 保存页码信息到同一目录 with open(os.path.join(save_path, "page_info.pkl"), "wb") as f: pickle.dump(page_info, f) print(f"页码信息已保存到: {os.path.join(save_path, 'page_info.pkl')}") return knowledgeBase def load_knowledge_base(load_path: str, embeddings = None) -> FAISS: """ 从磁盘加载向量数据库和页码信息 参数: load_path: 向量数据库的保存路径 embeddings: 可选,嵌入模型。如果为None,将创建一个新的DashScopeEmbeddings实例 返回: knowledgeBase: 加载的FAISS向量数据库对象 """ # 如果没有提供嵌入模型,则创建一个新的 if embeddings is None: embeddings = DashScopeEmbeddings( model="text-embedding-v4" ) # 加载FAISS向量数据库,添加allow_dangerous_deserialization=True参数以允许反序列化 knowledgeBase = FAISS.load_local(load_path, embeddings, allow_dangerous_deserialization=True) print(f"向量数据库已从 {load_path} 加载。") # 加载页码信息 page_info_path = os.path.join(load_path, "page_info.pkl") if os.path.exists(page_info_path): with open(page_info_path, "rb") as f: page_info = pickle.load(f) knowledgeBase.page_info = page_info print("页码信息已加载。") else: print("警告: 未找到页码信息文件。") return knowledgeBase # 读取PDF文件 pdf_reader = PdfReader('./浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf') # 提取文本和页码信息 text, page_numbers = extract_text_with_page_numbers(pdf_reader) print(f"提取的文本长度: {len(text)} 个字符。") # 处理文本并创建知识库,同时保存到磁盘 save_dir = "./vector_db" knowledgeBase = process_text_with_splitter(text, page_numbers, save_path=save_dir) # 处理文本并创建知识库 knowledgeBase = process_text_with_splitter(text, page_numbers) knowledgeBaseimport os from langchain_community.llms import Tongyi # 设置查询问题 query = "客户经理被投诉了,投诉一次扣多少分?" # query = "客户经理每年评聘申报时间是怎样的?" if query: # 调用阿里百炼平台文本嵌入模型,配置环境变量 DASHSCOPE_API_KEY embeddings = DashScopeEmbeddings( model = "text-embedding-v4" ) # 初始化对话大模型 llm = Tongyi( model_name="qwen-max", dashscope_api_key=os.getenv("DASHSCOPE_API_KEY") ) # 从磁盘加载向量数据库 loaded_knowledgeBase = load_knowledge_base("./vector_db", embeddings) # 使用加载的知识库进行查询 top_k 和 top_n # 一般比较优的文档个数 5 个左右 docs = loaded_knowledgeBase.similarity_search(query, k=5) print(docs) # 加载问答链 chain = load_qa_chain(llm, chain_type="stuff") # 准备输入数据 input_data = {"input_documents": docs, "question": query} # 执行问答链 response = chain.invoke(input=input_data) print(response["output_text"])
3.2. 改进索引算法
知识图谱: 利用知识图谱中的语义信息和实体关系,增强对查询和文档的理解,提升召回的相关性
3.3. 引入重排序(Reranking)
重排序模型: 对召回结果进行重排,提升问题和文档的相关性。常见的重排序模型有 BGE-Rerank 和 Cohere Rerank。
-
场景:用户查询“如何提高深度学习模型的训练效率?”
-
召回结果:初步召回10篇文档,其中包含与“深度学习”、“训练效率”相关的文章。
-
重排序:BGE-Rerank对召回的10篇文档进行重新排序,将与“训练效率”最相关的文档(如“优化深度学习训练的技巧”)排在最前面,而将相关性较低的文档(如“深度学习基础理论”)排在后面。
混合检索: 结合向量检索和关键词检索的优势,通过重排序模型对结果进行归一化处理,提升召回质量。
3.4. 优化查询扩展
相似语义改写: 使用大模型将用户查询改写成多个语义相近的查询,提升召回多样性。
- 例如,LangChain的
MultiQueryRetriever支持多查询召回,再进行回答问题。
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("DASHSCOPE_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
)
# 加载向量数据库,添加allow_dangerous_deserialization=True参数以允许反序列化
vectorstore = FAISS.load_local("./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[:200] + "..." if len(doc.page_content) > 200 else doc.page_content)
3.5. 双向改写
将查询改写成文档(Query2Doc)或为文档生成查询(Doc2Query),缓解短文本向量化效果差的问题
1. Query2Doc:将查询改写成文档
-
用户查询:“如何提高深度学习模型的训练效率?”
-
Query2Doc 改写:
-
原始查询较短,可能无法充分表达用户意图。
-
通过 Query2Doc 生成一段扩展文档:
提高深度学习模型的训练效率可以从以下几个方面入手:
-
使用更高效的优化算法,如AdamW或LAMB。
-
采用混合精度训练(Mixed Precision Training),减少显存占用并加速计算。
-
使用分布式训练技术,如数据并行或模型并行。
-
对数据进行预处理和增强,减少训练时的冗余计算。
-
调整学习率调度策略,避免训练过程中的震荡。
-
-
2. Doc2Query:为文档生成关联查询
-
文档内容:
本文介绍了深度学习模型训练中的优化技巧,包括:
-
使用AdamW优化器替代传统的SGD。
-
采用混合精度训练,减少显存占用。
-
使用分布式训练技术加速大规模模型的训练……
-
-
通过 Doc2Query 生成一组可能的查询:
-
如何选择深度学习模型的优化器?
-
混合精度训练有哪些优势?
-
分布式训练技术如何加速深度学习?
-
如何减少深度学习训练中的显存占用?
-
深度学习模型训练的最佳实践是什么?
-
3.5. 索引扩展
-
离散索引扩展: 使用关键词抽取、实体识别等技术生成离散索引,与向量检索互补,提升召回准确性。
-
连续索引扩展: 结合多种向量模型(如OpenAI的Ada、智源的BGE)进行多路召回,取长补短。
-
混合索引召回: 将BM25等离散索引与向量索引结合,通过Ensemble Retriever实现混合召回,提升召回多样性
3.5.1. 离散索引
使用关键词抽取、实体识别等技术生成离散索引,与向量检索互补,提升召回准确性。
1. 关键词抽取: 从文档中提取出重要的关键词,作为离散索引的一部分,用于补充向量检索的不足。
文档内容:
本文介绍了深度学习模型训练中的优化技巧,包括:
1. 使用AdamW优化器替代传统的SGD。
2. 采用混合精度训练,减少显存占用。
3. 使用分布式训练技术加速大规模模型的训练。
通过关键词抽取技术(如TF-IDF、TextRank)提取出以下关键词:
["深度学习", "模型训练", "优化技巧", "AdamW", "混合精度训练", "分布式训练"]
当用户查询“如何优化深度学习模型训练?”时,离散索引中的关键词能够快速匹配到相关文档。
2. 实体识别: 从文档中识别出命名实体(如人名、地点、组织等),作为离散索引的一部分,增强检索的精确性。
文档内容:
2023年诺贝尔物理学奖授予了三位科学家,以表彰他们在量子纠缠领域的研究成果。
通过实体识别技术(如SpaCy、BERT-based NER)提取出以下实体:
["2023年", "诺贝尔物理学奖", "量子纠缠"]
当用户查询“2023年诺贝尔物理学奖的获奖者是谁?”时,离散索引中的实体能够快速匹配到相关文档。
3.5.2. 混合索引召回
将离散索引(如关键词、实体)与向量索引结合,通过混合召回策略提升检索效果。
文档内容:
本文介绍了人工智能在医疗领域的应用,包括:
1. 使用深度学习技术进行医学影像分析。
2. 利用自然语言处理技术提取电子病历中的关键信息。
3. 开发智能诊断系统辅助医生决策。
关键词抽取:["人工智能", "医疗领域", "深度学习", "医学影像分析", "自然语言处理", "电子病历", "智能诊断系统"]
实体识别:["人工智能", "医疗领域", "深度学习", "自然语言处理"]
当用户查询“人工智能在医疗领域的应用有哪些?”时: 离散索引通过关键词和实体匹配到相关文档。 向量索引通过语义相似度匹配到相关文档。 综合两种召回结果,提升检索的准确性和覆盖率。
3.5.3. Small-to-Big
Small-to-Big 索引策略: 一种高效的检索方法,特别适用于处理长文档或多文档场景。核心思想是通过小规模内容(如摘要、关键句或段落)建立索引,并链接到大规模内容主体中。这种策略的优势在于能够快速定位相关的小规模内容,并通过链接获取更详细的上下文信息,从而提高检索效率和答案的逻辑连贯性。
小规模内容(索引部分):
摘要:从每篇论文中提取摘要作为索引内容。
-
摘要1:本文介绍了Transformer 模型在机器翻译任务中的应用,并提出了改进的注意力机制。
-
摘要2:本文探讨了Transformer 模型在文本生成任务中的性能,并与RNN 模型进行了对比。
关键句:从论文中提取与查询相关的关键句。
-
关键句1:Transformer 模型通过自注意力机制实现了高效的并行计算。
-
关键句2:BERT 是基于Transformer 的预训练模型,在多项NLP 任务中取得了显著效果。
大规模内容(链接部分):
每篇论文的完整内容作为大规模内容,通过链接与小规模内容关联。
-
论文1:链接到完整的PDF 文档,包含详细的实验和结果。
-
论文2:链接到完整的PDF 文档,包含模型架构和性能分析。
Small-to-Big机制:
1. 小规模内容检索: 用户输入查询后,系统首先在小规模内容(如摘要、关键句或段落)中检索匹配的内容。小规模内容通常是通过摘要生成、关键句提取等技术从大规模内容中提取的,并建立索引。
2. 链接到大规模内容: 当小规模内容匹配到用户的查询后,系统会通过预定义的链接(如文档ID、URL 或指针)找到对应的大规模内容(如完整的文档、文章)。大规模内容包含更详细的上下文信息,为RAG 提供丰富的背景知识。
3. 上下文补充: 将大规模内容作为RAG 系统的上下文输入,结合用户查询和小规模内容,生成更准确和连贯的答案。
实现原理:
-
文档 -> 生成摘要(LLM + Prompt)
原始文档文件夹 -> 向量数据库 faiss-1
摘要文档文件夹 -> 向量数据库 faiss-2
两者建立1v1对应关系
-
Small-to-Big 检索策略
query -> faiss-2 -> faiss-1
4. Qwen-Agent 构建 RAG
4.1. 基本介绍
Qwen-Agent是一个开发框架。充分利用基于通义千问模型(Qwen)的指令遵循、工具使用、规划、记忆能力。
Qwen-Agent支持的模型形式:
-
DashScope服务提供的Qwen模型服务
-
支持通过OpenAI API方式接入开源的Qwen模型服务
Github:https://github.com/QwenLM/Qwen-Agent
4.2. 构建智能体
Qwen-Agent构建的智能体包含三个复杂度级别,每一层都建立在前一层的基础上
4.2.1. Level-1:检索
处理100万字上下文的一种朴素方法是简单采用增强检索生成(RAG)。 RAG将上下文分割成较短的块,每块不超过512个字,然后仅保留最相关的块在8k字的上下文中。 挑战在于如何精准定位最相关的块。经过多次尝试,我们提出了一种基于关键词的解决方案:
-
步骤1:指导聊天模型将用户查询中的指令信息与非指令信息分开。
- 例如,将用户查询"回答时请用2000字详尽阐述,我的问题是,自行车是什么时候发明的?请用英文回复。"转化为{"信息": ["自行车是什么时候发明的"], "指令": ["回答时用2000字", "尽量详尽", "用英文回复"]}。
-
步骤2:要求聊天模型从查询的信息部分推导出多语言关键词。
- 例如,短语"自行车是什么时候发明的"会转换为{"关键词_英文": ["bicycles", "invented", "when"], "关键词_中文": ["自行车", "发明", "时间"]}。
-
步骤3:运用BM25这一传统的基于关键词的检索方法,找出与提取关键词最相关的块。
-
RAG CODE:https://github.com/QwenLM/Qwen-Agent/blob/main/examples/assistant_rag.py

4.2.2. Level-2:分块检索
上述RAG方法很快速,但常在相关块与用户查询关键词重叠程度不足时失效,导致这些相关的块未被检索到、没有提供给模型。尽管理论上向量检索可以缓解这一问题,但实际上效果有限。 为了解决这个局限,我们采用了一种暴力策略来减少错过相关上下文的几率:
-
步骤1:对于每个512字块,让聊天模型评估其与用户查询的相关性, 如果认为不相关则输出"无", 如果相关则输出相关句子。这些块会被并行处理以避免长时间等待。
-
步骤2:然后,取那些非"无"的输出(即相关句子),用它们作为搜索查询词,通过BM25检索出最相关的块 (总的检索结果长度控制在8k上下文限制内)。
-
步骤3:最后,基于检索到的上下文生成最终答案, 这一步骤的实现方式与通常的RAG相同。
AGENT CODE:https://github.com/QwenLM/Qwen-Agent/blob/main/examples/parallel_doc_qa.py

4.2.3. Level-3:逐步推理
在基于文档的问题回答中,一个典型的挑战是多跳推理。
-
例如,考虑回答问题:“与第五交响曲创作于同一世纪的交通工具是什么?
-
”模型首先需要确定子问题的答案,“第五交响曲是在哪个世纪创作的?”即19世纪。
-
然后,它才可以意识到包含“自行车于19世纪发明”的信息块实际上与原始问题相关的。
-
工具调用(也称为函数调用)智能体或ReAct智能体是经典的解决方案,它们内置了问题分解和逐步推理的能力。因此,我们将前述级别二的智能体(Lv2-智能体)封装为一个工具,由工具调用智能体(Lv3-智能体)调用。工具调用智能体进行多跳推理的流程如下:
向Lv3-智能体提出一个问题。
while (Lv3-智能体无法根据其记忆回答问题) {
Lv3-智能体提出一个新的子问题待解答。
Lv3-智能体向Lv2-智能体提问这个子问题。
将Lv2-智能体的回应添加到Lv3-智能体的记忆中。
}
Lv3-智能体提供原始问题的最终答案。

例如,Lv3-智能体最初向Lv2-智能体提出子问题:“贝多芬的第五交响曲是在哪个世纪创作的?
-
”收到“19世纪”的回复后,Lv3-智能体提出新的子问题:“19世纪期间发明了什么交通工具?”
-
通过整合Lv2-智能体的所有反馈,Lv3-智能体便能够回答原始问题:“与第五交响曲创作于同一世纪的交通工具是什么?”
4.3. RAG评测结果
我们在两个针对256k上下文设计的基准测试上进行了实验:
-
NeedleBench,一个测试模型是否能在充满大量无关句子的语境中找到最相关句子的基准,类似于“大海捞针”。回答一个问题可能需要同时找到多根“针”,并进行多跳逐步推理。
-
LV-Eval是一个要求同时理解众多证据片段的基准测试。我们对LV-Eval原始版本中的评估指标进行了调整,因为其匹配规则过于严苛,导致了许多假阴性结果。
我们比较了以下方法:
-
32k-模型:这是一个7B对话模型,主要在8k上下文样本上进行微调,并辅以少量32k上下文样本。为了扩展到256k上下文,我们采用了无需额外训练的方法,如基于RoPE的外推。
-
4k-RAG:使用与32k-模型相同的模型,但采取了Lv1-智能体的RAG策略。它仅检索并处理最相关的4k上下文。
-
4k-智能体:同样使用32k-模型的模型,但采用前文描述的更复杂的智能体策略。该智能体策略会并行处理多个片段,但每次请求模型时至多只使用模型的4k上下文。

实验结果说明了以下几点:
-
在短上下文场景中,4k-RAG的表现可能不如32k-模型。这可能是由于RAG方案难以检索到正确的信息或理解多个片段造成的。
-
相反,随着文档长度的增加,4k-RAG越发表现出超越32k-模型的趋势。这一趋势表明32k-模型在处理长上下文方面并没有训练到最优的状态。
-
值得注意的是,4k-智能体始终表现优于32k-模型和4k-RAG。它分块阅读所有上下文的方式使它能够避免原生模型在长上下文上训练不足而带来的限制。
总的来说,如果得到恰当的训练,32k-模型理应优于所有其他方案。然而,实际上由于训练不足,32k-模型的表现不及4k-智能体。
最后,我们还对该智能体进行了100万个字词的压力测试(在100万个字词的大海中寻找一根针),并发现它能够正常运行。
4.4. Qwen-Agent 使用
conda create -n qwen-agent python=3.11
4.4.1. 安装
# 从 PyPI 安装稳定版本:
# !pip install -U "qwen-agent[rag,code_interpreter,gui,mcp]"建议使用国内清华镜像源
pip install -U "qwen-agent[rag,code_interpreter,gui,mcp]" -i https://pypi.tuna.tsinghua.edu.cn/simple
# 或者,使用 `pip install -U qwen-agent` 来安装最小依赖。
# 可使用双括号指定如下的可选依赖:
# [gui] 用于提供基于 Gradio 的 GUI 支持;
# [rag] 用于支持 RAG;
# [code_interpreter] 用于提供代码解释器相关支持;
# [mcp] 用于支持 MCP。
4.4.2. 快速开发
import logging import io import os from qwen_agent.agents import Assistant # 创建一个自定义的日志处理器来捕获日志输出 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("DASHSCOPE_API_KEY"), 'generate_cfg': { 'top_p': 0.8 } } # 步骤 2:创建一个智能体。这里我们以 `Assistant` 智能体为例,它能够使用工具并读取文件。 system_instruction = '' tools = [] files = ['./浦发上海浦东发展银行西安分行个金客户经理考核办法.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")
5. RAG质量评估
当我们完成了一个RAG系统的开发工作以后,我们还需要对RAG系统的性能进行评估,那如何来对RAG系统的性能进行评估呢?我们可以仔细分析一下RAG系统的产出成果,比如检索器组件它产出的是检索出来的相关文档即context, 而生成器组件它产出的是最终的答案即answer,除此之外还有我们最初的用户问题即question。因此RAG系统的评估应该是将question、context、answer结合在一起进行评估。
5.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 提问的相关度。如果该得分低,反应出了可能答不对题。

5.2. Ragas评估
5.2.1. 什么是RAGAs评估
Ragas (Retrieval-Augmented Generation Assessment) 它是一个框架,它可以帮助我们来快速评估RAG系统的性能,为了评估RAG系统,Ragas需要以下信息:
-
question:用户输入的问题。
-
answer:从 RAG 系统生成的答案(由LLM给出)。
-
contexts:根据用户的问题从外部知识源检索的上下文即与问题相关的文档。
-
ground_truths: 人类提供的基于问题的真实(正确)答案。 这是唯一的需要人类提供的信息。
5.2.2. 评估指标
Ragas提供了五种评估指标包括:
-
忠实度(faithfulness)
-
答案相关性(Answer relevancy)
-
上下文精度(Context precision)
-
上下文召回率(Context recall)
-
上下文相关性(Context relevancy)
1. 忠实度(faithfulness)
忠实度(faithfulness)衡量了生成的答案(answer)与给定上下文(context)的事实一致性。它是根据answer和检索到的context计算得出的。并将计算结果缩放到 (0,1) 范围且越高越好。
如果答案(answer)中提出的所有基本事实(claims)都可以从给定的上下文(context)中推断出来,则生成的答案被认为是忠实的。为了计算这一点,首先从生成的答案中识别一组claims。然后,将这些claims中的每一项与给定的context进行交叉检查,以确定是否可以从给定的context中推断出它。忠实度分数由以下公式得出:

2. 答案相关性(Answer relevancy)
评估指标“答案相关性”重点评估生成的答案(answer)与用户问题(question)之间相关程度。不完整或包含冗余信息的答案将获得较低分数。该指标是通过计算question和answer获得的,它的取值范围在 0 到 1 之间,其中分数越高表示相关性越好。
当答案直接且适当地解决原始问题时,该答案被视为相关。重要的是,我们对答案相关性的评估不考虑真实情况,而是对答案缺乏完整性或包含冗余细节的情况进行惩罚。为了计算这个分数,LLM会被提示多次为生成的答案生成适当的问题,并测量这些生成的问题与原始问题之间的平均余弦相似度。基本思想是,如果生成的答案准确地解决了最初的问题,LLM应该能够从答案中生成与原始问题相符的问题。
示例:

3. 上下文精度(Context precision)
上下文精度是一种衡量标准,它评估所有在上下文(contexts)中呈现的与基本事实(ground-truth)相关的条目是否排名较高。理想情况下,所有相关文档块(chunks)必须出现在顶层。该指标使用question和计算contexts,值范围在 0 到 1 之间,其中分数越高表示精度越高。

4. 上下文召回率(Context recall)
上下文召回率(Context recall)衡量检索到的上下文(Context)与人类提供的真实答案(ground truth)的一致程度。它是根据ground truth和检索到的Context计算出来的,取值范围在 0 到 1 之间,值越高表示性能越好。
为了根据真实答案(ground truth)估算上下文召回率(Context recall),分析真实答案中的每个句子以确定它是否可以归因于检索到的Context。 在理想情况下,真实答案中的所有句子都应归因于检索到的Context。

5. 上下文相关性(Context relevancy)
该指标衡量检索到的上下文(Context)的相关性,根据用户问题(question)和上下文(Context)计算得到,并且取值范围在 (0, 1)之间,值越高表示相关性越好。理想情况下,检索到的Context应只包含解答question的信息。 我们首先通过识别检索到的Context中与回答question相关的句子数量来估计 |S| 的值。 最终分数由以下公式确定:

5.3. Ragas评估实操
5.3.1. 父文档检索器
文档切割时传统的做法是使用像CharacterTextSplitter, RecursiveCharacterTextSplitter这样的文档分割器将文档按指定的块大小(chunk_size)来均匀的切割文档,然后将每个文档块做向量化处理(Embedding)后将其保存到向量数据库中,而当我们在做文档检索时,会将用户的问题转换成的向量与向量数据库中的文档块的向量做相似度计算,并从中获取k个与用户问题向量相似度最高的文档块(也就是和用户问题相关的文档块),然后我们会把用户的问题以及相关的文档块一起发送给LLM, 最后LLM会给出一个对用户友好的回复。这就是一般的传统文档检索的方法。
传统检索方法其实存在一定的局限性,这是因为文档块的大小会影响和用户问题的匹配度,也就是说当我们切割的文档块越大时,它与用户问题的匹配度就会越低,当文档块越小时,它与用户问题的匹配度会越高,这是因为较大的文档块可能会包含较多的内容,当它被转换成一个固定维度的向量时,该向量可能不能够准确反应出该文档块中的所有内容,因而对用户问题的匹配度就会降低,而小的文档块包含的内容较少,当它被转换成一个固定维度的向量时,该向量基本能够准确反应出该文档块中的内容,因此它与用户问题的匹配度会教高,但是较小的文档块可能因为所包含的信息量较少,因而它可能不是一个全面且正确的答案。为了解决这些问题今天来介绍Langchain中的父文档检索器,它能够有效的解决文档块大小与用户问题匹配的问题。

由于我们在利用大模型进行文档检索的时候,常常会有相互矛盾的需求,比如:
- 希望得到较小的文档块,以便它们Embedding以后能够最准确地反映出文档的含义,如果文档块太大,Embedding就失去了意义。
- 希望得到较大的文档块以保留教多的内容,然后将它们发送给LLM以便得到全面且正确的答案。
面对这样矛盾的需求,Langchain的父文档检索器为我们提供了两种有效的解决方案:
- 检索完整文档
- 检索较大的文档块
5.3.2. 检索完整文档
所谓检索完整文档是指将原始文档均匀的切割成若干个较小的文档块,然后将它们与用户的问题进行匹配,最后将匹配到的文档块所在原始文档和用户问题一起发送给llm后由llm生成最终答案,如下图所示:

5.3.3. 检索较大的文档块
当原始文档比较大时,我们需要将原始文档按照两个层级进行切割,即切割成主文档块和子文档块,而用户的问题会与所有的子文档块进行匹配(相似度比较) ,当匹配到特定的子文档块后,将该子文档块所属的主文档块的全部内容以及用户问题发送给llm,最后由llm来生成答案。

安装环境
# 创建环境(使用清华镜像源加速) conda create -n ragas python=3.11 -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ # 激活环境 conda activate ragas # 配置pip使用清华镜像源(永久生效) pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple # 安装指定包(使用清华镜像源) pip install pypdf ragas Pillow dashscope chromadb langchain langchain-chroma此代码因为有环境版本的问题,未能跑通,具体的代码书写还需要去官网做进一步的查看
import os from langchain_community.document_loaders.pdf import PyPDFLoader from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.llms import Tongyi from langchain_chroma import Chroma # from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_text_splitters import RecursiveCharacterTextSplitter # from langchain.retrievers import ParentDocumentRetriever #from langchain_community.retrievers import ParentDocumentRetriever from langchain_community.retrievers import ParentDocumentRetriever # from langchain.storage import InMemoryStore from langchain_community.storage import InMemoryStore # from langchain.prompts import ChatPromptTemplate # from langchain.schema.runnable import RunnableMap # from langchain.schema.output_parser import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnableMap from langchain_core.output_parsers import StrOutputParser from datasets import Dataset from ragas import evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_recall, context_precision, ) # 自定义嵌入类 class BatchingDashScopeEmbeddings(DashScopeEmbeddings): """自定义嵌入类,确保每次请求不超过 DashScope API 限制(10 个文本)""" def embed_documents(self, texts): all_embeddings = [] batch_size = 10 # DashScope API 限制 for i in range(0, len(texts), batch_size): batch = texts[i:i + batch_size] try: embeddings = super().embed_documents(batch) all_embeddings.extend(embeddings) except Exception as e: raise RuntimeError(f"Error embedding batch {i}-{i + len(batch)}: {str(e)}") from e return all_embeddings # 解析 pdf 文档 docs = PyPDFLoader("./浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf").load() print(f"Loaded {len(docs)} document pages") # 初始化大语言模型 DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY") llm = Tongyi( model_name="qwen-max", dashscope_api_key=DASHSCOPE_API_KEY ) # message: <400> InternalError.Algo.InvalidParameter: Value error, batch size is invalid, it should not be larger than 10.: input.contents # 创建嵌入模型 embeddings = BatchingDashScopeEmbeddings( model="text-embedding-v4", 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(f"Stored {len(list(store.yield_keys()))} parent documents") # 创建prompt模板(RAG 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: """ # 修复:确保 context 是字符串而非 Document 对象列表 def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) # 由模板生成prompt prompt = ChatPromptTemplate.from_template(template) # 创建 chain(LCEL langchain 表达式语言) chain = RunnableMap({ "context": lambda x: format_docs(retriever.get_relevant_documents(x["question"])), "question": lambda x: x["question"] }) | prompt | llm | StrOutputParser() query = "客户经理被投诉了,投诉一次扣多少分?" response = chain.invoke({"question": query}) print("\nQuestion:", query) print("Answer:", response) # 保证问题需要多样性,场景化覆盖 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) # 评测结果 result = evaluate( dataset=dataset, metrics=[ context_precision, # 上下文精度 context_recall, # 上下文召回率 faithfulness, # 忠实度 answer_relevancy, # 答案相关性 ], embeddings=embeddings ) df = result.to_pandas() print(df)
6. 商业落地实施RAG工程的核心步骤
-
数据集的准备(语料)
- 文档结构化处理:采用现代的智能文档技术
-
测试集的准备(QA对)
- 使用主流的 LLM 模型来根据文档来生成 QA 对
-
技术选型
- 零代码搭建: DB-GPT / Dify / RAGFlow / Coze Studio...
- 低代码搭建: Qwen-Agent / TrustRAG / GraphRAG / LightRAG
- 高度定制开发:LlamaIndex / LangChain / LangGraph
-
构建知识库
-
向量数据库
-
-
测试和优化
- 根据不同的阶段来进行优化处理
- 数据预处理,结构化处理
- 切片策略
- 召回策略
- 重排序
- RAFT
-
最终效果评估
- Ragas 来进行 RAG 性能的评估
-
生产环境部署
- 本地模型部署 vLLM
更多推荐



所有评论(0)