【Task02】:四步构建简单rag(第一章3节)
1. 基于LangChain框架的RAG实现
围绕 “数据准备、索引构建、查询检索、生成集成” 四步,实现 LangChain 框架下的 RAG 应用,通过嵌入模型将文本转为向量索引,检索相关信息后结合大语言模型生成答案。
1.1 初始化设置
- 核心操作:导入依赖库(如
TextLoader
、HuggingFaceEmbeddings
)、加载环境变量(load_dotenv()
),为后续模块做基础配置。
1.2 数据准备 (Data Preparation)
- 加载文档:通过
TextLoader
加载指定路径的 Markdown 文件(如easy-rl-chapter1.md
),获取原始文档数据(docs
)。 - 文本分块:使用
RecursiveCharacterTextSplitter
(默认参数)分割长文档,核心规则如下:- 分隔符顺序:
\n\n
(段落)→\n
(行)→(空格)→空字符,优先保留语义单元。 - 默认配置:
chunk_size=4000
(单块最大长度)、chunk_overlap=200
(块间重叠,减少上下文丢失),保留分隔符(keep_separator=True
)。
- 分隔符顺序:
1.3 索引构建 (Index Construction)
1. 初始化嵌入模型:加载中文嵌入模型BAAI/bge-small-zh-v1.5
,配置 CPU 运行、启用向量归一化(normalize_embeddings=True
)。
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5",model_kwargs={'device': 'cpu'},encode_kwargs={'normalize_embeddings': True}
)
2. 构建向量存储:通过InMemoryVectorStore
,将分块文本(texts
)经嵌入模型转为向量,存储到内存中,形成可查询的向量索引。
1.4 查询与检索 (Query and Retrieval)
- 定义查询:设置用户问题(如 “文中举了哪些例子?”)。
- 相似性检索:调用向量存储的
similarity_search
方法,查询与问题最相关的k=3
个文本块(retrieved_docs
)。 - 准备上下文:用双换行符(
\n\n
)连接检索到的文本块内容(doc.page_content
),清晰区分不同语义片段,便于大模型理解。
3.5 生成集成 (Generation Integration)
- 构建提示模板:用
ChatPromptTemplate
定义规则,要求模型基于上下文回答,信息不足时返回指定话术。 - 配置大模型:初始化
ChatDeepSeek
,指定模型(deepseek-chat
)、温度(0.7
)、最大 Token 数(2048
),从环境变量加载 API 密钥。 - 生成并输出答案:将问题和上下文传入提示模板,调用
llm.invoke()
生成答案并打印。
2. 低代码(基于LlamaIndex)
import os
# os.environ['HF_ENDPOINT']='https://hf-mirror.com' # 注释掉的HF镜像配置(国内访问加速用)
from dotenv import load_dotenv # 用于加载环境变量
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings # LlamaIndex核心组件
from llama_index.llms.deepseek import DeepSeek # DeepSeek大语言模型集成
from llama_index.embeddings.huggingface import HuggingFaceEmbedding # HuggingFace嵌入模型
load_dotenv() # 从.env文件加载环境变量(通常存放API密钥等敏感信息)
# 配置大语言模型(LLM)为DeepSeek
Settings.llm = DeepSeek(model="deepseek-chat", # 使用的DeepSeek模型名称api_key=os.getenv("DEEPSEEK_API_KEY") # 从环境变量获取API密钥
)# 配置嵌入模型(用于将文本转换为向量)
Settings.embed_model = HuggingFaceEmbedding("BAAI/bge-small-zh-v1.5" # 百度的中文嵌入模型
)
documents = SimpleDirectoryReader(input_files=["../../data/C1/markdown/easy-rl-chapter1.md"] # 指定要加载的文档路径
).load_data() # 加载文档内容
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
print(query_engine.get_prompts()) # 打印当前查询引擎使用的提示词模板
print(query_engine.query("文中举了哪些例子?")) # 查询文档中提到的例子并打印结果
练习
- 修改Langchain代码中
RecursiveCharacterTextSplitter()
的参数chunk_size
和chunk_overlap
,观察输出结果有什么变化。
分块数量与大小变化:
chunk_size=1000, chunk_overlap=100
(默认):平衡的分块策略,适合大多数场景
chunk_size=300, chunk_overlap=50
(小块):分块数量显著增加,每块更短
chunk_size=2000, chunk_overlap=300
(大块):分块数量减少,每块更长
对回答的影响:
- 小块配置:
- 优点:检索到的内容更精确,相关性更高
- 缺点:可能因上下文片段过短导致信息不完整,例子可能被分割到多个块中,导致回答不全面
- 大块配置:
- 优点:上下文更完整,能保留例子的完整背景
- 缺点:可能包含过多无关信息,降低检索精度,甚至超出模型上下文窗口
- 小块配置:
- LangChain代码最终得到的输出携带了各种参数,查询相关资料尝试把这些参数过滤掉得到
content
里的具体回答。
# 原代码
answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)# 修改后
response = llm.invoke(prompt.format(question=question, context=docs_content))
# 提取纯文本回答
pure_answer = response.content
print(pure_answer)
根据上下文信息,文中举了以下例子:
1. **走迷宫机器人**:用于说明离散动作空间(只有往东、往南、往西、往北4种移动方式)和连续动作空间(可以向360度中的任意角度移动)。
2. **雅达利游戏(Pong)**:用于说明策略函数的输入(游戏的一帧)和输出(决定向左或向右移动),以及序列决策中奖励的延迟性(只有到游戏结束时才知道球是否被击打过去)。
3. **象棋**:作为奖励在事件结束时给出的例子(赢棋得正奖励,输棋得负奖励)。
4. **股票管理**:作为奖励由获取的奖励与损失决定的例子。
5. **走迷宫(从起点到终点)**:具体用于解释基于策略的强化学习(每个状态得到最佳动作)和基于价值的强化学习(每个状态返回一个价值)方法如何解决问题。
6. **Black jack游戏**和**自动驾驶**:用于说明部分可观测环境(智能体只能看到部分观测,如牌面上的牌或传感器信息)。