From 8c021c264e788e10cd5a48f677c1ca212d25e6cc Mon Sep 17 00:00:00 2001 From: root <953994191@qq.com> Date: Tue, 5 May 2026 13:30:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1581 +++++++---------- backend/app/agent/agent_service.py | 13 +- .../app/main_graph/utils/rag_initializer.py | 13 +- backend/app/memory/mem0_client.py | 134 +- tools/test/test_graph_branches.py | 26 +- 5 files changed, 730 insertions(+), 1037 deletions(-) diff --git a/README.md b/README.md index fdcbb18..8223c4f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AI Agent - 智能助手系统 -一个基于 LangGraph + FastAPI 的智能对话助手,支持多模型切换、RAG 知识库检索、联网搜索、可视化图表、以及多个专业子图模块(通讯录、词典、资讯分析)等功能。 +一个基于 LangGraph + FastAPI 的智能对话助手,支持多模型切换、联网搜索、可视化图表、以及多个专业子图模块(通讯录、词典、资讯分析)等功能。 --- @@ -8,12 +8,11 @@ - [核心功能](#-核心功能) - 面向用户的功能和技术特性 - [技术架构](#️-技术架构) - 技术栈、系统架构图、工作流流程图 -- [核心算法与实现原理](#-核心算法与实现原理) - RAG 检索、LangGraph 工作流、多模型路由、SSE 流式响应 -- [RAG 系统完整架构](#-rag-系统完整架构) - 离线索引构建、在线检索生成、演进路线 +- [核心算法与实现原理](#-核心算法与实现原理) - LangGraph 工作流、多模型路由、SSE 流式响应 - [快速开始](#-快速开始) - Docker 和本地部署指南 - [使用指南](#-使用指南) - 基础对话、工具调用、多模型切换 - [开发指南](#-开发指南) - 添加工具、添加模型、Docker 部署 -- [实现指南与最佳实践](#️-实现指南与最佳实践) - RAG 构建、性能优化、扩展开发、部署实践 +- [实现指南与最佳实践](#️-实现指南与最佳实践) - 性能优化、扩展开发、部署实践 - [环境配置](#️-环境配置) - 配置文件、环境变量 - [故障排查](#-故障排查) - 常见问题 @@ -24,7 +23,6 @@ ### 面向用户的功能 - 💬 **智能对话**:支持多轮对话,自动记忆上下文 -- 🔍 **知识库检索(RAG)**:基于向量数据库的智能问答 - 🌐 **联网搜索**:免费使用 DuckDuckGo 搜索,无需 API Key,支持引用溯源 - 📊 **可视化图表**:支持 Mermaid 图表和 matplotlib 图表生成 - 🔄 **多模型切换**:前端可选择不同大语言模型 @@ -56,26 +54,26 @@ ### 技术栈总览 -| 层级 | 组件 | 技术选型 | 版本 | 说明 | -|------|------|---------|------|------| -| **LLM 服务** | 云端模型 | 智谱 AI (glm-5.1) | v5.1 | 快速响应,适合日常对话 | -| | | DeepSeek (deepseek-v4-pro) | v4 | 深度推理,适合复杂问题 | -| | 本地模型 | Gemma-4-E4B-it | v4 | 本地部署,保护隐私 | -| **模型服务层** | Chat 服务 | chat_services.py | - | 统一的生成式大模型接口 | -| | Embedding 服务 | embedding_services.py | - | 统一的嵌入模型接口 | -| | Rerank 服务 | rerank_services.py | - | 统一的重排序接口 | -| | 服务基类 | base.py | - | BaseServiceProvider + FallbackServiceChain + SingletonServiceManager | -| **Embedding** | 向量嵌入 | llama.cpp server | latest | 本地 embedding 服务,支持多种模型 | -| **Agent 框架** | 工作流编排 | LangGraph + LangChain | latest | 状态机驱动的智能体工作流 | -| **子图系统** | 模块化子图 | subgraphs/ | - | 通讯录、词典、资讯分析等子图 | -| | 核心工具 | core/ | - | 状态基类、意图理解、格式化输出、人工审核、**联网搜索**、**可视化图表** | -| **主图系统** | 主流程 | main_graph/ | - | 主图节点、工具、构建器 | -| **向量数据库** | 向量检索 | Qdrant | v1.12+ | 高性能向量相似度检索(远程服务器) | -| **后端框架** | API 服务 | FastAPI + Uvicorn | v0.115+ | RESTful API + WebSocket + SSE 流式输出 | -| **前端框架** | Web 界面 | Streamlit | v1.40+ | 交互式对话界面,组件化设计 | -| **关系数据库** | 持久化存储 | PostgreSQL | v16 | 对话记忆持久化(远程服务器) | -| **容器化** | 服务编排 | Docker + Docker Compose | v24+ | 一键部署所有服务 | -| **CI/CD** | 自动化部署 | Gitea Workflows | latest | 代码推送自动构建部署 | +| 层级 | 组件 | 技术选型 | 说明 | +|------|------|---------|------| +| **Agent 框架** | 工作流编排 | LangGraph + LangChain | 状态机驱动的智能体工作流 | +| **主图系统** | 主流程 | main_graph/ | 混合路由 + React 循环 + 工具执行 | +| **子图系统** | 模块化子图 | subgraphs/ | 通讯录、词典、资讯分析等子图 | +| | 核心工具 | core/ | 意图理解、格式化输出、人工审核、联网搜索、可视化图表 | +| **向量数据库** | 向量检索 | Qdrant | 高性能向量相似度检索(远程服务器) | +| **后端框架** | API 服务 | FastAPI + Uvicorn | RESTful API + SSE 流式输出 | +| **前端框架** | Web 界面 | Streamlit | 交互式对话界面 | +| **关系数据库** | 持久化存储 | PostgreSQL | 对话记忆持久化(远程服务器) | +| **容器化** | 服务编排 | Docker + Docker Compose | 一键部署所有服务 | +| **CI/CD** | 自动化部署 | Gitea Workflows | 代码推送自动构建部署 | +| **LLM 服务** | 云端模型 | 智谱 AI (glm-4-plus) | 快速响应,适合日常对话 | +| | | DeepSeek (deepseek-chat-v3) | 深度推理,适合复杂问题 | +| | | OpenAI (gpt-4o-mini) | 通用对话 | +| | 本地模型 | Qwen3.5-9B.Q4_K_M | 本地部署 GGUF 格式 | +| **模型服务层** | Chat 服务 | chat_services.py | 统一的生成式大模型接口 | +| | Embedding 服务 | embedding_services.py | 统一的嵌入模型接口 | +| | Rerank 服务 | rerank_services.py | 统一的重排序接口 | +| **Embedding** | 向量嵌入 | llama.cpp server | 本地 embedding 服务 (:18001) | ### 系统架构流程图 @@ -85,81 +83,109 @@ graph TB Frontend -->|REST API| Backend[FastAPI 后端 :8079] Backend --> AgentService[AIAgentService] - Backend --> SubgraphAPI[子图 API 端点
/subgraph/contact
/subgraph/dictionary
/subgraph/news] - - AgentService -->|模型路由| ChatServices[模型服务层 chat_services] - ChatServices -->|自动降级| FallbackChain[FallbackServiceChain] - FallbackChain -->|创建| Zhipu[智谱 GLM-5.1] - FallbackChain -->|创建| DeepSeek[DeepSeek V4-Pro] - FallbackChain -->|创建| LocalGemma[本地 Gemma-4] AgentService -->|初始化| LangGraph[LangGraph 工作流引擎] - LangGraph -->|节点1| RetrieveMemory[记忆检索] - LangGraph -->|节点2| MemoryTrigger[记忆触发] - LangGraph -->|节点3| LLMCall[LLM调用 React模式] - LangGraph -->|节点4| ToolNode[工具执行] - LangGraph -->|节点5| Summarize[记忆摘要] - LangGraph -->|节点6| Finalize[最终处理] + LangGraph -->|阶段1| RetrieveMemory[记忆检索 retrieve_memory] + LangGraph -->|阶段2| MemoryTrigger[记忆触发 memory_trigger] + LangGraph -->|阶段3| InitState[初始化状态 init_state] + LangGraph -->|阶段4| HybridRouter[⭐ 混合路由 hybrid_router] + LangGraph -->|阶段5| ReactLoop[React 循环 react_loop] + LangGraph -->|阶段6| FastPath[快速路径 fast_*] + LangGraph -->|阶段7| LLMCall[LLM 调用 llm_call] + LangGraph -->|阶段8| Summarize[记忆摘要 summarize] + LangGraph -->|阶段9| Finalize[最终处理 finalize] - ToolNode -->|调用| Tools[工具集合] - Tools -->|RAG| RAGTool[知识库检索] - Tools -->|子图| SubgraphTools[子图工具: 通讯录/词典/资讯] + HybridRouter -->|闲聊| FastChitchat[fast_chitchat] + HybridRouter -->|知识查询| FastRAG[fast_rag] + HybridRouter -->|工具调用| FastTool[fast_tool] + HybridRouter -->|复杂任务| ReactLoop - RAGTool -->|检索| Qdrant[Qdrant向量库] - RAGTool -->|重排序| RerankService[rerank_services] - RAGTool -->|嵌入| EmbeddingService[embedding_services] + ReactLoop -->|推理| ReactReason[react_reason 推理节点] + ReactLoop -->|RAG检索| RAGRetrieve[rag_retrieve] + ReactLoop -->|联网搜索| WebSearch[web_search] + ReactLoop -->|通讯录| ContactSubgraph[contact_subgraph] + ReactLoop -->|词典| DictionarySubgraph[dictionary_subgraph] + ReactLoop -->|资讯分析| NewsSubgraph[news_analysis_subgraph] + ReactLoop -->|错误处理| HandleError[handle_error] - RetrieveMemory -->|存储| PostgreSQL[PostgreSQL] + RAGRetrieve -->|向量检索| Qdrant[Qdrant向量库] + RAGRetrieve -->|重排序| RerankService[Rerank服务] + RAGRetrieve -->|嵌入| EmbeddingService[Embedding服务] + + AgentService -->|模型路由| ChatServices[模型服务层 chat_services] + ChatServices -->|自动降级| FallbackChain[FallbackServiceChain] + FallbackChain -->|创建| Zhipu[智谱 GLM-4] + FallbackChain -->|创建| DeepSeek[DeepSeek V3] + FallbackChain -->|创建| OpenAI[OpenAI GPT-4o] + FallbackChain -->|创建| LocalQwen[本地 Qwen3.5-9B] + + RetrieveMemory -->|存储/读取| PostgreSQL[PostgreSQL] Summarize -->|存储| PostgreSQL - SubgraphAPI -->|路由| Subgraphs[子图系统 subgraphs] - Subgraphs --> Contact[通讯录子图] - Subgraphs --> Dictionary[词典子图] - Subgraphs --> NewsAnalysis[资讯分析子图] - Subgraphs --> Core[核心工具 core/] - Core --> Intent[意图理解 React] - Core --> HumanReview[人工审核] - Core --> Formatter[格式化输出] - Core --> StateBase[状态基类] - Core --> WebSearch[⭐ 联网搜索 DuckDuckGo] - Core --> Visualization[⭐ 可视化图表 Mermaid] - - Contact -->|数据库| ContactDB[PostgreSQL联系人] - Dictionary -->|数据库| DictionaryDB[PostgreSQL生词本] - NewsAnalysis -->|检索| NewsQdrant[Qdrant向量检索] - style User fill:#e1f5ff style Frontend fill:#fff4e1 style Backend fill:#e8f5e9 + style HybridRouter fill:#fff3e0,stroke:#ff9800,stroke-width:3px + style ReactLoop fill:#f3e5f5 + style FastPath fill:#e3f2fd + style LangGraph fill:#c8e6c9 style ChatServices fill:#c8e6c9 - style LangGraph fill:#f3e5f5 - style Subgraphs fill:#fff3e0 style PostgreSQL fill:#ffebee style Qdrant fill:#ffebee ``` --- -### 主图与子图的关联架构 +### 主图与子图架构 ```mermaid graph TB subgraph "主图 MainGraph" StartMain[START] - IntentMain[意图分类节点
判断用户意图] - ChatNode[普通对话节点
调用主 LLM] - SubgraphCaller[子图调用器
调用对应子图] + RetrieveMemory[记忆检索] + MemoryTrigger[记忆触发] + InitState[初始化状态] + HybridRouter[⭐ 混合路由
规则分流 + LLM意图分类] + FastChitchat[fast_chitchat
闲聊快速路径] + FastRAG[fast_rag
RAG快速路径] + FastTool[fast_tool
工具快速路径] + ReactReason[react_reason
React推理节点] + LLMCall[llm_call
LLM调用节点] FinalMain[最终响应] EndMain[END] - StartMain -->|用户输入| IntentMain - IntentMain -->|chat| ChatNode - IntentMain -->|contact| SubgraphCaller - IntentMain -->|dictionary| SubgraphCaller - IntentMain -->|news| SubgraphCaller - ChatNode --> FinalMain - SubgraphCaller --> FinalMain + StartMain -->|用户输入| RetrieveMemory + RetrieveMemory --> MemoryTrigger + MemoryTrigger --> InitState + InitState --> HybridRouter + + HybridRouter -->|闲聊| FastChitchat + HybridRouter -->|知识查询| FastRAG + HybridRouter -->|工具调用| FastTool + HybridRouter -->|复杂任务| ReactReason + + FastChitchat -->|成功| LLMCall + FastChitchat -->|失败| ReactReason + FastRAG -->|成功| LLMCall + FastRAG -->|失败| ReactReason + FastTool -->|成功| LLMCall + FastTool -->|失败| ReactReason + + ReactReason -->|rag_retrieve| RAGRetrieve[RAG检索] + ReactReason -->|web_search| WebSearchNode[联网搜索] + ReactReason -->|contact_subgraph| ContactNode[通讯录子图] + ReactReason -->|dictionary_subgraph| DictNode[词典子图] + ReactReason -->|news_analysis_subgraph| NewsNode[资讯子图] + ReactReason -->|llm_call| LLMCall + + RAGRetrieve --> ReactReason + WebSearchNode --> ReactReason + ContactNode --> ReactReason + DictNode --> ReactReason + NewsNode --> ReactReason + + LLMCall --> FinalMain FinalMain --> EndMain end @@ -243,76 +269,161 @@ graph TB FormatNews --> EndNews end - SubgraphCaller -.->|调用
状态传递| StartContact - SubgraphCaller -.->|调用
状态传递| StartDict - SubgraphCaller -.->|调用
状态传递| StartNews + ReactReason -.->|调用
状态传递| StartContact + ReactReason -.->|调用
状态传递| StartDict + ReactReason -.->|调用
状态传递| StartNews - style IntentMain fill:#ffe0b2 - style ChatNode fill:#e0e0e0 - style SubgraphCaller fill:#bbdefb - style FinalMain fill:#fff59d - style IntentContact fill:#c8e6c9 - style IntentDict fill:#e1bee7 - style IntentNews fill:#ffcdd2 -``` + style HybridRouter fill:#fff3e0,stroke:#ff9800,stroke-width:3px + style ReactReason fill:#e8eaf6 +--- -### LangGraph 工作流详细流程 +### 索引工作流(离线构建) ```mermaid -stateDiagram-v2 - [*] --> RetrieveMemory: 用户输入消息 +flowchart TB + subgraph 文档输入 + A1[文档源] + A2[PDF/DOCX/TXT/Markdown] + end - RetrieveMemory --> MemoryTrigger: 检索历史记忆 - MemoryTrigger --> LLMCall: 检查记忆触发条件 + subgraph 文档加载 + B1[rag_indexer/loaders.py] + B2[UnstructuredLoader] + B3[PyMuPDFLoader] + B4[TextLoader] + end - %% ⭐ React (Reasoning → Acting → Observing) 循环开始 - LLMCall --> CheckTools: LLM 输出 + subgraph 文本切分 + C1[rag_indexer/splitters.py] + C2[RecursiveCharacterTextSplitter
按分隔符递归切分] + C3[SemanticChunker
基于语义相似度] + C4[ParentChildSplitter
父子块切分] + end - CheckTools --> ToolNode: 需要调用工具 - CheckTools --> CheckSummary: 直接回复 + subgraph 嵌入生成 + D1[Embedding 生成] + D2[稠密向量
Qwen3-Embedding-0.6B
llama.cpp server:18001] + D3[稀疏向量 BM25
FastEmbed] + end - ToolNode --> ExecuteTool: 执行工具 - ExecuteTool --> LLMCall: ⭐ 工具结果返回(Observing → Reasoning 循环) - %% ⭐ React 循环结束 + subgraph 向量存储 + E1[Qdrant Vector Store] + E2[稠密向量索引
HNSW 算法] + E3[稀疏向量索引
BM25] + E4[rag_core/vector_store.py] + end - CheckSummary --> Summarize: 达到摘要阈值 - CheckSummary --> Finalize: 未达阈值,直接结束 + A1 --> A2 + A2 --> B1 + B1 --> B2 + B2 --> C1 + C1 --> C2 + C1 --> C3 + C1 --> C4 + C2 --> D1 + C3 --> D1 + C4 --> D1 + D1 --> D2 + D1 --> D3 + D2 --> E1 + D3 --> E1 + E1 --> E2 + E1 --> E3 - Summarize --> PostgreSQL: 保存摘要 - PostgreSQL --> Finalize: 继续对话 - - Finalize --> FormatResponse: 格式化响应 - FormatResponse --> [*]: SSE 流式输出 - - note right of LLMCall - ⭐ Reasoning - LLM 思考: - - 需要调用工具吗? - - 调用什么工具? - end note - - note right of ToolNode - ⭐ Acting - 执行工具: - - 天气查询 - - 文件读取 - - RAG 检索 - - 等等 - end note - - note right of ExecuteTool - ⭐ Observing - 观察工具结果, - 返回给 LLM 再次思考 - end note - - note right of LLMCall - ⭐ React 循环 - Reasoning → Acting → Observing → Reasoning... - 可以多次循环 - end note + style A1 fill:#e3f2fd + style B1 fill:#fff3e0 + style C1 fill:#f3e5f5 + style D1 fill:#e8f5e9 + style E1 fill:#ffebee ``` +**技术组件说明:** + +| 组件 | 技术选型 | 说明 | +|------|---------|------| +| 文档加载 | Unstructured / PyMuPDF / TextLoader | 支持多种文档格式 | +| 文本切分 | RecursiveCharacterTextSplitter | 按分隔符递归切分,默认 500 字符 | +| 语义切分 | SemanticChunker | 基于 Embedding 相似度自动切分 | +| 父子块切分 | ParentChildSplitter | 大块存储上下文,小块用于检索 | +| 稠密嵌入 | Qwen3-Embedding-0.6B-Q8_0 | llama.cpp server (:18001) | +| 稀疏嵌入 | FastEmbed BM25 | 本地计算,无需额外服务 | +| 向量存储 | Qdrant | HNSW 索引,高性能 ANN 检索 | + +### 检索工作流(在线查询) + +```mermaid +flowchart TB + subgraph 查询输入 + Q1[用户查询] + Q2[Query: "公司报销流程是什么?"] + end + + subgraph 查询处理 + R1[查询改写 MultiQuery] + R2[rag_indexer/splitters.py] + R3[使用 chat_services 生成多角度查询] + end + + subgraph 混合检索 + S1[并行检索] + S2[稠密向量检索
Embedding → 向量相似度] + S3[稀疏 BM25 检索
词频统计] + S4[rag_core/sparse_embedder.py] + end + + subgraph 结果融合 + F1[RRF 融合] + F2[rag_indexer/fusion.py] + F3[RRF(d) = Σ 1/(k + rank)] + F4[Qdrant Fusion API
服务端融合] + end + + subgraph 重排序 + P1[Cross-Encoder 重排] + P2[bge-reranker-v2-m3] + P3[rerank_services.py
llama.cpp server:18002] + P4[Query-Document 交互编码] + end + + subgraph LLM 生成 + G1[LLM 生成回答] + G2[chat_services.py] + G3[Context + 生成回答] + end + + Q1 --> Q2 + Q2 --> R1 + R1 --> R2 + R2 --> S1 + S1 --> S2 + S1 --> S3 + S2 --> F1 + S3 --> F1 + F1 --> P1 + P1 --> P2 + P2 --> G1 + G1 --> G2 + G2 --> G3 + + style Q1 fill:#e3f2fd + style R1 fill:#fff3e0 + style S1 fill:#f3e5f5 + style F1 fill:#e8f5e9 + style P1 fill:#ffebee + style G1 fill:#e1f5ff +``` + +**技术组件说明:** + +| 阶段 | 技术选型 | 说明 | +|------|---------|------| +| 查询改写 | MultiQuery | 使用 LLM 生成 3-5 个多角度查询 | +| 稠密检索 | Qwen3-Embedding | 向量相似度计算,余弦相似度 | +| 稀疏检索 | FastEmbed BM25 | 词频 TF-IDF 统计 | +| 结果融合 | Qdrant Fusion API | 服务端 RRF 融合,无需传输数据 | +| 重排序 | bge-reranker-v2-m3 | Cross-Encoder 交互编码,精度更高 | +| LLM 生成 | chat_services | 统一的大模型服务接口 | + ### 数据流向图 ``` @@ -333,15 +444,26 @@ stateDiagram-v2 │ │ ├─→ user_id: 用户标识 │ │ └─→ metadata: 元数据 │ │ - │ ├─→ 执行工作流 - │ │ ├─→ retrieve_memory: 从 PostgreSQL 检索历史 - │ │ ├─→ memory_trigger: 判断是否触发记忆 - │ │ ├─→ llm_call: 调用 LLM 生成响应 - │ │ ├─→ tool_node: 执行工具(如有需要) - │ │ ├─→ summarize: 生成对话摘要 - │ │ └─→ finalize: 格式化最终响应 - │ │ - │ └─→ 返回 SSE 流 + │ └─→ 执行工作流(混合路由 + React 循环) + │ ├─→ retrieve_memory: 从 PostgreSQL 检索历史 + │ ├─→ memory_trigger: 判断是否触发记忆 + │ ├─→ init_state: 初始化状态 + │ ├─→ hybrid_router: 混合路由决策 + │ │ ├─→ fast_chitchat: 闲聊快速路径 + │ │ ├─→ fast_rag: RAG 快速路径 + │ │ ├─→ fast_tool: 工具快速路径 + │ │ └─→ react_loop: React 循环(兜底) + │ │ + │ ├─→ React 循环(react_reason 节点) + │ │ ├─→ rag_retrieve: RAG 检索 + │ │ ├─→ web_search: 联网搜索 + │ │ ├─→ contact_subgraph: 通讯录子图 + │ │ ├─→ dictionary_subgraph: 词典子图 + │ │ ├─→ news_analysis_subgraph: 资讯子图 + │ │ └─→ llm_call: LLM 调用 + │ │ + │ ├─→ summarize: 生成对话摘要(如需要) + │ └─→ finalize: 格式化最终响应 │ └─→ 持久化存储 ├─→ PostgreSQL: 对话历史和摘要 @@ -372,35 +494,41 @@ Agent1/ │ │ │ │ │ ├── agent/ # ⭐ Agent 服务层 │ │ │ ├── __init__.py -│ │ │ ├── service.py # Agent 服务核心(使用 chat_services) +│ │ │ ├── agent_service.py # Agent 服务核心(使用 chat_services) │ │ │ ├── history.py # 历史查询服务 │ │ │ └── prompts.py # 提示词模板 │ │ │ -│ │ ├── main_graph/ # ⭐ 主图 - LangGraph 主流程 +│ │ ├── main_graph/ # ⭐ 主图 - LangGraph 主流程(混合路由 + React 循环) │ │ │ ├── __init__.py -│ │ │ ├── state.py # 主图状态定义 -│ │ │ ├── graph_builder.py # LangGraph 图构建器 +│ │ │ ├── state.py # 主图状态定义 MainGraphState +│ │ │ ├── graph.py # LangGraph 组件导出 +│ │ │ ├── config.py # 主图配置 │ │ │ ├── nodes/ # 主图节点 │ │ │ │ ├── __init__.py -│ │ │ │ ├── router.py # 路由决策 -│ │ │ │ ├── llm_call.py # LLM 调用节点(React 模式) -│ │ │ │ ├── tool_call.py # 工具执行节点 +│ │ │ │ ├── _utils.py # 节点公共工具 +│ │ │ │ ├── reasoning.py # React 推理节点 +│ │ │ │ ├── hybrid_router.py # 混合路由节点 +│ │ │ │ ├── fast_paths.py # 快速路径节点 +│ │ │ │ ├── llm_call.py # LLM 调用节点 +│ │ │ │ ├── routing.py # 路由决策(init_state, route_by_reasoning) +│ │ │ │ ├── rag_nodes.py # RAG 检索节点 +│ │ │ │ ├── web_search.py # 联网搜索节点 │ │ │ │ ├── retrieve_memory.py # 记忆检索节点 -│ │ │ │ ├── summarize.py # 记忆存储节点 -│ │ │ │ ├── finalize.py # 最终处理节点 │ │ │ │ ├── memory_trigger.py # 记忆触发节点 -│ │ │ │ ├── rag_nodes.py # RAG 集成节点 -│ │ │ │ └── react_nodes.py # React 模式节点 +│ │ │ │ ├── summarize.py # 记忆摘要节点 +│ │ │ │ ├── finalize.py # 最终处理节点 +│ │ │ │ ├── tool_call.py # 工具执行节点 +│ │ │ │ └── error_handling.py # 错误处理节点 │ │ │ ├── tools/ # 主图工具 │ │ │ │ ├── __init__.py -│ │ │ │ ├── graph_tools.py # 工具定义 -│ │ │ │ └── subgraph_tools.py # 子图调用工具 +│ │ │ │ ├── common_tools.py # 通用工具 +│ │ │ │ ├── subgraph_tools.py # 子图调用工具 +│ │ │ │ └── graph_tools.py # 图工具 │ │ │ └── utils/ # 主图工具函数 │ │ │ ├── __init__.py -│ │ │ ├── retry_utils.py # 重试工具 -│ │ │ ├── subgraph_builder.py # 子图构建器 -│ │ │ ├── rag_initializer.py # RAG 初始化工具 -│ │ │ └── visualize_graph.py # 图可视化工具 +│ │ │ ├── main_graph_builder.py # 主图构建器 +│ │ │ ├── rag_initializer.py # RAG 初始化 +│ │ │ └── retry_utils.py # 重试工具 │ │ │ │ │ ├── subgraphs/ # ⭐ 子图模块 │ │ │ ├── __init__.py @@ -430,6 +558,18 @@ Agent1/ │ │ │ ├── embedding_services.py # 嵌入模型服务 │ │ │ └── rerank_services.py # 重排序服务 │ │ │ +│ │ ├── mcp/ # MCP 模块 +│ │ │ ├── __init__.py +│ │ │ ├── mcp_manager.py # MCP 管理器 +│ │ │ ├── mcp_client.py # MCP 客户端 +│ │ │ ├── adapters/ # MCP 适配器 +│ │ │ │ ├── __init__.py +│ │ │ │ ├── base_adapter.py +│ │ │ │ ├── contact_adapter.py +│ │ │ │ ├── dictionary_adapter.py +│ │ │ │ └── news_adapter.py +│ │ │ └── mcp_example.py +│ │ │ │ │ ├── memory/ # 记忆模块 │ │ │ ├── __init__.py │ │ │ └── mem0_client.py # Mem0 客户端封装 @@ -451,17 +591,16 @@ Agent1/ │ │ │ └── init_db.py # 数据库初始化 │ │ │ │ │ └── utils/ # 工具模块 -│ │ └── __init__.py -│ └── rag_core/ +│ │ ├── __init__.py +│ │ └── logging.py # 日志工具 +│ └── rag_core/ # ⭐ RAG 核心库(统一组件) │ ├── __init__.py -│ ├── client.py # RAG 核心客户端 -│ ├── embedders.py # 嵌入模型 -│ ├── vector_store.py # 向量存储 -│ ├── retriever_factory.py # 检索器工厂 -│ └── store/ -│ ├── __init__.py -│ ├── factory.py # 存储工厂 -│ └── postgres.py # PostgreSQL 存储 +│ ├── config.py # RAG 配置 +│ ├── client.py # RAG 核心客户端 +│ ├── embedders.py # 嵌入模型 +│ ├── sparse_embedder.py # BM25 稀疏嵌入 +│ ├── vector_store.py # 向量存储(Dense + Sparse) +│ └── doc_store.py # 文档存储 ├── frontend/ │ ├── run.py # 前端启动脚本 │ ├── requirements.txt @@ -516,288 +655,197 @@ Agent1/ ### 1. RAG 检索算法 -#### 1.1 文本切分策略 +项目采用稠密 + 稀疏混合检索架构,结合 RRF 融合和 Cross-Encoder 重排序,实现高精度知识库问答。 -项目支持三种文本切分策略,适应不同场景需求: +**核心特性:** +- 三种文本切分策略:递归字符切分、语义切分、父子块切分 +- 稠密向量检索(Embedding)+ 稀疏 BM25 检索 +- RRF 融合算法实现多检索源结果合并 +- Cross-Encoder 重排序提升相关性 -**递归字符切分(Recursive Character Splitting)** -``` -算法思路: - 按优先级分隔符逐级切分:["\n\n", "\n", "。", "!", "?", " ", ""] - ↓ - 确保每个块不超过 chunk_size(默认 500 字符) - ↓ - 保留 chunk_overlap(默认 50 字符)避免上下文丢失 - -优势:简单高效,适合结构化文档 -实现:langchain_text_splitters.RecursiveCharacterTextSplitter -``` - -**语义切分(Semantic Chunking)** -``` -算法思路: - 1. 将文档按句子边界切分 - 2. 使用 Embedding 模型计算相邻句子的向量相似度 - 3. 计算相似度变化率(breakpoint threshold) - 4. 在相似度骤降处切分(语义主题变化点) - -阈值策略: - - percentile(百分位数):默认,取相似度分布的 95 百分位 - - standard_deviation(标准差):低于均值 1.5 标准差处切分 - - interquartile(四分位距):使用 IQR 方法检测异常点 - -优势:保持语义完整性,切分更自然 -实现:langchain_experimental.text_splitter.SemanticChunker -``` - -**父子块切分(Parent-Child Chunking)** -``` -算法思路: - 父块(大块):1000 字符,保留完整上下文 - ↓ - 子块(小块):语义切分,用于向量检索 - ↓ - 建立映射关系:child_id → parent_id - ↓ - 检索时:用子块检索,返回时扩展为父块上下文 - -检索流程: - 用户查询 → 向量检索匹配子块 → 通过映射获取父块 → 返回完整上下文 - -优势:检索精度高,上下文完整 -实现:自定义 ParentChildSplitter 类 -``` - -#### 1.2 混合检索(Dense + Sparse) - -``` -检索架构: -┌─────────────────────────────────────────────┐ -│ 用户查询 │ -└──────────────────┬──────────────────────────┘ - │ - ┌──────────┴──────────┐ - ↓ ↓ -┌───────────────┐ ┌───────────────┐ -│ 稠密向量检索 │ │ 稀疏 BM25 检索 │ -│ (语义相似) │ │ (关键词匹配) │ -│ │ │ │ -│ Embedding │ │ Token 化 │ -│ → 向量相似度 │ │ → 词频统计 │ -└───────┬───────┘ └───────┬───────┘ - │ │ - └──────────┬─────────┘ - ↓ - ┌────────────────┐ - │ 结果融合 │ - │ (RRF 算法) │ - └────────┬───────┘ - ↓ - ┌────────────────┐ - │ Cross-Encoder │ - │ 重排序 │ - └────────┬───────┘ - ↓ - ┌────────────────┐ - │ Top-K 结果返回 │ - └────────────────┘ -``` - -**稠密向量检索(Dense Retrieval)** -- 使用 Embedding 模型将查询和文档映射到同一向量空间 -- 计算余弦相似度,返回语义最相似的文档 -- 适合:语义理解、同义词匹配、概念检索 - -**稀疏向量检索(Sparse Retrieval - BM25)** -- 基于词频(TF)和逆文档频率(IDF)计算相关性 -- 适合:专有名词、精确匹配、术语检索 - -#### 1.3 RRF 融合算法(Reciprocal Rank Fusion) - -```python -# 核心算法实现 -def reciprocal_rank_fusion(doc_lists: List[List[Document]], k: int = 60) -> List[Document]: - """ - RRF 公式:RRF(d) = Σ (1 / (k + rank(d))) - - 参数说明: - - k: 平滑常数,通常设为 60 - - k 值越大,排名影响越小,结果越平滑 - - k 值越小,高排名文档优势越大 - - 算法步骤: - 1. 遍历每个检索结果列表 - 2. 对每个文档,根据其排名计算 RRF 得分 - 3. 累加同一文档在不同列表中的得分 - 4. 按总得分降序排序 - - 示例: - 文档 A 在列表 1 中排名第 1,在列表 2 中排名第 3 - RRF(A) = 1/(60+1) + 1/(60+3) = 0.0164 + 0.0159 = 0.0323 - - 文档 B 在列表 1 中排名第 2,在列表 2 中排名第 2 - RRF(B) = 1/(60+2) + 1/(60+2) = 0.0161 + 0.0161 = 0.0322 - - 结果:A 排名高于 B - """ - doc_to_score: Dict[str, float] = {} - doc_map: Dict[str, Document] = {} - - for docs in doc_lists: - for rank, doc in enumerate(docs, start=1): - doc_id = f"{doc.page_content[:200]}_{doc.metadata.get('source', '')}" - if doc_id not in doc_map: - doc_map[doc_id] = doc - score = doc_to_score.get(doc_id, 0.0) + 1.0 / (k + rank) - doc_to_score[doc_id] = score - - sorted_ids = sorted(doc_to_score.keys(), key=lambda x: doc_to_score[x], reverse=True) - return [doc_map[doc_id] for doc_id in sorted_ids] -``` - -**RRF 算法优势:** -- 无需归一化:不同检索器的分数范围可能不同,RRF 直接使用排名 -- 参数简单:仅需调整 k 值,默认 60 在大多数场景表现良好 -- 鲁棒性强:对异常值和噪声不敏感 - -#### 1.4 Cross-Encoder 重排序 - -``` -重排序流程: -┌──────────────────────────────────────────────┐ -│ 输入:RRF 融合后的 Top-20 文档 │ -└──────────────────┬───────────────────────────┘ - │ - ↓ -┌──────────────────────────────────────────────┐ -│ Cross-Encoder 模型(bge-reranker-v2-m3) │ -│ │ -│ 工作原理: │ -│ - 将 Query 和 Document 拼接输入模型 │ -│ - "[CLS] Query [SEP] Document [SEP]" │ -│ - 通过 Transformer 计算交互注意力 │ -│ - 输出相关性得分(0-1) │ -│ │ -│ 与 Bi-Encoder 的区别: │ -│ - Bi-Encoder:分别编码,计算余弦相似度(快) │ -│ - Cross-Encoder:联合编码,计算交互得分(准) │ -└──────────────────┬───────────────────────────┘ - │ - ↓ -┌──────────────────────────────────────────────┐ -│ 按相关性得分排序,返回 Top-5 │ -└──────────────────────────────────────────────┘ -``` - -**实现细节:** -- 使用远程 llama.cpp 服务部署重排序模型 -- 兼容 OpenAI Rerank API 格式 -- 超时保护:60 秒超时,失败时降级为原始排序 +**详细文档:** +- 算法原理详见 [backend/docs/RAG_ALGORITHM.md](backend/docs/RAG_ALGORITHM.md) +- 系统架构详见 [backend/docs/RAG_ARCHITECTURE.md](backend/docs/RAG_ARCHITECTURE.md) --- -### 1.5 RAG 评估方法 ⭐ +### LangGraph 工作流详细流程 -如何评估 RAG 系统的召回率和相关性? +#### 1. 混合路由 + React 循环架构 ⭐⭐ -**核心指标:** -- **Recall@k**:前 k 个结果中包含多少比例的相关文档 -- **Precision@k**:前 k 个结果中有多少比例是相关文档 -- **F1@k**:召回率和精确率的调和平均数 -- **MRR**:平均倒数排名 -- **相关性评分**:0-5 分的相关性评估 +**设计理念**:混合路由(Hybrid Router)作为前置决策,快速路径处理简单任务,React 循环作为复杂任务的兜底方案。 -**详细指南:** -参见 [backend/docs/RAG_EVALUATION_GUIDE.md](backend/docs/RAG_EVALUATION_GUIDE.md) - -**快速使用:** -```bash -# 运行评估脚本 -cd backend -python scripts/evaluate_rag.py +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 主图执行流程 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 阶段1: 记忆检索 (retrieve_memory) │ │ +│ │ - 从 PostgreSQL 检索用户历史对话 │ │ +│ │ - 生成 memory_context 供后续使用 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 阶段2: 记忆触发 (memory_trigger) │ │ +│ │ - 判断是否需要激活记忆上下文 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 阶段3: 初始化状态 (init_state) │ │ +│ │ - 初始化 MainGraphState │ │ +│ │ - 设置 user_query、messages 等 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 阶段4: 混合路由 (hybrid_router) ⭐ │ │ +│ │ - 规则分流:闲聊关键词、子图关键词(<5ms) │ │ +│ │ - LLM 分类:使用轻量级模型进行意图分类(chitchat/knowledge/tool) │ │ +│ │ - 输出决策:fast_chitchat / fast_rag / fast_tool / react_loop │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────┼──────────────────────┐ │ +│ ↓ ↓ ↓ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │快速路径 │ │快速路径 │ │快速路径 │ │ +│ │fast_* │ │fast_rag │ │fast_tool│ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ +│ │ ┌────────────────┘ ┌──────────────┘ │ +│ │ │ │ │ +│ ↓ ↓ ↓ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 阶段5: React 循环 (react_reason) │ │ +│ │ - 调用 app/core/intent.py 的 react_reason_async() │ │ +│ │ - 使用 app/model_services/ 获取 chat 服务 │ │ +│ │ - 推理下一步动作(rag_retrieve/web_search/子图/llm_call) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ React 循环内节点 │ │ +│ │ rag_retrieve → react_reason(回到推理) │ │ +│ │ web_search → react_reason(回到推理) │ │ +│ │ contact_subgraph → react_reason(回到推理) │ │ +│ │ dictionary_subgraph → react_reason(回到推理) │ │ +│ │ news_analysis_subgraph → react_reason(回到推理) │ │ +│ │ handle_error → react_reason(错误处理后继续推理) │ │ +│ │ llm_call → 退出循环,进入完成阶段 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 阶段6: LLM 调用 (llm_call) │ │ +│ │ - 调用主 LLM 生成最终回答 │ │ +│ │ - 使用 llm.bind_tools(tools) 绑定工具 │ │ +│ │ - 支持流式输出到前端 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 阶段7: 记忆摘要 (summarize) / 最终处理 (finalize) │ │ +│ │ - 对话轮数 >= 5 时触发摘要 │ │ +│ │ - 保存对话到 PostgreSQL │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ ``` ---- +#### 2. React 推理循环详解 -### 2. LangGraph 工作流算法 - -#### 2.1 React (Reasoning → Acting → Observing) 模式 ⭐ - -**设计理念**:让 LLM 先思考(Reasoning),再行动(Acting),然后观察结果(Observing),可以多次循环。 +React 推理循环使用 `app/core/intent.py` 中的 `react_reason_async()` 函数: ``` ┌─────────────────────────────────────────────────────────────────┐ -│ React 模式循环 │ +│ React 推理循环 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 1. Reasoning (思考) │ │ -│ │ LLMCall 节点 │ │ -│ │ - 分析用户问题 │ │ -│ │ - 决定是否需要调用工具 │ │ -│ │ - 决定调用哪个工具 │ │ +│ │ 1. Reasoning (推理) - react_reason 节点 │ │ +│ │ - 调用 react_reason_async() │ │ +│ │ - 传入上下文:retrieved_docs、reasoning_history、 │ │ +│ │ previous_actions、messages、errors │ │ +│ │ - LLM 决定下一步 action │ │ +│ │ - 记录到 reasoning_history │ │ │ └───────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 2. Acting (行动) │ │ -│ │ ToolNode 节点 │ │ -│ │ - 执行工具调用 │ │ -│ │ - 天气查询 / 文件读取 / RAG 检索等 │ │ +│ │ 2. Acting (行动) │ │ +│ │ - rag_retrieve: RAG 检索 │ │ +│ │ - web_search: 联网搜索 │ │ +│ │ - contact_subgraph: 通讯录子图 │ │ +│ │ - dictionary_subgraph: 词典子图 │ │ +│ │ - news_analysis_subgraph: 资讯分析子图 │ │ +│ │ - handle_error: 错误处理 │ │ │ └───────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 3. Observing (观察) │ │ -│ │ ExecuteTool → LLMCall │ │ -│ │ - 观察工具结果 │ │ -│ │ - 返回给 LLM 再次思考 │ │ +│ │ 3. Observing (观察) / 循环 │ │ +│ │ - 工具结果返回给 react_reason │ │ +│ │ - 再次推理下一步 │ │ +│ │ - 最多 10 次循环 (max_steps=10) │ │ +│ │ - 或直到推理决定 llm_call │ │ │ └───────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 4. 循环或结束 │ │ -│ │ should_continue 路由 │ │ -│ │ - 还需要调用工具吗? → 继续循环 │ │ -│ │ - 不需要了 → 结束流程 │ │ +│ │ 4. 退出条件 │ │ +│ │ - action == llm_call: 退出循环,进入 llm_call 节点 │ │ +│ │ - max_steps 达到: 强制退出到 llm_call │ │ +│ │ - 错误累积过多: 进入 handle_error │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` **关键实现点**: -1. **`llm.bind_tools(tools)`** - 在 `create_llm_call_node` 中,让 LLM 知道可以调用哪些工具 -2. **`should_continue` 路由函数** - 检查 LLM 输出是否包含 `tool_calls` -3. **`tool_node → llm_call` 循环边** - 工具结果返回给 LLM 再次思考 -4. **可以多次循环** - LLM 可以调用多个工具,或者同一个工具多次 - -**实际代码位置**: -- `backend/app/main_graph/graph_builder.py` 第 79 行:`builder.add_edge("tool_node", "llm_call")` -- `backend/app/main_graph/nodes/router.py`:`should_continue` 函数检查 `last_message.tool_calls` +1. **`react_reason_async()`** - 在 `app/core/intent.py` 中,使用 chat_services 获取 LLM 进行推理 +2. **`route_by_reasoning`** - 路由函数,根据推理结果决定下一步节点 +3. **循环边** - 工具节点执行后回到 react_reason 继续推理 +4. **自动升级** - 快速路径失败时,回到 react_reason 继续推理 #### 2.2 状态机设计 ```python # 核心状态定义 -class AgentState(TypedDict): - messages: Annotated[list, add_messages] # 对话历史(自动合并) - user_id: str # 用户标识 - memory_context: str # 检索到的记忆上下文 - should_summarize: bool # 是否需要生成摘要 - tool_calls: list # 工具调用列表 - final_response: str # 最终响应 +class MainGraphState(TypedDict): + messages: Annotated[list, add_messages] # 对话历史(自动合并) + user_id: str # 用户标识 + user_query: str # 用户查询 + memory_context: str # 检索到的记忆上下文 + memory_triggered: bool # 记忆是否触发 + should_summarize: bool # 是否需要生成摘要 + retrieved_docs: list # RAG 检索到的文档 + reasoning_history: list # React 推理历史 + previous_actions: list # 之前的动作 + errors: list # 错误列表 + current_step: int # 当前步骤 + max_steps: int # 最大步骤数 + llm_override: str # LLM 覆盖 + final_response: str # 最终响应 ``` **状态流转规则**: ``` -初始状态 → retrieve_memory → memory_trigger → [条件分支] - ↓ - ┌───────────────────┼───────────────┐ - ↓ ↓ ↓ - should_summarize 直接回复 需要工具 - ↓ ↓ ↓ - summarize → save finalize tool_node - ↓ ↓ ↓ - llm_call ←───────────────┘ llm_call ←┘ - ↓ - finalize +初始状态 → retrieve_memory → memory_trigger → init_state → hybrid_router + ↓ + ┌───────────────────────────┼───────────────────────────┐ + ↓ ↓ ↓ + fast_chitchat fast_rag fast_tool + ↓ ↓ ↓ + [成功→llm_call / 失败→react_reason] [成功→llm_call / 失败→react_reason] [成功→llm_call / 失败→react_reason] + ↓ + ┌───────────────────────────────────────────────┘ + ↓ + react_reason + ↓ + ┌──────────────┼──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐ + ↓ ↓ ↓ ↓ ↓ ↓ ↓ + rag_retrieve web_search contact_subgraph dictionary_subgraph news_analysis_subgraph handle_error llm_call + ↓ ↓ ↓ ↓ ↓ ↓ ↓ + react_reason ←────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘ + ↓ + llm_call + ↓ + summarize + ↓ + finalize ``` #### 2.3 记忆管理算法 @@ -834,17 +882,17 @@ class AgentState(TypedDict): │ 用户选择模型 │ └──────────────────┬──────────────────────────┘ │ - ┌──────────┼──────────┐ - ↓ ↓ ↓ - ┌──────┐ ┌──────┐ ┌──────┐ - │ zhipu│ │deep │ │local │ - └──┬───┘ └──┬───┘ └──┬───┘ - │ │ │ - ↓ ↓ ↓ - ChatZhipu ChatOpenAI ChatOpenAI - (官方SDK) (DeepSeek) (vLLM/Gemma) - │ │ │ - └─────────┼─────────┘ + ┌──────────┼──────────┬──────────┐ + ↓ ↓ ↓ ↓ + ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ + │ zhipu│ │deep │ │openai│ │local │ + └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ + │ │ │ │ + ↓ ↓ ↓ ↓ + ChatZhipu ChatOpenAI ChatOpenAI ChatOpenAI + (官方SDK) (DeepSeek) (OpenAI) (llama.cpp/Qwen) + │ │ │ │ + └─────────┴─────────┴─────────┘ ↓ ┌────────────────┐ │ 统一接口输出 │ @@ -1052,14 +1100,11 @@ RAG 系统分为两个独立但协同的阶段: 实现代码: from app.rag.tools import search_knowledge_base - from app.graph.graph_builder import GraphBuilder - - # 将 RAG 工具添加到工具列表 - tools = AVAILABLE_TOOLS + [search_knowledge_base] - + from app.main_graph.utils.main_graph_builder import MainGraphBuilder + # 构建图 - builder = GraphBuilder(llm, tools, tools_by_name) - graph = builder.build().compile(checkpointer=checkpointer) + builder = MainGraphBuilder() + graph = builder.build_graph().compile(checkpointer=checkpointer) ``` #### Level 5: GraphRAG 集成 (基于图和关系的 RAG) @@ -1546,27 +1591,30 @@ streamlit run frontend/src/frontend_main.py ``` 你好,请介绍一下自己 -今天北京天气怎么样? -帮我总结一下 a.txt 的内容 +帮我写一个 Python 脚本 ``` -### 工具调用示例 +### 主要功能 -| 功能 | 示例提问 | -|------|---------| -| 🌤️ 天气查询 | "上海今天天气如何?" | -| 📄 读取文本 | "读取 a.txt 的内容" | -| 📑 解析 PDF | "总结 b.pdf 的主要内容" | -| 📊 Excel 数据 | "显示 c.xlsx 的数据" | -| 🌐 网页抓取 | "抓取 https://example.com 的内容" | -| 🔍 长期记忆 | "记住我喜欢吃川菜" → "我有什么饮食偏好?" | +| 功能 | 说明 | 示例提问 | +|------|------|---------| +| 🧠 混合路由智能分流 | 自动判断任务类型,选择最佳路径 | 自然对话即可 | +| ⚡ 快速路径 | 闲聊、RAG查询、工具调用可走快速路径 | "你好"、"什么是 RAG" | +| 🔄 React 推理循环 | 复杂任务走完整的思考-行动-观察循环 | "帮我分析一下这个文档" | +| 🌐 联网搜索 | 免费 DuckDuckGo 搜索 | "今天北京天气怎么样?" | +| 📚 RAG 知识库检索 | 检索本地知识库 | "如何配置系统?" | +| 📇 通讯录管理 | 联系人 CRUD、邮件处理 | "帮我查看一下张三的联系方式" | +| 📖 智能词典 | 翻译、生词本、专业术语提取 | "帮我翻译这句话" | +| 📰 资讯分析 | 资讯获取、内容分析 | "帮我分析一下这篇新闻" | +| 📊 可视化图表 | 支持 Mermaid 图表生成 | "帮我画一个流程图" | ### 多模型切换 1. 在左侧边栏选择模型: - - **智谱 GLM-5.1**:在线服务,速度快 - - **DeepSeek V4-Pro**:深度推理模型 - - **本地 Gemma-4**:本地部署,隐私性好 + - **智谱 GLM-4**:在线服务,速度快 + - **DeepSeek V3**:深度推理模型 + - **OpenAI GPT-4o-mini**:通用对话模型 + - **本地 Qwen3.5-9B**:本地部署,隐私性好 2. 可随时切换,甚至在同一会话中 @@ -1578,17 +1626,20 @@ streamlit run frontend/src/frontend_main.py ### 添加新工具 -在 [backend/app/main_graph/tools/graph_tools.py](file:///home/huang/Study/AIProject/Agent1/backend/app/main_graph/tools/graph_tools.py) 中添加新的 `@tool` 装饰函数: +在 [backend/app/main_graph/tools/common_tools.py](file:///home/huang/Study/AIProject/Agent1/backend/app/main_graph/tools/common_tools.py) 中添加新的 `@tool` 装饰函数: ```python +from langchain_core.tools import tool +from typing import Optional + @tool def my_new_tool(param: str) -> str: """ 工具描述(会显示给 LLM) - + Args: param: 参数说明 - + Returns: 返回值说明 """ @@ -1596,11 +1647,11 @@ def my_new_tool(param: str) -> str: return result ``` -工具会自动注册到 `AVAILABLE_TOOLS` 列表中。 +然后在 [backend/app/main_graph/tools/graph_tools.py](file:///home/huang/Study/AIProject/Agent1/backend/app/main_graph/tools/graph_tools.py) 的 `AVAILABLE_TOOLS` 列表中注册。 ### 添加新模型 -在 [backend/app/model_services/chat_services.py](file:///root/projects/ailine/backend/app/model_services/chat_services.py) 中添加新的服务提供者: +在 [backend/app/model_services/chat_services.py](file:///home/huang/Study/AIProject/Agent1/backend/app/model_services/chat_services.py) 中添加新的服务提供者: ```python class NewModelChatProvider(BaseServiceProvider[BaseChatModel]): @@ -1647,58 +1698,127 @@ class NewModelChatProvider(BaseServiceProvider[BaseChatModel]): ```python CHAT_PROVIDERS: Dict[str, Callable[[], BaseServiceProvider[BaseChatModel]]] = { - "local": lambda: LocalVLLMChatProvider(), + "local": lambda: LocalChatProvider(), "zhipu": lambda: ZhipuChatProvider(), "deepseek": lambda: DeepSeekChatProvider(), + "openai": lambda: OpenAIChatProvider(), "new_model": lambda: NewModelChatProvider(), # 新增 } ``` -### 使用模型服务 +### 添加新的子图 -#### 生成式大模型 -```python -from app.model_services import get_chat_service, get_all_chat_services +#### 1. 创建子图目录结构 -# 自动选择可用服务(优先本地,降级智谱,再降级 DeepSeek) -llm = get_chat_service() +在 `backend/app/subgraphs/` 下创建新的子图目录: -# 获取所有可用模型(用于多模型切换) -all_llms = get_all_chat_services() # Dict[str, BaseChatModel] +``` +backend/app/subgraphs/ +└── my_subgraph/ + ├── __init__.py + ├── state.py # 子图状态定义 + ├── nodes.py # 子图节点实现 + ├── graph.py # 子图构建 + └── api_client.py # 外部 API 客户端(可选) ``` -#### 嵌入服务 +#### 2. 定义子图状态 + +在 `state.py` 中定义: + ```python -from app.model_services import get_embedding_service +from typing import TypedDict, Annotated, Literal +from langgraph.graph.message import add_messages -# 自动选择可用服务(优先本地,降级智谱) -embeddings = get_embedding_service() - -# 使用 -vector = embeddings.embed_query("hello") +class MySubgraphState(TypedDict): + """子图状态""" + messages: Annotated[list, add_messages] + user_id: str + query: str + result: str + step: Literal["init", "process", "format", "end"] ``` -#### 重排服务 +#### 3. 实现子图节点 + +在 `nodes.py` 中实现节点函数: + ```python -from app.model_services import get_rerank_service -from app.rag.rerank import create_document_reranker +from .state import MySubgraphState -# 获取原始重排服务(仅计算分数) -rerank_service = get_rerank_service() -scores = rerank_service.compute_scores("query", ["doc1", "doc2"]) +def process_query(state: MySubgraphState) -> MySubgraphState: + """处理查询""" + query = state["query"] + # 处理逻辑 + return { + "step": "format", + "result": "处理结果" + } -# 使用业务逻辑层(完整的文档重排) -reranker = create_document_reranker() -sorted_docs = reranker.compress_documents(docs, "query", top_n=5) +def format_output(state: MySubgraphState) -> MySubgraphState: + """格式化输出""" + result = state["result"] + return { + "step": "end", + "result": f"格式化后的结果: {result}" + } ``` +#### 4. 构建子图 + +在 `graph.py` 中构建: + +```python +from langgraph.graph import StateGraph, END +from .state import MySubgraphState +from .nodes import process_query, format_output + +def create_my_subgraph() -> StateGraph: + """创建子图""" + graph = StateGraph(MySubgraphState) + + graph.add_node("process_query", process_query) + graph.add_node("format_output", format_output) + + graph.set_entry_point("process_query") + + graph.add_edge("process_query", "format_output") + graph.add_edge("format_output", END) + + return graph +``` + +#### 5. 在主图中注册子图工具 + +在 [backend/app/main_graph/tools/subgraph_tools.py](file:///home/huang/Study/AIProject/Agent1/backend/app/main_graph/tools/subgraph_tools.py) 中添加子图调用工具: + +```python +@tool +async def my_subgraph_tool(query: str) -> str: + """ + 我的子图工具描述 + + Args: + query: 用户查询 + + Returns: + 子图执行结果 + """ + # 调用子图逻辑 + return result +``` + +#### 6. 在 React Reason 中添加路由 + +在 [backend/app/core/intent.py](file:///home/huang/Study/AIProject/Agent1/backend/app/core/intent.py) 的 `react_reason_async` 函数中添加对子图工具的支持。 + ### Docker 部署 项目包含完整的 Docker 配置: - **docker-compose.yml**:服务编排(Backend + Frontend,连接远程数据库) -- **Dockerfile.backend**:后端镜像构建 -- **Dockerfile.frontend**:前端镜像构建 +- **docker/Dockerfile.backend**:后端镜像构建 +- **docker/Dockerfile.frontend**:前端镜像构建 - **.gitea/workflows/deploy.yml**:CI/CD 自动化部署 详见 [QUICKSTART.md](QUICKSTART.md) 的 Docker 部署章节。 @@ -1721,27 +1841,38 @@ sorted_docs = reranker.compress_documents(docs, "query", top_n=5) - **本地开发**:`cp .env.docker .env`,修改为本地服务地址 - **Docker 部署**:`cp .env.docker .env`,使用远程服务器地址 -### 必需的环境变量 +### 重要配置(必需) -| 变量名 | 说明 | 本地开发示例 | Docker 部署示例 | -|--------|------|------------|----------------| -| `ZHIPUAI_API_KEY` | 智谱AI API密钥 | `your-api-key` | `your-api-key` | -| `DEEPSEEK_API_KEY` | DeepSeek API密钥 | `your-api-key` | `your-api-key` | -| `LLAMACPP_API_KEY` | llama.cpp API 密钥 | `token-abc123` | `token-abc123` | -| `LLM_API_KEY` | 主 LLM 服务 API 密钥 | `token-abc123` | `token-abc123` | -| `VLLM_BASE_URL` | LLM 服务地址 | `http://127.0.0.1:8081/v1` | `http://your-server:8081/v1` | -| `LLAMACPP_EMBEDDING_URL` | Embedding 服务地址 | `http://127.0.0.1:8082/v1` | `http://your-server:8082/v1` | -| `LLAMACPP_RERANKER_URL` | Rerank 服务地址 | `http://127.0.0.1:8083/v1` | `http://your-server:8083/v1` | -| `ZHIPU_EMBEDDING_MODEL` | 智谱嵌入模型(可选) | `embedding-3` | `embedding-3` | -| `ZHIPU_RERANK_MODEL` | 智谱重排模型(可选) | `rerank-2` | `rerank-2` | -| `ZHIPU_API_BASE` | 智谱 API 基础地址(可选) | `https://open.bigmodel.cn/api/paas/v4` | 同左 | -| `DB_URI` | PostgreSQL 连接字符串 | `postgresql://...@115.190.121.151:5432/langgraph_db` | 同左 | -| `QDRANT_URL` | Qdrant 向量数据库地址 | `http://115.190.121.151:6333` | 同左 | -| `QDRANT_API_KEY` | Qdrant API 密钥 | `your-api-key` | `your-api-key` | -| `QDRANT_COLLECTION_NAME` | Qdrant 集合名称 | `rag_documents` | `rag_documents` | -| `LOG_LEVEL` | 日志级别 | `INFO` | `WARNING` | -| `ENABLE_GRAPH_TRACE` | 是否启用图流转追踪 | `true` | `false` | -| `MEMORY_SUMMARIZE_INTERVAL` | 对话摘要生成间隔 | `10` | `10` | +| 变量名 | 说明 | 示例值 | +|--------|------|-------| +| `ZHIPUAI_API_KEY` | 智谱AI API密钥 | `your-api-key` | +| `DEEPSEEK_API_KEY` | DeepSeek API密钥 | `your-api-key` | +| `LLAMACPP_API_KEY` | llama.cpp API密钥 | `your-api-key` | +| `VLLM_BASE_URL` | 主 LLM 服务地址 | `http://127.0.0.1:18000/v1` | +| `LLAMACPP_EMBEDDING_URL` | Embedding 服务地址 | `http://127.0.0.1:18001/v1` | +| `LLAMACPP_RERANKER_URL` | Rerank 服务地址 | `http://127.0.0.1:18002/v1` | +| `DB_HOST` | PostgreSQL 主机 | `115.190.121.151` | +| `DB_PORT` | PostgreSQL 端口 | `5432` | +| `DB_USER` | PostgreSQL 用户名 | `postgres` | +| `DB_PASSWORD` | PostgreSQL 密码 | `your-password` | +| `DB_NAME` | PostgreSQL 数据库名 | `langgraph_db` | +| `QDRANT_URL` | Qdrant 向量数据库地址 | `http://115.190.121.151:6333` | +| `QDRANT_API_KEY` | Qdrant API 密钥 | `your-api-key` | + +### 其他配置(有默认值) + +| 变量名 | 说明 | 默认值 | +|--------|------|-------| +| `BACKEND_PORT` | 后端服务端口 | `8079` | +| `API_URL` | 前端调用后端地址 | `http://backend:8079/chat` | +| `MEMORY_SUMMARIZE_INTERVAL` | 对话摘要生成间隔 | `10` | +| `ENABLE_GRAPH_TRACE` | 是否启用图追踪 | `true` | +| `FASTEMBED_CACHE_PATH` | FastEmbed 缓存路径 | `/app/fastembed_cache` | +| `RAG_COLLECTION_NAME` | RAG 集合名称 | `rag_documents` | +| `RAG_STRATEGY` | RAG 切分策略 | `parent-child` | +| `RAG_STORAGE_TYPE` | RAG 存储类型 | `postgres` | +| `LOG_LEVEL` | 日志级别 | `DEBUG` | +| `DEBUG` | 调试模式 | `true` | ### 注意事项 @@ -1751,460 +1882,7 @@ sorted_docs = reranker.compress_documents(docs, "query", top_n=5) --- -## �️ 实现指南与最佳实践 - -### 1. RAG 知识库构建指南 - -#### 1.1 离线索引构建流程 - -```bash -# 步骤 1:准备文档 -# 将文档放入指定目录(如 ./documents/) -# 支持格式:TXT, PDF, Markdown, DOCX - -# 步骤 2:选择切分策略 -# 根据文档类型选择: -# - 结构化文档(如手册、API 文档)→ Recursive Splitting -# - 非结构化文档(如文章、报告)→ Semantic Chunking -# - 长文档(如书籍、论文)→ Parent-Child Chunking - -# 步骤 3:构建索引 -cd rag_indexer -python cli.py build \ - --input-dir ../documents \ - --collection-name my_knowledge \ - --splitter-type semantic \ - --chunk-size 500 - -# 步骤 4:验证索引 -python cli.py query \ - --collection-name my_knowledge \ - --query "如何配置系统?" \ - --top-k 5 -``` - -#### 1.2 切分策略选择建议 - -| 文档类型 | 推荐策略 | chunk_size | 说明 | -|---------|---------|------------|------| -| API 文档 | Recursive | 300-500 | 结构清晰,按章节切分 | -| 技术文章 | Semantic | 自适应 | 保持语义完整性 | -| 法律合同 | Parent-Child | 父:1000, 子:200 | 需要完整上下文 | -| 问答对 | Recursive | 200-300 | 短文本,精确匹配 | -| 产品手册 | Parent-Child | 父:1500, 子:300 | 长文档,跨章节检索 | - -#### 1.3 Embedding 模型选择 - -| 模型 | 维度 | 语言 | 速度 | 精度 | 适用场景 | -|------|------|------|------|------|---------| -| bge-large-zh-v1.5 | 1024 | 中文 | 中 | 高 | 中文文档检索 | -| bge-m3 | 1024 | 多语言 | 中 | 高 | 多语言混合文档 | -| text-embedding-3-small | 1536 | 英文 | 快 | 中 | 英文文档检索 | -| gte-large | 1024 | 中文 | 慢 | 很高 | 高精度要求场景 | - -### 2. 性能优化指南 - -#### 2.1 检索性能优化 - -```python -# 优化 1:调整检索参数 -search_kwargs = { - "k": 20, # 召回数量(增加召回,提高精度) - "score_threshold": 0.3, # 相似度阈值(过滤低质量结果) - "fetch_k": 50, # 初始召回数量(用于 MMR 去重) - "lambda_mult": 0.7, # MMR 多样性参数(0=去重,1=不去重) -} - -# 优化 2:使用缓存 -from functools import lru_cache - -@lru_cache(maxsize=1000) -def cached_retrieve(query: str) -> List[Document]: - """缓存常见查询结果""" - return retriever.invoke(query) - -# 优化 3:批量 Embedding -# 构建索引时,使用批量处理提高效率 -batch_size = 32 -for i in range(0, len(documents), batch_size): - batch = documents[i:i+batch_size] - embeddings = embedder.embed_documents([doc.page_content for doc in batch]) -``` - -#### 2.2 LLM 调用优化 - -```python -# 优化 1:使用流式响应 -# 减少首字延迟,提升用户体验 -response = await llm.astream(messages) -async for chunk in response: - yield chunk.content - -# 优化 2:控制上下文长度 -MAX_CONTEXT_LENGTH = 4000 - -def truncate_context(context: str, max_length: int = MAX_CONTEXT_LENGTH) -> str: - """截断上下文,保留开头和结尾""" - if len(context) <= max_length: - return context - half = max_length // 2 - return context[:half] + "\n...\n" + context[-half:] - -# 优化 3:Prompt 优化 -# 使用结构化 Prompt,提高 LLM 理解能力 -prompt_template = """ -你是一个智能助手。请根据以下上下文回答用户问题。 - -上下文: -{context} - -问题:{question} - -回答要求: -1. 仅基于上下文回答,不要编造信息 -2. 如果上下文中没有答案,明确告知用户 -3. 引用上下文中的具体信息时,注明来源 -""" -``` - -#### 2.3 数据库性能优化 - -```sql --- PostgreSQL 优化 --- 1. 为对话历史表添加索引 -CREATE INDEX idx_conversations_user_id ON conversations(user_id); -CREATE INDEX idx_conversations_created_at ON conversations(created_at); - --- 2. 为记忆摘要表添加索引 -CREATE INDEX idx_summaries_user_id ON summaries(user_id); - --- 3. 定期清理旧数据(保留最近 90 天) -DELETE FROM conversations -WHERE created_at < NOW() - INTERVAL '90 days'; - --- Qdrant 优化 --- 1. 使用 HNSW 索引(默认已启用) --- 2. 调整 ef_construct 和 m 参数平衡速度和精度 --- 3. 定期优化集合 -curl -X POST http://localhost:6333/collections/my_docs/actions \ - -H "Content-Type: application/json" \ - -d '{"actions": [{"optimize": {}}]}' -``` - -### 3. 扩展开发指南 - -#### 3.1 添加自定义工具 - -```python -# 在 backend/app/main_graph/tools/graph_tools.py 中添加 - -from langchain_core.tools import tool -from typing import Optional - -@tool -def calculate_expression( - expression: str, - precision: int = 2 -) -> str: - """ - 计算数学表达式的值。 - - 支持基本运算:加减乘除、幂运算、括号。 - - Args: - expression: 数学表达式,如 "2 + 3 * 4" - precision: 结果精度(小数位数),默认 2 - - Returns: - 计算结果 - """ - try: - # 安全计算(避免 eval 的安全风险) - import ast - import operator - - # 定义允许的操作符 - ops = { - ast.Add: operator.add, - ast.Sub: operator.sub, - ast.Mult: operator.mul, - ast.Div: operator.truediv, - ast.Pow: operator.pow, - } - - def eval_node(node): - if isinstance(node, ast.Num): - return node.n - elif isinstance(node, ast.BinOp): - left = eval_node(node.left) - right = eval_node(node.right) - return ops[type(node.op)](left, right) - else: - raise ValueError(f"不支持的操作: {type(node)}") - - tree = ast.parse(expression, mode='eval') - result = eval_node(tree.body) - return f"{result:.{precision}f}" - except Exception as e: - return f"计算错误: {str(e)}" - -# 工具会自动注册,无需手动添加到 AVAILABLE_TOOLS -``` - -#### 3.2 添加自定义 LLM 提供商 - -```python -# 在 backend/app/agent/llm_factory.py 中添加 - -from langchain_openai import ChatOpenAI -from langchain_core.language_models import BaseChatModel - -class LLMFactory: - @staticmethod - def create_anthropic_model() -> BaseChatModel: - """创建 Anthropic Claude 模型""" - api_key = os.getenv("ANTHROPIC_API_KEY") - if not api_key: - raise ValueError("ANTHROPIC_API_KEY 环境变量未设置") - - return ChatAnthropic( - model="claude-3-sonnet-20240229", - temperature=0.1, - max_tokens=4096, - api_key=api_key, - ) - - # 注册到 CREATORS - CREATORS = { - "local": create_local, - "deepseek": create_deepseek, - "zhipu": create_zhipu, - "anthropic": create_anthropic_model, # 新增 - } -``` - -#### 3.3 添加自定义 RAG 检索器 - -```python -# 在 backend/app/rag/retriever.py 中添加 - -from langchain_core.retrievers import BaseRetriever -from langchain_core.documents import Document - -class HybridRetriever(BaseRetriever): - """混合检索器:结合多种检索策略""" - - dense_retriever: BaseRetriever - sparse_retriever: BaseRetriever - reranker: Optional[BaseReranker] = None - top_k: int = 5 - - def _get_relevant_documents(self, query: str) -> List[Document]: - # 并行检索 - dense_docs = self.dense_retriever.invoke(query) - sparse_docs = self.sparse_retriever.invoke(query) - - # RRF 融合 - fused_docs = reciprocal_rank_fusion([dense_docs, sparse_docs]) - - # 重排序 - if self.reranker: - fused_docs = self.reranker.compress_documents(fused_docs[:20], query) - - return fused_docs[:self.top_k] -``` - -### 4. 部署最佳实践 - -#### 4.1 生产环境配置 - -```yaml -# docker-compose.prod.yml -version: '3.8' - -services: - backend: - build: - context: . - dockerfile: docker/Dockerfile.backend - environment: - - LOG_LEVEL=WARNING # 生产环境降低日志级别 - - ENABLE_GRAPH_TRACE=false # 关闭图追踪(提升性能) - - MEMORY_SUMMARIZE_INTERVAL=5 # 更频繁的摘要生成 - deploy: - resources: - limits: - cpus: '2' - memory: 4G - reservations: - cpus: '1' - memory: 2G - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8083/health"] - interval: 30s - timeout: 10s - retries: 3 - - frontend: - build: - context: . - dockerfile: docker/Dockerfile.frontend - environment: - - STREAMLIT_SERVER_PORT=8501 - - STREAMLIT_SERVER_HEADLESS=true - deploy: - resources: - limits: - cpus: '1' - memory: 2G - restart: unless-stopped -``` - -#### 4.2 监控与告警 - -```python -# 添加健康检查端点 -from fastapi import FastAPI -from datetime import datetime - -app = FastAPI() - -@app.get("/health") -async def health_check(): - """健康检查端点""" - return { - "status": "healthy", - "timestamp": datetime.now().isoformat(), - "services": { - "postgresql": await check_postgresql(), - "qdrant": await check_qdrant(), - "llm": await check_llm_service(), - } - } - -async def check_postgresql() -> bool: - try: - # 尝试连接数据库 - async with asyncpg.connect(DB_URI) as conn: - await conn.fetchval("SELECT 1") - return True - except Exception: - return False - -async def check_qdrant() -> bool: - try: - client = QdrantClient(url=QDRANT_URL) - client.get_collections() - return True - except Exception: - return False -``` - -#### 4.3 安全最佳实践 - -```python -# 1. 使用环境变量管理密钥 -# 永远不要硬编码 API Key -import os -from typing import Optional - -def get_api_key(env_var: str, default: Optional[str] = None) -> str: - api_key = os.getenv(env_var, default) - if not api_key: - raise ValueError(f"{env_var} 环境变量未设置") - return api_key - -# 2. 输入验证 -from pydantic import BaseModel, validator - -class ChatRequest(BaseModel): - message: str - user_id: str - model: str = "zhipu" - - @validator('message') - def validate_message(cls, v): - if len(v) > 4000: - raise ValueError("消息长度不能超过 4000 字符") - return v.strip() - - @validator('model') - def validate_model(cls, v): - allowed_models = {"zhipu", "deepseek", "local"} - if v not in allowed_models: - raise ValueError(f"不支持的模型: {v}") - return v - -# 3. 速率限制 -from slowapi import Limiter -from slowapi.util import get_remote_address - -limiter = Limiter(key_func=get_remote_address) - -@app.post("/api/chat") -@limiter.limit("10/minute") # 每分钟最多 10 次请求 -async def chat(request: Request, chat_request: ChatRequest): - ... -``` - -### 5. 调试与测试 - -#### 5.1 启用调试模式 - -```bash -# 启用详细日志 -export LOG_LEVEL=DEBUG - -# 启用 LangGraph 图追踪 -export ENABLE_GRAPH_TRACE=true - -# 查看图执行流程 -# 启动后访问:http://localhost:8083/graph/trace -``` - -#### 5.2 单元测试示例 - -```python -# tests/test_rag.py -import pytest -from app.rag.fusion import reciprocal_rank_fusion -from langchain_core.documents import Document - -def test_rrf_fusion(): - # 准备测试数据 - docs1 = [ - Document(page_content="文档 A"), - Document(page_content="文档 B"), - ] - docs2 = [ - Document(page_content="文档 B"), - Document(page_content="文档 C"), - ] - - # 执行 RRF 融合 - result = reciprocal_rank_fusion([docs1, docs2]) - - # 验证结果 - assert len(result) == 3 - assert result[0].page_content == "文档 B" # B 在两个列表中都出现,应该排第一 - -@pytest.mark.asyncio -async def test_retriever(): - from app.rag.retriever import create_base_retriever - - retriever = create_base_retriever( - collection_name="test_collection", - embeddings=mock_embeddings, - ) - - docs = await retriever.ainvoke("测试查询") - assert len(docs) > 0 -``` - ---- - -## �� 故障排查 +## 🔍 故障排查 ### 常见问题 @@ -2218,15 +1896,20 @@ curl http://115.190.121.151:6333/collections ``` **Q: 后端启动失败?** -- 确认端口 8083 未被占用 +- 确认端口 8079 未被占用 - 检查 `.env` 中的 API Key 是否正确 - 查看启动日志确认模型初始化成功 **Q: 模型切换后无响应?** - 检查所选模型的配置是否正确 -- 确认 vLLM 容器是否运行(如使用本地模型) +- 确认 llama.cpp 服务是否运行(如使用本地模型) - 尝试切换到另一个模型 +**Q: 混合路由异常?** +- 检查 `ENABLE_GRAPH_TRACE=true` 查看详细执行流程 +- 确认快速路径工具是否正确注册 +- 查看 React Reason 节点的输出 + 更多问题排查请查看 [QUICKSTART.md](QUICKSTART.md) --- diff --git a/backend/app/agent/agent_service.py b/backend/app/agent/agent_service.py index d5d29b6..aa8a8c8 100644 --- a/backend/app/agent/agent_service.py +++ b/backend/app/agent/agent_service.py @@ -10,7 +10,6 @@ import asyncio from ..main_graph.utils.main_graph_builder import build_react_main_graph from ..main_graph.tools.graph_tools import AVAILABLE_TOOLS, TOOLS_BY_NAME from ..main_graph.config import set_stream_writer -from ..model_services.chat_services import get_all_chat_services, LocalVLLMChatProvider from ..main_graph.utils.rag_initializer import init_rag_tool from ..core.intent_classifier import get_intent_classifier from ..logger import info, warning, error @@ -33,18 +32,10 @@ class AIAgentService: async def initialize(self): # 0. 初始化 Mem0 客户端 from ..memory.mem0_client import Mem0Client - # 创建一个临时的 LLM 用于 Mem0(用第一个可用的) - chat_services = get_all_chat_services() - temp_llm = None - if chat_services: - temp_llm = list(chat_services.values())[0] - self.mem0_client = Mem0Client(temp_llm) + self.mem0_client = Mem0Client() # 1. 初始化 RAG 工具(如果需要) - def create_local_llm(): - provider = LocalVLLMChatProvider() - return provider.get_service() - rag_tool = await init_rag_tool(create_local_llm) + rag_tool = await init_rag_tool() if rag_tool: self.tools.append(rag_tool) self.tools_by_name[rag_tool.name] = rag_tool diff --git a/backend/app/main_graph/utils/rag_initializer.py b/backend/app/main_graph/utils/rag_initializer.py index 7351635..becd5e7 100644 --- a/backend/app/main_graph/utils/rag_initializer.py +++ b/backend/app/main_graph/utils/rag_initializer.py @@ -20,12 +20,11 @@ def is_initialized() -> bool: return _initialized -async def init_rag_tool(local_llm_creator, force: bool = False): +async def init_rag_tool(force: bool = False): """ - 初始化 RAG 工具(注册到模块级变量) + 初始化 RAG 工具(注册到模块级变量,内部获取所需服务) Args: - local_llm_creator: 返回 LLM 实例的函数 force: 是否强制重新初始化 Returns: @@ -39,20 +38,22 @@ async def init_rag_tool(local_llm_creator, force: bool = False): return _rag_tool try: + from app.model_services.chat_services import get_chat_service + info("🔄 正在初始化 RAG 检索系统...") embeddings = get_embedding_service() retriever = create_parent_hybrid_retriever( collection_name="rag_documents", search_k=5, - embeddings=embeddings + embeddings=embeddings, ) - rewrite_llm = local_llm_creator() + rewrite_llm = get_chat_service() rag_tool = create_rag_tool( retriever=retriever, llm=rewrite_llm, num_queries=3, - rerank_top_n=5 + rerank_top_n=5, ) _rag_tool = rag_tool diff --git a/backend/app/memory/mem0_client.py b/backend/app/memory/mem0_client.py index f004fe6..1c53bf7 100644 --- a/backend/app/memory/mem0_client.py +++ b/backend/app/memory/mem0_client.py @@ -1,33 +1,36 @@ -from app.config import ( - LLM_API_KEY, ZHIPUAI_API_KEY, - VLLM_BASE_URL, QDRANT_URL, QDRANT_COLLECTION_NAME, QDRANT_API_KEY, - LLAMACPP_EMBEDDING_URL, LLAMACPP_API_KEY, - ZHIPU_EMBEDDING_MODEL, ZHIPU_API_BASE -) -from ..model_services import get_embedding_service -from app.logger import info, warning, error -import time """ Mem0 记忆层客户端封装模块 负责 Mem0 的初始化、检索和存储 """ import asyncio -from typing import Optional, List, Dict +import time +from typing import Optional, List + from mem0 import AsyncMemory +from app.config import ( + LLM_API_KEY, + ZHIPUAI_API_KEY, + VLLM_BASE_URL, + QDRANT_URL, + QDRANT_COLLECTION_NAME, + QDRANT_API_KEY, + LLAMACPP_EMBEDDING_URL, + LLAMACPP_API_KEY, + ZHIPU_EMBEDDING_MODEL, + ZHIPU_API_BASE, +) +from app.logger import info, warning, error +from app.model_services import get_embedding_service +from app.model_services.chat_services import get_chat_service + class Mem0Client: """Mem0 异步客户端封装类""" - def __init__(self, llm_instance): - """ - 初始化 Mem0 客户端 - - Args: - llm_instance: LangChain LLM 实例(用于事实提取) - """ - self.llm = llm_instance + def __init__(self): + """初始化 Mem0 客户端(内部获取所需服务)""" self.mem0: Optional[AsyncMemory] = None self._initialized = False @@ -35,7 +38,7 @@ class Mem0Client: """异步初始化 Mem0 客户端,并进行实际连接测试""" if self._initialized: return - + try: # 获取可用的 embedding 服务并确定维度 info("🔄 正在获取嵌入服务...") @@ -43,14 +46,16 @@ class Mem0Client: test_embedding = embeddings.embed_query("test") embedding_dim = len(test_embedding) info(f"✅ 嵌入服务可用,向量维度: {embedding_dim}") - - # 构建 embedder 配置 - 改进的方法 - # 检查本地 provider - from ..model_services.embedding_services import LocalLlamaCppEmbeddingProvider, ZhipuEmbeddingProvider - + + # 构建 embedder 配置 + from app.model_services.embedding_services import ( + LocalLlamaCppEmbeddingProvider, + ZhipuEmbeddingProvider, + ) + embedder_config = None local_provider = LocalLlamaCppEmbeddingProvider() - + if local_provider.is_available(): info("✅ 使用本地 llama.cpp 作为 mem0 embedder") embedder_config = { @@ -59,22 +64,20 @@ class Mem0Client: "model": "Qwen3-Embedding-0.6B-Q8_0", "api_key": LLAMACPP_API_KEY or "dummy-key", "openai_base_url": LLAMACPP_EMBEDDING_URL, - } + }, } else: # 检查智谱 zhipu_provider = ZhipuEmbeddingProvider() if zhipu_provider.is_available(): info("✅ 使用智谱 API 作为 mem0 embedder") - # 使用自定义 embedder 或者 openai 兼容方式 - # 注意:这里我们使用一个特殊的配置方法 embedder_config = { "provider": "openai", "config": { "model": ZHIPU_EMBEDDING_MODEL, "api_key": ZHIPUAI_API_KEY, "openai_base_url": ZHIPU_API_BASE, - } + }, } else: # 都不可用,使用 dummy 配置并警告 @@ -83,12 +86,17 @@ class Mem0Client: "provider": "openai", "config": { "model": "text-embedding-ada-002", - "api_key": "dummy-key", + "api_key": "***", "openai_base_url": "http://localhost:8080/v1", - } + }, } - - # Mem0 配置 - 简化配置,先确保能启动 + + # 获取 LLM 服务(内部获取) + info("🔄 正在获取 LLM 服务...") + chat_llm = get_chat_service() + info("✅ LLM 服务获取成功") + + # Mem0 配置 info("🔄 正在构建 Mem0 配置...") config = { "vector_store": { @@ -98,7 +106,7 @@ class Mem0Client: "api_key": QDRANT_API_KEY, "collection_name": QDRANT_COLLECTION_NAME, "embedding_model_dims": embedding_dim, - } + }, }, "llm": { "provider": "openai", @@ -108,31 +116,30 @@ class Mem0Client: "openai_base_url": VLLM_BASE_URL or ZHIPU_API_BASE, "temperature": 0.1, "max_tokens": 2000, - } + }, }, "embedder": embedder_config, - "version": "v1.1" + "version": "v1.1", } - + info("🔄 正在初始化 Mem0 实例...") self.mem0 = AsyncMemory.from_config(config) info("✅ Mem0 配置加载成功") - + # 尝试进行连接测试,但失败不会阻止初始化 try: info("🔄 正在测试 Mem0 连接...") - # 使用短超时的测试 await asyncio.wait_for( self.mem0.search("ping", user_id="test", limit=1), - timeout=10.0 + timeout=10.0, ) info("✅ Mem0 连接测试成功") except Exception as e: warning(f"⚠️ Mem0 连接测试遇到问题(但继续使用): {e}") - + self._initialized = True info("🎉 Mem0 初始化完成") - + except asyncio.TimeoutError: error("❌ Mem0 初始化超时") self.mem0 = None @@ -140,11 +147,14 @@ class Mem0Client: except Exception as e: error(f"❌ Mem0 初始化失败: {e}") import traceback + error(f"详细错误信息:\n{traceback.format_exc()}") self.mem0 = None self._initialized = False - async def search_memories(self, query: str, user_id: str, limit: int = 5) -> List[str]: + async def search_memories( + self, query: str, user_id: str, limit: int = 5 + ) -> List[str]: """ 检索相关记忆 @@ -163,7 +173,7 @@ class Mem0Client: try: memories = await asyncio.wait_for( self.mem0.search(query, user_id=user_id, limit=limit), - timeout=30.0 + timeout=30.0, ) if memories and "results" in memories: @@ -183,17 +193,25 @@ class Mem0Client: return [] async def add_memories(self, messages, user_id): - if not self.mem0: - return False - try: - start = time.time() - info(f"📝 开始 Mem0 add,消息数: {len(messages)}") - await asyncio.wait_for( - self.mem0.add(messages, user_id=user_id, metadata={"type": "conversation"}), - timeout=60.0 - ) - info(f"✅ Mem0 add 完成,耗时: {time.time() - start:.2f}s") - return True - except asyncio.TimeoutError: - error(f"❌ Mem0 记忆添加超时 (60s),已等待 {time.time() - start:.2f}s") - return False \ No newline at end of file + """添加记忆""" + if not self.mem0: + return False + try: + start = time.time() + info(f"📝 开始 Mem0 add,消息数: {len(messages)}") + await asyncio.wait_for( + self.mem0.add( + messages, user_id=user_id, metadata={"type": "conversation"} + ), + timeout=60.0, + ) + info(f"✅ Mem0 add 完成,耗时: {time.time() - start:.2f}s") + return True + except asyncio.TimeoutError: + error( + f"❌ Mem0 记忆添加超时 (60s),已等待 {time.time() - start:.2f}s" + ) + return False + except Exception as e: + error(f"❌ Mem0 add 失败: {e}") + return False diff --git a/tools/test/test_graph_branches.py b/tools/test/test_graph_branches.py index 32ca0f3..a25aa80 100644 --- a/tools/test/test_graph_branches.py +++ b/tools/test/test_graph_branches.py @@ -31,25 +31,25 @@ TEST_CASES = [ "query": "吕布的事迹?", "description": "测试快速 RAG 分支" }, - # # 测试3: 需要推理的复杂问题 - 应该直接到 React 循环 - # { - # "name": "复杂推理测试", - # "query": "请帮我分析:如果我有10万元,想要在一年内获得15%的收益,有哪些低风险的投资方案?", - # "description": "测试 React 循环推理分支" - # }, + # 测试3: 需要推理的复杂问题 - 应该直接到 React 循环 + { + "name": "复杂推理测试", + "query": "请帮我分析:如果我有10万元,想要在一年内获得15%的收益,有哪些低风险的投资方案?", + "description": "测试 React 循环推理分支" + }, # # 测试4: 需要工具调用的问题 # { # "name": "工具调用测试", # "query": "搜索一下今天的天气怎么样", # "description": "测试工具调用分支" # }, - # # 测试5: 带记忆的对话 - # { - # "name": "记忆测试", - # "query": "你刚才回答了我什么问题?", - # "description": "测试记忆检索分支", - # "thread_id": "test_memory_thread" - # } + # 测试5: 带记忆的对话 + { + "name": "记忆测试", + "query": "你刚才回答了我什么问题?", + "description": "测试记忆检索分支", + "thread_id": "test_memory_thread" + } ]