一、图RAG系统架构与环境配置
1. 核心业务节点
-
Recipe(菜谱):核心节点,承载菜名、烹饪时长、人均份等基础属性; -
Ingredient(食材):食材节点,记录食材名称、单位、是否常见等; -
CookingStep(烹饪步骤):步骤节点,记录步骤描述、顺序号; -
RecipeCategory(菜谱分类):分类节点,比如按菜系、口味(辣/不辣)划分; -
DifficultyLevel(难度等级):难度节点,如入门、进阶、大师级。
2. 层次化辅助节点
-
Root(根节点):用于统一管理所有节点,相当于图模型的“入口”,方便批量维护; -
CookingMethod(烹饪方法):如炒、炖、蒸、煮等; -
CookingTool(烹饪工具):如炒锅、砂锅、电饭煲等。
2.3 关系设计的核心原则
关系是图模型的灵魂,设计时要遵循“语义清晰、粒度适中”的原则。比如:
-
避免模糊的关系名(不用
RELATE_TO,而是用CONTAINS/HAS_STEP这种精准的语义); -
关系可以带属性(比如
Recipe-CONTAINS->Ingredient的关系里,加amount: 200g属性,记录食材用量)。
2.1 创建虚拟环境
# 使用conda创建环境
conda create -n graph-rag python=3.12.7
conda activate graph-rag
2.2 安装核心依赖
cd code/C9
pip install -r requirements.txt
2.3 Neo4j数据库配置
使用Docker Compose方式安装Neo4j,配置文件位于 data/C9/docker-compose.yml:
2.3.1 启动Neo4j服务
# 进入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
导入的数据包括:
-
菜谱节点:包含菜名、难度、烹饪时间、菜系等信息
-
食材节点:包含食材名称、分类、营养信息等
-
烹饪步骤节点:包含步骤描述、烹饪方法、所需工具等
-
关系网络:菜谱与食材、步骤之间的复杂关系
Milvus向量数据库配置
2.4.1 使用Docker安装Milvus
# 下载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 验证安装
# 检查Milvus服务状态
docker-compose ps
2.5 配置连接参数
在项目根目录创建 .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
二 、图数据建模与Neo4j集成
2.1 数据来源与转换
基于 Markdown 菜谱数据,通过 LLM 提取实体 / 关系,生成nodes.csv/relationships.csv,经 Cypher 脚本导入 Neo4j。
2.2 图数据模型设计
核心实体
Recipe(菜谱)、Ingredient(食材)、CookingStep(烹饪步骤)、RecipeCategory(菜谱分类)、DifficultyLevel(难度等级)(均含唯一 nodeId 及业务属性)。
核心关系
| 关系类型 | 关联实体 | 核心属性 |
|---|---|---|
| REQUIRES | Recipe → Ingredient | amount、unit |
| CONTAINS_STEP | Recipe → CookingStep | step_order |
| BELONGS_TO_CATEGORY | Recipe → RecipeCategory | - |
| HAS_DIFFICULTY_LEVEL | Recipe → DifficultyLevel | - |
结构特点
Root 根节点构建 “根节点 - 实体类型 - 实例节点” 三层结构,支撑多维度检索。
2.3Neo4j 数据导入
- Python 封装
GraphDataPreparationModule实现 Neo4j 连接与数据加载; - 按规范定义 CSV 格式(节点 / 关系含核心属性);
- 批量导入后为高频检索字段建索引。
2.4 图数据查询
- 基础查询:实体精准检索、多属性筛选、多跳关系查询(如菜谱完整流程);
- 复杂推理:低难度 / 短时菜谱、同食材 / 分类菜谱。
2.5 图数据转 RAG 文档
- 结构化构建:提取图数据生成 Markdown 格式
Document,绑定节点元数据; - 分块策略:短文档完整保留,长文档按 “菜谱 - 食材 - 步骤” 语义分块,所有 chunk 通过
parent_id关联图节点; - 兼顾文本语义与图结构,上下文更丰富。
三、Milvus索引构建
在图 RAG 系统中,Milvus 作为核心向量存储方案,其索引构建遵循 “图数据库→结构化文档→智能分块→文本向量化→Milvus 索引落地” 的端到端流程,
通过模块化的MilvusIndexConstructionModule封装连接管理、向量化、索引操作等功能,生成向量,并设计包含node_id、菜谱名称、菜系、难度等图谱特有字段的专属 Schema。
"""
基于图RAG的智能烹饪助手
"""
import os
import sys
import time
import logging
from typing import List, Optional
import streamlit as st
#页面配置
st.set_page_config(
page_title="智能烹饪助手 - 图RAG版",
page_icon="🍳",
layout="wide",
initial_sidebar_state="expanded"
)
# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 添加当前目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from dotenv import load_dotenv
from config import DEFAULT_CONFIG, GraphRAGConfig
from rag_modules import (
GraphDataPreparationModule,
MilvusIndexConstructionModule,
GenerationIntegrationModule
)
from rag_modules.hybrid_retrieval import HybridRetrievalModule
from rag_modules.graph_rag_retrieval import GraphRAGRetrieval
from rag_modules.intelligent_query_router import IntelligentQueryRouter, QueryAnalysis
# 加载环境变量
load_dotenv(".env")
#会话状态初始化
# 用st.session_state保存系统实例和状态,避免每次交互重新初始化
if "rag_system" not in st.session_state:
st.session_state.rag_system = None
if "system_ready" not in st.session_state:
st.session_state.system_ready = False
if "knowledge_base_built" not in st.session_state:
st.session_state.knowledge_base_built = False
#核心类
class AdvancedGraphRAGSystem:
"""
图RAG系统
核心特性:
1. 智能路由:自动选择最适合的检索策略
2. 双引擎检索:传统混合检索 + 图RAG检索
3. 图结构推理:多跳遍历、子图提取、关系推理
4. 查询复杂度分析:深度理解用户意图
5. 自适应学习:基于反馈优化系统性能
"""
def __init__(self, config: Optional[GraphRAGConfig] = None):
self.config = config or DEFAULT_CONFIG
# 核心模块
self.data_module = None
self.index_module = None
self.generation_module = None
# 检索引擎
self.traditional_retrieval = None
self.graph_rag_retrieval = None
self.query_router = None
# 系统状态
self.system_ready = False
def initialize_system(self):
"""初始化高级图RAG系统(适配Streamlit输出)"""
st.info("🚀 启动高级图RAG系统...")
progress_bar = st.progress(0)
progress_steps = ["数据准备模块", "Milvus向量索引", "生成模块", "传统混合检索", "图RAG检索引擎",
"智能查询路由器"]
current_step = 0
try:
# 1. 数据准备模块
current_step += 1
progress_bar.progress(current_step / len(progress_steps))
st.write(f"🔧 初始化{progress_steps[current_step - 1]}...")
self.data_module = GraphDataPreparationModule(
uri=self.config.neo4j_uri,
user=self.config.neo4j_user,
password=self.config.neo4j_password,
database=self.config.neo4j_database
)
# 2. 向量索引模块
current_step += 1
progress_bar.progress(current_step / len(progress_steps))
st.write(f"🔧 初始化{progress_steps[current_step - 1]}...")
self.index_module = MilvusIndexConstructionModule(
host=self.config.milvus_host,
port=self.config.milvus_port,
collection_name=self.config.milvus_collection_name,
dimension=self.config.milvus_dimension,
model_name=self.config.embedding_model
)
# 3. 生成模块
current_step += 1
progress_bar.progress(current_step / len(progress_steps))
st.write(f"🔧 初始化{progress_steps[current_step - 1]}...")
self.generation_module = GenerationIntegrationModule(
model_name=self.config.llm_model,
temperature=self.config.temperature,
max_tokens=self.config.max_tokens
)
# 4. 传统混合检索模块
current_step += 1
progress_bar.progress(current_step / len(progress_steps))
st.write(f"🔧 初始化{progress_steps[current_step - 1]}...")
self.traditional_retrieval = HybridRetrievalModule(
config=self.config,
milvus_module=self.index_module,
data_module=self.data_module,
llm_client=self.generation_module.client
)
# 5. 图RAG检索模块
current_step += 1
progress_bar.progress(current_step / len(progress_steps))
st.write(f"🔧 初始化{progress_steps[current_step - 1]}...")
self.graph_rag_retrieval = GraphRAGRetrieval(
config=self.config,
llm_client=self.generation_module.client
)
# 6. 智能查询路由器
current_step += 1
progress_bar.progress(current_step / len(progress_steps))
st.write(f"🔧 初始化{progress_steps[current_step - 1]}...")
self.query_router = IntelligentQueryRouter(
traditional_retrieval=self.traditional_retrieval,
graph_rag_retrieval=self.graph_rag_retrieval,
llm_client=self.generation_module.client,
config=self.config
)
progress_bar.progress(1.0)
st.success("✅ 高级图RAG系统初始化完成!")
self.system_ready = True
except Exception as e:
logger.error(f"系统初始化失败: {e}")
st.error(f"❌ 系统初始化失败: {str(e)}")
raise
def build_knowledge_base(self):
"""构建知识库(适配Streamlit输出)"""
st.info("\n📚 检查知识库状态...")
try:
# 检查Milvus集合是否存在
if self.index_module.has_collection():
st.success("✅ 发现已存在的知识库,尝试加载...")
if self.index_module.load_collection():
st.success("知识库加载成功!")
# 重要:即使从已存在的知识库加载,也需要加载图数据以支持图索引
st.write("📊 加载图数据以支持图检索...")
self.data_module.load_graph_data()
st.write("📄 构建菜谱文档...")
self.data_module.build_recipe_documents()
st.write("✂️ 进行文档分块...")
chunks = self.data_module.chunk_documents(
chunk_size=self.config.chunk_size,
chunk_overlap=self.config.chunk_overlap
)
self._initialize_retrievers(chunks)
st.session_state.knowledge_base_built = True
return
else:
st.warning("❌ 知识库加载失败,开始重建...")
st.info("未找到已存在的集合,开始构建新的知识库...")
# 从Neo4j加载图数据
st.write("🔗 从Neo4j加载图数据...")
self.data_module.load_graph_data()
# 构建菜谱文档
st.write("📄 构建菜谱文档...")
self.data_module.build_recipe_documents()
# 进行文档分块
st.write("✂️ 进行文档分块...")
chunks = self.data_module.chunk_documents(
chunk_size=self.config.chunk_size,
chunk_overlap=self.config.chunk_overlap
)
# 构建Milvus向量索引
st.write("🧮 构建Milvus向量索引...")
if not self.index_module.build_vector_index(chunks):
raise Exception("构建向量索引失败")
# 初始化检索器
self._initialize_retrievers(chunks)
# 显示统计信息
self._show_knowledge_base_stats()
st.success("✅ 知识库构建完成!")
st.session_state.knowledge_base_built = True
except Exception as e:
logger.error(f"知识库构建失败: {e}")
st.error(f"❌ 知识库构建失败: {str(e)}")
raise
def _initialize_retrievers(self, chunks: List = None):
"""初始化检索器"""
st.write("🔍 初始化检索引擎...")
# 如果没有chunks,从数据模块获取
if chunks is None:
chunks = self.data_module.chunks or []
# 初始化传统检索器
self.traditional_retrieval.initialize(chunks)
# 初始化图RAG检索器
self.graph_rag_retrieval.initialize()
self.system_ready = True
st.success("✅ 检索引擎初始化完成!")
def _show_knowledge_base_stats(self):
"""显示知识库统计信息(Streamlit格式化展示)"""
st.subheader("📈 知识库统计")
# 数据统计
stats = self.data_module.get_statistics()
col1, col2, col3 = st.columns(3)
with col1:
st.metric("菜谱数量", stats.get('total_recipes', 0))
st.metric("食材数量", stats.get('total_ingredients', 0))
with col2:
st.metric("烹饪步骤", stats.get('total_cooking_steps', 0))
st.metric("文档数量", stats.get('total_documents', 0))
with col3:
st.metric("文本块数", stats.get('total_chunks', 0))
# Milvus统计
milvus_stats = self.index_module.get_collection_stats()
st.metric("向量索引记录数", milvus_stats.get('row_count', 0))
# 分类展示
if stats.get('categories'):
categories = list(stats['categories'].keys())[:10]
st.write(f"🏷️ 主要分类: {', '.join(categories)}")
def ask_question_with_routing(self, question: str, explain_routing: bool = False):
"""
智能问答:自动选择最佳检索策略(适配Streamlit流式输出)
"""
if not self.system_ready:
raise ValueError("系统未就绪,请先构建知识库")
st.write(f"\n### ❓ 用户问题: {question}")
# 显示路由决策解释(可选)
if explain_routing:
explanation = self.query_router.explain_routing_decision(question)
st.info(f"📝 路由决策解释: {explanation}")
start_time = time.time()
try:
# 1. 智能路由检索
st.write("🔄 执行智能查询路由...")
relevant_docs, analysis = self.query_router.route_query(question, self.config.top_k)
# 2. 显示路由信息
strategy_icons = {
"hybrid_traditional": "🔍",
"graph_rag": "🕸️",
"combined": "🔄"
}
strategy_icon = strategy_icons.get(analysis.recommended_strategy.value, "❓")
col1, col2, col3 = st.columns(3)
with col1:
st.metric("使用策略", f"{strategy_icon} {analysis.recommended_strategy.value}")
with col2:
st.metric("查询复杂度", f"{analysis.query_complexity:.2f}")
with col3:
st.metric("关系密集度", f"{analysis.relationship_intensity:.2f}")
# 3. 显示检索结果信息
if relevant_docs:
doc_info = []
for doc in relevant_docs:
recipe_name = doc.metadata.get('recipe_name', '未知内容')
search_type = doc.metadata.get('search_type', doc.metadata.get('route_strategy', 'unknown'))
score = doc.metadata.get('final_score', doc.metadata.get('relevance_score', 0))
doc_info.append(f"{recipe_name}({search_type}, {score:.3f})")
st.write(f"📋 找到 {len(relevant_docs)} 个相关文档: {', '.join(doc_info[:3])}")
if len(doc_info) > 3:
st.write(f" 等 {len(relevant_docs)} 个结果...")
else:
st.warning("抱歉,没有找到相关的烹饪信息。请尝试其他问题。")
return "抱歉,没有找到相关的烹饪信息。请尝试其他问题。", analysis
# 4. 生成回答(流式输出)
st.write("🎯 智能生成回答...")
answer_placeholder = st.empty()
full_answer = ""
try:
for chunk_text in self.generation_module.generate_adaptive_answer_stream(question, relevant_docs):
full_answer += chunk_text
answer_placeholder.markdown(f"### 📝 回答:\n{full_answer}")
time.sleep(0.05) # 避免刷新过快
except Exception as stream_error:
logger.error(f"流式输出过程中出现错误: {stream_error}")
st.warning(f"⚠️ 流式输出中断,切换到标准模式...")
# 使用非流式作为后备
full_answer = self.generation_module.generate_adaptive_answer(question, relevant_docs)
answer_placeholder.markdown(f"### 📝 回答:\n{full_answer}")
# 5. 性能统计
end_time = time.time()
st.write(f"\n⏱️ 问答完成,耗时: {end_time - start_time:.2f}秒")
return full_answer, analysis
except Exception as e:
logger.error(f"问答处理失败: {e}")
st.error(f"❌ 问答处理失败: {str(e)}")
return f"抱歉,处理问题时出现错误:{str(e)}", None
def show_system_stats(self):
"""显示系统统计信息(Streamlit格式化)"""
st.subheader("📊 系统运行统计")
st.divider()
# 路由统计
route_stats = self.query_router.get_route_statistics()
total_queries = route_stats.get('total_queries', 0)
if total_queries > 0:
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("总查询次数", total_queries)
with col2:
st.metric("传统检索",
f"{route_stats.get('traditional_count', 0)} ({route_stats.get('traditional_ratio', 0):.1%})")
with col3:
st.metric("图RAG检索",
f"{route_stats.get('graph_rag_count', 0)} ({route_stats.get('graph_rag_ratio', 0):.1%})")
with col4:
st.metric("组合策略",
f"{route_stats.get('combined_count', 0)} ({route_stats.get('combined_ratio', 0):.1%})")
else:
st.info("暂无查询记录")
# 知识库统计
self._show_knowledge_base_stats()
def rebuild_knowledge_base(self):
"""重建知识库(适配Streamlit交互)"""
st.warning("⚠️ 即将重建知识库,这将删除现有的向量数据!")
# 构建知识库
st.write("🗑️ 删除现有的Milvus集合...")
if self.index_module.delete_collection():
st.success("✅ 现有集合已删除")
else:
st.warning("删除集合时出现问题,继续重建...")
# 重新构建知识库
st.write("🔨 开始重建知识库...")
self.build_knowledge_base()
st.success("✅ 知识库重建完成!")
def cleanup(self):
"""清理资源"""
if self.data_module:
self.data_module.close()
if self.traditional_retrieval:
self.traditional_retrieval.close()
if self.graph_rag_retrieval:
self.graph_rag_retrieval.close()
if self.index_module:
self.index_module.close()
#Streamlit界面构建
def main():
"""Streamlit主界面"""
st.title("🍳 智能烹饪助手 - 图RAG版")
st.divider()
# 侧边栏:系统操作
with st.sidebar:
st.header("⚙️ 系统操作")
# 初始化系统按钮
if st.button("🚀 初始化图RAG系统", type="primary", disabled=st.session_state.system_ready):
with st.spinner("系统初始化中..."):
rag_system = AdvancedGraphRAGSystem()
rag_system.initialize_system()
st.session_state.rag_system = rag_system
st.session_state.system_ready = True
st.rerun() # 刷新页面更新状态
# 构建知识库按钮
if st.button("📚 构建/加载知识库",
disabled=not st.session_state.system_ready or st.session_state.knowledge_base_built):
with st.spinner("知识库构建中..."):
st.session_state.rag_system.build_knowledge_base()
st.rerun()
# 重建知识库按钮
if st.button("🔨 重建知识库", disabled=not st.session_state.knowledge_base_built):
if st.checkbox("确认重建知识库(会删除现有数据)"):
with st.spinner("知识库重建中..."):
st.session_state.rag_system.rebuild_knowledge_base()
st.rerun()
# 查看统计按钮
if st.button("📈 查看系统统计", disabled=not st.session_state.knowledge_base_built):
with st.expander("系统统计信息", expanded=True):
st.session_state.rag_system.show_system_stats()
# 清理资源按钮
if st.button("🧹 清理系统资源"):
if st.session_state.rag_system:
st.session_state.rag_system.cleanup()
st.session_state.rag_system = None
st.session_state.system_ready = False
st.session_state.knowledge_base_built = False
st.success("✅ 资源清理完成")
st.rerun()
st.divider()
st.info("💡 操作提示:\n1. 先初始化系统\n2. 构建/加载知识库\n3. 在主界面输入问题提问")
# 主界面:问答交互
st.header("💬 智能问答")
if not st.session_state.knowledge_base_built:
st.warning("请先在侧边栏完成【初始化系统】和【构建知识库】操作后再进行问答!")
return
# 问题输入
question = st.text_input("请输入你的烹饪相关问题(例如:鱼香肉丝怎么做?)",
placeholder="输入问题后按回车或点击提交按钮")
explain_routing = st.checkbox("显示路由决策解释", value=False)
# 提交按钮
if st.button("🚀 提交问题", disabled=not question):
with st.spinner("正在处理你的问题..."):
st.session_state.rag_system.ask_question_with_routing(question, explain_routing=explain_routing)
if __name__ == "__main__":
try:
main()
except Exception as e:
logger.error(f"系统运行失败: {e}")
st.error(f"\n❌ 系统错误: {str(e)}")
# 可选:显示详细错误栈
with st.expander("查看详细错误信息"):
import traceback
st.code(traceback.format_exc())





&spm=1001.2101.3001.5002&articleId=156652009&d=1&t=3&u=48a67f7f358d4172a2162be0b87f8d6c)
489

被折叠的 条评论
为什么被折叠?



