RAG全栈指南(task6)

一、图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界面

启动成功后,可以通过以下方式访问:

导入的数据包括:

  • 菜谱节点:包含菜名、难度、烹饪时间、菜系等信息

  • 食材节点:包含食材名称、分类、营养信息等

  • 烹饪步骤节点:包含步骤描述、烹饪方法、所需工具等

  • 关系网络:菜谱与食材、步骤之间的复杂关系

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 及业务属性)。

核心关系

关系类型关联实体核心属性
REQUIRESRecipe → Ingredientamount、unit
CONTAINS_STEPRecipe → CookingStepstep_order
BELONGS_TO_CATEGORYRecipe → RecipeCategory-
HAS_DIFFICULTY_LEVELRecipe → DifficultyLevel-

结构特点

Root 根节点构建 “根节点 - 实体类型 - 实例节点” 三层结构,支撑多维度检索。

2.3Neo4j 数据导入

  1. Python 封装GraphDataPreparationModule实现 Neo4j 连接与数据加载;
  2. 按规范定义 CSV 格式(节点 / 关系含核心属性);
  3. 批量导入后为高频检索字段建索引。

2.4 图数据查询

  1. 基础查询:实体精准检索、多属性筛选、多跳关系查询(如菜谱完整流程);
  2. 复杂推理:低难度 / 短时菜谱、同食材 / 分类菜谱。

2.5 图数据转 RAG 文档

  1. 结构化构建:提取图数据生成 Markdown 格式Document,绑定节点元数据;
  2. 分块策略:短文档完整保留,长文档按 “菜谱 - 食材 - 步骤” 语义分块,所有 chunk 通过parent_id关联图节点;
  3. 兼顾文本语义与图结构,上下文更丰富。

三、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())

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值