RAG学习(一)
**声明:**本人是通过Github的文章All-in-rag学习有关rag的内容,原文链接:https://datawhalechina.github.io/all-in-rag/#/,github链接:https://github.com/datawhalechina/all-in-rag。其中github仓库中提供了完整代码。
第一节 RAG
1.1 RAG核心定义
检索增强生成(Retrieval-Augmented Generation) 是一种将信息检索与文本生成相结合的AI架构范式。其核心定义包含三个关键要素:
RAG=LLM生成能力⊕知识库检索系统\text{RAG} = \text{LLM}_{\text{生成能力}} \oplus \text{知识库}_{\text{检索系统}} RAG=LLM生成能力⊕知识库检索系统
- 突破传统LLM的知识局限:通过外部知识源扩展模型的知识边界
- 解决"知识截止"问题:可动态更新知识库而不需重新训练模型
-
核心特征:
- 可验证性:答案来源可追溯(基于检索结果)
- 动态性:知识库可实时更新
- 抗幻觉:显著降低模型虚构事实的概率(相比纯生成模型降低40-60%)
-
本质目标:
“将世界知识存储与语言理解能力分离,使模型专注于其最擅长的语言生成任务”
1.2 RAG的技术原理
RAG系统由三大核心组件构成,形成完整的处理闭环:
- 关键组件:
- 索引(Indexing) 📑:将非结构化文档(PDF/Word等)分割为片段,通过嵌入模型转换为向量数据。
- 检索(Retrieval) 🔍️:基于查询语义,从向量数据库召回最相关的文档片段(Context)。
- 生成(Generation) ✨:将检索结果作为上下文输入LLM,生成自然语言响应。
1.3 技术演进与分类
RAG 架构经历 朴素版(Naive RAG)、高级版(Advanced RAG)、模块化版(Modular RAG) 的演进:
-
Naive RAG 以 “索引→检索→生成” 线性流程 构建基础能力,无中间优化;
-
Advanced RAG 新增 预检索(Query Rewrite、HyDE 优化查询) 和 后检索(Rerank 重排、Filter 结果提纯),强化中间环节精度;
-
Modular RAG
进一步解耦为
七大核心模块(索引、预检索、检索、后检索、生成、编排、知识引导)
,每个模块细分技术子层:
- 索引 支持 Chunk 优化(小→大 Chunk 扩展)与结构化组织(知识图谱 / 文档层级建模);
- 预检索 覆盖查询变换(假设答案 / 查询生成)、扩展(子查询分解)、构造(Text-to-Cypher/SQL)及路由分发(硬 / 软 Prompt 管道);
- 检索 实现检索器微调(LM 监督训练)、多源检索(句子 / Chunk / 子图)与策略选择(嵌入 / 关键词 / 混合检索);
- 后检索 集成重排(多模态文档重排)、压缩(Long LLM Lingua 精简)、验证(知识冲突检测);
- 生成 支持 Generator FT(结合外部知识 / 大模型增强);
- 编排 动态调度流程(检索必要性判断、重查决策);
- 知识引导 通过知识图谱规划推理路径,辅助检索与生成。
模块化设计赋能 多场景适配(结构化数据、多模态处理)、细粒度优化(Chunk 切分、检索器调优)与动态流程控制(路由、拒答机制),推动 RAG 向更智能、可靠的方向发展。
第二节 配置环境
2.1 API申请
- 访问 Deepseek 开放平台打开浏览器,访问 Deepseek 开放平台。
- 登录或注册账号 如果你已有账号,请直接登录。如果没有,请点击页面上的注册按钮,使用邮箱或手机号完成注册。
- 创建新的 API 密钥 登录成功后,在页面左侧的导航栏中找到并点击
API Keys
。在 API 管理页面,点击创建 API key
按钮。输入一个跟其他api key不重复的名称后点击创建 - 保存 API Key 系统会为你生成一个新的 API 密钥。请立即复制并将其保存在一个安全的地方。
注意:你需要使用你的钞能力才能使用它,要不然你会出现:
openai.APIStatusError: Error code: 402 - {'error': {'message': 'Insufficient Balance', 'type': 'unknown_error', 'param': None, 'code': 'invalid_request_error'}}
这个错误。当然,如果你不想使用这个付费的api key,你也可以本地下载cache_model在本地直接使用下载好的模型。或者你可以找不需要付费的api key。
2.2 虚拟环境的创建、配置与代码的拉取
首先我们需要创建虚拟环境,在windows下,我们打开事先安装好的anaconda中的anaconda prompt,输入以下代码:
conda create -n [环境名] python=3.12.7
举个例子,我想让我的环境名叫做“all-in-rag”,则我需要输入conda create -n all-in-rag python=3.12.7
。接下来的过程中再输入一个[y]就可以了,随后就是激活环境:
conda activate [环境名]
我们创建的虚拟环境空空如也,里面能用于rag的python包少之又少,所以我们需要配置我们的虚拟环境,使用pip install
来下载需要的包。作者已经在源码中给大家一个requirements.txt
文件,里面有所有需要安装的包。首先我们需要拉取项目代码。如果你的电脑有git,那么你可以直接使用以下代码来直接克隆项目代码:
git clone https://github.com/datawhalechina/all-in-rag.git
如果没有,你可以上GitHub直接下载项目代码。代码的文件结构如下:
all-in-rag/
├── docs/ # 教程文档
├── code/ # 代码示例
├── data/ # 示例数据
├── models/ # 预训练模型
└── README.md # 项目说明
其中,配置文件位于code中,我们同样在anaconda prompt,输入:
cd all-in-rag(可以替换为你本地存放代码的文件地址)
如果你发现cd+文件位置无法跳转,不妨试一试:
cd /d+文件地址
注意/d+文件地址
是需要连在一起的,例如/dF:\Python Project\Project\Natural Language Processing\RAG\all-in-rag\code
。
随后执行:
pip install -r requirements.txt
就可以了。
2.3配置api key的环境变量
在windows中,找到“此电脑”,点击属性,在“相关链接”中找到“高级系统设置”,进入点击“环境变量”,选择用户变量下的新建按钮,变量名为“DEEPSEEK_API_KEY”.变量值为你之前申请的api密钥。
第三节 代码运行测试与分析
接下来你就可以通过vscode或者bash直接运行python代码:
python 01_langchain_example.py
示例代码:
import os
# hugging face镜像设置,如果国内环境无法使用启用该设置
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from dotenv import load_dotenv
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_deepseek import ChatDeepSeekload_dotenv()markdown_path = r"F:\Python Project\Project\Natural Language Processing\RAG\all-in-rag\data\C1\markdown\easy-rl-chapter1.md"# 加载本地markdown文件
loader = UnstructuredMarkdownLoader(markdown_path)
docs = loader.load()# 文本分块
text_splitter = RecursiveCharacterTextSplitter()
chunks = text_splitter.split_documents(docs)# 中文嵌入模型
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5",model_kwargs={'device': 'cpu'},encode_kwargs={'normalize_embeddings': True}
)# 构建向量存储
vectorstore = InMemoryVectorStore(embeddings)
vectorstore.add_documents(chunks)# 提示词模板
prompt = ChatPromptTemplate.from_template("""请根据下面提供的上下文信息来回答问题。
请确保你的回答完全基于这些上下文。
如果上下文中没有足够的信息来回答问题,请直接告知:“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”上下文:
{context}问题: {question}回答:""")# 配置大语言模型
llm = ChatDeepSeek(model="deepseek-chat",temperature=0.7,max_tokens=2048,api_key=os.getenv("DEEPSEEK_API_KEY")
)# 用户查询
question = "文中举了哪些例子?"# 在向量存储中查询相关文档
retrieved_docs = vectorstore.similarity_search(question, k=3)
docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)
得到的结果如下所示:
content='根据提供的上下文,文中举了以下例子:\n\n1. **自然界中的羚羊**:羚羊出生后通过试错学习站立和奔跑,适应环境。\n2. **股票交易**:通过买卖股票并根据市场反馈学习最大化奖励。\n3. **雅达利游戏(如Breakout和Pong)**:通过试错学习游戏策略以通关或获胜。\n4. **AlphaGo**:强化学习算法击败人类顶尖棋手 ,展示超人类表现。\n5. **选择餐馆**:利用(去已知喜欢的餐馆)与探索(尝试新餐馆)的权衡。\n6. **做广告**:利用(采取已知最优广告策略)与探索(尝试新广告策略)。\n7. **挖油**:利用(在已知地点挖油)与探索(在新地点挖油,可能发现大油田)。\n8. **玩游戏(如《街头霸王》)**:利用(固定策略如蹲角落出脚)与探索(尝试新招式如“大招”)。\n\n这些例子分别用于说明强化学习的应用场景、 探索与利用的权衡,以及其与监督学习的区别。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 231, 'prompt_tokens': 5549, 'total_tokens': 5780, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 5504}, 'prompt_cache_hit_tokens': 5504, 'prompt_cache_miss_tokens': 45}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_8802369eaa_prod0623_fp8_kvcache', 'id': '296a05f3-5105-446b-8290-27c15798c4e7', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None} id='run--742a5a71-bd27-4d2e-88fd-9d35bd66e341-0' usage_metadata={'input_tokens': 5549, 'output_tokens': 231, 'total_tokens': 5780, 'input_token_details': {'cache_read': 5504}, 'output_token_details': {}}
有点难看懂,解释一下:
content
(核心回答内容):模型生成的最终回答文本additional_kwargs
(附加参数):refusal
表示模型是否拒绝回答,如果值为True
,表示模型认为问题不合适而拒绝回答response_metadata
(响应元数据):
'token_usage': {'completion_tokens': 231, # 生成回答消耗的token数'prompt_tokens': 5549, # 输入提示消耗的token数'total_tokens': 5780, # 总token数 = 5549 + 231'completion_tokens_details': None,'prompt_tokens_details': {'audio_tokens': None, # 音频token数(本请求未使用)'cached_tokens': 5504 # 从缓存复用的token数},'prompt_cache_hit_tokens': 5504, # 提示缓存命中的token数'prompt_cache_miss_tokens': 45 # 提示缓存未命中的token数
}
其他元数据:
model_name
: 使用的模型名称(‘deepseek-chat’)system_fingerprint
: 系统指纹标识(用于追踪模型版本和配置)id
: 本次请求的唯一ID(可用于日志追踪)service_tier
: 服务等级(None表示默认等级)finish_reason
: 生成结束原因('stop’表示正常结束)logprobs
: token概率信息(None表示未返回)
我们可以将上面代码的流程概括为以下几点:
- 文档分割: D→d1,d2,...,dnD → {d_1, d_2, ..., d_n}D→d1,d2,...,dn
- 向量映射: ∀di→vi=f(di)∀d_i → v_i = f(d_i)∀di→vi=f(di)
- 查询处理: q=f(question)q = f(question)q=f(question)
- 相似检索: R=argmaxi∈[1,n]k(q⋅vi)R = \arg\max_{i \in [1,n]}^k \ (q \cdot v_i)R=argmaxi∈[1,n]k (q⋅vi)
- 上下文构建: C=⨁r∈Rtext(r)C = \bigoplus_{r \in R} \text{text}(r)C=⨁r∈Rtext(r)
- 提示工程: P=Template(C,question)P = \text{Template}(C, question)P=Template(C,question)
- 生成回答: answer=LLM(P)\text{answer} = \text{LLM}(P)answer=LLM(P)
其中我们系要注意以下几点:
- 无论输入文本长度如何(100字或1000字),输出都是固定维度的向量
- 向量库V={(vi,texti,metadatai)}i=1nV = \{ (v_i, \text{text}_i, \text{metadata}_i) \}_{i=1}^nV={(vi,texti,metadatai)}i=1n中所有向量维度相同:768维
- 相似度计算:similarity(q,vi)=cos(θ)=q⋅vi∣∣q∣∣⋅∣∣vi∣∣\text{similarity}(q, v_i) = \cos(\theta) = \frac{q \cdot v_i}{||q|| \cdot ||v_i||}similarity(q,vi)=cos(θ)=∣∣q∣∣⋅∣∣vi∣∣q⋅vi,由于归一化,则简化为similarity(q,vi)=q⋅vi=∑j=1768qjvij\text{similarity}(q, v_i) = q \cdot v_i = \sum_{j=1}^{768} q_j v_{ij}similarity(q,vi)=q⋅vi=∑j=1768qjvij
- 最近邻搜索:Retrieve(q,V,k)={vi∣rank(q⋅vi)≤k}\text{Retrieve}(q, V, k) = \{ v_i \ | \ \text{rank}(q \cdot v_i) \leq k \}Retrieve(q,V,k)={vi ∣ rank(q⋅vi)≤k}
在这一节中,我们提到四步构建最小可行系统分别是数据准备、索引构建、检索优化和生成集成。接下来将围绕这四个方面来实现一个基于LangChain框架的RAG应用。
3.1 初始化设置
首先进行基础配置,包括导入必要的库、加载环境变量以及下载嵌入模型。
import os
# os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_deepseek import ChatDeepSeek# 加载环境变量
load_dotenv()
3.2 数据准备
-
加载原始文档: 先定义Markdown文件的路径,然后使用
TextLoader
加载该文件作为知识源。markdown_path = "../../data/C1/markdown/easy-rl-chapter1.md" loader = TextLoader(markdown_path) docs = loader.load()
-
文本分块 (Chunking): 为了便于后续的嵌入和检索,长文档被分割成较小的、可管理的文本块(chunks)。这里采用了递归字符分割策略,使用其默认参数进行分块。当不指定参数初始化
RecursiveCharacterTextSplitter()
时,其默认行为旨在最大程度保留文本的语义结构:- 默认分隔符与语义保留: 按顺序尝试使用一系列预设的分隔符
["\n\n" (段落), "\n" (行), " " (空格), "" (字符)]
来递归分割文本。这种策略的目的是尽可能保持段落、句子和单词的完整性,因为它们通常是语义上最相关的文本单元,直到文本块达到目标大小。 - 保留分隔符: 默认情况下 (
keep_separator=True
),分隔符本身会被保留在分割后的文本块中。 - 默认块大小与重叠: 使用其基类
TextSplitter
中定义的默认参数chunk_size=4000
(块大小)和chunk_overlap=200
(块重叠)。这些参数确保文本块符合预定的大小限制,并通过重叠来减少上下文信息的丢失。
- 默认分隔符与语义保留: 按顺序尝试使用一系列预设的分隔符
我们来看一下使用chunk_size=1000
与chunk_overlap=50
参数的结果,使得检索到的内容更宏观,代码修改为:
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000,chunk_overlap=50
)
结果:
content='根据提供的上下文,文中举了以下例子:\n\n1. **DeepMind 研发的走路的智能体**:智能体学习在曲折道路上行走,通过举手保持平衡以更快前进,并能适应环境扰动。 \n2. **机械臂抓取**:通过强化学习训练机械臂抓取不同形状的物 体,避免传统算法需对每个物体单独建模的问题。 \n3. **OpenAI 的机械臂翻魔方**:机械臂先在虚拟环境中训练,再应用到现实,实现灵活操作魔方的能力。 \n4. **穿衣服的智能体**:训练智能体完成穿衣服的精细操作,并能抵抗扰动(尽管可 能失败)。 \n\n此外,在“探索和利用”部分还补充了其他生活化例子: \n- 选择餐馆(利用已知餐馆 vs 探索新餐馆) \n- 做广告(利用现有策略 vs 尝试新策略) \n- 挖油(在已知地点开采 vs 勘探新油田) \n- 玩游戏(固定策略 vs 尝试新招式)。 \n\n这些例子均用于说明强化学习的不同应用场景或核心概念(如奖励、探索与利用)。
3.3 索引构建
数据准备完成后,接下来构建向量索引:
-
初始化中文嵌入模型: 使用
HuggingFaceEmbeddings
加载之前在初始化设置中下载的中文嵌入模型。配置模型在CPU上运行,并启用嵌入归一化 (normalize_embeddings: True
)。embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5",model_kwargs={'device': 'cpu'},encode_kwargs={'normalize_embeddings': True} )
-
构建向量存储: 将分割后的文本块 (
texts
) 通过初始化好的嵌入模型转换为向量表示,然后使用InMemoryVectorStore
将这些向量及其对应的原始文本内容添加进去,从而在内存中构建出一个向量索引。vectorstore = InMemoryVectorStore(embeddings) vectorstore.add_documents(texts)
3.4 检索与查询
索引构建完毕后,便可以针对用户问题进行查询与检索:
- 定义用户查询: 设置一个具体的用户问题字符串。
- 在向量存储中查询相关文档: 使用向量存储的
similarity_search
方法,根据用户问题在索引中查找最相关的k
(此处示例中k=3
) 个文本块。 - 准备上下文: 将检索到的多个文本块的页面内容 (
doc.page_content
) 合并成一个单一的字符串,并使用双换行符 ("\n\n"
) 分隔各个块,形成最终的上下文信息 (docs_content
) 供大语言模型参考。
注意:使用 "\n\n"
(双换行符) 而不是 "\n"
(单换行符) 来连接不同的检索文档块,主要是为了在传递给大型语言模型(LLM)时,能够更清晰地在语义上区分这些独立的文本片段。双换行符通常代表段落的结束和新段落的开始,这种格式有助于LLM将每个块视为一个独立的上下文来源,从而更好地理解和利用这些信息来生成回答。
3.5 生成集成
最后一步是将检索到的上下文与用户问题结合,利用大语言模型(LLM)生成答案:
- 构建提示词模板: 使用
ChatPromptTemplate.from_template
创建一个结构化的提示模板。此模板指导LLM根据提供的上下文 (context
) 回答用户的问题 (question
),并明确指出在信息不足时应如何回应。 - 配置大语言模型: 初始化
ChatDeepSeek
客户端,配置所用模型 (deepseek-chat
)、生成答案的温度参数 (temperature=0.7
)、最大Token数 (max_tokens=2048
) 以及API密钥 (从环境变量加载)。 - 调用LLM生成答案并输出: 将用户问题 (
question
) 和先前准备好的上下文 (docs_content
) 格式化到提示模板中,然后调用ChatDeepSeek的invoke
方法获取生成的答案。
prompt = ChatPromptTemplate.from_template("""请根据下面提供的上下文信息来回答问题。
请确保你的回答完全基于这些上下文。
如果上下文中没有足够的信息来回答问题,请直接告知:“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”上下文:
{context}问题: {question}回答:""")
#-------------------------------------------
llm = ChatDeepSeek(model="deepseek-chat",temperature=0.7,max_tokens=2048,api_key=os.getenv("DEEPSEEK_API_KEY")
)
#-------------------------------------------
answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)
n}
回答:“”"
)
#-------------------------------------------
llm = ChatDeepSeek(
model=“deepseek-chat”,
temperature=0.7,
max_tokens=2048,
api_key=os.getenv(“DEEPSEEK_API_KEY”)
)
#-------------------------------------------
answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)