Initial commit
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
# 第一节 环境配置与项目架构
|
||||
|
||||
> 经过前面十几天的鏖战也是终于来到了项目实战环节。接下来,通过一个完整的实战项目来把前面学到的知识串联起来,构建一个真正可用的RAG系统。
|
||||
|
||||
## 一、项目背景
|
||||
|
||||
这个项目的灵感来自于笔者前段时间刷视频时,偶然看到了一个有趣的开源项目介绍——[程序员做饭指南](https://github.com/Anduin2017/HowToCook)。这是一个菜谱项目,用Markdown格式记录了各种菜品的制作方法,从简单的家常菜到复杂的宴客菜,应有尽有。更完美的是,这个项目中每道菜的Markdown文件都严格使用统一的小标题。
|
||||
|
||||
看到这个项目,笔者立刻想到:能不能构建一个智能问答系统来解决我的选择困难症?每天面对"今天吃什么"这个世纪难题,如果有个AI助手能根据我的需求推荐菜品、告诉我怎么做,那该多好!于是就有了搭建这个**尝尝咸淡RAG系统**的想法。
|
||||
|
||||
## 二、环境配置
|
||||
|
||||
### 2.1 创建虚拟环境
|
||||
|
||||
```bash
|
||||
# 使用conda创建环境
|
||||
conda create -n cook-rag-1 python=3.12.7
|
||||
conda activate cook-rag-1
|
||||
```
|
||||
|
||||
### 2.2 安装核心依赖
|
||||
|
||||
老规矩,进入本章对应项目目录安装依赖包
|
||||
|
||||
```bash
|
||||
cd code/C8
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
如果 API Key 已经配置好了,可以直接使用下面命令运行项目
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 2.3 申请Kimi API Key
|
||||
|
||||
Kimi2 发布第八天来尝尝咸淡,申请地址:[Kimi API官网](https://platform.moonshot.cn/console/api-keys)。目前注册会送15元的额度,绰绰有余了。
|
||||
|
||||
### 2.4 API配置
|
||||
|
||||
参考前面章节 [**环境准备**](../chapter1/02_preparation.md) 中关于api_key的配置方法。在windows下,配置完成后应该如下图所示:
|
||||
|
||||

|
||||
|
||||
## 三、项目架构
|
||||
|
||||
### 3.1 项目目标
|
||||
|
||||
我们将基于HowToCook项目的菜谱数据,构建一个智能的食谱问答系统。用户可以:
|
||||
|
||||
- 询问具体菜品的制作方法:"宫保鸡丁怎么做?"
|
||||
- 寻求菜品推荐:"推荐几个简单的素菜"
|
||||
- 获取食材信息:"红烧肉需要什么食材?"
|
||||
|
||||
### 3.2 数据分析
|
||||
|
||||
#### 3.2.1 文档分析
|
||||
|
||||
HowToCook项目包含了大约300多个Markdown格式的菜谱文件。这些菜谱有两个关键特点:一是结构高度规整,每个文件都严格按照统一的格式来组织内容;二是内容篇幅较短,单个菜谱通常在700字左右。
|
||||
|
||||
打开任意一个菜谱文件,可以发现它们都遵循着相似的结构模式。通常以菜品做法作为一级标题,开头会有一段简介和难度评级,然后分为"必备原料和工具"、"计算"、"操作"、"附加内容"等几个主要部分。比如西红柿炒鸡蛋这道菜:
|
||||
|
||||
```markdown
|
||||
# 西红柿炒鸡蛋的做法
|
||||
|
||||
西红柿炒蛋是中国家常几乎最常见的一道菜肴...
|
||||
预估烹饪难度:★★
|
||||
|
||||
## 必备原料和工具
|
||||
* 西红柿
|
||||
* 鸡蛋
|
||||
* 食用油...
|
||||
|
||||
## 计算
|
||||
每次制作前需要确定计划做几份...
|
||||
* 西红柿 = 1 个(约 180g) * 份数
|
||||
* 鸡蛋 = 1.5 个 * 份数,向上取整...
|
||||
|
||||
## 操作
|
||||
- 西红柿洗净
|
||||
- 可选:去掉西红柿的外表皮...
|
||||
|
||||
## 附加内容
|
||||
这道菜根据不同的口味偏好,存在诸多版本...
|
||||
```
|
||||
|
||||
从数据上来看,这种高度结构化的数据不需要过多处理就可以直接用于RAG系统构建。还记得我们在第2章学过的[**Markdown结构分块**](../chapter2/05_text_chunking.md#34-基于文档结构的分块)吗?这个数据完全契合那种按标题层级分块的思路。更重要的是,每个菜谱文件的内容都不算太长,单个章节的内容通常在几百字左右,这意味着可以直接按照标题进行分块,而不用担心第2章提到的那个问题——某个章节内容过长超出模型上下文窗口,需要与常规分块方法(如`RecursiveCharacterTextSplitter`)组合使用。
|
||||
|
||||
#### 3.2.2 结构分块局限
|
||||
|
||||
虽然Markdown结构分块看起来很理想,但在实际使用中可能会遇到一个问题:按照标题严格分块会把内容切得太细,导致上下文信息不完整。比如用户问"宫保鸡丁怎么做",如果严格按标题分块,可能只检索到"操作"这一个章节,但缺少了"必备原料和工具"的信息,LLM就无法给出完整的制作指导。甚至有时候检索到的是"附加内容"中的某个变化做法,没有基础制作步骤,回答就会显得莫名其妙。如果你尝试直接把整个菜谱文档作为一个块,可以发现效果反而比结构分块要好,因为上下文信息是完整的。
|
||||
|
||||
为了解决这个矛盾,可以采用父子文本块的策略:用小的子块进行精确检索,但在生成时传递完整的父文档给LLM。这种方法在第3章的索引优化中虽然没有专门介绍,但本质上也属于上下文拓展的一种应用。通过这种方式,我们既保证了检索的精确性,又确保了生成时上下文的完整性。
|
||||
|
||||
> 反正都是把整个文档传给LLM,我为什么不直接用整个文档分块呢?
|
||||
|
||||
这个问题问得很好!关键在于当用户问"宫保鸡丁需要什么调料"时,如果直接用整个文档做向量检索,这个具体问题在整个文档中的占比很小,很可能检索不到或者排名很靠后。但如果用小块检索,"必备原料和工具"这个章节就能精确匹配用户的需求。
|
||||
|
||||
简单来说,这种设计是"小块检索,大块生成"——用小块的精确性找到相关内容,用大块的完整性保证回答质量。如果直接用整个文档分块,就失去了检索的精确性优势。
|
||||
|
||||
### 3.3 整体架构
|
||||
|
||||
数据处理好之后,剩余的部分就是四个主要流程的组合,每个流程对工具进行筛选和优化后就可以构建出一个简单的rag系统。当前项目的架构如下图所示:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% 系统初始化
|
||||
START[🚀 系统启动] --> CONFIG[⚙️ 加载配置<br/>RAGConfig]
|
||||
CONFIG --> INIT[🔧 初始化模块]
|
||||
|
||||
%% 索引加载/构建
|
||||
INIT --> INDEX_CHECK{📂 检查索引缓存}
|
||||
INDEX_CHECK -->|存在| LOAD_INDEX[⚡ 加载已保存索引<br/>秒级启动]
|
||||
INDEX_CHECK -->|不存在| BUILD_NEW[🔨 构建新索引]
|
||||
|
||||
%% 构建新索引的顺序流程
|
||||
BUILD_NEW --> DataPrep
|
||||
DataPrep --> IndexBuild
|
||||
IndexBuild --> SAVE_INDEX[💾 保存索引到配置路径]
|
||||
|
||||
%% 加载已有索引也需要数据准备(用于检索模块)
|
||||
LOAD_INDEX --> DataPrepForRetrieval[📚 加载文档和分块<br/>用于检索模块]
|
||||
DataPrepForRetrieval --> READY[✅ 系统就绪]
|
||||
SAVE_INDEX --> READY
|
||||
|
||||
%% 用户交互开始
|
||||
READY --> A[👤 用户输入问题]
|
||||
A --> B{🎯 查询路由}
|
||||
|
||||
%% 查询路由分支
|
||||
B -->|list| C[📋 推荐查询]
|
||||
B -->|detail| D[📖 详细查询]
|
||||
B -->|general| E[ℹ️ 一般查询]
|
||||
|
||||
%% 查询重写逻辑 - 合并相同处理
|
||||
C --> KEEP[📝 保持原查询]
|
||||
D --> KEEP
|
||||
E --> REWRITE[🔄 查询重写]
|
||||
|
||||
%% 所有查询都进入统一的检索流程
|
||||
KEEP --> F[🔍 混合检索<br/>top_k=config.top_k]
|
||||
REWRITE --> F
|
||||
|
||||
%% 检索阶段
|
||||
F --> G[📊 向量检索<br/>config.embedding_model]
|
||||
F --> H[🔤 BM25检索<br/>关键词匹配]
|
||||
|
||||
%% RRF重排
|
||||
G --> I[⚡ RRF重排融合]
|
||||
H --> I
|
||||
I --> J[📖 检索到子块]
|
||||
|
||||
%% 父子文档处理
|
||||
J --> K[🧠 智能去重<br/>按相关性排序]
|
||||
K --> L[📚 获取父文档]
|
||||
|
||||
%% 生成阶段 - 根据路由类型选择不同模式
|
||||
L --> M{🎨 生成模式路由}
|
||||
M -->|list查询| N[📋 生成菜品列表<br/>简洁输出]
|
||||
M -->|detail查询| O[📝 分步指导模式<br/>config.llm_model<br/>详细步骤]
|
||||
M -->|general查询| P[💬 基础回答模式<br/>config.temperature<br/>一般信息]
|
||||
|
||||
%% 输出结果
|
||||
N --> Q[✨ 返回结果]
|
||||
O --> Q
|
||||
P --> Q
|
||||
|
||||
%% 数据准备子流程
|
||||
subgraph DataPrep [📚 数据准备模块]
|
||||
R[📁 加载Markdown文件<br/>config.data_path] --> S[🔧 元数据增强]
|
||||
S --> T[✂️ 按标题分块]
|
||||
T --> U[🏷️ 父子关系建立]
|
||||
U --> CHUNKS[📦 输出文本块chunks]
|
||||
end
|
||||
|
||||
%% 索引构建子流程
|
||||
subgraph IndexBuild [🔍 索引构建模块]
|
||||
CHUNKS --> V[🤖 BGE嵌入模型<br/>config.embedding_model]
|
||||
V --> W[📊 FAISS向量索引]
|
||||
W --> X[💾 索引持久化<br/>config.index_save_path]
|
||||
end
|
||||
|
||||
%% 配置管理子流程
|
||||
subgraph ConfigMgmt [⚙️ 配置管理]
|
||||
CFG1[🎛️ 默认配置<br/>DEFAULT_CONFIG]
|
||||
CFG2[🔧 自定义配置<br/>RAGConfig]
|
||||
CFG3[🌐 环境变量<br/>HF_ENDPOINT]
|
||||
end
|
||||
|
||||
%% 连接配置到各模块
|
||||
ConfigMgmt --> DataPrep
|
||||
ConfigMgmt --> IndexBuild
|
||||
ConfigMgmt --> F
|
||||
ConfigMgmt --> O
|
||||
ConfigMgmt --> P
|
||||
|
||||
%% 样式定义
|
||||
classDef startup fill:#e3f2fd,stroke:#0277bd,stroke-width:2px
|
||||
classDef config fill:#f1f8e9,stroke:#388e3c,stroke-width:2px
|
||||
classDef userInput fill:#e1f5fe,stroke:#01579b,stroke-width:2px
|
||||
classDef routing fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
|
||||
classDef rewrite fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
|
||||
classDef retrieval fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
|
||||
classDef generation fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef output fill:#fce4ec,stroke:#880e4f,stroke-width:2px
|
||||
classDef module fill:#f1f8e9,stroke:#33691e,stroke-width:2px
|
||||
classDef cache fill:#fff8e1,stroke:#f57c00,stroke-width:2px
|
||||
classDef dataflow fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
|
||||
|
||||
%% 应用样式
|
||||
class START,INIT startup
|
||||
class CONFIG,ConfigMgmt,CFG1,CFG2,CFG3 config
|
||||
class INDEX_CHECK,LOAD_INDEX,SAVE_INDEX cache
|
||||
class A userInput
|
||||
class B,C,D,E,M routing
|
||||
class KEEP,REWRITE rewrite
|
||||
class F,G,H,I,J,K,L retrieval
|
||||
class N,O,P generation
|
||||
class Q output
|
||||
class DataPrep,IndexBuild module
|
||||
class BUILD_NEW,READY,DataPrepForRetrieval startup
|
||||
class CHUNKS dataflow
|
||||
```
|
||||
|
||||
### 3.4 项目结构
|
||||
|
||||
基于上面的架构,可以构建出如下项目结构:
|
||||
|
||||
```text
|
||||
code/C8/
|
||||
├── config.py # 配置管理
|
||||
├── main.py # 主程序入口
|
||||
├── requirements.txt # 依赖列表
|
||||
├── rag_modules/ # 核心模块
|
||||
│ ├── __init__.py
|
||||
│ ├── data_preparation.py # 数据准备模块
|
||||
│ ├── index_construction.py # 索引构建模块
|
||||
│ ├── retrieval_optimization.py # 检索优化模块
|
||||
│ └── generation_integration.py # 生成集成模块
|
||||
└── vector_index/ # 向量索引缓存(自动生成)
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
本节从项目背景出发,完成了RAG系统的环境配置和整体架构设计。从下一节开始,我们将深入学习各个模块的具体实现,看看如何将这些设计思路转化为可运行的代码。
|
||||
@@ -0,0 +1,315 @@
|
||||
# 第二节 数据准备模块实现
|
||||
|
||||
RAG系统的效果很大程度上取决于数据准备的质量。在上一节中,我们明确了"小块检索,大块生成"的父子文本块策略。接下来学习如何将数据准备部分的架构思想转化为可运行的代码。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
%% 数据准备模块流程
|
||||
START[📁 加载Markdown文件] --> ENHANCE[🔧 元数据增强]
|
||||
ENHANCE --> SPLIT[✂️ 按标题分块]
|
||||
SPLIT --> RELATION[🏷️ 父子关系建立]
|
||||
RELATION --> DEDUP[🧠 智能去重机制]
|
||||
DEDUP --> OUTPUT[📦 输出文本块chunks]
|
||||
|
||||
%% 子流程详细说明
|
||||
subgraph LoadProcess [文档加载过程]
|
||||
L1[📂 递归查找md文件]
|
||||
L2[📄 读取文件内容]
|
||||
L3[🆔 分配父文档ID]
|
||||
L1 --> L2 --> L3
|
||||
end
|
||||
|
||||
subgraph EnhanceProcess [元数据增强过程]
|
||||
E1[🏷️ 提取菜品分类]
|
||||
E2[📝 提取菜品名称]
|
||||
E3[⭐ 分析难度等级]
|
||||
E1 --> E2 --> E3
|
||||
end
|
||||
|
||||
subgraph SplitProcess [结构分块过程]
|
||||
S1[一级标题分割]
|
||||
S2[二级标题分割]
|
||||
S3[三级标题分割]
|
||||
S1 --> S2 --> S3
|
||||
end
|
||||
|
||||
%% 连接子流程
|
||||
START -.-> LoadProcess
|
||||
ENHANCE -.-> EnhanceProcess
|
||||
SPLIT -.-> SplitProcess
|
||||
|
||||
%% 样式定义
|
||||
classDef process fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
|
||||
classDef subprocess fill:#f1f8e9,stroke:#33691e,stroke-width:2px
|
||||
classDef output fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
|
||||
|
||||
%% 应用样式
|
||||
class START,ENHANCE,SPLIT,RELATION,DEDUP process
|
||||
class LoadProcess,EnhanceProcess,SplitProcess subprocess
|
||||
class OUTPUT output
|
||||
```
|
||||
|
||||
## 一、核心设计
|
||||
|
||||
数据准备模块的核心是实现"小块检索,大块生成"的父子文本块架构。
|
||||
|
||||
**父子文本块映射关系**:
|
||||
```
|
||||
父文档(完整菜谱)
|
||||
├── 子块1:菜品介绍 + 难度评级
|
||||
├── 子块2:必备原料和工具
|
||||
├── 子块3:计算(用量配比)
|
||||
├── 子块4:操作(制作步骤)
|
||||
└── 子块5:附加内容(变化做法)
|
||||
```
|
||||
|
||||
**基本流程**:
|
||||
- **检索阶段**:使用小的子块进行精确匹配,提高检索准确性
|
||||
- **生成阶段**:传递完整的父文档给LLM,确保上下文完整性
|
||||
- **智能去重**:当检索到同一道菜的多个子块时,合并为一个完整菜谱
|
||||
|
||||
**元数据增强**:
|
||||
- **菜品分类**:从文件路径推断(荤菜、素菜、汤品等)
|
||||
- **难度等级**:从内容中的星级标记提取
|
||||
- **菜品名称**:从文件名提取
|
||||
- **文档关系**:建立父子文档的ID映射关系
|
||||
|
||||
## 二、模块实现详解
|
||||
|
||||
> [data_preparation.py完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C8/rag_modules/data_preparation.py)
|
||||
|
||||
### 2.1 类结构设计
|
||||
|
||||
```python
|
||||
class DataPreparationModule:
|
||||
"""数据准备模块 - 负责数据加载、清洗和预处理"""
|
||||
|
||||
def __init__(self, data_path: str):
|
||||
self.data_path = data_path
|
||||
self.documents: List[Document] = [] # 父文档(完整食谱)
|
||||
self.chunks: List[Document] = [] # 子文档(按标题分割的小块)
|
||||
self.parent_child_map: Dict[str, str] = {} # 子块ID -> 父文档ID的映射
|
||||
```
|
||||
|
||||
- `documents`: 存储完整的菜谱文档(父文档)
|
||||
- `chunks`: 存储按标题分割的小块(子文档)
|
||||
- `parent_child_map`: 维护父子关系映射
|
||||
|
||||
### 2.2 文档加载实现
|
||||
|
||||
#### 2.2.1 批量加载Markdown文件
|
||||
|
||||
```python
|
||||
def load_documents(self) -> List[Document]:
|
||||
"""加载文档数据"""
|
||||
documents = []
|
||||
data_path_obj = Path(self.data_path)
|
||||
|
||||
for md_file in data_path_obj.rglob("*.md"):
|
||||
# 读取文件内容,保持Markdown格式
|
||||
with open(md_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 为每个父文档分配唯一ID
|
||||
parent_id = str(uuid.uuid4())
|
||||
|
||||
# 创建Document对象
|
||||
doc = Document(
|
||||
page_content=content,
|
||||
metadata={
|
||||
"source": str(md_file),
|
||||
"parent_id": parent_id,
|
||||
"doc_type": "parent" # 标记为父文档
|
||||
}
|
||||
)
|
||||
documents.append(doc)
|
||||
|
||||
# 增强文档元数据
|
||||
for doc in documents:
|
||||
self._enhance_metadata(doc)
|
||||
|
||||
self.documents = documents
|
||||
return documents
|
||||
```
|
||||
|
||||
- `rglob("*.md")`: 递归查找所有Markdown文件
|
||||
- `parent_id`: 为每个父文档分配唯一ID,建立父子关系的关键
|
||||
- `doc_type`: 标记为"parent",便于区分父子文档
|
||||
|
||||
#### 2.2.2 元数据增强
|
||||
|
||||
```python
|
||||
def _enhance_metadata(self, doc: Document):
|
||||
"""增强文档元数据"""
|
||||
file_path = Path(doc.metadata.get('source', ''))
|
||||
path_parts = file_path.parts
|
||||
|
||||
# 提取菜品分类
|
||||
category_mapping = {
|
||||
'meat_dish': '荤菜', 'vegetable_dish': '素菜', 'soup': '汤品',
|
||||
'dessert': '甜品', 'breakfast': '早餐', 'staple': '主食',
|
||||
'aquatic': '水产', 'condiment': '调料', 'drink': '饮品'
|
||||
}
|
||||
|
||||
# 从文件路径推断分类
|
||||
doc.metadata['category'] = '其他'
|
||||
for key, value in category_mapping.items():
|
||||
if key in file_path.parts:
|
||||
doc.metadata['category'] = value
|
||||
break
|
||||
|
||||
# 提取菜品名称
|
||||
doc.metadata['dish_name'] = file_path.stem
|
||||
|
||||
# 分析难度等级
|
||||
content = doc.page_content
|
||||
if '★★★★★' in content:
|
||||
doc.metadata['difficulty'] = '非常困难'
|
||||
elif '★★★★' in content:
|
||||
doc.metadata['difficulty'] = '困难'
|
||||
# ... (其他难度等级判断)
|
||||
|
||||
```
|
||||
|
||||
- **分类推断**: 从HowToCook项目的目录结构推断菜品分类
|
||||
- **难度提取**: 从内容中的星级标记自动提取难度等级
|
||||
- **名称提取**: 直接使用文件名作为菜品名称
|
||||
|
||||
### 2.3 Markdown结构分块
|
||||
|
||||
将完整的菜谱文档按照Markdown标题结构进行分块,实现父子文本块架构。
|
||||
|
||||
#### 2.3.1 分块策略
|
||||
|
||||
```python
|
||||
def chunk_documents(self) -> List[Document]:
|
||||
"""Markdown结构感知分块"""
|
||||
if not self.documents:
|
||||
raise ValueError("请先加载文档")
|
||||
|
||||
# 使用Markdown标题分割器
|
||||
chunks = self._markdown_header_split()
|
||||
|
||||
# 为每个chunk添加基础元数据
|
||||
for i, chunk in enumerate(chunks):
|
||||
if 'chunk_id' not in chunk.metadata:
|
||||
# 如果没有chunk_id(比如分割失败的情况),则生成一个
|
||||
chunk.metadata['chunk_id'] = str(uuid.uuid4())
|
||||
chunk.metadata['batch_index'] = i # 在当前批次中的索引
|
||||
chunk.metadata['chunk_size'] = len(chunk.page_content)
|
||||
|
||||
self.chunks = chunks
|
||||
return chunks
|
||||
```
|
||||
|
||||
#### 2.3.2 Markdown标题分割器
|
||||
|
||||
```python
|
||||
def _markdown_header_split(self) -> List[Document]:
|
||||
"""使用Markdown标题分割器进行结构化分割"""
|
||||
# 定义要分割的标题层级
|
||||
headers_to_split_on = [
|
||||
("#", "主标题"), # 菜品名称
|
||||
("##", "二级标题"), # 必备原料、计算、操作等
|
||||
("###", "三级标题") # 简易版本、复杂版本等
|
||||
]
|
||||
|
||||
# 创建Markdown分割器
|
||||
markdown_splitter = MarkdownHeaderTextSplitter(
|
||||
headers_to_split_on=headers_to_split_on,
|
||||
strip_headers=False # 保留标题,便于理解上下文
|
||||
)
|
||||
|
||||
all_chunks = []
|
||||
for doc in self.documents:
|
||||
# 对每个文档进行Markdown分割
|
||||
md_chunks = markdown_splitter.split_text(doc.page_content)
|
||||
|
||||
# 为每个子块建立与父文档的关系
|
||||
parent_id = doc.metadata["parent_id"]
|
||||
|
||||
for i, chunk in enumerate(md_chunks):
|
||||
# 为子块分配唯一ID并建立父子关系
|
||||
child_id = str(uuid.uuid4())
|
||||
chunk.metadata.update(doc.metadata)
|
||||
chunk.metadata.update({
|
||||
"chunk_id": child_id,
|
||||
"parent_id": parent_id,
|
||||
"doc_type": "child", # 标记为子文档
|
||||
"chunk_index": i # 在父文档中的位置
|
||||
})
|
||||
|
||||
# 建立父子映射关系
|
||||
self.parent_child_map[child_id] = parent_id
|
||||
|
||||
all_chunks.extend(md_chunks)
|
||||
|
||||
return all_chunks
|
||||
```
|
||||
|
||||
- **三级标题分割**: 按照`#`、`##`、`###`进行层级分割
|
||||
- **保留标题**: 设置`strip_headers=False`,保留标题信息便于理解上下文
|
||||
- **父子关系**: 每个子块都记录其父文档的`parent_id`
|
||||
- **唯一标识**: 每个子块都有独立的`child_id`
|
||||
|
||||
#### 2.3.3 分块效果示例
|
||||
|
||||
以"西红柿炒鸡蛋"为例,分块后的效果:
|
||||
|
||||
```
|
||||
原文档:西红柿炒鸡蛋的做法.md (父文档)
|
||||
├── 子块1:# 西红柿炒鸡蛋的做法 + 简介 + 难度评级
|
||||
├── 子块2:## 必备原料和工具 + 食材清单
|
||||
├── 子块3:## 计算 + 用量配比公式
|
||||
├── 子块4:## 操作 + 详细制作步骤
|
||||
└── 子块5:## 附加内容
|
||||
```
|
||||
|
||||
**分块逻辑**:
|
||||
- **子块1**: 包含一级标题及其下的所有内容(简介、难度评级),直到遇到下一个二级标题
|
||||
- **子块2-5**: 每个二级标题及其下的内容形成一个独立子块
|
||||
- **精确检索**: 用户问"需要什么食材"时,能精确匹配到子块2
|
||||
- **上下文完整**: 生成时传递完整的父文档,包含所有必要信息
|
||||
|
||||
### 2.4 智能去重
|
||||
|
||||
当用户询问"宫保鸡丁怎么做"时,可能会检索到同一道菜的多个子块。我们需要智能去重,避免重复信息。
|
||||
|
||||
```python
|
||||
def get_parent_documents(self, child_chunks: List[Document]) -> List[Document]:
|
||||
"""根据子块获取对应的父文档(智能去重)"""
|
||||
# 统计每个父文档被匹配的次数(相关性指标)
|
||||
parent_relevance = {}
|
||||
parent_docs_map = {}
|
||||
|
||||
# 收集所有相关的父文档ID和相关性分数
|
||||
for chunk in child_chunks:
|
||||
parent_id = chunk.metadata.get("parent_id")
|
||||
if parent_id:
|
||||
# 增加相关性计数
|
||||
parent_relevance[parent_id] = parent_relevance.get(parent_id, 0) + 1
|
||||
|
||||
# 缓存父文档(避免重复查找)
|
||||
if parent_id not in parent_docs_map:
|
||||
for doc in self.documents:
|
||||
if doc.metadata.get("parent_id") == parent_id:
|
||||
parent_docs_map[parent_id] = doc
|
||||
break
|
||||
|
||||
# 按相关性排序并构建去重后的父文档列表
|
||||
sorted_parent_ids = sorted(parent_relevance.keys(),
|
||||
key=lambda x: parent_relevance[x], reverse=True)
|
||||
|
||||
# 构建去重后的父文档列表
|
||||
parent_docs = []
|
||||
for parent_id in sorted_parent_ids:
|
||||
if parent_id in parent_docs_map:
|
||||
parent_docs.append(parent_docs_map[parent_id])
|
||||
|
||||
return parent_docs
|
||||
```
|
||||
|
||||
**去重逻辑**:
|
||||
1. **统计相关性**: 计算每个父文档被匹配的子块数量
|
||||
2. **按相关性排序**: 匹配子块越多的菜谱排名越靠前
|
||||
3. **去重输出**: 每个菜谱只输出一次完整文档
|
||||
@@ -0,0 +1,281 @@
|
||||
# 第三节 索引构建与检索优化
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
%% 索引构建与检索优化流程
|
||||
INPUT[📦 接收文本块chunks] --> INDEX_CHECK{📂 检查索引缓存}
|
||||
INDEX_CHECK -->|存在| LOAD_INDEX[⚡ 加载已保存索引]
|
||||
INDEX_CHECK -->|不存在| BUILD_INDEX[🔨 构建新索引]
|
||||
|
||||
BUILD_INDEX --> EMBED[🤖 BGE嵌入模型]
|
||||
EMBED --> FAISS[📊 FAISS向量索引]
|
||||
FAISS --> SAVE[💾 保存索引]
|
||||
|
||||
LOAD_INDEX --> SETUP[🔧 设置检索器]
|
||||
SAVE --> SETUP
|
||||
|
||||
SETUP --> QUERY[❓ 用户查询]
|
||||
QUERY --> HYBRID[🔍 RRF混合检索]
|
||||
|
||||
%% 混合检索详细流程
|
||||
subgraph HybridProcess [RRF混合检索过程]
|
||||
H1[📊 向量检索语义相似度]
|
||||
H2[🔤 BM25检索关键词匹配]
|
||||
H3[⚡ RRF重排融合]
|
||||
H1 --> H3
|
||||
H2 --> H3
|
||||
end
|
||||
|
||||
%% 索引构建详细流程
|
||||
subgraph IndexProcess [索引构建过程]
|
||||
I1[📝 文本向量化]
|
||||
I2[🗂️ 构建FAISS索引]
|
||||
I3[💾 索引持久化]
|
||||
I1 --> I2 --> I3
|
||||
end
|
||||
|
||||
%% 检索器设置流程
|
||||
subgraph SetupProcess [检索器设置过程]
|
||||
S1[🔍 向量检索器设置]
|
||||
S2[📋 BM25检索器设置]
|
||||
S1 --> S2
|
||||
end
|
||||
|
||||
HYBRID --> RESULT[📖 检索结果]
|
||||
|
||||
%% 连接子流程
|
||||
BUILD_INDEX -.-> IndexProcess
|
||||
HYBRID -.-> HybridProcess
|
||||
SETUP -.-> SetupProcess
|
||||
|
||||
%% 样式定义
|
||||
classDef index fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef retrieval fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
|
||||
classDef cache fill:#fff8e1,stroke:#f57c00,stroke-width:2px
|
||||
classDef subprocess fill:#f1f8e9,stroke:#33691e,stroke-width:2px
|
||||
classDef output fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
|
||||
|
||||
%% 应用样式
|
||||
class BUILD_INDEX,EMBED,FAISS,SAVE index
|
||||
class SETUP,QUERY,HYBRID retrieval
|
||||
class INDEX_CHECK,LOAD_INDEX cache
|
||||
class IndexProcess,HybridProcess,SetupProcess subprocess
|
||||
class INPUT,RESULT output
|
||||
```
|
||||
|
||||
## 一、核心设计
|
||||
|
||||
### 1.1 索引构建
|
||||
|
||||
索引构建模块的核心任务是将文本块转换为向量表示,并构建高效的检索索引。这里选择之前一直使用的BGE-small-zh-v1.5作为嵌入模型,并使用FAISS作为向量数据库来存储和检索向量。为了提升系统启动速度,实现索引缓存机制。首次构建后会将FAISS索引保存到本地,后续启动时直接加载已有索引,可以将启动时间从几分钟缩短到几秒钟。
|
||||
|
||||
### 1.2 混合检索
|
||||
|
||||
检索优化模块实现了多种检索策略的组合。采用双路检索的方式:向量检索基于语义相似度,擅长理解查询意图;BM25检索基于关键词匹配,擅长精确匹配。为了综合两种检索方式的优势,我们使用RRF(Reciprocal Rank Fusion)算法来融合检索结果。这个算法会综合考虑两种检索结果的排名信息,避免过度依赖单一检索方式。
|
||||
|
||||
> RRF 可能并不是效果最好的重排方式,但是够用🫠。如果想使用 ColBERT、RankLLM 等更先进的重排方法可以自行尝试。
|
||||
|
||||
此外,系统还支持基于元数据的智能过滤,可以按菜品分类、难度等级等条件进行筛选检索。
|
||||
|
||||
## 二、索引构建模块
|
||||
|
||||
> [index_construction.py完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C8/rag_modules/index_construction.py)
|
||||
|
||||
### 2.1 类结构设计
|
||||
|
||||
```python
|
||||
class IndexConstructionModule:
|
||||
"""索引构建模块 - 负责向量化和索引构建"""
|
||||
|
||||
def __init__(self, model_name: str = "BAAI/bge-small-zh-v1.5",
|
||||
index_save_path: str = "./vector_index"):
|
||||
self.model_name = model_name
|
||||
self.index_save_path = index_save_path
|
||||
self.embeddings = None
|
||||
self.vectorstore = None
|
||||
self.setup_embeddings()
|
||||
```
|
||||
|
||||
- `index_save_path`: 索引保存路径
|
||||
- `embeddings`: HuggingFace嵌入模型实例
|
||||
- `vectorstore`: FAISS向量存储实例
|
||||
|
||||
|
||||
|
||||
### 2.2 嵌入模型初始化
|
||||
|
||||
```python
|
||||
def setup_embeddings(self):
|
||||
"""初始化嵌入模型"""
|
||||
self.embeddings = HuggingFaceEmbeddings(
|
||||
model_name=self.model_name,
|
||||
model_kwargs={'device': 'cpu'},
|
||||
encode_kwargs={'normalize_embeddings': True}
|
||||
)
|
||||
```
|
||||
|
||||
### 2.3 向量索引构建
|
||||
|
||||
```python
|
||||
def build_vector_index(self, chunks: List[Document]) -> FAISS:
|
||||
"""构建向量索引"""
|
||||
if not chunks:
|
||||
raise ValueError("文档块列表不能为空")
|
||||
|
||||
# 提取文本内容
|
||||
texts = [chunk.page_content for chunk in chunks]
|
||||
metadatas = [chunk.metadata for chunk in chunks]
|
||||
|
||||
# 构建FAISS向量索引
|
||||
self.vectorstore = FAISS.from_texts(
|
||||
texts=texts,
|
||||
embedding=self.embeddings,
|
||||
metadatas=metadatas
|
||||
)
|
||||
|
||||
return self.vectorstore
|
||||
```
|
||||
|
||||
使用FAISS作为向量数据库,它的检索速度很快,同时保存了文本内容和元数据信息,支持大规模向量的高效检索。
|
||||
|
||||
### 2.4 索引缓存机制
|
||||
|
||||
```python
|
||||
def save_index(self):
|
||||
"""保存向量索引到配置的路径"""
|
||||
if not self.vectorstore:
|
||||
raise ValueError("请先构建向量索引")
|
||||
|
||||
# 确保保存目录存在
|
||||
Path(self.index_save_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.vectorstore.save_local(self.index_save_path)
|
||||
|
||||
def load_index(self):
|
||||
"""从配置的路径加载向量索引"""
|
||||
if not self.embeddings:
|
||||
self.setup_embeddings()
|
||||
|
||||
if not Path(self.index_save_path).exists():
|
||||
return None
|
||||
|
||||
self.vectorstore = FAISS.load_local(
|
||||
self.index_save_path,
|
||||
self.embeddings,
|
||||
allow_dangerous_deserialization=True
|
||||
)
|
||||
return self.vectorstore
|
||||
```
|
||||
|
||||
索引缓存的效果很明显:首次运行时构建索引需要几分钟,但后续运行时加载索引只需几秒钟。索引文件通常只有几十MB,存储效率很高。
|
||||
|
||||
## 三、检索优化模块
|
||||
|
||||
> [retrieval_optimization.py完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C8/rag_modules/retrieval_optimization.py)
|
||||
|
||||
### 3.1 类结构设计
|
||||
|
||||
```python
|
||||
class RetrievalOptimizationModule:
|
||||
"""检索优化模块 - 负责混合检索和过滤"""
|
||||
|
||||
def __init__(self, vectorstore: FAISS, chunks: List[Document]):
|
||||
self.vectorstore = vectorstore
|
||||
self.chunks = chunks
|
||||
self.setup_retrievers()
|
||||
```
|
||||
|
||||
- `vectorstore`: FAISS向量存储实例
|
||||
- `chunks`: 文档块列表,用于BM25检索
|
||||
|
||||
### 3.2 检索器设置
|
||||
|
||||
```python
|
||||
def setup_retrievers(self):
|
||||
"""设置向量检索器和BM25检索器"""
|
||||
# 向量检索器
|
||||
self.vector_retriever = self.vectorstore.as_retriever(
|
||||
search_type="similarity",
|
||||
search_kwargs={"k": 5}
|
||||
)
|
||||
|
||||
# BM25检索器
|
||||
self.bm25_retriever = BM25Retriever.from_documents(
|
||||
self.chunks,
|
||||
k=5
|
||||
)
|
||||
```
|
||||
|
||||
### 3.3 RRF混合检索
|
||||
|
||||
```python
|
||||
def hybrid_search(self, query: str, top_k: int = 3) -> List[Document]:
|
||||
"""混合检索 - 结合向量检索和BM25检索,使用RRF重排"""
|
||||
# 分别获取向量检索和BM25检索结果
|
||||
vector_docs = self.vector_retriever.get_relevant_documents(query)
|
||||
bm25_docs = self.bm25_retriever.get_relevant_documents(query)
|
||||
|
||||
# 使用RRF重排
|
||||
reranked_docs = self._rrf_rerank(vector_docs, bm25_docs)
|
||||
return reranked_docs[:top_k]
|
||||
|
||||
def _rrf_rerank(self, vector_results: List[Document], bm25_results: List[Document]) -> List[Document]:
|
||||
"""RRF (Reciprocal Rank Fusion) 重排"""
|
||||
|
||||
# RRF融合算法
|
||||
rrf_scores = {}
|
||||
k = 60 # RRF参数
|
||||
|
||||
# 计算向量检索的RRF分数
|
||||
for rank, doc in enumerate(vector_results):
|
||||
doc_id = id(doc)
|
||||
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (k + rank + 1)
|
||||
|
||||
# 计算BM25检索的RRF分数
|
||||
for rank, doc in enumerate(bm25_results):
|
||||
doc_id = id(doc)
|
||||
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (k + rank + 1)
|
||||
|
||||
# 合并所有文档并按RRF分数排序
|
||||
all_docs = {id(doc): doc for doc in vector_results + bm25_results}
|
||||
sorted_docs = sorted(all_docs.items(),
|
||||
key=lambda x: rrf_scores.get(x[0], 0),
|
||||
reverse=True)
|
||||
|
||||
return [doc for _, doc in sorted_docs]
|
||||
```
|
||||
|
||||
在当前系统中,两种检索方式各有优势:
|
||||
|
||||
**向量检索的优势**:
|
||||
- 理解语义相似性,如"简单易做的菜"能匹配到标记为"简单"的菜谱
|
||||
- 处理同义词和近义词,如"制作方法"和"做法"、"烹饪步骤"
|
||||
- 理解用户意图,如"适合新手"能找到难度较低的菜谱
|
||||
|
||||
**BM25检索的优势**:
|
||||
- 精确匹配菜名,如"宫保鸡丁"能准确找到对应菜谱
|
||||
- 匹配具体食材,如"土豆丝"、"西红柿"等关键词
|
||||
- 处理专业术语,如"爆炒"、"红烧"等烹饪手法
|
||||
|
||||
RRF算法能综合两种检索方式的排名信息,既保证了语义理解的准确性,又确保了关键词匹配的精确性。当然还可以用路由的方式,根据查询类型智能选择使用向量检索还是BM25检索。这种方法针对性强,能为不同类型的查询选择最优的检索方式;不足是路由规则的设计和维护比较复杂,边界情况难以处理,而且通常需要调用LLM来判断查询类型,会增加延迟和成本。
|
||||
|
||||
### 3.4 元数据过滤检索
|
||||
|
||||
```python
|
||||
def metadata_filtered_search(self, query: str, filters: Dict[str, Any],
|
||||
top_k: int = 5) -> List[Document]:
|
||||
"""基于元数据过滤的检索"""
|
||||
# 先进行向量检索
|
||||
vector_retriever = self.vectorstore.as_retriever(
|
||||
search_type="similarity",
|
||||
search_kwargs={"k": top_k * 3, "filter": filters} # 扩大检索范围
|
||||
)
|
||||
|
||||
results = vector_retriever.invoke(query)
|
||||
return results[:top_k]
|
||||
```
|
||||
|
||||
**过滤检索应用场景**:
|
||||
- 用户询问"推荐几道素菜"时,可以按菜品分类过滤,只检索素菜相关的内容
|
||||
- 新手用户问"有什么简单的菜谱"时,可以按难度等级过滤,只返回标记为"简单"的菜谱
|
||||
- 想做汤品时询问"今天喝什么汤",可以按分类过滤出所有汤品菜谱
|
||||
@@ -0,0 +1,414 @@
|
||||
# 第四节 生成集成与系统整合
|
||||
|
||||
Boss要打完喽!在最后一节来学习一下如何实现智能的生成集成模块,以及将所有模块整合成一个完整的RAG系统。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
%% 生成集成与系统整合流程
|
||||
INPUT[📖 检索结果] --> ROUTE{🎯 查询路由}
|
||||
|
||||
%% 查询路由分支
|
||||
ROUTE -->|list| LIST_QUERY[📋 列表查询]
|
||||
ROUTE -->|detail| DETAIL_QUERY[📖 详细查询]
|
||||
ROUTE -->|general| GENERAL_QUERY[ℹ️ 一般查询]
|
||||
|
||||
%% 查询重写处理
|
||||
LIST_QUERY --> KEEP[📝 保持原查询]
|
||||
DETAIL_QUERY --> KEEP
|
||||
GENERAL_QUERY --> REWRITE[🔄 查询重写]
|
||||
|
||||
%% 父子文档处理
|
||||
KEEP --> PARENT[📚 获取父文档]
|
||||
REWRITE --> PARENT
|
||||
PARENT --> DEDUP[🧠 智能去重排序]
|
||||
|
||||
%% 生成模式路由
|
||||
DEDUP --> GEN_ROUTE{🎨 生成模式路由}
|
||||
GEN_ROUTE -->|list| LIST_GEN[📋 列表生成模式]
|
||||
GEN_ROUTE -->|detail| DETAIL_GEN[📝 分步指导模式]
|
||||
GEN_ROUTE -->|general| BASIC_GEN[💬 基础回答模式]
|
||||
|
||||
%% 最终输出
|
||||
LIST_GEN --> OUTPUT[✨ 返回结果]
|
||||
DETAIL_GEN --> OUTPUT
|
||||
BASIC_GEN --> OUTPUT
|
||||
|
||||
%% 查询路由详细流程
|
||||
subgraph RouteProcess [查询路由过程]
|
||||
R1[🔍 分析查询类型]
|
||||
R2[📊 判断用户意图]
|
||||
R3[🎯 选择处理策略]
|
||||
R1 --> R2 --> R3
|
||||
end
|
||||
|
||||
%% 查询重写详细流程
|
||||
subgraph RewriteProcess [查询重写过程]
|
||||
W1[📝 分析查询模糊度]
|
||||
W2[🔧 优化查询表达]
|
||||
W3[✅ 输出重写结果]
|
||||
W1 --> W2 --> W3
|
||||
end
|
||||
|
||||
%% 生成模式详细流程
|
||||
subgraph GenerationProcess [多模式生成过程]
|
||||
G1[📋 简洁列表输出]
|
||||
G2[📝 结构化详细指导]
|
||||
G3[💬 基础信息回答]
|
||||
G1 --> G2 --> G3
|
||||
end
|
||||
|
||||
%% 系统整合流程
|
||||
subgraph SystemProcess [系统整合过程]
|
||||
SYS1[🔧 模块初始化]
|
||||
SYS2[📚 知识库构建]
|
||||
SYS3[🔄 交互式问答]
|
||||
SYS1 --> SYS2 --> SYS3
|
||||
end
|
||||
|
||||
%% 连接子流程
|
||||
ROUTE -.-> RouteProcess
|
||||
REWRITE -.-> RewriteProcess
|
||||
GEN_ROUTE -.-> GenerationProcess
|
||||
OUTPUT -.-> SystemProcess
|
||||
|
||||
%% 样式定义
|
||||
classDef routing fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
|
||||
classDef rewrite fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
|
||||
classDef generation fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef system fill:#e3f2fd,stroke:#0277bd,stroke-width:2px
|
||||
classDef subprocess fill:#f1f8e9,stroke:#33691e,stroke-width:2px
|
||||
classDef output fill:#fce4ec,stroke:#880e4f,stroke-width:2px
|
||||
|
||||
%% 应用样式
|
||||
class ROUTE,LIST_QUERY,DETAIL_QUERY,GENERAL_QUERY,GEN_ROUTE routing
|
||||
class KEEP,REWRITE rewrite
|
||||
class LIST_GEN,DETAIL_GEN,BASIC_GEN generation
|
||||
class PARENT,DEDUP system
|
||||
class RouteProcess,RewriteProcess,GenerationProcess,SystemProcess subprocess
|
||||
class INPUT,OUTPUT output
|
||||
```
|
||||
|
||||
## 一、生成集成模块
|
||||
|
||||
生成集成模块是整个RAG系统的"大脑",负责理解用户意图、路由查询类型,并生成高质量的回答。
|
||||
|
||||
> [generation_integration.py完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C8/rag_modules/generation_integration.py)
|
||||
|
||||
### 1.1 设计思路
|
||||
|
||||
**智能查询路由**:根据用户查询自动判断是列表查询、详细查询还是一般查询,选择最适合的生成策略。
|
||||
|
||||
**查询重写优化**:对模糊不清的查询进行智能重写,提升检索效果。比如将"做菜"重写为"简单易做的家常菜谱"。
|
||||
|
||||
**多模式生成**:
|
||||
- **列表模式**:适用于推荐类查询,返回简洁的菜品列表
|
||||
- **详细模式**:适用于制作类查询,提供分步骤的详细指导
|
||||
- **基础模式**:适用于一般性问题,提供常规回答
|
||||
|
||||
> 上面说到的两种主要方法可以回顾 [**查询重构与分发**](https://github.com/datawhalechina/all-in-rag/blob/main/docs/chapter4/14_query_rewriting.md)
|
||||
|
||||
### 1.2 类结构设计
|
||||
|
||||
```python
|
||||
class GenerationIntegrationModule:
|
||||
"""生成集成模块 - 负责LLM集成和回答生成"""
|
||||
|
||||
def __init__(self, model_name: str = "kimi-k2-0711-preview",
|
||||
temperature: float = 0.1, max_tokens: int = 2048):
|
||||
self.model_name = model_name
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.llm = None
|
||||
self.setup_llm()
|
||||
```
|
||||
|
||||
- `temperature`: 生成温度,控制回答的创造性
|
||||
- `max_tokens`: 最大生成长度
|
||||
- `llm`: Moonshot Chat模型实例
|
||||
|
||||
### 1.3 查询路由实现
|
||||
|
||||
```python
|
||||
def query_router(self, query: str) -> str:
|
||||
"""查询路由 - 根据查询类型选择不同的处理方式"""
|
||||
prompt = ChatPromptTemplate.from_template("""
|
||||
根据用户的问题,将其分类为以下三种类型之一:
|
||||
|
||||
1. 'list' - 用户想要获取菜品列表或推荐,只需要菜名
|
||||
例如:推荐几个素菜、有什么川菜、给我3个简单的菜
|
||||
|
||||
2. 'detail' - 用户想要具体的制作方法或详细信息
|
||||
例如:宫保鸡丁怎么做、制作步骤、需要什么食材
|
||||
|
||||
3. 'general' - 其他一般性问题
|
||||
例如:什么是川菜、制作技巧、营养价值
|
||||
|
||||
请只返回分类结果:list、detail 或 general
|
||||
|
||||
用户问题: {query}
|
||||
|
||||
分类结果:""")
|
||||
|
||||
# ... (LCEL链式调用)
|
||||
return result
|
||||
```
|
||||
|
||||
查询路由是整个系统的关键,决定了后续的处理流程。通过LLM自动判断查询意图,比简单的关键词匹配更准确。
|
||||
|
||||
### 1.4 查询重写优化
|
||||
|
||||
```python
|
||||
def query_rewrite(self, query: str) -> str:
|
||||
"""智能查询重写 - 让大模型判断是否需要重写查询"""
|
||||
# 使用LLM分析查询是否需要重写
|
||||
# 具体明确的查询(如"宫保鸡丁怎么做")保持原样
|
||||
# 模糊查询(如"做菜"、"推荐个菜")进行重写优化
|
||||
|
||||
# ... (提示词设计和LCEL链式调用)
|
||||
return response
|
||||
```
|
||||
|
||||
查询重写能够将模糊的用户输入转换为更适合检索的查询,显著提升系统的实用性。重写规则包括:保持原意不变、增加相关烹饪术语、优先推荐简单易做的菜品。
|
||||
|
||||
### 1.5 多模式生成
|
||||
|
||||
**列表模式生成**:
|
||||
```python
|
||||
def generate_list_answer(self, query: str, context_docs: List[Document]) -> str:
|
||||
"""生成列表式回答 - 适用于推荐类查询"""
|
||||
# 提取菜品名称
|
||||
dish_names = []
|
||||
for doc in context_docs:
|
||||
dish_name = doc.metadata.get('dish_name', '未知菜品')
|
||||
if dish_name not in dish_names:
|
||||
dish_names.append(dish_name)
|
||||
|
||||
# 构建简洁的列表回答
|
||||
if len(dish_names) <= 3:
|
||||
return f"为您推荐以下菜品:\n" + "\n".join([f"{i+1}. {name}" for i, name in enumerate(dish_names)])
|
||||
# ... (其他情况处理)
|
||||
```
|
||||
|
||||
**详细模式生成**:
|
||||
```python
|
||||
def generate_step_by_step_answer(self, query: str, context_docs: List[Document]) -> str:
|
||||
"""生成分步骤回答"""
|
||||
# 使用结构化提示词,包含:
|
||||
# - 🥘 菜品介绍
|
||||
# - 🛒 所需食材
|
||||
# - 👨🍳 制作步骤
|
||||
# - 💡 制作技巧
|
||||
|
||||
# ... (提示词设计和LCEL链式调用)
|
||||
return response
|
||||
```
|
||||
|
||||
详细模式使用结构化的提示词设计,让LLM能够生成格式规范、内容丰富的分步骤指导,重点突出实用性和可操作性。
|
||||
|
||||
## 二、系统整合
|
||||
|
||||
主程序负责协调各个模块,实现完整的RAG流程:数据准备 → 索引构建 → 检索优化 → 生成集成。同时提供了索引缓存、交互式问答等实用功能。
|
||||
|
||||
> [main.py完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C8/main.py)
|
||||
|
||||
### 2.1 主系统类设计
|
||||
|
||||
```python
|
||||
class RecipeRAGSystem:
|
||||
"""食谱RAG系统主类"""
|
||||
|
||||
def __init__(self, config: RAGConfig = None):
|
||||
self.config = config or DEFAULT_CONFIG
|
||||
self.data_module = None
|
||||
self.index_module = None
|
||||
self.retrieval_module = None
|
||||
self.generation_module = None
|
||||
|
||||
# 检查数据路径和API密钥
|
||||
if not Path(self.config.data_path).exists():
|
||||
raise FileNotFoundError(f"数据路径不存在: {self.config.data_path}")
|
||||
if not os.getenv("MOONSHOT_API_KEY"):
|
||||
raise ValueError("请设置 MOONSHOT_API_KEY 环境变量")
|
||||
```
|
||||
|
||||
主系统类负责协调所有模块,确保系统的完整性和一致性。
|
||||
|
||||
### 2.2 系统初始化流程
|
||||
|
||||
```python
|
||||
def initialize_system(self):
|
||||
"""初始化所有模块"""
|
||||
# 1. 初始化数据准备模块
|
||||
self.data_module = DataPreparationModule(self.config.data_path)
|
||||
|
||||
# 2. 初始化索引构建模块
|
||||
self.index_module = IndexConstructionModule(
|
||||
model_name=self.config.embedding_model,
|
||||
index_save_path=self.config.index_save_path
|
||||
)
|
||||
|
||||
# 3. 初始化生成集成模块
|
||||
self.generation_module = GenerationIntegrationModule(
|
||||
model_name=self.config.llm_model,
|
||||
temperature=self.config.temperature,
|
||||
max_tokens=self.config.max_tokens
|
||||
)
|
||||
```
|
||||
|
||||
初始化过程按照依赖关系有序进行,保证每个模块都能正确设置。
|
||||
|
||||
### 2.3 知识库构建流程
|
||||
|
||||
```python
|
||||
def build_knowledge_base(self):
|
||||
"""构建知识库"""
|
||||
# 1. 尝试加载已保存的索引
|
||||
vectorstore = self.index_module.load_index()
|
||||
|
||||
if vectorstore is not None:
|
||||
# 加载已有索引,但仍需要文档和分块用于检索模块
|
||||
self.data_module.load_documents()
|
||||
chunks = self.data_module.chunk_documents()
|
||||
else:
|
||||
# 构建新索引的完整流程
|
||||
self.data_module.load_documents()
|
||||
chunks = self.data_module.chunk_documents()
|
||||
vectorstore = self.index_module.build_vector_index(chunks)
|
||||
self.index_module.save_index()
|
||||
|
||||
# 初始化检索优化模块
|
||||
self.retrieval_module = RetrievalOptimizationModule(vectorstore, chunks)
|
||||
```
|
||||
|
||||
这个流程运用了之前设计的索引缓存机制,能够大幅提升系统启动速度。
|
||||
|
||||
### 2.4 智能问答流程
|
||||
|
||||
```python
|
||||
def ask_question(self, question: str, stream: bool = False):
|
||||
"""回答用户问题"""
|
||||
# 1. 查询路由
|
||||
route_type = self.generation_module.query_router(question)
|
||||
|
||||
# 2. 智能查询重写(根据路由类型)
|
||||
if route_type == 'list':
|
||||
rewritten_query = question # 列表查询保持原样
|
||||
else:
|
||||
rewritten_query = self.generation_module.query_rewrite(question)
|
||||
|
||||
# 3. 检索相关子块
|
||||
relevant_chunks = self.retrieval_module.hybrid_search(rewritten_query, top_k=self.config.top_k)
|
||||
|
||||
# 4. 根据路由类型选择回答方式
|
||||
if route_type == 'list':
|
||||
# 列表查询:返回菜品名称列表
|
||||
relevant_docs = self.data_module.get_parent_documents(relevant_chunks)
|
||||
return self.generation_module.generate_list_answer(question, relevant_docs)
|
||||
else:
|
||||
# 详细查询:获取完整文档并生成详细回答
|
||||
relevant_docs = self.data_module.get_parent_documents(relevant_chunks)
|
||||
|
||||
if route_type == "detail":
|
||||
# 详细查询使用分步指导模式
|
||||
return self.generation_module.generate_step_by_step_answer(question, relevant_docs)
|
||||
else:
|
||||
# 一般查询使用基础回答模式
|
||||
return self.generation_module.generate_basic_answer(question, relevant_docs)
|
||||
```
|
||||
|
||||
这部分展示了程序执行流程:智能路由 → 查询优化 → 混合检索 → 父子文档处理 → 多模式生成。
|
||||
|
||||
### 2.5 实际使用示例
|
||||
|
||||
#### 2.5.1 不同查询类型的效果
|
||||
|
||||
**列表查询示例**:
|
||||
```
|
||||
用户问题: "推荐几道简单的素菜"
|
||||
查询类型: list
|
||||
生成结果:
|
||||
为您推荐以下菜品:
|
||||
1. 西红柿炒鸡蛋
|
||||
2. 土豆丝
|
||||
3. 青椒炒豆腐
|
||||
```
|
||||
|
||||
**详细查询示例**:
|
||||
```
|
||||
用户问题: "宫保鸡丁怎么做?"
|
||||
查询类型: detail
|
||||
生成结果:
|
||||
## 🥘 菜品介绍
|
||||
宫保鸡丁是一道经典川菜,口感麻辣鲜香...
|
||||
|
||||
## 🛒 所需食材
|
||||
- 鸡胸肉 300g
|
||||
- 花生米 100g
|
||||
- 干辣椒 10个
|
||||
...
|
||||
|
||||
## 👨🍳 制作步骤
|
||||
1. 鸡肉切丁,用料酒和生抽腌制15分钟
|
||||
2. 热锅下油,爆炒花生米至微黄盛起
|
||||
...
|
||||
```
|
||||
|
||||
#### 2.5.2 交互式问答
|
||||
|
||||
系统提供了完整的命令行交互界面,启动时会显示"尝尝咸淡RAG系统"的欢迎信息:
|
||||
|
||||
```python
|
||||
def run_interactive(self):
|
||||
"""运行交互式问答"""
|
||||
print("=" * 60)
|
||||
print("🍽️ 尝尝咸淡RAG系统 - 交互式问答 🍽️")
|
||||
print("=" * 60)
|
||||
print("💡 解决您的选择困难症,告别'今天吃什么'的世纪难题!")
|
||||
|
||||
# 初始化系统和构建知识库
|
||||
self.initialize_system()
|
||||
self.build_knowledge_base()
|
||||
|
||||
while True:
|
||||
user_input = input("\n您的问题: ").strip()
|
||||
if user_input.lower() in ['退出', 'quit', 'exit']:
|
||||
break
|
||||
|
||||
# 询问是否使用流式输出
|
||||
stream_choice = input("是否使用流式输出? (y/n, 默认y): ").strip().lower()
|
||||
use_stream = stream_choice != 'n'
|
||||
|
||||
if use_stream:
|
||||
# 流式输出,实时显示生成过程
|
||||
for chunk in self.ask_question(user_input, stream=True):
|
||||
print(chunk, end="", flush=True)
|
||||
else:
|
||||
# 普通输出
|
||||
answer = self.ask_question(user_input, stream=False)
|
||||
print(answer)
|
||||
```
|
||||
|
||||
**运行效果示例**:
|
||||
```
|
||||
============================================================
|
||||
🍽️ 尝尝咸淡RAG系统 - 交互式问答 🍽️
|
||||
============================================================
|
||||
💡 解决您的选择困难症,告别'今天吃什么'的世纪难题!
|
||||
|
||||
✅ 成功加载已保存的向量索引!
|
||||
✅ 系统初始化完成!
|
||||
|
||||
您的问题: 推荐几道简单的素菜
|
||||
是否使用流式输出? (y/n, 默认y): y
|
||||
|
||||
为您推荐以下素菜:
|
||||
1. 西红柿炒鸡蛋 - 经典家常菜,简单易做
|
||||
2. 土豆丝 - 爽脆可口,适合新手
|
||||
3. 青椒炒豆腐 - 营养丰富,制作简单
|
||||
```
|
||||
|
||||
流式输出的实现通过LangChain的`chain.stream()`方法,它会返回一个生成器,每次yield一个文本片段。在交互式界面中,通过`print(chunk, end="", flush=True)`实时输出每个片段,`end=""`避免换行,`flush=True`确保立即显示,从而实现逐字逐句的流式效果。
|
||||
|
||||
## 三、优化方向
|
||||
|
||||
虽然当前系统已经具备了完整的RAG功能,但仍有许多优化空间。未来的优化可以聚焦于几个关键方向的融合与深化:可以通过 **集成图数据库** 将食谱数据构建为知识图谱,来揭示食材、菜品与烹饪方法间的复杂关联,进而支持复杂关系查询(如“和鸡肉搭配的食材有哪些”)、发掘潜在的食材组合并实现基于图的智能推荐。还可以 **融合多模态数据**,结合菜品图片等视觉信息,利用多模态模型进行图文联合检索,不仅能支持“这是什么菜”的视觉搜索,还可以通过图像识别食材来推荐相关菜谱。或者通过 **增强专业知识**,集成营养成分数据库、烹饪技巧知识图谱以及食材替换规则库等外部知识源,系统将能提供精准的营养分析、专业的烹饪指导,并灵活适应用户的饮食过敏或个人偏好。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user