Initial commit
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
# 第一节 图RAG系统架构与环境配置
|
||||
|
||||
> 在前面章节的基础上,接下来构建一个更先进的图RAG系统。通过引入Neo4j图数据库和智能查询路由机制,实现真正的知识图谱增强检索,解决传统RAG在复杂查询和关系推理方面的局限性。
|
||||
|
||||

|
||||
|
||||
## 一、项目背景与目标
|
||||
|
||||
### 1.1 从传统RAG到图RAG的演进
|
||||
|
||||
上一章中,我们构建了基于向量检索的传统RAG系统,采用了父子文本块的分块策略,能够有效回答简单的菜谱查询。但在处理复杂的关系推理和多跳查询时仍存在明显局限:
|
||||
|
||||
- **关系理解缺失**:虽然父子分块保持了文档结构,但无法显式建模食材、菜谱、烹饪方法之间的语义关系
|
||||
- **跨文档关联困难**:难以发现不同菜谱之间的相似性、替代关系等隐含联系
|
||||
- **推理能力有限**:缺乏基于知识图谱的多跳推理能力,难以回答需要复杂逻辑推理的问题
|
||||
|
||||
### 1.2 图RAG系统的核心优势
|
||||
|
||||
通过引入知识图谱,我们的新系统将具备:
|
||||
|
||||
- **结构化知识表达**:以图的形式显式编码实体间的语义关系
|
||||
- **增强推理能力**:支持多跳推理和复杂关系查询
|
||||
- **智能查询路由**:根据查询复杂度自动选择最适合的检索策略
|
||||
- **事实性与可解释性**:基于图结构的推理路径提供可追溯的答案
|
||||
|
||||
## 二、环境配置
|
||||
|
||||
> 若需要进行外部访问,需更换本地或服务器环境
|
||||
|
||||
### 2.1 创建虚拟环境
|
||||
|
||||
```bash
|
||||
# 使用conda创建环境
|
||||
conda create -n graph-rag python=3.12.7
|
||||
conda activate graph-rag
|
||||
```
|
||||
|
||||
### 2.2 安装核心依赖
|
||||
|
||||
```bash
|
||||
cd code/C9
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2.3 Neo4j数据库配置
|
||||
|
||||
使用Docker Compose方式安装Neo4j,配置文件位于 [`data/C9/docker-compose.yml`](https://github.com/datawhalechina/all-in-rag/blob/main/data/C9/docker-compose.yml):
|
||||
|
||||
#### 2.3.1 启动Neo4j服务
|
||||
|
||||
```bash
|
||||
# 进入docker-compose.yml所在目录
|
||||
cd data/C9
|
||||
|
||||
# 启动Neo4j服务
|
||||
docker-compose up -d
|
||||
|
||||
# 检查服务状态
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
#### 2.3.2 访问Neo4j Web界面
|
||||
|
||||
启动成功后,可以通过以下方式访问:
|
||||
- **Web界面**:http://localhost:7474
|
||||
- **用户名**:neo4j
|
||||
- **密码**:all-in-rag
|
||||
|
||||
> 当前网址为本地访问,如果你是部署在远程服务器上,需要将 `localhost` 修改为你的服务器IP地址。
|
||||
|
||||
#### 2.3.3 数据导入
|
||||
|
||||
Docker Compose配置中包含了自动数据导入功能。启动服务时会自动执行以下步骤:
|
||||
|
||||
1. **等待Neo4j服务就绪**:通过健康检查确保数据库可用
|
||||
2. **执行导入脚本**:自动运行 `data/C9/cypher/neo4j_import.cypher`
|
||||
3. **导入菜谱数据**:包括菜谱、食材、烹饪步骤等节点和关系
|
||||
|
||||
导入的数据包括:
|
||||
- **菜谱节点**:包含菜名、难度、烹饪时间、菜系等信息
|
||||
- **食材节点**:包含食材名称、分类、营养信息等
|
||||
- **烹饪步骤节点**:包含步骤描述、烹饪方法、所需工具等
|
||||
- **关系网络**:菜谱与食材、步骤之间的复杂关系
|
||||
|
||||
如果需要手动重新导入数据:
|
||||
|
||||
```bash
|
||||
# 进入容器执行导入脚本
|
||||
docker exec -it neo4j-db cypher-shell -u neo4j -p all-in-rag -f /import/cypher/neo4j_import.cypher
|
||||
```
|
||||
|
||||
### 2.4 Milvus向量数据库配置
|
||||
|
||||
#### 2.4.1 使用Docker安装Milvus
|
||||
|
||||
> 如果前面已经安装过了可以跳过此步,通过 `docker-compose ps` 确认Milvus服务正在运行即可。
|
||||
|
||||
```bash
|
||||
# 下载Milvus standalone配置文件
|
||||
wget https://github.com/milvus-io/milvus/releases/download/v2.5.11/milvus-standalone-docker-compose.yml -O docker-compose.yml
|
||||
|
||||
# 启动Milvus
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### 2.4.2 验证安装
|
||||
|
||||
```bash
|
||||
# 检查Milvus服务状态
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 2.5 配置连接参数
|
||||
|
||||
在项目根目录创建 `.env` 文件:
|
||||
|
||||
```env
|
||||
# Neo4j配置
|
||||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USER=neo4j
|
||||
NEO4J_PASSWORD=all-in-rag
|
||||
NEO4J_DATABASE=neo4j
|
||||
|
||||
# Milvus配置
|
||||
MILVUS_HOST=localhost
|
||||
MILVUS_PORT=19530
|
||||
|
||||
# LLM API配置
|
||||
MOONSHOT_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
## 三、系统架构设计
|
||||
|
||||
### 3.1 整体架构
|
||||
|
||||
我们的图RAG系统采用模块化设计,包含以下核心组件:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% 系统启动和初始化
|
||||
START["🚀 启动高级图RAG系统"] --> CONFIG["⚙️ 加载配置<br/>GraphRAGConfig"]
|
||||
CONFIG --> INIT_CHECK{"🔍 检查系统依赖"}
|
||||
|
||||
%% 依赖检查
|
||||
INIT_CHECK -->|Neo4j连接失败| NEO4J_ERROR["❌ Neo4j连接错误<br/>检查图数据库状态"]
|
||||
INIT_CHECK -->|Milvus连接失败| MILVUS_ERROR["❌ Milvus连接错误<br/>检查向量数据库"]
|
||||
INIT_CHECK -->|LLM API失败| LLM_ERROR["❌ LLM API错误<br/>检查API密钥"]
|
||||
INIT_CHECK -->|依赖正常| INIT_MODULES["✅ 初始化核心模块"]
|
||||
|
||||
%% 知识库状态检查
|
||||
INIT_MODULES --> KB_CHECK{"📚 检查知识库状态"}
|
||||
KB_CHECK -->|Milvus集合存在| LOAD_KB["⚡ 加载已存在知识库"]
|
||||
KB_CHECK -->|集合不存在| BUILD_KB["🔨 构建新知识库"]
|
||||
|
||||
%% 加载已有知识库
|
||||
LOAD_KB --> LOAD_SUCCESS{"加载成功?"}
|
||||
LOAD_SUCCESS -->|成功| SYSTEM_READY["✅ 系统就绪<br/>显示统计信息"]
|
||||
LOAD_SUCCESS -->|失败| REBUILD_KB["🔄 重建知识库"]
|
||||
|
||||
%% 构建新知识库流程
|
||||
BUILD_KB --> NEO4J_LOAD["🔗 从Neo4j加载图数据<br/>菜谱、食材、烹饪步骤节点"]
|
||||
REBUILD_KB --> NEO4J_LOAD
|
||||
NEO4J_LOAD --> BUILD_DOCS["📝 构建结构化菜谱文档<br/>组合图数据为完整文档"]
|
||||
BUILD_DOCS --> CHUNK_DOCS["✂️ 智能文档分块<br/>按章节或长度分块"]
|
||||
CHUNK_DOCS --> BUILD_VECTOR["🎯 构建Milvus向量索引"]
|
||||
BUILD_VECTOR --> SYSTEM_READY
|
||||
|
||||
%% 用户交互循环
|
||||
SYSTEM_READY --> USER_INPUT["👤 用户输入查询"]
|
||||
USER_INPUT --> SPECIAL_CMD{"🔍 特殊命令检查"}
|
||||
|
||||
%% 特殊命令处理
|
||||
SPECIAL_CMD -->|stats| STATS["📊 显示系统统计<br/>路由统计、知识库状态"]
|
||||
SPECIAL_CMD -->|rebuild| REBUILD_CMD["🔄 重建知识库命令"]
|
||||
SPECIAL_CMD -->|quit| EXIT["👋 退出系统"]
|
||||
|
||||
%% 普通查询处理 - 智能路由核心
|
||||
SPECIAL_CMD -->|普通查询| QUERY_ANALYSIS["🧠 深度查询分析"]
|
||||
|
||||
%% 查询分析的四个维度
|
||||
QUERY_ANALYSIS --> COMPLEXITY_ANALYSIS["📊 复杂度分析<br/>0.0-0.3: 简单查找<br/>0.4-0.7: 中等复杂<br/>0.8-1.0: 高复杂推理"]
|
||||
QUERY_ANALYSIS --> RELATION_ANALYSIS["🔗 关系密集度分析<br/>0.0-0.3: 单一实体<br/>0.4-0.7: 实体关系<br/>0.8-1.0: 复杂关系网络"]
|
||||
QUERY_ANALYSIS --> REASONING_ANALYSIS["🤔 推理需求判断<br/>多跳推理?因果分析?<br/>对比分析?"]
|
||||
QUERY_ANALYSIS --> ENTITY_ANALYSIS["🏷️ 实体识别统计<br/>实体数量和类型"]
|
||||
|
||||
%% LLM智能分析
|
||||
COMPLEXITY_ANALYSIS --> LLM_ANALYSIS["🤖 LLM智能分析<br/>综合评估查询特征"]
|
||||
RELATION_ANALYSIS --> LLM_ANALYSIS
|
||||
REASONING_ANALYSIS --> LLM_ANALYSIS
|
||||
ENTITY_ANALYSIS --> LLM_ANALYSIS
|
||||
|
||||
%% 分析结果和降级处理
|
||||
LLM_ANALYSIS --> ANALYSIS_SUCCESS{"分析成功?"}
|
||||
ANALYSIS_SUCCESS -->|成功| ROUTE_DECISION["🎯 智能路由决策"]
|
||||
ANALYSIS_SUCCESS -->|失败| RULE_FALLBACK["📋 降级到规则分析<br/>基于关键词匹配"]
|
||||
RULE_FALLBACK --> ROUTE_DECISION
|
||||
|
||||
%% 三种检索策略路由
|
||||
ROUTE_DECISION -->|简单查询<br/>复杂度<0.4| HYBRID_SEARCH["🔍 传统混合检索<br/>保底策略"]
|
||||
ROUTE_DECISION -->|复杂推理<br/>关系密集>0.7| GRAPH_RAG_SEARCH["🕸️ 图RAG检索<br/>高级复杂策略"]
|
||||
ROUTE_DECISION -->|中等复杂<br/>需要组合| COMBINED_SEARCH["🔄 组合检索策略<br/>融合两种方法"]
|
||||
|
||||
%% 检索执行和错误处理
|
||||
HYBRID_SEARCH --> HYBRID_SUCCESS{"检索成功?"}
|
||||
GRAPH_RAG_SEARCH --> GRAPH_SUCCESS{"检索成功?"}
|
||||
COMBINED_SEARCH --> COMBINED_SUCCESS{"检索成功?"}
|
||||
|
||||
%% 高级策略失败时降级到传统混合检索
|
||||
GRAPH_SUCCESS -->|失败| FALLBACK_TO_HYBRID["⬇️ 降级到传统混合检索<br/>保底方案"]
|
||||
COMBINED_SUCCESS -->|失败| FALLBACK_TO_HYBRID
|
||||
|
||||
%% 传统混合检索失败时直接异常
|
||||
HYBRID_SUCCESS -->|失败| SYSTEM_ERROR["❌ 系统检索异常<br/>传统混合检索失败<br/>无更低级降级"]
|
||||
FALLBACK_TO_HYBRID --> FALLBACK_SUCCESS{"降级检索成功?"}
|
||||
FALLBACK_SUCCESS -->|失败| SYSTEM_ERROR
|
||||
|
||||
%% 成功路径
|
||||
HYBRID_SUCCESS -->|成功| GENERATE["🎨 生成回答"]
|
||||
GRAPH_SUCCESS -->|成功| GENERATE
|
||||
COMBINED_SUCCESS -->|成功| GENERATE
|
||||
FALLBACK_SUCCESS -->|成功| GENERATE
|
||||
|
||||
%% 固定的流式输出
|
||||
GENERATE --> STREAM_OUTPUT["📺 流式输出回答<br/>use_stream = True<br/>逐字符实时显示"]
|
||||
|
||||
%% 统计更新和循环
|
||||
STREAM_OUTPUT --> UPDATE_STATS["📈 更新路由统计"]
|
||||
UPDATE_STATS --> USER_INPUT
|
||||
|
||||
%% 特殊命令返回循环
|
||||
STATS --> USER_INPUT
|
||||
REBUILD_CMD --> BUILD_KB
|
||||
|
||||
%% 错误处理返回
|
||||
NEO4J_ERROR --> EXIT
|
||||
MILVUS_ERROR --> EXIT
|
||||
LLM_ERROR --> EXIT
|
||||
SYSTEM_ERROR --> USER_INPUT
|
||||
|
||||
%% 详细子流程
|
||||
subgraph DataFlow ["📊 图数据处理流程"]
|
||||
NEO4J_DB["🗄️ Neo4j图数据库<br/>存储菜谱、食材、烹饪步骤<br/>以及它们之间的关系网络"]
|
||||
RECIPE_BUILD["📝 结构化菜谱文档构建<br/>菜谱名称 + 分类 + 难度<br/>+ 食材列表 + 制作步骤<br/>+ 时间信息 + 标签"]
|
||||
DOC_CHUNK["✂️ 智能文档分块<br/>按章节分块:## 所需食材、## 制作步骤<br/>或按长度分块:chunk_size=500<br/>重叠处理:chunk_overlap=50"]
|
||||
MILVUS_INDEX["🎯 Milvus向量索引<br/>BGE-small-zh-v1.5<br/>512维向量空间"]
|
||||
|
||||
NEO4J_DB --> RECIPE_BUILD
|
||||
RECIPE_BUILD --> DOC_CHUNK
|
||||
DOC_CHUNK --> MILVUS_INDEX
|
||||
end
|
||||
|
||||
subgraph HybridFlow ["🔍 传统混合检索流程(保底)"]
|
||||
DUAL_RETRIEVAL["🎯 双层检索<br/>实体级+主题级"]
|
||||
VECTOR_SEARCH["📊 增强向量检索<br/>语义相似度匹配"]
|
||||
RRF_MERGE["⚖️ RRF轮询融合<br/>公平合并不同结果"]
|
||||
INTERNAL_FALLBACK["🔧 内部降级机制<br/>关键词提取失败→简单分词<br/>图索引不足→Neo4j补充<br/>Neo4j失败→静默失败"]
|
||||
|
||||
DUAL_RETRIEVAL --> RRF_MERGE
|
||||
VECTOR_SEARCH --> RRF_MERGE
|
||||
INTERNAL_FALLBACK --> RRF_MERGE
|
||||
end
|
||||
|
||||
subgraph GraphRAGFlow ["🕸️ 图RAG检索流程(高级复杂)"]
|
||||
GRAPH_UNDERSTAND["🧠 图查询理解<br/>entity_relation/multi_hop<br/>subgraph/path_finding"]
|
||||
MULTI_HOP["🔄 多跳图遍历<br/>最大深度3跳<br/>发现隐含关联"]
|
||||
SUBGRAPH_EXTRACT["🕸️ 知识子图提取<br/>完整知识网络<br/>最大100节点"]
|
||||
GRAPH_REASONING["🤔 图结构推理<br/>推理链构建<br/>可信度验证"]
|
||||
|
||||
GRAPH_UNDERSTAND --> MULTI_HOP
|
||||
GRAPH_UNDERSTAND --> SUBGRAPH_EXTRACT
|
||||
MULTI_HOP --> GRAPH_REASONING
|
||||
SUBGRAPH_EXTRACT --> GRAPH_REASONING
|
||||
end
|
||||
|
||||
subgraph CombinedFlow ["🔄 组合检索流程"]
|
||||
SPLIT_QUOTA["📊 分配检索配额<br/>traditional_k = top_k // 2<br/>graph_k = top_k - traditional_k"]
|
||||
PARALLEL_SEARCH["⚡ 并行执行检索<br/>传统检索 + 图RAG检索"]
|
||||
ROUND_ROBIN["🔄 Round-robin合并<br/>交替添加结果<br/>图RAG优先"]
|
||||
DEDUP["🧹 去重和排序<br/>基于内容哈希"]
|
||||
|
||||
SPLIT_QUOTA --> PARALLEL_SEARCH
|
||||
PARALLEL_SEARCH --> ROUND_ROBIN
|
||||
ROUND_ROBIN --> DEDUP
|
||||
end
|
||||
|
||||
subgraph FallbackStrategy ["⬇️ 降级策略(有限降级)"]
|
||||
LEVEL3["🕸️ 图RAG检索<br/>最高级:多跳推理+子图提取"]
|
||||
LEVEL2["🔄 组合检索<br/>中级:融合两种方法"]
|
||||
LEVEL1["🔍 传统混合检索<br/>保底:无更低级降级"]
|
||||
ERROR_LEVEL["❌ 系统异常<br/>传统混合检索失败"]
|
||||
|
||||
LEVEL3 -->|失败| LEVEL1
|
||||
LEVEL2 -->|失败| LEVEL1
|
||||
LEVEL1 -->|失败| ERROR_LEVEL
|
||||
end
|
||||
|
||||
%% 样式定义
|
||||
classDef startup fill:#e3f2fd,stroke:#0277bd,stroke-width:2px
|
||||
classDef config fill:#f1f8e9,stroke:#388e3c,stroke-width:2px
|
||||
classDef basic fill:#fff3e0,stroke:#f57c00,stroke-width:2px
|
||||
classDef advanced fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
|
||||
classDef knowledge fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
|
||||
classDef analysis fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
|
||||
classDef routing fill:#e1f5fe,stroke:#01579b,stroke-width:2px
|
||||
classDef generation fill:#fce4ec,stroke:#880e4f,stroke-width:2px
|
||||
classDef userflow fill:#fff8e1,stroke:#f57c00,stroke-width:2px
|
||||
classDef error fill:#ffebee,stroke:#c62828,stroke-width:2px
|
||||
classDef success fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
|
||||
classDef fallback fill:#fff3e0,stroke:#ff6f00,stroke-width:2px
|
||||
classDef stream fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
|
||||
classDef combined fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
|
||||
classDef graphdata fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
|
||||
|
||||
%% 应用样式
|
||||
class START,INIT_MODULES,SYSTEM_READY startup
|
||||
class CONFIG config
|
||||
class HYBRID_SEARCH,HybridFlow,LEVEL1 basic
|
||||
class GRAPH_RAG_SEARCH,GraphRAGFlow,LEVEL3 advanced
|
||||
class KB_CHECK,LOAD_KB,BUILD_KB,NEO4J_LOAD,BUILD_DOCS,CHUNK_DOCS,BUILD_VECTOR knowledge
|
||||
class QUERY_ANALYSIS,COMPLEXITY_ANALYSIS,RELATION_ANALYSIS,REASONING_ANALYSIS,ENTITY_ANALYSIS,LLM_ANALYSIS analysis
|
||||
class ROUTE_DECISION,ANALYSIS_SUCCESS,RULE_FALLBACK routing
|
||||
class GENERATE generation
|
||||
class USER_INPUT,SPECIAL_CMD,STATS,REBUILD_CMD,EXIT userflow
|
||||
class NEO4J_ERROR,MILVUS_ERROR,LLM_ERROR,SYSTEM_ERROR,ERROR_LEVEL error
|
||||
class LOAD_SUCCESS,INIT_CHECK,HYBRID_SUCCESS,GRAPH_SUCCESS,COMBINED_SUCCESS,FALLBACK_SUCCESS success
|
||||
class FALLBACK_TO_HYBRID,FallbackStrategy fallback
|
||||
class STREAM_OUTPUT,UPDATE_STATS stream
|
||||
class COMBINED_SEARCH,CombinedFlow,LEVEL2 combined
|
||||
class DataFlow,NEO4J_DB graphdata
|
||||
```
|
||||
|
||||
### 3.2 核心模块说明
|
||||
|
||||
#### 图数据准备模块 (GraphDataPreparationModule)
|
||||
- **功能**:连接Neo4j数据库,加载图数据,构建结构化菜谱文档
|
||||
- **特点**:支持图数据到文档的智能转换,保持知识结构完整性
|
||||
|
||||
#### 向量索引模块 (MilvusIndexConstructionModule)
|
||||
- **功能**:构建和管理Milvus向量索引,支持语义相似度检索
|
||||
- **特点**:使用BGE-small-zh-v1.5模型,512维向量空间
|
||||
|
||||
#### 混合检索模块 (HybridRetrievalModule)
|
||||
- **功能**:传统的混合检索策略,结合向量检索和图扩展
|
||||
- **特点**:双层检索(实体级+主题级),RRF轮询融合
|
||||
|
||||
#### 图RAG检索模块 (GraphRAGRetrieval)
|
||||
- **功能**:基于图结构的高级检索,支持多跳推理和子图提取
|
||||
- **特点**:图查询理解、多跳遍历、知识子图提取
|
||||
|
||||
#### 智能查询路由 (IntelligentQueryRouter)
|
||||
- **功能**:分析查询特征,自动选择最适合的检索策略
|
||||
- **特点**:LLM驱动的查询分析,动态策略选择
|
||||
|
||||
#### 生成集成模块 (GenerationIntegrationModule)
|
||||
- **功能**:基于检索结果生成最终答案,支持流式输出
|
||||
- **特点**:自适应生成策略,错误处理与重试机制
|
||||
|
||||
### 3.3 数据流程
|
||||
|
||||
1. **数据准备阶段**:
|
||||
- 从Neo4j加载图数据(菜谱、食材、步骤节点及其关系)
|
||||
- 构建结构化菜谱文档,保持知识完整性
|
||||
- 进行智能文档分块,支持章节和长度双重分块策略
|
||||
- 构建Milvus向量索引,支持语义检索
|
||||
|
||||
2. **查询处理阶段**:
|
||||
- 用户输入查询
|
||||
- 智能查询路由器分析查询特征(复杂度、关系密集度、推理需求)
|
||||
- 根据分析结果选择检索策略:
|
||||
- 简单查询 → 传统混合检索
|
||||
- 复杂推理 → 图RAG检索
|
||||
- 中等复杂 → 组合检索策略
|
||||
- 执行相应的检索操作
|
||||
- 生成模块基于检索结果生成答案
|
||||
|
||||
3. **错误处理与降级**:
|
||||
- 高级策略失败时自动降级到传统混合检索
|
||||
- 传统混合检索失败时返回系统异常
|
||||
- 支持流式输出中断时的自动重试机制
|
||||
|
||||
## 四、项目文件结构
|
||||
|
||||
```
|
||||
code/C9/
|
||||
├── main.py # 主程序入口
|
||||
├── config.py # 配置文件
|
||||
├── requirements.txt # 依赖包列表
|
||||
└── rag_modules/ # RAG模块包
|
||||
├── __init__.py
|
||||
├── graph_data_preparation.py # 图数据准备模块
|
||||
├── milvus_index_construction.py # Milvus索引构建模块
|
||||
├── hybrid_retrieval.py # 混合检索模块
|
||||
├── graph_rag_retrieval.py # 图RAG检索模块
|
||||
├── intelligent_query_router.py # 智能查询路由器
|
||||
└── generation_integration.py # 生成集成模块
|
||||
```
|
||||
|
||||
## 五、快速开始
|
||||
|
||||
### 5.1 启动系统
|
||||
|
||||
```bash
|
||||
# 确保Neo4j和Milvus服务已启动
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 5.2 系统初始化
|
||||
|
||||
首次运行时,系统会自动:
|
||||
1. 检查并连接Neo4j和Milvus数据库
|
||||
2. 加载图数据并构建菜谱文档
|
||||
3. 创建向量索引
|
||||
4. 初始化各个检索模块
|
||||
5. 显示系统统计信息
|
||||
|
||||
### 5.3 交互式问答
|
||||
|
||||
系统启动后,可以进行交互式问答:
|
||||
|
||||
```
|
||||
您的问题: 川菜有哪些特色菜?
|
||||
您的问题: 如何制作宫保鸡丁?
|
||||
您的问题: 减肥期间适合吃什么菜?
|
||||
您的问题: stats # 查看系统统计
|
||||
您的问题: quit # 退出系统
|
||||
```
|
||||
@@ -0,0 +1,445 @@
|
||||
# 第二节 图数据建模与Neo4j集成
|
||||
|
||||
> [本节完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C9/rag_modules/graph_data_preparation.py)
|
||||
|
||||
## 一、数据来源与转换
|
||||
|
||||
### 1.1 从Markdown到图数据的转换
|
||||
|
||||
本章的图数据来源于第八章中使用的Markdown格式菜谱数据。为了构建知识图谱,笔者用AI开发了一个简单的[Agent](https://github.com/datawhalechina/all-in-rag/tree/main/code/C9/agent(%E4%BB%A3%E7%A0%81%E7%B3%BBai%E7%94%9F%E6%88%90)),通过LLM将结构化的Markdown菜谱数据转换为CSV格式的图数据。
|
||||
|
||||
**转换流程**:
|
||||
1. **读取Markdown菜谱**:从第八章的数据源加载菜谱文件
|
||||
2. **LLM解析提取**:使用大语言模型识别和提取实体及关系
|
||||
3. **结构化输出**:生成nodes.csv和relationships.csv文件
|
||||
4. **图数据导入**:通过Cypher脚本导入Neo4j数据库
|
||||
|
||||
### 1.2 图数据文件结构
|
||||
|
||||
转换后的图数据包含两个核心文件:
|
||||
|
||||
```
|
||||
data/C9/cypher/
|
||||
├── nodes.csv # 节点数据(菜谱、食材、步骤等)
|
||||
├── relationships.csv # 关系数据(菜谱-食材、菜谱-步骤等)
|
||||
└── neo4j_import.cypher # 数据导入脚本
|
||||
```
|
||||
|
||||
## 二、图数据模型设计
|
||||
|
||||
### 2.1 实际数据结构分析
|
||||
|
||||
基于LLM转换后的实际图数据,知识图谱包含以下核心实体类型。如果你有游戏逆向经验,可以把这些实体类型想象成虚幻引擎烹饪游戏中的对象类,节点间的关系就像对象间的指针引用:
|
||||
|
||||
**核心实体类型**:
|
||||
- **Recipe (菜谱)**:具体的菜品,包含难度、菜系、时间等属性
|
||||
- **Ingredient (食材)**:制作菜品所需的原料,包含分类、用量、单位等
|
||||
- **CookingStep (烹饪步骤)**:详细的制作步骤,包含方法、工具、时间估计
|
||||
- **CookingMethod (烹饪方法)**:如炒、煮、蒸、炸等烹饪技法
|
||||
- **CookingTool (烹饪工具)**:如炒锅、蒸锅、刀具等
|
||||
- **DifficultyLevel (难度等级)**:一星到五星的难度分级
|
||||
- **RecipeCategory (菜谱分类)**:素菜、荤菜、水产、早餐等分类
|
||||
|
||||
**实际数据特点**:
|
||||
- **统一编码体系**:使用nodeId进行唯一标识(如201000001)
|
||||
- **多语言支持**:包含preferredTerm、fsn等多语言字段
|
||||
- **丰富属性**:每个实体包含详细的属性信息
|
||||
- **层次化结构**:从抽象概念到具体实例的层次化组织
|
||||
|
||||
### 2.2 实际节点模型
|
||||
|
||||
基于实际数据的图数据模型:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
%% 定义节点样式
|
||||
classDef recipeNode fill:#e1f5fe,stroke:#01579b,stroke-width:2px
|
||||
classDef ingredientNode fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
|
||||
classDef stepNode fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
|
||||
classDef categoryNode fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef difficultyNode fill:#fce4ec,stroke:#880e4f,stroke-width:2px
|
||||
|
||||
%% 菜谱节点
|
||||
Recipe["🍽️ Recipe<br/>菜谱节点<br/>---<br/>nodeId: String<br/>name: String<br/>preferredTerm: String<br/>fsn: String<br/>conceptType: String<br/>synonyms: String<br/>category: String<br/>difficulty: Float<br/>cuisineType: String<br/>prepTime: String<br/>cookTime: String<br/>servings: String<br/>tags: String<br/>filePath: String"]
|
||||
|
||||
%% 食材节点
|
||||
Ingredient["🥬 Ingredient<br/>食材节点<br/>---<br/>nodeId: String<br/>name: String<br/>preferredTerm: String<br/>category: String<br/>amount: String<br/>unit: String<br/>isMain: Boolean<br/>synonyms: String"]
|
||||
|
||||
%% 烹饪步骤节点
|
||||
CookingStep["👨🍳 CookingStep<br/>烹饪步骤节点<br/>---<br/>nodeId: String<br/>name: String<br/>description: String<br/>stepNumber: Float<br/>methods: String<br/>tools: String<br/>timeEstimate: String"]
|
||||
|
||||
%% 菜谱分类节点
|
||||
RecipeCategory["📂 RecipeCategory<br/>菜谱分类节点<br/>---<br/>nodeId: String<br/>name: String<br/>preferredTerm: String<br/>fsn: String"]
|
||||
|
||||
%% 难度等级节点
|
||||
DifficultyLevel["⭐ DifficultyLevel<br/>难度等级节点<br/>---<br/>nodeId: String<br/>name: String<br/>preferredTerm: String<br/>fsn: String"]
|
||||
|
||||
%% 关系连接
|
||||
Recipe -->|REQUIRES<br/>需要食材<br/>amount, unit| Ingredient
|
||||
Recipe -->|CONTAINS_STEP<br/>包含步骤<br/>step_order| CookingStep
|
||||
Recipe -->|BELONGS_TO_CATEGORY<br/>属于分类| RecipeCategory
|
||||
Recipe -->|HAS_DIFFICULTY_LEVEL<br/>具有难度| DifficultyLevel
|
||||
|
||||
%% 应用样式
|
||||
class Recipe recipeNode
|
||||
class Ingredient ingredientNode
|
||||
class CookingStep stepNode
|
||||
class RecipeCategory categoryNode
|
||||
class DifficultyLevel difficultyNode
|
||||
```
|
||||
|
||||
**节点类型说明**:
|
||||
|
||||
- **🍽️ Recipe (菜谱节点)**: 核心实体,包含菜谱的完整信息
|
||||
- **🥬 Ingredient (食材节点)**: 制作菜谱所需的食材信息
|
||||
- **👨🍳 CookingStep (烹饪步骤节点)**: 详细的制作步骤和方法
|
||||
- **📂 RecipeCategory (菜谱分类节点)**: 菜品分类(素菜、荤菜、水产等)
|
||||
- **⭐ DifficultyLevel (难度等级节点)**: 制作难度分级(一星到五星)
|
||||
|
||||
### 2.3 实际关系模型
|
||||
|
||||
基于实际数据的关系结构:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
%% 定义节点样式
|
||||
classDef recipeNode fill:#e1f5fe,stroke:#01579b,stroke-width:3px
|
||||
classDef ingredientNode fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
|
||||
classDef stepNode fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
|
||||
classDef categoryNode fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef difficultyNode fill:#fce4ec,stroke:#880e4f,stroke-width:2px
|
||||
classDef rootNode fill:#f5f5f5,stroke:#424242,stroke-width:2px
|
||||
classDef methodNode fill:#e3f2fd,stroke:#0277bd,stroke-width:2px
|
||||
classDef toolNode fill:#f1f8e9,stroke:#33691e,stroke-width:2px
|
||||
|
||||
%% 核心节点
|
||||
Recipe["🍽️ Recipe<br/>菜谱"]
|
||||
Ingredient["🥬 Ingredient<br/>食材"]
|
||||
CookingStep["👨🍳 CookingStep<br/>烹饪步骤"]
|
||||
RecipeCategory["📂 RecipeCategory<br/>菜谱分类"]
|
||||
DifficultyLevel["⭐ DifficultyLevel<br/>难度等级"]
|
||||
|
||||
%% 层次化节点
|
||||
Root["🌳 Root<br/>根节点"]
|
||||
CookingMethod["🔥 CookingMethod<br/>烹饪方法"]
|
||||
CookingTool["🔧 CookingTool<br/>烹饪工具"]
|
||||
|
||||
%% 主要关系 - 带属性标注
|
||||
Recipe -.->|"REQUIRES<br/>relationshipId: String<br/>amount: String<br/>unit: String<br/><br/>示例: 300g, 2个"| Ingredient
|
||||
Recipe -.->|"CONTAINS_STEP<br/>relationshipId: String<br/>step_order: Float<br/><br/>示例: 1.0, 2.0"| CookingStep
|
||||
Recipe -->|"BELONGS_TO_CATEGORY<br/>菜谱分类关系"| RecipeCategory
|
||||
Recipe -->|"HAS_DIFFICULTY_LEVEL<br/>难度等级关系"| DifficultyLevel
|
||||
|
||||
%% 层次化关系
|
||||
Root -->|"IS_A<br/>概念层次"| Recipe
|
||||
Root -->|"IS_A<br/>概念层次"| Ingredient
|
||||
Root -->|"IS_A<br/>概念层次"| CookingMethod
|
||||
Root -->|"IS_A<br/>概念层次"| CookingTool
|
||||
|
||||
%% 应用样式
|
||||
class Recipe recipeNode
|
||||
class Ingredient ingredientNode
|
||||
class CookingStep stepNode
|
||||
class RecipeCategory categoryNode
|
||||
class DifficultyLevel difficultyNode
|
||||
class Root rootNode
|
||||
class CookingMethod methodNode
|
||||
class CookingTool toolNode
|
||||
```
|
||||
|
||||
**关系类型说明**:
|
||||
|
||||
| 关系编码 | 关系类型 | 说明 | 属性 |
|
||||
|---------|---------|------|------|
|
||||
| **801000001** | REQUIRES | 菜谱-食材关系 | relationshipId, amount, unit |
|
||||
| **801000003** | CONTAINS_STEP | 菜谱-步骤关系 | relationshipId, step_order |
|
||||
| **801000004** | HAS_DIFFICULTY_LEVEL | 菜谱-难度关系 | relationshipId |
|
||||
| **801000005** | BELONGS_TO_CATEGORY | 菜谱-分类关系 | relationshipId |
|
||||
|
||||
**关系特点**:
|
||||
- **虚线箭头**:表示带有丰富属性的关系(如REQUIRES、CONTAINS_STEP)
|
||||
- **实线箭头**:表示简单的分类关系
|
||||
- **层次化结构**:Root节点作为概念层次的顶层节点
|
||||
|
||||
## 三、Neo4j数据导入
|
||||
|
||||
### 3.1 数据准备脚本
|
||||
|
||||
系统通过 `GraphDataPreparationModule` 来处理图数据的加载和管理:
|
||||
|
||||
```python
|
||||
class GraphDataPreparationModule:
|
||||
def __init__(self, neo4j_config: dict):
|
||||
"""
|
||||
初始化图数据准备模块
|
||||
|
||||
Args:
|
||||
neo4j_config: Neo4j连接配置
|
||||
"""
|
||||
self.driver = GraphDatabase.driver(
|
||||
neo4j_config['uri'],
|
||||
auth=(neo4j_config['user'], neo4j_config['password'])
|
||||
)
|
||||
|
||||
def load_graph_data(self) -> List[Dict]:
|
||||
"""
|
||||
从Neo4j加载图数据
|
||||
|
||||
Returns:
|
||||
包含菜谱信息的字典列表
|
||||
"""
|
||||
query = """
|
||||
MATCH (r:Recipe)
|
||||
OPTIONAL MATCH (r)-[:REQUIRES]->(i:Ingredient)
|
||||
OPTIONAL MATCH (r)-[:HAS_STEP]->(s:Step)
|
||||
OPTIONAL MATCH (r)-[:BELONGS_TO]->(c:Category)
|
||||
RETURN r, collect(DISTINCT i) as ingredients,
|
||||
collect(DISTINCT s) as steps,
|
||||
collect(DISTINCT c) as categories
|
||||
ORDER BY r.name
|
||||
"""
|
||||
|
||||
with self.driver.session() as session:
|
||||
result = session.run(query)
|
||||
return [record for record in result]
|
||||
```
|
||||
|
||||
### 3.2 实际CSV数据格式
|
||||
|
||||
转换后的CSV文件格式(基于实际数据):
|
||||
|
||||
**nodes.csv结构**:
|
||||
```csv
|
||||
nodeId,labels,name,preferredTerm,fsn,conceptType,synonyms,category,difficulty,cuisineType,prepTime,cookTime,servings,tags,filePath,amount,unit,isMain,description,stepNumber,methods,tools,timeEstimate
|
||||
```
|
||||
|
||||
**实际数据示例**:
|
||||
```csv
|
||||
201000184,Recipe,干煎阿根廷红虾,干煎阿根廷红虾,,Recipe,"[{'term': '干pan-fried阿根廷红虾', 'language': 'zh'}]",水产,3.0,,提前1天冷藏解冻+10分钟,约5分钟,1人,"趁热吃,柠檬可增酸提味",dishes\aquatic\干煎阿根廷红虾\干煎阿根廷红虾.md,,,,,,,,
|
||||
201000185,Ingredient,阿根廷红虾,阿根廷红虾,,Ingredient,,蛋白质,,,,,,,,2-3,只,True,,,,,
|
||||
201000196,CookingStep,步骤1,步骤1,,CookingStep,,,,,,,,,,,,,阿根廷红虾提前1天从速冻取出放到冷藏里自然解冻,1.0,解冻,冰箱,24小时
|
||||
```
|
||||
|
||||
**relationships.csv结构**:
|
||||
```csv
|
||||
startNodeId,endNodeId,relationshipType,relationshipId,amount,unit,step_order
|
||||
```
|
||||
|
||||
**实际关系示例**:
|
||||
```csv
|
||||
201000184,201000185,801000001,R_000001,2-3,只,
|
||||
201000184,201000196,801000003,R_000010,,,1.0
|
||||
201000184,720000000,801000002,R_000020,,,
|
||||
```
|
||||
|
||||
## 四、图数据查询与检索
|
||||
|
||||
### 4.1 基础查询模式
|
||||
|
||||
#### 简单实体查询
|
||||
```cypher
|
||||
// 查找所有水产类菜谱
|
||||
MATCH (r:Recipe)
|
||||
WHERE r.category = "水产"
|
||||
RETURN r.name, r.difficulty, r.prepTime, r.cookTime
|
||||
|
||||
// 查找包含特定食材的菜谱
|
||||
MATCH (r:Recipe)-[:REQUIRES]->(i:Ingredient)
|
||||
WHERE i.name CONTAINS "虾"
|
||||
RETURN r.name, r.difficulty, i.name, i.amount, i.unit
|
||||
|
||||
// 使用全文搜索查找菜谱
|
||||
CALL db.index.fulltext.queryNodes("recipe_fulltext_index", "川菜 OR 辣椒")
|
||||
YIELD node, score
|
||||
RETURN node.name, node.category, score
|
||||
ORDER BY score DESC
|
||||
```
|
||||
|
||||
#### 多跳关系查询
|
||||
```cypher
|
||||
// 查找某个难度等级的所有菜谱(基于属性查询)
|
||||
MATCH (r:Recipe)
|
||||
WHERE r.difficulty = 3.0
|
||||
RETURN r.name, r.category, r.prepTime, r.cookTime, r.difficulty
|
||||
|
||||
// 查找菜谱的完整制作流程
|
||||
MATCH (r:Recipe {name: "干煎阿根廷红虾"})-[:CONTAINS_STEP]->(s:CookingStep)
|
||||
RETURN r.name, s.stepNumber, s.description, s.methods, s.tools
|
||||
ORDER BY s.stepNumber
|
||||
```
|
||||
|
||||
### 4.2 复杂推理查询
|
||||
|
||||
#### 基于约束的菜谱推荐
|
||||
```cypher
|
||||
// 查找适合新手的简单菜谱(低难度、步骤少)
|
||||
MATCH (r:Recipe)
|
||||
WHERE r.difficulty <= 2.0
|
||||
AND r.stepCount <= 5
|
||||
RETURN r.name, r.difficulty, r.stepCount, r.category
|
||||
ORDER BY r.difficulty, r.stepCount
|
||||
|
||||
// 查找制作时间短的菜谱
|
||||
MATCH (r:Recipe)
|
||||
WHERE r.prepTime IS NOT NULL AND r.cookTime IS NOT NULL
|
||||
AND r.prepTime CONTAINS "分钟" AND r.cookTime CONTAINS "分钟"
|
||||
RETURN r.name, r.prepTime, r.cookTime, r.category
|
||||
ORDER BY r.name
|
||||
```
|
||||
|
||||
#### 菜谱组合推荐
|
||||
```cypher
|
||||
// 查找同一分类下的不同菜谱
|
||||
MATCH (r1:Recipe), (r2:Recipe)
|
||||
WHERE r1.category = r2.category
|
||||
AND r1.category = "水产"
|
||||
AND r1.nodeId <> r2.nodeId
|
||||
RETURN r1.name, r2.name, r1.category
|
||||
LIMIT 5
|
||||
|
||||
// 查找包含相同食材的不同菜谱
|
||||
MATCH (r1:Recipe)-[:REQUIRES]->(i:Ingredient)<-[:REQUIRES]-(r2:Recipe)
|
||||
WHERE r1.nodeId <> r2.nodeId
|
||||
AND i.name = "阿根廷红虾"
|
||||
RETURN r1.name, r2.name, i.name
|
||||
```
|
||||
|
||||
## 五、图数据到文档的转换
|
||||
|
||||
### 5.1 结构化文档构建
|
||||
|
||||
```python
|
||||
def build_recipe_documents(self, graph_data: List[Dict]) -> List[Document]:
|
||||
"""将图数据转换为结构化文档"""
|
||||
|
||||
documents = []
|
||||
for record in graph_data:
|
||||
recipe = record['r']
|
||||
ingredients = record['ingredients']
|
||||
steps = record['steps']
|
||||
categories = record['categories']
|
||||
|
||||
# 构建结构化文档内容
|
||||
content_parts = [
|
||||
f"# {recipe['name']}",
|
||||
f"分类: {', '.join([c['name'] for c in categories])}",
|
||||
f"难度: {recipe['difficulty']}星",
|
||||
# ... 时间、份量等基本信息
|
||||
"",
|
||||
"## 所需食材"
|
||||
]
|
||||
|
||||
# 添加食材列表
|
||||
for i, ingredient in enumerate(ingredients, 1):
|
||||
content_parts.append(f"{i}. {ingredient['name']}")
|
||||
|
||||
content_parts.extend(["", "## 制作步骤"])
|
||||
|
||||
# 添加制作步骤(按顺序排序)
|
||||
sorted_steps = sorted(steps, key=lambda x: x.get('order', 0))
|
||||
for step in sorted_steps:
|
||||
content_parts.extend([
|
||||
f"### 第{step['order']}步",
|
||||
step['description'],
|
||||
""
|
||||
])
|
||||
|
||||
# 创建Document对象
|
||||
document = Document(
|
||||
page_content="\n".join(content_parts),
|
||||
metadata={
|
||||
'recipe_name': recipe['name'],
|
||||
'node_id': recipe.get('nodeId'), # 关键:保持与图节点的关联
|
||||
'difficulty': recipe.get('difficulty', 0),
|
||||
'categories': [c['name'] for c in categories],
|
||||
'ingredients': [i['name'] for i in ingredients]
|
||||
# ... 其他元数据
|
||||
}
|
||||
)
|
||||
documents.append(document)
|
||||
|
||||
return documents
|
||||
```
|
||||
|
||||
> **为什么不直接读取原始Markdown文件?**
|
||||
>
|
||||
> 虽然第八章中HowToCook项目的Markdown格式是统一的,但图RAG的价值在于提供更丰富的信息:
|
||||
>
|
||||
> **原始Markdown的特点**:
|
||||
> - **格式统一**:HowToCook项目有良好的Markdown结构(`#`、`##`、`###`层级)
|
||||
> - **信息完整**:包含菜品名称、原料、制作步骤等基本信息
|
||||
> - **元数据推断**:可以从文件路径推断分类,从`★★★★★`符号推断难度
|
||||
>
|
||||
> **图数据构建文档的额外价值**:
|
||||
> 1. **关系信息丰富**:包含食材间的替代关系、菜谱间的相似性等图关系
|
||||
> 2. **结构化查询**:可以通过图关系快速获取相关信息(如"包含鸡肉的所有菜谱")
|
||||
> 3. **动态内容生成**:根据图关系动态生成推荐内容(如"相似菜谱"、"替代食材")
|
||||
> 4. **语义增强**:图数据库可以存储更丰富的语义信息和计算结果
|
||||
> 5. **查询优化**:图查询在复杂关系检索上比文本搜索更高效
|
||||
|
||||
### 5.2 图RAG中的分块策略
|
||||
|
||||
在图RAG系统中,分块策略与上个项目有所不同,主要体现在**数据来源和上下文获取方式**的差异:
|
||||
|
||||
**图RAG vs 传统RAG的分块对比**:
|
||||
|
||||
| 特性 | 第八章 传统RAG | 第九章 图RAG |
|
||||
|------|-----------------|----------------|
|
||||
| **数据来源** | 直接读取Markdown文件 | 从图数据库构建文档 |
|
||||
| **上下文获取** | 父子文档映射 | 图关系遍历 |
|
||||
| **关系信息** | 有限(仅父子关系) | 丰富(多种图关系) |
|
||||
| **分块策略** | 按Markdown标题分块 | 按语义+长度智能分块 |
|
||||
| **元数据来源** | 文件路径+内容推断 | 图节点结构化数据 |
|
||||
|
||||
**图RAG分块的特点**:
|
||||
1. **保持图关联**:每个chunk通过`parent_id`与图节点关联
|
||||
2. **语义优先分块**:优先按章节分块,保持语义完整性
|
||||
3. **丰富的元数据**:直接从图节点获取结构化信息
|
||||
4. **双重上下文**:既有文本块关系,又有图关系信息
|
||||
|
||||
### 5.3 实际分块实现
|
||||
|
||||
在图RAG系统中,采用的实际分块策略:
|
||||
|
||||
```python
|
||||
def chunk_documents(self, chunk_size: int = 500, chunk_overlap: int = 50) -> List[Document]:
|
||||
"""图RAG文档分块:结合图结构优势的智能分块策略"""
|
||||
|
||||
chunks = []
|
||||
for doc in self.documents:
|
||||
content = doc.page_content
|
||||
|
||||
if len(content) <= chunk_size:
|
||||
# 短文档:保持完整,避免破坏语义
|
||||
chunk = Document(
|
||||
page_content=content,
|
||||
metadata={
|
||||
**doc.metadata,
|
||||
"parent_id": doc.metadata["node_id"], # 关键:保持与图节点的关联
|
||||
"chunk_index": 0,
|
||||
"doc_type": "chunk"
|
||||
}
|
||||
)
|
||||
chunks.append(chunk)
|
||||
else:
|
||||
# 长文档:智能分块策略
|
||||
sections = content.split('\n## ')
|
||||
|
||||
if len(sections) <= 1:
|
||||
# 无章节结构:按长度分块(带重叠)
|
||||
total_chunks = (len(content) - 1) // (chunk_size - chunk_overlap) + 1
|
||||
for i in range(total_chunks):
|
||||
start = i * (chunk_size - chunk_overlap)
|
||||
end = min(start + chunk_size, len(content))
|
||||
# ... 创建chunk,保持parent_id关联
|
||||
else:
|
||||
# 有章节结构:按语义分块(推荐)
|
||||
for i, section in enumerate(sections):
|
||||
chunk_content = section if i == 0 else f"## {section}"
|
||||
# ... 创建chunk,包含section_title信息
|
||||
|
||||
return chunks
|
||||
```
|
||||
|
||||
图RAG的分块策略在保持语义完整性的基础上,充分利用图数据库的结构化优势。与第八章直接读取Markdown文件不同,这里从图数据库构建标准化文档,每个chunk通过`parent_id`与原始Recipe节点保持关联,既继承了传统的父子文档映射关系,又能通过图关系遍历获取更丰富的上下文信息。在具体实现上,采用智能分块策略:短文档保持完整避免破坏语义,长文档优先按`##`标题进行章节分块,必要时才进行长度分块,同时为每个chunk提供丰富的元数据(如chunk_id、chunk_index、total_chunks等),确保后续处理的灵活性和可追溯性。
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
# 第三节 Milvus索引构建
|
||||
|
||||
在图RAG系统中,索引构建是连接图数据和向量检索的关键环节。本节介绍如何将图数据转换为可检索的向量索引。
|
||||
|
||||
在第三章中,我们已经详细介绍了Milvus的基本概念、部署方式和基础操作。本节将在此基础上,专门针对图RAG场景进行深度应用。如果你对Milvus还不熟悉,建议先阅读[Milvus介绍及多模态检索实践](https://github.com/datawhalechina/all-in-rag/blob/main/docs/chapter3/09_milvus.md)。
|
||||
|
||||
> [本节完整代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C9/rag_modules/milvus_index_construction.py)
|
||||
|
||||
## 一、索引构建概述
|
||||
|
||||
### 1.1 索引构建流程
|
||||
|
||||
图RAG的索引构建需要将从图数据库构建的结构化文档转换为向量表示,并存储到向量数据库中:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[图数据库] --> B[文档构建]
|
||||
B --> C[文档分块]
|
||||
C --> D[向量化]
|
||||
D --> E[Milvus索引]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style E fill:#e8f5e8
|
||||
```
|
||||
|
||||
### 1.2 核心组件
|
||||
|
||||
- **文档构建器**:从图数据构建结构化文档
|
||||
- **分块处理器**:智能分块策略
|
||||
- **向量化模型**:文本转向量
|
||||
- **Milvus索引**:高性能向量存储和检索
|
||||
|
||||
## 二、Milvus索引构建实现
|
||||
|
||||
### 2.1 索引构建器核心架构
|
||||
|
||||
```python
|
||||
class MilvusIndexConstructionModule:
|
||||
"""Milvus索引构建模块 - 负责向量化和Milvus索引构建"""
|
||||
|
||||
def __init__(self,
|
||||
host: str = "localhost",
|
||||
port: int = 19530,
|
||||
collection_name: str = "cooking_knowledge",
|
||||
dimension: int = 512,
|
||||
model_name: str = "BAAI/bge-small-zh-v1.5"):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.collection_name = collection_name
|
||||
self.dimension = dimension
|
||||
self.model_name = model_name
|
||||
|
||||
self.client = None
|
||||
self.embeddings = None
|
||||
self.collection_created = False
|
||||
|
||||
self._setup_client()
|
||||
self._setup_embeddings()
|
||||
```
|
||||
|
||||
**代码解读**:
|
||||
- **模块化设计**:将Milvus操作封装为独立模块,便于复用和维护
|
||||
- **配置灵活性**:支持自定义Milvus连接参数和嵌入模型
|
||||
- **中文优化**:默认使用`BAAI/bge-small-zh-v1.5`,专门针对中文文本优化
|
||||
- **延迟初始化**:在构造函数中设置连接,避免启动时的阻塞
|
||||
|
||||
### 2.2 向量化处理
|
||||
|
||||
```python
|
||||
def _vectorize_documents(self, documents: List[Document]) -> Tuple[List[List[float]], List[Dict]]:
|
||||
"""文档向量化处理"""
|
||||
vectors = []
|
||||
metadatas = []
|
||||
|
||||
for i, doc in enumerate(documents):
|
||||
try:
|
||||
# 向量化文档内容
|
||||
vector = self.embedding_model.embed_query(doc.page_content)
|
||||
vectors.append(vector)
|
||||
|
||||
# 准备元数据
|
||||
metadata = {
|
||||
"id": i,
|
||||
"content": doc.page_content,
|
||||
"source": doc.metadata.get("source", ""),
|
||||
"chunk_id": doc.metadata.get("chunk_id", ""),
|
||||
"parent_id": doc.metadata.get("parent_id", ""),
|
||||
# ... 其他元数据
|
||||
}
|
||||
metadatas.append(metadata)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文档 {i} 向量化失败: {e}")
|
||||
continue
|
||||
|
||||
return vectors, metadatas
|
||||
```
|
||||
|
||||
### 2.3 图RAG专用集合Schema设计
|
||||
|
||||
```python
|
||||
def _create_collection_schema(self):
|
||||
"""创建集合schema"""
|
||||
fields = [
|
||||
FieldSchema(name="id", dtype=DataType.VARCHAR, max_length=150, is_primary=True),
|
||||
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=self.dimension),
|
||||
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=15000),
|
||||
FieldSchema(name="node_id", dtype=DataType.VARCHAR, max_length=100),
|
||||
FieldSchema(name="recipe_name", dtype=DataType.VARCHAR, max_length=300),
|
||||
FieldSchema(name="node_type", dtype=DataType.VARCHAR, max_length=100),
|
||||
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=100),
|
||||
FieldSchema(name="cuisine_type", dtype=DataType.VARCHAR, max_length=200),
|
||||
FieldSchema(name="difficulty", dtype=DataType.INT64),
|
||||
FieldSchema(name="doc_type", dtype=DataType.VARCHAR, max_length=50),
|
||||
FieldSchema(name="chunk_id", dtype=DataType.VARCHAR, max_length=150),
|
||||
FieldSchema(name="parent_id", dtype=DataType.VARCHAR, max_length=100)
|
||||
]
|
||||
|
||||
schema = CollectionSchema(
|
||||
fields=fields,
|
||||
description="中式烹饪知识图谱向量集合"
|
||||
)
|
||||
return schema
|
||||
```
|
||||
|
||||
**Schema设计亮点**:
|
||||
- **图数据特化**:专门为烹饪知识图谱设计的字段结构
|
||||
- **丰富元数据**:包含菜谱名称、节点类型、菜系、难度等图谱特有信息
|
||||
- **长度优化**:根据实际数据特点设置合理的字段长度限制
|
||||
- **检索友好**:所有关键字段都可用于过滤和检索条件
|
||||
|
||||
## 三、索引优化策略
|
||||
|
||||
### 3.1 批量插入优化
|
||||
|
||||
```python
|
||||
def _batch_insert(self, vectors: List[List[float]], metadatas: List[Dict]):
|
||||
"""批量插入优化"""
|
||||
batch_size = self.config.batch_size
|
||||
collection_name = self.config.milvus_collection_name
|
||||
|
||||
for i in range(0, len(vectors), batch_size):
|
||||
batch_vectors = vectors[i:i + batch_size]
|
||||
batch_metadatas = metadatas[i:i + batch_size]
|
||||
|
||||
# 准备插入数据
|
||||
insert_data = [
|
||||
[meta["id"] for meta in batch_metadatas], # id
|
||||
batch_vectors, # vector
|
||||
[meta["content"] for meta in batch_metadatas], # content
|
||||
[meta["source"] for meta in batch_metadatas], # source
|
||||
[meta["chunk_id"] for meta in batch_metadatas], # chunk_id
|
||||
[meta["parent_id"] for meta in batch_metadatas], # parent_id
|
||||
]
|
||||
|
||||
# 执行插入
|
||||
self.milvus_client.insert(collection_name, insert_data)
|
||||
logger.info(f"批次 {i//batch_size + 1} 插入完成,数量: {len(batch_vectors)}")
|
||||
```
|
||||
|
||||
### 3.2 索引创建
|
||||
|
||||
```python
|
||||
def _create_index(self):
|
||||
"""创建向量索引"""
|
||||
collection_name = self.config.milvus_collection_name
|
||||
|
||||
# 索引参数
|
||||
index_params = {
|
||||
"metric_type": "COSINE", # 余弦相似度
|
||||
"index_type": "IVF_FLAT", # 索引类型
|
||||
"params": {"nlist": 1024} # 索引参数
|
||||
}
|
||||
|
||||
# 创建索引
|
||||
self.milvus_client.create_index(
|
||||
collection_name=collection_name,
|
||||
field_name="vector",
|
||||
index_params=index_params
|
||||
)
|
||||
|
||||
# 加载集合到内存
|
||||
self.milvus_client.load_collection(collection_name)
|
||||
|
||||
logger.info("向量索引创建完成")
|
||||
```
|
||||
|
||||
## 四、索引构建流程
|
||||
|
||||
### 4.1 核心向量构建流程
|
||||
|
||||
```python
|
||||
def build_vector_index(self, chunks: List[Document]) -> bool:
|
||||
"""构建向量索引"""
|
||||
logger.info(f"正在构建Milvus向量索引,文档数量: {len(chunks)}...")
|
||||
|
||||
try:
|
||||
# 1. 创建集合(如果schema不兼容则强制重新创建)
|
||||
if not self.create_collection(force_recreate=True):
|
||||
return False
|
||||
|
||||
# 2. 准备数据
|
||||
logger.info("正在生成向量embeddings...")
|
||||
texts = [chunk.page_content for chunk in chunks]
|
||||
vectors = self.embeddings.embed_documents(texts)
|
||||
|
||||
# 3. 准备插入数据
|
||||
entities = []
|
||||
for i, (chunk, vector) in enumerate(zip(chunks, vectors)):
|
||||
entity = {
|
||||
"id": self._safe_truncate(chunk.metadata.get("chunk_id", f"chunk_{i}"), 150),
|
||||
"vector": vector,
|
||||
"text": self._safe_truncate(chunk.page_content, 15000),
|
||||
"node_id": self._safe_truncate(chunk.metadata.get("node_id", ""), 100),
|
||||
"recipe_name": self._safe_truncate(chunk.metadata.get("recipe_name", ""), 300),
|
||||
# ... 更多字段
|
||||
}
|
||||
entities.append(entity)
|
||||
|
||||
# 4. 批量插入数据
|
||||
batch_size = 100
|
||||
for i in range(0, len(entities), batch_size):
|
||||
batch = entities[i:i + batch_size]
|
||||
self.client.insert(collection_name=self.collection_name, data=batch)
|
||||
```
|
||||
|
||||
**关键技术点解读**:
|
||||
|
||||
1. **强制重建策略**:`force_recreate=True`确保Schema一致性,避免字段不匹配错误
|
||||
|
||||
2. **批量向量化**:一次性处理所有文档的向量化,提高效率
|
||||
```python
|
||||
texts = [chunk.page_content for chunk in chunks]
|
||||
vectors = self.embeddings.embed_documents(texts) # 批量处理
|
||||
```
|
||||
|
||||
3. **安全截断机制**:`_safe_truncate`方法防止字段长度超限
|
||||
```python
|
||||
def _safe_truncate(self, text: str, max_length: int) -> str:
|
||||
if text is None:
|
||||
return ""
|
||||
return str(text)[:max_length]
|
||||
```
|
||||
|
||||
4. **图数据元数据保留**:完整保留图谱中的结构化信息,支持后续的复合检索
|
||||
|
||||
### 4.2 索引验证
|
||||
|
||||
```python
|
||||
def verify_index(self) -> bool:
|
||||
"""验证索引构建结果"""
|
||||
try:
|
||||
collection_name = self.config.milvus_collection_name
|
||||
|
||||
# 检查集合状态
|
||||
collection_info = self.milvus_client.describe_collection(collection_name)
|
||||
logger.info(f"集合信息: {collection_info}")
|
||||
|
||||
# 检查数据量
|
||||
count = self.milvus_client.query(
|
||||
collection_name=collection_name,
|
||||
expr="",
|
||||
output_fields=["count(*)"]
|
||||
)
|
||||
logger.info(f"索引中文档数量: {count}")
|
||||
|
||||
# 简单检索测试
|
||||
test_results = self.milvus_client.search(
|
||||
collection_name=collection_name,
|
||||
data=[[0.1] * self.config.embedding_dim], # 测试向量
|
||||
anns_field="vector",
|
||||
param={"metric_type": "COSINE", "params": {"nprobe": 10}},
|
||||
limit=1
|
||||
)
|
||||
|
||||
logger.info("索引验证通过")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"索引验证失败: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
## 五、为什么从FAISS切换到Milvus?
|
||||
|
||||
在第八章中,使用的是FAISS作为向量存储方案。虽然FAISS在研究和原型开发中表现出色,但在生产环境和复杂应用场景下,Milvus提供了更多优势:
|
||||
|
||||
**FAISS的局限性**:
|
||||
- **纯库模式**:FAISS是一个向量搜索库,缺乏数据库的完整功能
|
||||
- **无持久化**:需要手动管理数据持久化和备份
|
||||
- **单机限制**:难以实现分布式部署和水平扩展
|
||||
- **元数据支持有限**:无法高效存储和查询复杂的结构化元数据
|
||||
- **并发性能**:在高并发场景下性能受限
|
||||
|
||||
**Milvus的优势**:
|
||||
- **完整数据库功能**:提供CRUD操作、事务支持、数据一致性保证
|
||||
- **云原生架构**:支持分布式部署、自动扩缩容、高可用性
|
||||
- **丰富的元数据支持**:支持复杂Schema设计,适合图RAG的多维度数据
|
||||
- **生产级特性**:监控、日志、备份恢复等企业级功能
|
||||
@@ -0,0 +1,437 @@
|
||||
# 第四节 智能查询路由与检索策略
|
||||
|
||||
> 不同类型的查询需要不同的检索策略。本节将详细介绍如何构建智能查询路由器,实现查询复杂度分析和检索策略的自动选择,以及三种核心检索策略的设计与实现。
|
||||
|
||||
## 一、智能查询路由器设计
|
||||
|
||||
### 1.1 查询路由的必要性
|
||||
|
||||
在图RAG系统中,可以实现更多样化的查询类型:
|
||||
|
||||
**简单查询**:
|
||||
- "川菜有哪些?"
|
||||
- "宫保鸡丁怎么做?"
|
||||
- "减肥菜推荐"
|
||||
|
||||
**复杂推理查询**:
|
||||
- "适合糖尿病人吃的低糖川菜有哪些,并且制作时间不超过30分钟?"
|
||||
- "如果我只有鸡肉和蔬菜,能做什么菜,最好是不同菜系的?"
|
||||
- "哪些菜可以用豆腐替代肉类,并且保持相似的口感?"
|
||||
|
||||
**中等复杂查询**:
|
||||
- "家常菜中哪些适合新手制作?"
|
||||
- "有什么菜可以用剩余的土豆和胡萝卜?"
|
||||
|
||||
不同复杂度的查询需要不同的检索策略来获得最佳效果。
|
||||
|
||||
### 1.2 查询分析框架
|
||||
|
||||
智能查询路由器通过四个维度分析查询特征:
|
||||
|
||||
```python
|
||||
class IntelligentQueryRouter:
|
||||
def __init__(self, traditional_retrieval, graph_rag_retrieval, llm_client, config):
|
||||
self.traditional_retrieval = traditional_retrieval
|
||||
self.graph_rag_retrieval = graph_rag_retrieval
|
||||
self.llm_client = llm_client
|
||||
self.config = config
|
||||
|
||||
# 路由统计
|
||||
self.route_stats = {
|
||||
"traditional_count": 0,
|
||||
"graph_rag_count": 0,
|
||||
"combined_count": 0,
|
||||
"total_queries": 0
|
||||
}
|
||||
|
||||
def analyze_query(self, query: str) -> QueryAnalysis:
|
||||
"""深度分析查询特征,决定最佳检索策略"""
|
||||
|
||||
analysis_prompt = f"""
|
||||
作为RAG系统的查询分析专家,请深度分析以下查询的特征:
|
||||
|
||||
查询:{query}
|
||||
|
||||
请从以下维度分析:
|
||||
|
||||
1. 查询复杂度 (0-1):
|
||||
- 0.0-0.3: 简单信息查找(如:红烧肉怎么做?)
|
||||
- 0.4-0.7: 中等复杂度(如:川菜有哪些特色菜?)
|
||||
- 0.8-1.0: 高复杂度推理(如:为什么川菜用花椒而不是胡椒?)
|
||||
|
||||
2. 关系密集度 (0-1):
|
||||
- 0.0-0.3: 单一实体信息(如:西红柿的营养价值)
|
||||
- 0.4-0.7: 实体间关系(如:鸡肉配什么蔬菜?)
|
||||
- 0.8-1.0: 复杂关系网络(如:川菜的形成与地理、历史的关系)
|
||||
|
||||
3. 推理需求:是否需要多跳推理、因果分析、对比分析?
|
||||
4. 实体识别:查询中包含多少个明确实体?
|
||||
|
||||
基于分析推荐检索策略:
|
||||
- hybrid_traditional: 适合简单直接的信息查找
|
||||
- graph_rag: 适合复杂关系推理和知识发现
|
||||
- combined: 需要两种策略结合
|
||||
|
||||
返回JSON格式:
|
||||
{{
|
||||
"query_complexity": 0.6,
|
||||
"relationship_intensity": 0.8,
|
||||
"reasoning_required": true,
|
||||
"entity_count": 3,
|
||||
"recommended_strategy": "graph_rag",
|
||||
"confidence": 0.85,
|
||||
"reasoning": "该查询涉及多个实体间的复杂关系,需要图结构推理"
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.llm_client.chat.completions.create(
|
||||
model=self.config.llm_model,
|
||||
messages=[{"role": "user", "content": analysis_prompt}],
|
||||
temperature=0.1,
|
||||
max_tokens=800
|
||||
)
|
||||
|
||||
result = json.loads(response.choices[0].message.content.strip())
|
||||
|
||||
# 构建QueryAnalysis对象
|
||||
analysis = QueryAnalysis(
|
||||
query_complexity=result.get("query_complexity", 0.5),
|
||||
relationship_intensity=result.get("relationship_intensity", 0.5),
|
||||
reasoning_required=result.get("reasoning_required", False),
|
||||
entity_count=result.get("entity_count", 1),
|
||||
recommended_strategy=SearchStrategy(result.get("recommended_strategy", "hybrid_traditional")),
|
||||
confidence=result.get("confidence", 0.5),
|
||||
reasoning=result.get("reasoning", "默认分析")
|
||||
)
|
||||
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询分析失败: {e}")
|
||||
# 降级方案:基于规则的简单分析
|
||||
return self._rule_based_analysis(query)
|
||||
```
|
||||
|
||||
### 1.3 规则基础的降级分析
|
||||
|
||||
当LLM分析失败时,使用基于规则的降级分析:
|
||||
|
||||
```python
|
||||
def _rule_based_analysis(self, query: str) -> QueryAnalysis:
|
||||
"""基于规则的降级分析"""
|
||||
# 简单的规则判断
|
||||
complexity_keywords = ["为什么", "如何", "关系", "影响", "原因", "比较", "区别"]
|
||||
relation_keywords = ["配", "搭配", "组合", "相关", "联系", "连接"]
|
||||
|
||||
complexity = sum(1 for kw in complexity_keywords if kw in query) / len(complexity_keywords)
|
||||
relation_intensity = sum(1 for kw in relation_keywords if kw in query) / len(relation_keywords)
|
||||
|
||||
# 策略选择
|
||||
if complexity > 0.3 or relation_intensity > 0.3:
|
||||
strategy = SearchStrategy.GRAPH_RAG
|
||||
else:
|
||||
strategy = SearchStrategy.HYBRID_TRADITIONAL
|
||||
|
||||
return QueryAnalysis(
|
||||
query_complexity=complexity,
|
||||
relationship_intensity=relation_intensity,
|
||||
reasoning_required=complexity > 0.3,
|
||||
entity_count=len(query.split()), # 简单估算
|
||||
recommended_strategy=strategy,
|
||||
confidence=0.6,
|
||||
reasoning="基于规则的简单分析"
|
||||
)
|
||||
```
|
||||
|
||||
### 1.4 智能路由执行
|
||||
|
||||
基于分析结果,路由到最适合的检索策略:
|
||||
|
||||
```python
|
||||
def route_query(self, query: str, top_k: int = 5) -> Tuple[List[Document], QueryAnalysis]:
|
||||
"""智能路由查询到最适合的检索引擎"""
|
||||
logger.info(f"开始智能路由: {query}")
|
||||
|
||||
# 1. 分析查询特征
|
||||
analysis = self.analyze_query(query)
|
||||
|
||||
# 2. 更新统计
|
||||
self._update_route_stats(analysis.recommended_strategy)
|
||||
|
||||
# 3. 根据策略执行检索
|
||||
try:
|
||||
if analysis.recommended_strategy == SearchStrategy.HYBRID_TRADITIONAL:
|
||||
logger.info("使用传统混合检索")
|
||||
documents = self.traditional_retrieval.hybrid_search(query, top_k)
|
||||
|
||||
elif analysis.recommended_strategy == SearchStrategy.GRAPH_RAG:
|
||||
logger.info("🕸️ 使用图RAG检索")
|
||||
documents = self.graph_rag_retrieval.graph_rag_search(query, top_k)
|
||||
|
||||
elif analysis.recommended_strategy == SearchStrategy.COMBINED:
|
||||
logger.info("🔄 使用组合检索策略")
|
||||
documents = self._combined_search(query, top_k)
|
||||
|
||||
# 4. 结果后处理
|
||||
documents = self._post_process_results(documents, analysis)
|
||||
|
||||
return documents, analysis
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询路由失败: {e}")
|
||||
# 降级到传统检索
|
||||
documents = self.traditional_retrieval.hybrid_search(query, top_k)
|
||||
return documents, analysis
|
||||
|
||||
def _combined_search(self, query: str, top_k: int) -> List[Document]:
|
||||
"""组合搜索策略:结合传统检索和图RAG的优势"""
|
||||
# 分配结果数量
|
||||
traditional_k = max(1, top_k // 2)
|
||||
graph_k = top_k - traditional_k
|
||||
|
||||
# 执行两种检索
|
||||
traditional_docs = self.traditional_retrieval.hybrid_search(query, traditional_k)
|
||||
graph_docs = self.graph_rag_retrieval.graph_rag_search(query, graph_k)
|
||||
|
||||
# 合并和去重(简化实现)
|
||||
# ... 具体的合并逻辑
|
||||
|
||||
return combined_docs
|
||||
```
|
||||
|
||||
## 二、三种检索策略详解
|
||||
|
||||
### 2.1 传统混合检索策略
|
||||
|
||||
> [混合检索模块代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C9/rag_modules/hybrid_retrieval.py)
|
||||
|
||||
适用于简单查询,结合双层检索和向量检索:
|
||||
|
||||
```python
|
||||
class HybridRetrievalModule:
|
||||
def hybrid_search(self, query: str, top_k: int = 5) -> List[Document]:
|
||||
"""
|
||||
混合检索:使用Round-robin轮询合并策略
|
||||
公平轮询合并不同检索结果,不使用权重配置
|
||||
"""
|
||||
logger.info(f"开始混合检索: {query}")
|
||||
|
||||
# 1. 双层检索(实体+主题检索)
|
||||
dual_docs = self.dual_level_retrieval(query, top_k)
|
||||
|
||||
# 2. 增强向量检索
|
||||
vector_docs = self.vector_search_enhanced(query, top_k)
|
||||
|
||||
# 3. Round-robin轮询合并
|
||||
merged_docs = []
|
||||
seen_doc_ids = set()
|
||||
max_len = max(len(dual_docs), len(vector_docs))
|
||||
|
||||
# Round-robin策略:交替从两个结果列表中取文档
|
||||
# 这种方法确保了不同检索方法的结果都能得到公平的展示机会
|
||||
for i in range(max_len):
|
||||
# 先添加双层检索结果
|
||||
if i < len(dual_docs):
|
||||
doc = dual_docs[i]
|
||||
doc_id = doc.metadata.get("node_id", hash(doc.page_content))
|
||||
if doc_id not in seen_doc_ids:
|
||||
seen_doc_ids.add(doc_id)
|
||||
doc.metadata["search_method"] = "dual_level"
|
||||
doc.metadata["final_score"] = doc.metadata.get("relevance_score", 0.0)
|
||||
merged_docs.append(doc)
|
||||
|
||||
# 再添加向量检索结果
|
||||
if i < len(vector_docs):
|
||||
doc = vector_docs[i]
|
||||
doc_id = doc.metadata.get("node_id", hash(doc.page_content))
|
||||
if doc_id not in seen_doc_ids:
|
||||
seen_doc_ids.add(doc_id)
|
||||
doc.metadata["search_method"] = "vector"
|
||||
doc.metadata["final_score"] = doc.metadata.get("relevance_score", 0.0)
|
||||
merged_docs.append(doc)
|
||||
|
||||
return merged_docs[:top_k]
|
||||
```
|
||||
|
||||
**Round-robin轮询合并原理**:Round-robin(轮询)是一种公平调度算法,在RAG系统中用于融合多个检索结果。其核心是按顺序轮流从不同的结果列表中选择文档,而不是基于分数权重进行合并。这种方法确保了每种检索策略的结果都能得到公平的展示机会,避免了某种方法因排序靠前而被过度选择的问题。相比复杂的加权融合,Round-robin实现简单且稳定,无需调优权重参数,自然保持了结果的多样性。
|
||||
|
||||
### 2.2 图RAG检索策略
|
||||
|
||||
> [图RAG检索模块代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C9/rag_modules/graph_rag_retrieval.py)
|
||||
|
||||
适用于复杂推理查询,基于图结构进行多跳推理:
|
||||
|
||||
```python
|
||||
class GraphRAGRetrieval:
|
||||
def graph_rag_search(self, query: str, top_k: int = 5) -> List[Document]:
|
||||
"""
|
||||
图RAG主搜索接口:整合所有图RAG能力
|
||||
"""
|
||||
logger.info(f"开始图RAG检索: {query}")
|
||||
|
||||
# 1. 查询意图理解
|
||||
graph_query = self.understand_graph_query(query)
|
||||
logger.info(f"查询类型: {graph_query.query_type.value}")
|
||||
|
||||
results = []
|
||||
|
||||
try:
|
||||
# 2. 根据查询类型执行不同策略
|
||||
if graph_query.query_type in [QueryType.MULTI_HOP, QueryType.PATH_FINDING]:
|
||||
# 多跳遍历
|
||||
paths = self.multi_hop_traversal(graph_query)
|
||||
results.extend(self._paths_to_documents(paths, query))
|
||||
|
||||
elif graph_query.query_type == QueryType.SUBGRAPH:
|
||||
# 子图提取
|
||||
subgraph = self.extract_knowledge_subgraph(graph_query)
|
||||
|
||||
# 图结构推理
|
||||
reasoning_chains = self.graph_structure_reasoning(subgraph, query)
|
||||
|
||||
results.extend(self._subgraph_to_documents(subgraph, reasoning_chains, query))
|
||||
|
||||
elif graph_query.query_type == QueryType.ENTITY_RELATION:
|
||||
# 实体关系查询
|
||||
paths = self.multi_hop_traversal(graph_query)
|
||||
results.extend(self._paths_to_documents(paths, query))
|
||||
|
||||
# 3. 图结构相关性排序
|
||||
results = self._rank_by_graph_relevance(results, query)
|
||||
|
||||
return results[:top_k]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"图RAG检索失败: {e}")
|
||||
return []
|
||||
```
|
||||
|
||||
**图RAG检索流程**:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[用户查询] --> B[查询意图理解]
|
||||
B --> C{查询类型判断}
|
||||
|
||||
C -->|简单关系| D1[实体关系查询]
|
||||
C -->|复杂推理| D2[多跳推理查询]
|
||||
C -->|知识网络| D3[子图提取查询]
|
||||
|
||||
D1 --> E1[直接关系检索]
|
||||
D2 --> E2[多跳图遍历]
|
||||
D3 --> E3[知识子图提取]
|
||||
|
||||
E1 --> F[结果转换与排序]
|
||||
E2 --> F
|
||||
E3 --> F
|
||||
|
||||
F --> G[返回Top-K结果]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style C fill:#fff3e0
|
||||
style F fill:#f3e5f5
|
||||
style G fill:#e8f5e8
|
||||
```
|
||||
|
||||
**多跳推理**:
|
||||
|
||||
多跳推理是指通过图中的多个节点和关系进行间接推理,这是图RAG相比传统RAG的核心优势。传统检索只能找到直接匹配的信息,而多跳推理能够发现数据中的隐含关联。
|
||||
|
||||
- **工作原理**:
|
||||
1. **路径发现**:在知识图谱中寻找连接起始实体和目标实体的路径
|
||||
2. **关系传递**:通过中间节点传递语义关系
|
||||
3. **隐含推理**:发现原始数据中没有明确表达的知识关联
|
||||
|
||||
- **具体示例**:用户问"鸡肉配什么蔬菜好?"
|
||||
|
||||
```
|
||||
传统检索:只能找到直接提到"鸡肉+蔬菜"的文档(可能很少)
|
||||
|
||||
多跳推理:
|
||||
1跳:鸡肉 → 宫保鸡丁、口水鸡、白切鸡...
|
||||
2跳:宫保鸡丁 → 胡萝卜、青椒、花生米...
|
||||
3跳:胡萝卜 → 蔬菜类别
|
||||
|
||||
推理结果:鸡肉经常与胡萝卜、青椒等蔬菜搭配
|
||||
```
|
||||
|
||||
- **多跳推理的价值**:
|
||||
- **知识发现**:挖掘数据中的隐含关系
|
||||
- **推荐增强**:提供更丰富的搭配建议
|
||||
- **语义理解**:模拟人类的联想思维过程
|
||||
- **数据利用**:充分利用图结构的关系信息
|
||||
|
||||
通过这种多跳遍历,系统能发现"鸡肉"和"胡萝卜"之间的隐含关系:它们经常在同一道菜中出现,即使在原始数据中没有直接的"鸡肉-胡萝卜"关系。
|
||||
|
||||
### 2.3 组合检索策略
|
||||
|
||||
> [智能查询路由器代码](https://github.com/datawhalechina/all-in-rag/blob/main/code/C9/rag_modules/intelligent_query_router.py)
|
||||
|
||||
适用于中等复杂查询,结合传统检索和图RAG的优势:
|
||||
|
||||
```python
|
||||
def _combined_search(self, query: str, top_k: int) -> List[Document]:
|
||||
"""组合搜索策略:结合传统检索和图RAG的优势"""
|
||||
# 分配结果数量
|
||||
traditional_k = max(1, top_k // 2)
|
||||
graph_k = top_k - traditional_k
|
||||
|
||||
# 执行两种检索
|
||||
traditional_docs = self.traditional_retrieval.hybrid_search(query, traditional_k)
|
||||
graph_docs = self.graph_rag_retrieval.graph_rag_search(query, graph_k)
|
||||
|
||||
# Round-robin轮询合并(参考LightRAG的融合策略)
|
||||
combined_docs = []
|
||||
seen_contents = set()
|
||||
|
||||
# 交替添加结果,保持多样性(Round-robin策略)
|
||||
max_len = max(len(traditional_docs), len(graph_docs))
|
||||
for i in range(max_len):
|
||||
# 添加传统检索结果
|
||||
if i < len(traditional_docs):
|
||||
doc = traditional_docs[i]
|
||||
if doc.page_content not in seen_contents:
|
||||
seen_contents.add(doc.page_content)
|
||||
doc.metadata["search_strategy"] = "traditional"
|
||||
combined_docs.append(doc)
|
||||
|
||||
# 添加图RAG结果
|
||||
if i < len(graph_docs):
|
||||
doc = graph_docs[i]
|
||||
if doc.page_content not in seen_contents:
|
||||
seen_contents.add(doc.page_content)
|
||||
doc.metadata["search_strategy"] = "graph_rag"
|
||||
combined_docs.append(doc)
|
||||
|
||||
return combined_docs[:top_k]
|
||||
```
|
||||
|
||||
**Round-robin轮询合并机制**:在组合检索中,Round-robin算法按照固定的轮转顺序从传统检索和图RAG检索的结果中交替选择文档。具体过程是:第1个位置选择传统检索的第1个结果,第2个位置选择图RAG的第1个结果,第3个位置选择传统检索的第2个结果,以此类推。这种机制避免了复杂的分数融合计算,通过位置轮转自然实现了不同检索策略结果的均衡分布,是一种简单而有效的多源信息融合方法。
|
||||
|
||||
## 三、路由决策逻辑
|
||||
|
||||
智能查询路由器通过分析查询特征,自动选择最适合的检索策略:
|
||||
|
||||
**决策规则**:
|
||||
- **简单查询**(复杂度 < 0.4)→ 传统混合检索
|
||||
- **复杂推理查询**(复杂度 > 0.7 或关系密集度 > 0.7)→ 图RAG检索
|
||||
- **中等复杂查询**(0.4 ≤ 复杂度 ≤ 0.7)→ 组合检索策略
|
||||
|
||||
**路由统计与优化**:
|
||||
|
||||
```python
|
||||
def _update_route_stats(self, strategy: SearchStrategy):
|
||||
"""更新路由统计信息"""
|
||||
self.route_stats["total_queries"] += 1
|
||||
if strategy == SearchStrategy.HYBRID_TRADITIONAL:
|
||||
self.route_stats["traditional_count"] += 1
|
||||
elif strategy == SearchStrategy.GRAPH_RAG:
|
||||
self.route_stats["graph_rag_count"] += 1
|
||||
elif strategy == SearchStrategy.COMBINED:
|
||||
self.route_stats["combined_count"] += 1
|
||||
```
|
||||
|
||||
> 最后的生成部分就不过多赘述了,和第八章类似,可以自行查阅代码。本章项目并不完善,仅作为对 GraphRAG 流程和架构的理解。可根据前面所学内容自行优化。
|
||||
>
|
||||
> [What-to-eat-today 给当前项目加个前端并做了点优化,可以参考](https://github.com/FutureUnreal/What-to-eat-today)
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user