Files
2026-05-12 09:41:56 +08:00

250 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第三节 四步构建RAG
通过第一节的学习,我们对RAG已经有了基本认识,并且也准备好了虚拟环境和api_key,接下来将尝试使用[**LangChain**](https://python.langchain.com/docs/introduction/)和[**LlamaIndex**](https://docs.llamaindex.ai/en/stable/)框架完成第一个RAG应用的实现与运行。通过一个示例,演示如何加载本地Markdown文档,利用嵌入模型处理文本,并结合大型语言模型(LLM)来回答与文档内容相关的问题。
## 一、启动虚拟环境
### 1.1 激活虚拟环境
假设已经按照前一章节的指导,创建了名为 `all-in-rag` 的 Conda 虚拟环境。在运行脚本前,先激活虚拟环境:
> 如果使用是Cloud Studio,需要确认当前是否是用户环境,如果不是请运行 `su ubuntu` 切换到用户环境。
```bash
conda activate all-in-rag
```
### 1.2 切换到项目目录
```bash
# 假设当前在 all-in-rag 项目的根目录下
cd code/C1
```
每章内容中的代码文件都存放在 `code/Cx` 目录下,其中 `x` 表示章节编号。
## 二、运行RAG示例代码
完成上述所有设置后,就可以运行RAG示例了。
打开终端,确保虚拟环境已激活,然后执行以下命令:
```bash
python 01_langchain_example.py
```
> 若出现nltk相关报错,尝试运行代码路径下[fix_nltk.py](https://github.com/datawhalechina/all-in-rag/blob/main/code/C1/fix_nltk.py)
代码运行后,可以看到类似下面的输出(格式化后):
```bash
Downloading Model from https://www.modelscope.cn to directory: Path\to\all-in-rag\models\bge-small-zh-v1.5
2025-06-08 02:36:19,318 - modelscope - INFO - Target directory already exists, skipping creation.
content='
文中举了以下例子:
1. **自然界中的羚羊**:刚出生的羚羊通过试错学习站立和奔跑,适应环境。
2. **股票交易**:通过买卖股票并根据市场反馈调整策略,最大化奖励。
3. **雅达利游戏(如Breakout和Pong)**:通过不断试错学习如何通关或赢得游戏。
4. **选择餐馆**:利用(去已知喜欢的餐馆)与探索(尝试新餐馆)的权衡。
5. **做广告**:利用(采取已知最优广告策略)与探索(尝试新广告策略)。
6. **挖油**:利用(在已知地点挖油)与探索(在新地点挖油,可能发现大油田)。
7. **玩游戏(如《街头霸王》)**:利用(固定策略如蹲角落出脚)与探索(尝试新招式如“大招”)。
这些例子用于说明强化学习中的核心概念(如探索与利用、延迟奖励等)及其在实际场景中的应用。
'
additional_kwargs={'refusal': None}
response_metadata={
'token_usage': {
'completion_tokens': 209,
'prompt_tokens': 5576,
'total_tokens': 5785,
'completion_tokens_details': None,
'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 5568},
'prompt_cache_hit_tokens': 5568,
'prompt_cache_miss_tokens': 8
},
'model_name': 'deepseek-chat',
'system_fingerprint': 'fp_8802369eaa_prod0425fp8',
'id': '67a0580d-78b1-44d6-bccf-f654ae0e9bba',
'service_tier': None,
'finish_reason': 'stop',
'logprobs': None
}
id='run--919cedcd-771e-4aed-8dfd-cf436795792e-0'
usage_metadata={
'input_tokens': 5576,
'output_tokens': 209,
'total_tokens': 5785,
'input_token_details': {'cache_read': 5568},
'output_token_details': {}
}
```
> 首次运行时,脚本会下载`BAAI/bge-small-zh-v1.5`嵌入模型。
输出参数解析:
- **`content`**: 这是最核心的部分,即大型语言模型(LLM)根据你的问题和提供的上下文生成的具体回答。
- **`additional_kwargs`**: 包含一些额外的参数,在这个例子中是 `{'refusal': None}`,表示模型没有拒绝回答。
- **`response_metadata`**: 包含了关于LLM响应的元数据。
- `token_usage`: 显示了本次调用消耗的token数量,包括完成(completion_tokens)、提示(prompt_tokens)和总量(total_tokens)。
- `model_name`: 使用的LLM模型名称,当前是 `deepseek-chat`
- `system_fingerprint`, `id`, `service_tier`, `finish_reason`, `logprobs`: 这些是更详细的API响应信息,例如 `finish_reason: 'stop'` 表示模型正常完成了生成。
- **`id`**: 本次运行的唯一标识符。
- **`usage_metadata`**: 与 `response_metadata` 中的 `token_usage` 类似,提供了输入和输出token的统计。
## 三、基于 LangChain 框架的 RAG 实现
在第一节中,我们提到四步构建最小可行系统分别是数据准备、索引构建、检索优化和生成集成。下面将围绕这四个方面来实现一个基于 LangChain 框架的 RAG 应用。
> [本节完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C1/01_langchain_example.py)
### 3.1 初始化设置
首先进行基础配置,包括导入必要的库、加载环境变量以及下载嵌入模型。
```python
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 ChatOpenAI
# 加载环境变量
load_dotenv()
```
### 3.2 数据准备
- **加载原始文档**: 先定义Markdown文件的路径,然后使用`TextLoader`加载该文件作为知识源。
```python
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`(块重叠)。这些参数确保文本块符合预定的大小限制,并通过重叠来减少上下文信息的丢失。
```python
text_splitter = RecursiveCharacterTextSplitter()
texts = text_splitter.split_documents(docs)
```
### 3.3 索引构建
数据准备完成后,接下来构建向量索引:
- **初始化中文嵌入模型**: 使用`HuggingFaceEmbeddings`加载之前在初始化设置中下载的中文嵌入模型。配置模型在CPU上运行,并启用嵌入归一化 (`normalize_embeddings: True`)。
```python
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
```
- **构建向量存储**: 将分割后的文本块 (`texts`) 通过初始化好的嵌入模型转换为向量表示,然后使用`InMemoryVectorStore`将这些向量及其对应的原始文本内容添加进去,从而在内存中构建出一个向量索引。
```python
vectorstore = InMemoryVectorStore(embeddings)
vectorstore.add_documents(texts)
```
这个过程完成后,便构建了一个可供查询的知识索引。
### 3.4 查询与检索
索引构建完毕后,便可以针对用户问题进行查询与检索:
- **定义用户查询**: 设置一个具体的用户问题字符串。
```python
question = "文中举了哪些例子?"
```
- **在向量存储中查询相关文档**: 使用向量存储的`similarity_search`方法,根据用户问题在索引中查找最相关的 `k` (此处示例中 `k=3`) 个文本块。
```python
retrieved_docs = vectorstore.similarity_search(question, k=3)
```
- **准备上下文**: 将检索到的多个文本块的页面内容 (`doc.page_content`) 合并成一个单一的字符串,并使用双换行符 (`"\n\n"`) 分隔各个块,形成最终的上下文信息 (`docs_content`) 供大语言模型参考。
```python
docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)
```
> 使用 `"\n\n"` (双换行符) 而不是 `"\n"` (单换行符) 来连接不同的检索文档块,主要是为了在传递给大型语言模型(LLM)时,能够更清晰地在语义上区分这些独立的文本片段。双换行符通常代表段落的结束和新段落的开始,这种格式有助于LLM将每个块视为一个独立的上下文来源,从而更好地理解和利用这些信息来生成回答。
### 3.5 生成集成
最后一步是将检索到的上下文与用户问题结合,利用大语言模型(LLM)生成答案:
- **构建提示词模板**: 使用`ChatPromptTemplate.from_template`创建一个结构化的提示模板。此模板指导LLM根据提供的上下文 (`context`) 回答用户的问题 (`question`),并明确指出在信息不足时应如何回应。
```python
prompt = ChatPromptTemplate.from_template("""请根据下面提供的上下文信息来回答问题。
请确保你的回答完全基于这些上下文。
如果上下文中没有足够的信息来回答问题,请直接告知:“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”
上下文:
{context}
问题: {question}
回答:"""
)
```
- **配置大语言模型**: 初始化 `ChatOpenAI` 客户端,配置所用模型(`glm-4.7-flash-free`)、生成答案的温度参数(`temperature=0.7`)、最大Token数 (`max_tokens=2048`) 以及API密钥(从环境变量加载)和 url。
```python
llm = ChatOpenAI(
model="glm-4.7-flash-free",
temperature=0.7,
max_tokens=2048,
api_key=os.getenv("DEEPSEEK_API_KEY")
base_url="https://aihubmix.com/v1"
)
```
- **调用LLM生成答案并输出**: 将用户问题 (`question`) 和先前准备好的上下文 (`docs_content`) 格式化到提示模板中,然后调用ChatDeepSeek的`invoke`方法获取生成的答案。
```python
answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)
```
> 老湿老湿,Langchain 很强大但还是太吃操作了,有没有更加简单又好用的框架推荐呢?
> 有的兄弟,有的!像这样好用的框架还有LlamaIndex😉
## 四、低代码(基于LlamaIndex
在 RAG 方面,LlamaIndex 提供了更多封装好的 API 接口,这无疑降低了上手门槛,下面是一个简单实现:
```python
import os
# os.environ['HF_ENDPOINT']='https://hf-mirror.com'
from dotenv import load_dotenv
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.llms.openai_like import OpenAILike
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
load_dotenv()
Settings.llm = OpenAILike(
model="glm-4.7-flash-free",
api_key=os.getenv("DEEPSEEK_API_KEY"),
api_base="https://aihubmix.com/v1",
is_chat_model=True
)
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代码最终得到的输出携带了各种参数,查询相关资料尝试把这些参数过滤掉得到`content`里的具体回答。
- 修改Langchain代码中`RecursiveCharacterTextSplitter()`的参数`chunk_size`和`chunk_overlap`,观察输出结果有什么变化。
- 给LlamaIndex代码添加代码注释。