diff --git a/backend/app/agent/prompts.py b/backend/app/agent/prompts.py
index cc3a973..6c2e789 100644
--- a/backend/app/agent/prompts.py
+++ b/backend/app/agent/prompts.py
@@ -1,45 +1,69 @@
-# app/prompts.py
+"""
+Agent 提示词定义
+"""
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
-def create_system_prompt() -> ChatPromptTemplate:
- """
- 创建系统提示模板,整合多子系统能力、检索策略与回答规范。
- """
- # 使用 f-string 将 tools_section 直接嵌入,而 memory_context 用双花括号转义保留为变量
- system_template = f'''你是一个智能助手,具备以下专业子系统和检索能力。请使用中文交流。
-## 核心功能
-1. 📚 词典/翻译子系统 – 查询单词、翻译文本、提取术语、每日一词
-2. 📰 资讯分析子系统 – 查询新闻、分析URL、提取关键词、生成报告
-3. 📇 通讯录子系统 – 查询联系人、添加联系人、管理通讯录
-4. 🔍 RAG检索 – 从知识库中检索相关信息回答问题
+SYSTEM_PROMPT = """你是一个智能助手,能够通过工具来增强自己的能力。
+
+## 可用工具
+1. rag_search(query: str) - 检索知识库获取相关信息
+2. web_search(query: str) - 联网搜索获取最新信息
+3. contact_lookup(query: str) - 查询通讯录信息
+4. dictionary_lookup(word: str) - 查询词典获取单词释义
+5. news_analysis(topic: str) - 分析热点新闻和资讯
+
+## 信息获取优先级(严格遵守)
+当需要获取信息时,按以下顺序处理:
+1. **RAG 检索**:首先尝试从知识库查找答案
+2. **ReRAG**:如果结果不相关,优化查询后再次 RAG(最多 1 次)
+3. **联网搜索**:RAG 失败后才使用联网搜索
-## 检索与信息获取策略
-当收到用户问题时,请按以下优先级处理:
-1. **RAG 检索(第1次)**:首先尝试从知识库中查找答案。
-2. **ReRAG(第2次优化检索)**:如果第一次检索结果不相关或不充分,可以优化查询后再次进行 RAG 检索。
-3. **联网搜索**:如果两次 RAG 检索后仍无法获得满意答案,必须使用联网搜索获取最新信息。
**重要约束**:
-- 最多进行 **2 次** RAG 检索尝试。
-- 第3次决定获取信息时,必须选择**联网搜索**,禁止无休止的本地检索。
-- 如果已经明确知识库不包含该信息(例如用户询问实时新闻),可以直接进入联网搜索。
+- RAG 最多尝试 2 次(1次RAG + 1次ReRAG)
+- 第3次必须联网搜索,禁止无限本地检索
+- 如果明确是实时问题(新闻、天气等),可直接联网搜索
+
+## 思维链要求
+每次决定前,简要说明:
+- 当前问题是什么
+- 为什么需要/不需要调用工具
+- 如果调用,选择哪个工具,为什么
+
+格式:[思考] xxx
+
+## 工具调用约束(严格遵守)
+**同一工具连续调用不超过2次**,且必须满足以下条件之一才继续:
+1. 第二次调用**明显改进了参数**(更精确的查询、不同的关键词)
+2. 第二次结果**提供了增量信息**(与第一次有明显不同)
+
+**禁止**:
+- 参数完全相同或高度相似的重复调用
+- 返回内容高度相似的循环
+- 不改进参数就一直调用
+
+如果第二次调用仍无增量或参数未改进,**必须停止调用该工具**,直接基于已有信息回答或说明无法回答。
+
+## 回答规范
+1. 回答简洁明了,重点突出
+2. 如有引用,使用【来源: xxx】标注
+3. 结合用户背景信息进行个性化回复
+4. 无可靠依据时,如实说明无法回答
## 用户背景信息
-以下是当前用户的已知信息和长期记忆,你应在回答中优先利用这些信息进行个性化回复:
-{{memory_context}}
-若无相关信息,可礼貌询问或提供通用帮助。
+{memory_context}
-## 回答要求(必须严格遵守)
-1. **来源标注**:回答开头必须明确标注信息来源,格式如下:
- - 使用知识库时:`【知识库:来源描述】`
- - 使用联网搜索时:`【联网搜索:来源描述】`
- - 若同时用到多个来源,按实际使用顺序标注,例如:`【知识库:三国演义】【联网搜索:百度百科】`
-2. **思维链**:如果问题需要深入推理或复杂思考,请将推理过程用 `...` 标签包裹,放在回答最前面(来源标注之前)。
-3. **简洁直接**:回答应重点突出、条理清晰,避免冗长。
-4. **个性化**:结合用户信息进行针对性回复。
-5. **无依据时**:若既无知识库支撑也无联网搜索结果,请如实说明无法回答,并建议用户提供更多信息或尝试其他方式。
-'''
+## 特别注意
+- 不要暴露工具调用的技术细节
+- 闲聊直接回复,禁止调用工具
+- 每次工具调用后,检查结果是否足够回答问题
+现在,请遵循以上规则处理用户的每一次输入。"""
+
+
+def create_agent_prompt():
+ """创建 Agent 使用的 PromptTemplate"""
return ChatPromptTemplate.from_messages([
- ("system", system_template),
+ ("system", SYSTEM_PROMPT),
MessagesPlaceholder(variable_name="messages")
- ])
\ No newline at end of file
+ ])
diff --git a/backend/app/core/intent_classifier.py b/backend/app/core/intent_classifier.py
deleted file mode 100644
index 9452ae5..0000000
--- a/backend/app/core/intent_classifier.py
+++ /dev/null
@@ -1,190 +0,0 @@
-# backend/app/agent/intent_classifier.py
-
-from enum import Enum
-from dataclasses import dataclass
-from typing import Optional, Dict, Any
-import sys
-import os
-
-from ..model_services.chat_services import get_small_llm_service
-
-
-class IntentType(Enum):
- """意图类型枚举"""
- KNOWLEDGE = "knowledge" # 知识查询 → RAG
- REALTIME = "realtime" # 实时数据 → 工具
- ACTION = "action" # 执行操作 → 工具
- CHITCHAT = "chitchat" # 闲聊 → 直接回答
- CLARIFY = "clarify" # 需要澄清 → 反问用户
- MIXED = "mixed" # 复杂任务 → React 循环
- UNKNOWN = "unknown" # 未知意图
-
-
-@dataclass
-class IntentResult:
- """意图识别结果"""
- intent_type: IntentType
- confidence: float
- reasoning: str
- metadata: Dict[str, Any] = None
-
-
-class IntentClassifier:
- """意图分类器"""
-
- def __init__(self):
- self.llm = get_small_llm_service()
- self._intent_examples = self._build_examples()
-
- def _build_examples(self) -> str:
- """构建少样本示例"""
- return """
- <示例>
- 用户: "公司的报销政策是什么?"
- 意图: knowledge
- 推理: 用户询问公司内部政策,需要查询知识库
-
- 用户: "帮我查一下订单 12345 的状态"
- 意图: realtime
- 推理: 需要查询实时订单数据
-
- 用户: "帮我申请退款,订单号 67890"
- 意图: action
- 推理: 需要执行退款操作
-
- 用户: "今天天气怎么样?"
- 意图: realtime
- 推理: 需要查询实时天气数据
-
- 用户: "帮我写一份邮件给客户,查询订单状态,然后附上退款政策"
- 意图: mixed
- 推理: 需要查询订单、查询政策、生成邮件,多步骤任务
-
- 用户: "你好"
- 意图: chitchat
- 推理: 简单寒暄
-
- 用户: "我想查点东西..."
- 意图: clarify
- 推理: 用户没有说清楚要查什么
- 示例>
- """
-
- async def classify(self, user_input: str, context: Optional[str] = None) -> IntentResult:
- """
- 分类用户意图
-
- Args:
- user_input: 用户输入
- context: 对话上下文(可选)
-
- Returns:
- IntentResult
- """
- prompt = self._build_classification_prompt(user_input, context)
-
- try:
- response = await self.llm.ainvoke(prompt)
- result = self._parse_response(response.content)
- return result
- except Exception as e:
- print(f"Intent classification error: {e}")
- # 降级策略:默认返回 mixed,走 React 循环
- return IntentResult(
- intent_type=IntentType.MIXED,
- confidence=0.5,
- reasoning="分类失败,走通用路径"
- )
-
- def _build_classification_prompt(self, user_input: str, context: Optional[str]) -> str:
- """构建分类提示词"""
- context_part = f"\n对话上下文:\n{context}" if context else ""
-
- return f"""
- 你是一个专业的意图识别助手。请分析用户的输入,判断其意图类型。
-
- 可选意图类型:
- - knowledge: 用户询问知识、政策、文档等,需要查询知识库
- - realtime: 用户需要查询实时数据(订单状态、天气、股票等)
- - action: 用户需要执行某项操作(退款、下单、发送邮件等)
- - chitchat: 用户只是闲聊、打招呼,不需要工具或检索
- - clarify: 用户的问题不明确,需要追问澄清
- - mixed: 复杂任务,需要多步骤处理(同时需要检索+工具)
-
- {self._intent_examples}
-
- 用户输入: {user_input}
- {context_part}
-
- 请按以下格式输出(纯JSON):
- {{
- "intent": "knowledge|realtime|action|chitchat|clarify|mixed",
- "confidence": 0.85,
- "reasoning": "简要说明为什么这个意图"
- }}
- """
-
- def _parse_response(self, response: str) -> IntentResult:
- """解析 LLM 响应"""
- import json
- import re
-
- # 尝试提取 JSON
- json_match = re.search(r'\{[\s\S]*\}', response)
- if json_match:
- try:
- data = json.loads(json_match.group())
- return IntentResult(
- intent_type=IntentType(data['intent']),
- confidence=float(data['confidence']),
- reasoning=data['reasoning']
- )
- except:
- pass
-
- # 降级策略:关键词匹配
- return self._fallback_classify(response)
-
- def _fallback_classify(self, user_input: str) -> IntentResult:
- """关键词匹配降级策略"""
- keywords = {
- IntentType.KNOWLEDGE: ['政策', '文档', '规定', '手册', '指南', '什么是', '怎么'],
- IntentType.REALTIME: ['订单', '状态', '天气', '股票', '价格', '库存'],
- IntentType.ACTION: ['退款', '取消', '发送', '申请', '修改', '删除'],
- IntentType.CHITCHAT: ['你好', 'hi', 'hello', '嗨', '早上好', '晚上好'],
- }
-
- for intent_type, words in keywords.items():
- if any(word in user_input.lower() for word in words):
- return IntentResult(
- intent_type=intent_type,
- confidence=0.7,
- reasoning=f"关键词匹配: {', '.join(words)}"
- )
-
- # 默认走混合路径
- return IntentResult(
- intent_type=IntentType.MIXED,
- confidence=0.5,
- reasoning="无法明确分类,走通用路径"
- )
-
- async def batch_classify(self, inputs: list[str]) -> list[IntentResult]:
- """批量分类(带缓存)"""
- # 可以添加缓存逻辑
- results = []
- for inp in inputs:
- results.append(await self.classify(inp))
- return results
-
-
-# 全局实例
-_classifier: Optional[IntentClassifier] = None
-
-
-def get_intent_classifier() -> IntentClassifier:
- """获取意图分类器实例"""
- global _classifier
- if _classifier is None:
- _classifier = IntentClassifier()
- return _classifier
\ No newline at end of file
diff --git a/backend/app/deprecated/_utils.py b/backend/app/deprecated/_utils.py
deleted file mode 100644
index e09f105..0000000
--- a/backend/app/deprecated/_utils.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""
-主图节点通用工具模块
-包含事件发送、状态更新等通用功能
-"""
-
-from typing import Dict, Any, Optional
-from langchain_core.runnables.config import RunnableConfig
-
-
-async def dispatch_custom_event(
- event_name: str,
- data: Dict[str, Any],
- config: Optional[RunnableConfig] = None,
-) -> None:
- """
- 安全地发送自定义事件,忽略发送失败
-
- Args:
- event_name: 事件名称
- data: 事件数据
- config: LangChain 配置
- """
- if not config:
- return
- try:
- from langchain_core.callbacks.manager import adispatch_custom_event
- await adispatch_custom_event(event_name, data, config=config)
- except Exception:
- # 事件发送失败不应中断主流程
- pass
-
-
-def make_react_event(
- step: int,
- action: str,
- confidence: float = 1.0,
- reasoning: str = ""
-) -> Dict[str, Any]:
- """
- 构造标准推理事件数据
-
- Args:
- step: 当前步数
- action: 动作名称
- confidence: 置信度
- reasoning: 推理过程
-
- Returns:
- 事件数据字典
- """
- return {
- "step": step,
- "action": action,
- "confidence": confidence,
- "reasoning": reasoning
- }
diff --git a/backend/app/deprecated/error_handling.py b/backend/app/deprecated/error_handling.py
deleted file mode 100644
index e077fbd..0000000
--- a/backend/app/deprecated/error_handling.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""
-错误处理节点 - 处理子图/工具调用错误
-"""
-
-from ...main_graph.state import MainGraphState, ErrorSeverity
-from backend.app.logger import info
-
-
-def error_handling_node(state: MainGraphState) -> MainGraphState:
- """
- 错误处理节点:处理子图/工具调用错误
-
- 返回结构化错误信息,格式如下:
- {
- "tool/node": "...",
- "status": "failed",
- "error": "...",
- "retries_exceeded": true/false,
- "suggestion": "..."
- }
- """
- state.current_phase = "error_handling"
-
- if not state.current_error:
- state.current_phase = "react_reasoning"
- return state
-
- error = state.current_error
-
- # 更新错误状态
- state.error_message = f"{error.error_type}: {error.error_message}"
-
- # 记录结构化错误信息
- structured_error = {
- "tool": error.source,
- "status": "failed",
- "error": error.error_message,
- "retries_exceeded": error.retry_count >= error.max_retries,
- "retry_count": error.retry_count,
- "max_retries": error.max_retries
- }
-
- # 根据错误类型添加建议
- if "RAG" in error.error_type:
- structured_error["suggestion"] = "尝试重新表述问题或直接询问"
- elif "subgraph" in error.source or "contact" in error.source:
- structured_error["suggestion"] = "子图执行失败,请尝试简化查询"
- elif "timeout" in error.error_message.lower():
- structured_error["suggestion"] = "请求超时,请稍后再试"
- else:
- structured_error["suggestion"] = "请尝试其他方式提问"
-
- state.debug_info["structured_error"] = structured_error
-
- # 策略1: 检查是否可以重试
- can_retry = (
- error.severity in [ErrorSeverity.WARNING, ErrorSeverity.ERROR]
- and error.retry_count < error.max_retries
- )
-
- if can_retry:
- error.retry_count += 1
- state.retry_action = error.source
- state.debug_info["retry_count"] = error.retry_count
-
- if "RAG" in error.error_type:
- state.last_action = "RE_RETRIEVE_RAG"
- elif "subgraph" in error.source:
- state.last_action = "DIRECT_RESPONSE"
- else:
- state.last_action = "REASON"
-
- state.current_phase = "retrying"
- return state
-
- # 策略2: 无法重试,尝试降级方案
- if error.severity != ErrorSeverity.FATAL:
- state.final_result = (
- f"⚠️ 遇到一些问题:\n"
- f"```json\n{structured_error}\n```\n"
- f"但我会尽力用现有信息回答您。"
- )
- state.success = True
- state.current_phase = "finalizing"
- return state
-
- # 策略3: 致命错误
- state.final_result = (
- f"❌ 服务暂时不可用,请稍后再试。\n"
- f"```json\n{structured_error}\n```"
- )
- state.success = False
- state.current_phase = "finalizing"
-
- return state
diff --git a/backend/app/deprecated/fast_paths.py b/backend/app/deprecated/fast_paths.py
deleted file mode 100644
index b076937..0000000
--- a/backend/app/deprecated/fast_paths.py
+++ /dev/null
@@ -1,226 +0,0 @@
-"""
-快速路径节点模块
-包含闲聊、RAG、工具等快速处理节点
-"""
-
-from typing import Optional
-from langchain_core.runnables.config import RunnableConfig
-
-from ..state import MainGraphState
-from backend.app.logger import info, debug
-from ...model_services.chat_services import get_small_llm_service, get_chat_service
-from .rag_nodes import rag_retrieve_node
-from ._utils import dispatch_custom_event
-
-
-# ========== 闲聊回复模板 ==========
-CHITCHAT_TEMPLATES = {
- "谢谢": "不客气!如果还有其他问题,请随时告诉我 😊",
- "再见": "再见!期待下次为您服务 👋",
- "你好": "你好!有什么我可以帮您的吗?",
- "默认": None # 使用 LLM 生成
-}
-
-CHITCHAT_KEYWORDS = {
- "谢谢": ["谢谢", "感谢", "thanks", "thank you"],
- "再见": ["再见", "拜拜", "bye", "goodbye"],
- "你好": ["你好", "您好", "hi", "hello", "hey", "早上好", "晚上好", "下午好"],
-}
-
-
-# ========== 闲聊节点 ==========
-async def fast_chitchat_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- """快速闲聊节点"""
- state.current_phase = "fast_chitchat"
- query = state.user_query or ""
- info(f"[Fast Chitchat] 处理: {query[:50]}")
-
- # 发送开始事件
- await dispatch_custom_event("fast_path_start", {"path": "fast_chitchat"}, config)
-
- # 清除之前的 final_result,让 llm_call 生成新回答
- state.final_result = None
-
- # 标记快速路径成功,但不设置 final_result,让 llm_call 生成回答
- state.success = True
- state.current_phase = "llm_call"
- state.fast_path.chitchat_success = True
-
- # 发送完成事件
- await dispatch_custom_event("fast_path_end", {"path": "fast_chitchat", "success": True}, config)
-
- return state
-
-
-def _match_chitchat_template(query: str) -> str:
- """匹配闲聊模板"""
- query_clean = query.strip().lower()
-
- for intent, keywords in CHITCHAT_KEYWORDS.items():
- if any(kw in query_clean for kw in keywords):
- return CHITCHAT_TEMPLATES[intent]
-
- # 默认:使用 LLM 生成
- try:
- llm = get_small_llm_service()
- response = llm.invoke(f"你是一个友好的助手。用户说:{query}。请简短友好地回复:")
- return response.content
- except Exception:
- return "你好!有什么我可以帮您的吗?"
-
-
-# ========== 快速 RAG 节点 ==========
-async def fast_rag_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- """快速 RAG 节点:只负责 RAG 检索,然后交给 llm_call 生成回答"""
- state.current_phase = "fast_rag"
- query = state.user_query or ""
- info(f"[Fast RAG] 开始处理: {query[:50]}")
-
- # 获取 RAG 工具
- from backend.app.main_graph.utils.rag_initializer import get_rag_tool
- rag_tool = get_rag_tool()
- info(f"[Fast RAG] 获取到 rag_tool: {rag_tool is not None}")
-
- # 发送开始事件
- await dispatch_custom_event("fast_path_start", {"path": "fast_rag"}, config)
-
- # 清除之前的 final_result,让 llm_call 生成新回答
- state.final_result = None
-
- # 如果没有 rag_tool,升级到 React 循环
- if not rag_tool:
- info("[Fast RAG] 未找到 RAG 工具,升级到 React 循环")
- return _mark_fast_path_failed(state, "未找到 RAG 工具")
-
- try:
- # 尝试 RAG 检索
- state = await rag_retrieve_node(state, config)
-
- # 检查检索结果
- if _has_valid_rag_results(state):
- info(f"[Fast RAG] 检索有效,进入 llm_call 生成回答")
- await dispatch_custom_event("fast_path_end", {"path": "fast_rag", "success": True}, config)
- # 注意:这里不设置 final_result,让 llm_call 节点处理
- return state
-
- # 检索结果无效:标记失败,升级到 React 循环
- info("[Fast RAG] 无有效检索结果,升级到 React 循环")
- await dispatch_custom_event("fast_path_end", {"path": "fast_rag", "success": False}, config)
- return _mark_fast_path_failed(state, "无有效检索结果")
-
- except Exception as e:
- info(f"[Fast RAG] 执行失败: {e}")
- return _mark_fast_path_failed(state, str(e))
-
-
-def _has_valid_rag_results(state: MainGraphState) -> bool:
- """检查 RAG 结果是否有效(基于置信度)"""
- from .rag_nodes import RAG_CONFIDENCE_THRESHOLD
- rag_context = getattr(state, "rag_context", "")
- rag_confidence = getattr(state, "rag_confidence", 0.0)
-
- # 有结果且置信度足够
- has_content = rag_context and len(rag_context) > 0
- has_confidence = rag_confidence >= RAG_CONFIDENCE_THRESHOLD
-
- info(f"[Fast RAG Check] has_content={has_content}, rag_confidence={rag_confidence:.2f}, threshold={RAG_CONFIDENCE_THRESHOLD}")
-
- return has_content and has_confidence
-
-
-async def _generate_fast_answer(state: MainGraphState, query: str) -> MainGraphState:
- """使用小模型快速生成回答"""
- try:
- chat_llm = get_chat_service()
- rag_context = state.rag_context or str(state.rag_docs)[:2000]
-
- prompt = f"""请根据以下信息回答用户问题:
-
-检索到的信息:
-{rag_context}
-
-用户问题:{query}
-
-请给出简洁、准确的回答:"""
-
- # 使用流式输出
- from backend.app.main_graph.config import get_stream_writer
- writer = get_stream_writer()
-
- full_content = ""
- async for chunk in chat_llm.astream(prompt):
- content = getattr(chunk, 'content', '')
- if content:
- full_content += content
- # 流式输出
- if writer and hasattr(writer, '__call__'):
- try:
- writer({
- "type": "llm_token",
- "token": content
- })
- except Exception:
- pass
-
- state.final_result = full_content
- state.success = True
- state.current_phase = "finalizing"
- state.fast_path.rag_success = True
- return state
-
- except Exception as e:
- info(f"[Fast RAG] 快速回答生成失败: {e}")
- return _mark_fast_path_failed(state, "回答生成失败")
-
-
-# ========== 快速工具节点 ==========
-async def fast_tool_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- """快速工具节点"""
- state.current_phase = "fast_tool"
-
- decision = state.hybrid_router.decision
- suggested_tools = decision.suggested_tools if (decision and hasattr(decision, 'suggested_tools')) else []
- info(f"[Fast Tool] 开始处理,建议工具: {suggested_tools}")
-
- await dispatch_custom_event("fast_path_start", {"path": "fast_tool", "suggested_tools": suggested_tools}, config)
-
- # 无明确工具建议,升级到 React 循环
- if not suggested_tools:
- info("[Fast Tool] 无明确工具建议,升级到 React 循环")
- return _mark_fast_path_failed(state, "无明确工具建议")
-
- # 当前版本暂不支持快速工具调用,升级到 React 循环
- info("[Fast Tool] 快速工具调用暂未完善,升级到 React 循环")
- return _mark_fast_path_failed(state, "快速工具调用暂未完善")
-
-
-# ========== 条件路由函数 ==========
-def check_fast_path_success(state: MainGraphState) -> str:
- """检查快速路径是否成功 - 使用新的结构化字段"""
- if state.fast_path.failed:
- info("[Fast Path Check] 快速路径失败,升级到 React 循环")
- return "escalate"
-
- info("[Fast Path Check] 快速路径成功,进入 llm_call")
- return "llm_call"
-
-
-# ========== 公共函数 ==========
-def _mark_fast_path_failed(state: MainGraphState, reason: str = "") -> MainGraphState:
- """标记快速路径失败,准备升级到 React 循环 - 使用新的结构化字段"""
- state.fast_path.failed = True
- state.fast_path.fail_reason = reason
- state.success = False
-
- info(f"[Fast Path] 标记失败,准备升级: {reason}")
- return state
-
-
-# ========== 导出 ==========
-__all__ = [
- "fast_chitchat_node",
- "fast_rag_node",
- "fast_tool_node",
- "check_fast_path_success",
- "_mark_fast_path_failed",
-]
diff --git a/backend/app/deprecated/finalize.py b/backend/app/deprecated/finalize.py
deleted file mode 100644
index 784c340..0000000
--- a/backend/app/deprecated/finalize.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""
-完成事件节点模块
-负责发送完成事件,包含token使用情况和耗时信息
-"""
-
-from typing import Any, Dict
-
-# 本地模块
-from ...main_graph.state import MainGraphState
-from ...utils.logging import log_state_change
-from backend.app.logger import info, warning
-
-from langchain_core.runnables.config import RunnableConfig
-
-
-async def finalize_node(state: MainGraphState, config: RunnableConfig) -> Dict[str, Any]:
- """
- 完成事件节点 - 发送完成事件,包含token使用情况和耗时信息
-
- Args:
- state: 当前对话状态
- config: 运行时配置
-
- Returns:
- 更新后的状态(包含 final_result)
- """
- log_state_change("finalize", state, "进入")
-
- # 确保 final_result 被传递出去
- result = {
- "final_result": state.final_result,
- "success": state.success,
- "current_phase": "done"
- }
-
- try:
- # 获取流式写入器并发送完成事件
- from backend.app.main_graph.config import get_stream_writer
- writer = get_stream_writer()
-
- # 只在 writer 存在且不是 noop 时才发送
- if writer and hasattr(writer, '__call__'):
- try:
- writer({
- "type": "custom",
- "data": {
- "type": "done",
- "token_usage": state.last_token_usage,
- "elapsed_time": state.last_elapsed_time,
- "final_result": state.final_result
- }
- })
- info("🏁 [完成事件] 已发送完成事件")
- except Exception as e:
- warning(f"⚠️ [完成事件] 发送完成事件失败 (非致命): {e}")
- except Exception as e:
- warning(f"⚠️ [完成事件] 处理失败 (非致命): {e}")
-
- log_state_change("finalize", state, "离开")
- return result
\ No newline at end of file
diff --git a/backend/app/deprecated/finalize_new.py b/backend/app/deprecated/finalize_new.py
deleted file mode 100644
index e418caf..0000000
--- a/backend/app/deprecated/finalize_new.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""
-完成事件节点模块(新架构版本)
-负责发送完成事件
-"""
-
-from typing import Any, Dict
-from datetime import datetime
-
-# 本地模块
-from .state import AgentState
-from backend.app.logger import info, warning
-
-from langchain_core.runnables.config import RunnableConfig
-
-
-async def finalize_node(state: AgentState, config: RunnableConfig) -> Dict[str, Any]:
- """
- 完成事件节点(新架构版本)
-
- Args:
- state: 当前对话状态
- config: 运行时配置
-
- Returns:
- 空(不修改状态)
- """
- info("[Finalize] 进入完成节点")
-
- try:
- # 获取流式写入器并发送完成事件
- from backend.app.main_graph.config import get_stream_writer
- writer = get_stream_writer()
-
- # 提取最后的回复
- final_reply = ""
- if state.messages:
- last_msg = state.messages[-1]
- final_reply = last_msg.content if hasattr(last_msg, 'content') else str(last_msg)
-
- # 只在 writer 存在且不是 noop 时才发送
- if writer and hasattr(writer, '__call__'):
- try:
- writer({
- "type": "custom",
- "data": {
- "type": "done",
- "token_usage": state.last_token_usage,
- "elapsed_time": state.last_elapsed_time,
- "final_result": final_reply
- }
- })
- info("🏁 [完成事件] 已发送完成事件")
- except Exception as e:
- warning(f"⚠️ [完成事件] 发送完成事件失败 (非致命): {e}")
- except Exception as e:
- warning(f"⚠️ [完成事件] 处理失败 (非致命): {e}")
-
- info("[Finalize] 离开完成节点")
- return {}
diff --git a/backend/app/deprecated/hybrid_router.py b/backend/app/deprecated/hybrid_router.py
deleted file mode 100644
index beaa30c..0000000
--- a/backend/app/deprecated/hybrid_router.py
+++ /dev/null
@@ -1,215 +0,0 @@
-"""
-混合路由节点模块 - 前置路由决策
-负责决定走快速路径还是 React 循环
-
-复用 intent.py 的推理逻辑,保证判断一致!
-"""
-
-from typing import Optional
-from dataclasses import dataclass, field
-from datetime import datetime
-from langchain_core.runnables.config import RunnableConfig
-
-from ..state import MainGraphState
-from backend.app.logger import info, debug
-# 直接复用 intent.py 的推理逻辑!
-from backend.app.core.intent import (
- react_reason_async,
- ReasoningResult,
- ReasoningAction,
-)
-from ._utils import dispatch_custom_event
-
-
-# ========== 核心数据类型 ==========
-@dataclass
-class HybridRouterResult:
- """混合路由结果"""
- intent: str = "complex" # chitchat / knowledge / tool / complex
- confidence: float = 0.0
- suggested_tools: list = field(default_factory=list)
- path: str = "react_loop" # fast_chitchat / fast_rag / fast_tool / react_loop
- reasoning: str = ""
- reasoning_result: Optional[ReasoningResult] = None # 保存完整的 ReasoningResult,用于复用!
-
-
-# ========== 规则配置 ==========
-# 保留规则分流,保持快速响应
-CHITCHAT_KEYWORDS = {
- "你好", "您好", "hi", "hello", "hey", "早上好", "晚上好", "下午好",
- "谢谢", "感谢", "多谢", "thanks", "thank you",
- "再见", "拜拜", "goodbye", "bye"
-}
-
-SUBGRAPH_KEYWORDS = {
- "contact": ["通讯录", "联系人", "contact", "email", "邮件", "邮箱"],
- "dictionary": ["词典", "单词", "翻译", "dictionary", "translate", "生词"],
- "news_analysis": ["资讯", "新闻", "分析", "news", "report", "热点"]
-}
-
-
-# ========== 从 ReasoningResult 映射到 HybridRouterResult ==========
-def _map_reasoning_to_router(reasoning_result: ReasoningResult) -> HybridRouterResult:
- """将 intent.py 的推理结果映射为 hybrid_router 的结果"""
-
- # ReasoningAction -> intent 映射
- intent_map = {
- ReasoningAction.DIRECT_RESPONSE: "chitchat",
- ReasoningAction.RETRIEVE_RAG: "knowledge",
- ReasoningAction.RE_RETRIEVE_RAG: "knowledge",
- ReasoningAction.WEB_SEARCH: "complex", # WEB_SEARCH 走 React循环
- ReasoningAction.ROUTE_SUBGRAPH: "tool",
- ReasoningAction.CLARIFY: "chitchat",
- ReasoningAction.UNKNOWN: "complex",
- }
-
- # ReasoningAction -> path 映射
- path_map = {
- ReasoningAction.DIRECT_RESPONSE: "fast_chitchat",
- ReasoningAction.RETRIEVE_RAG: "fast_rag",
- ReasoningAction.RE_RETRIEVE_RAG: "fast_rag",
- ReasoningAction.WEB_SEARCH: "react_loop", # WEB_SEARCH 走 React循环
- ReasoningAction.ROUTE_SUBGRAPH: "fast_tool",
- ReasoningAction.CLARIFY: "fast_chitchat",
- ReasoningAction.UNKNOWN: "react_loop",
- }
-
- intent = intent_map.get(reasoning_result.action, "complex")
- path = path_map.get(reasoning_result.action, "react_loop")
-
- suggested_tools = []
- if reasoning_result.action == ReasoningAction.ROUTE_SUBGRAPH:
- target_subgraph = reasoning_result.metadata.get("target_subgraph")
- if target_subgraph:
- suggested_tools = [target_subgraph]
-
- return HybridRouterResult(
- intent=intent,
- confidence=reasoning_result.confidence,
- suggested_tools=suggested_tools,
- path=path,
- reasoning=reasoning_result.reasoning,
- reasoning_result=reasoning_result # 保存完整结果!
- )
-
-
-# ========== 规则分流(<5ms) ==========
-def _rule_based_redirect(query: str) -> Optional[HybridRouterResult]:
- """规则分流:处理明显不需要推理的情况"""
- query_clean = query.strip().lower()
-
- # 1. 闲聊
- if query_clean in CHITCHAT_KEYWORDS or any(kw in query_clean for kw in CHITCHAT_KEYWORDS):
- return HybridRouterResult(
- intent="chitchat",
- confidence=1.0,
- path="fast_chitchat",
- reasoning="规则匹配:闲聊类请求"
- )
-
- # 2. 子图关键词
- for subgraph_name, keywords in SUBGRAPH_KEYWORDS.items():
- if any(kw in query_clean for kw in keywords):
- return HybridRouterResult(
- intent="tool",
- confidence=0.9,
- suggested_tools=[subgraph_name],
- path="fast_tool",
- reasoning=f"规则匹配:{subgraph_name} 子图关键词"
- )
-
- # 3. 短问题
- if len(query_clean) < 3 or (query_clean.endswith("?") and len(query_clean) < 5):
- return HybridRouterResult(
- intent="complex",
- confidence=0.3,
- path="react_loop",
- reasoning="规则匹配:问题过于简短"
- )
-
- return None
-
-
-# ========== 默认结果 ==========
-def _default_result() -> HybridRouterResult:
- """默认结果"""
- return HybridRouterResult(
- intent="complex",
- confidence=0.3,
- path="react_loop",
- reasoning="降级到默认值,走 React 循环"
- )
-
-
-# ========== 主路由节点 ==========
-async def hybrid_router_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- """混合路由节点:前置路由,决定走快速路径还是 React循环"""
- state.current_phase = "hybrid_router"
- query = state.user_query or ""
-
- info(f"[Hybrid Router] 开始路由: {query[:50]}...")
-
- # 1. 规则分流
- rule_result = _rule_based_redirect(query)
- if rule_result:
- decision = rule_result
- info(f"[Hybrid Router] 规则命中: {decision.path}")
- else:
- # 2. 复用 intent.py 的推理逻辑!保证判断一致!
- info("[Hybrid Router] 规则未命中,使用 intent.py 推理")
- try:
- reasoning_result = await react_reason_async(query, {})
- decision = _map_reasoning_to_router(reasoning_result)
- info(f"[Hybrid Router] 推理结果: action={reasoning_result.action.name}, path={decision.path}")
- except Exception as e:
- debug(f"[Hybrid Router] intent.py 推理失败: {e}")
- decision = _default_result()
-
- # 3. 更新状态
- state.hybrid_router.decision = decision
- state.hybrid_router.start_time = datetime.now().isoformat()
-
- # 4. 发送事件
- await dispatch_custom_event("intent_classified", {
- "intent": decision.intent,
- "confidence": decision.confidence,
- "reasoning": decision.reasoning,
- "suggested_tools": decision.suggested_tools
- }, config)
-
- await dispatch_custom_event("path_decision", {
- "path": decision.path,
- "intent": decision.intent,
- "reasoning": decision.reasoning
- }, config)
-
- info(f"[Hybrid Router] 路由决策: {decision.path} (intent={decision.intent}, confidence={decision.confidence})")
- return state
-
-
-# ========== 条件路由函数 ==========
-def route_from_hybrid_decision(state: MainGraphState) -> str:
- """从混合路由决策获取下一步节点"""
- decision = state.hybrid_router.decision
- if decision and hasattr(decision, 'path'):
- return decision.path
- return "react_loop"
-
-
-def check_fast_path_success(state: MainGraphState) -> str:
- """检查快速路径是否成功"""
- if state.fast_path.failed:
- info("[Fast Path Check] 快速路径失败,升级到 React 循环")
- return "escalate"
-
- info("[Fast Path Check] 快速路径成功,进入 llm_call")
- return "llm_call"
-
-
-# ========== 导出 ==========
-__all__ = [
- "hybrid_router_node",
- "route_from_hybrid_decision",
- "check_fast_path_success",
- "HybridRouterResult",
-]
diff --git a/backend/app/deprecated/intent.py b/backend/app/deprecated/intent.py
deleted file mode 100644
index c9bf2f7..0000000
--- a/backend/app/deprecated/intent.py
+++ /dev/null
@@ -1,547 +0,0 @@
-"""
-意图理解与推理模块(React 模式)
-
-核心改进:
-1. 使用统一的 JSON 解析器,保证稳定性
-2. 优化 Prompt,更清晰的指令
-3. 更好的错误处理和降级策略
-"""
-
-import re
-import json
-from typing import Dict, Any, Optional, List
-from dataclasses import dataclass, field
-from enum import Enum, auto
-
-from backend.app.core.json_parser import (
- extract_and_parse_json,
- safe_get,
- safe_get_float,
- safe_get_str,
-)
-
-
-# ========== 1. 核心数据类型 ==========
-
-class ReasoningAction(Enum):
- """推理动作枚举 - 决定下一步做什么"""
- DIRECT_RESPONSE = auto() # 直接回答,不需要额外信息
- RETRIEVE_RAG = auto() # 需要调用 RAG 检索
- RE_RETRIEVE_RAG = auto() # 需要重新检索(更多/更好结果)
- WEB_SEARCH = auto() # 需要联网搜索
- ROUTE_SUBGRAPH = auto() # 需要路由到子图(contact/dictionary/news_analysis/research)
- CLARIFY = auto() # 需要澄清用户的问题
- UNKNOWN = auto() # 未知动作
-
-
-@dataclass
-class RetrievalConfig:
- """检索配置"""
- need_retrieval: bool = False
- need_re_retrieval: bool = False
- retrieval_query: Optional[str] = None
- target_subgraph: Optional[str] = None
- collection_name: Optional[str] = None
- k: int = 5
- metadata: Dict[str, Any] = field(default_factory=dict)
-
-
-@dataclass
-class ReasoningResult:
- """推理结果数据类"""
- action: ReasoningAction = ReasoningAction.UNKNOWN
- confidence: float = 0.0
- reasoning: str = ""
- retrieval_config: RetrievalConfig = field(default_factory=RetrievalConfig)
- extracted_entities: Dict[str, Any] = field(default_factory=dict)
- next_hints: List[str] = field(default_factory=list)
- original_query: str = ""
- metadata: Dict[str, Any] = field(default_factory=dict)
-
-
-# ========== 2. React 推理器 ==========
-
-class ReactIntentReasoner:
- """
- React 模式意图推理器
-
- 核心功能:
- 1. 使用 LLM 分析用户意图
- 2. 决定是否需要 RAG 检索/重新检索
- 3. 决定是否需要路由到子图
- 4. 提供降级策略(规则匹配)
-
- 可以选择使用大模型或小模型
- """
-
- def __init__(self, use_small_llm: bool = False):
- """
- 初始化推理器
-
- Args:
- use_small_llm: 是否使用轻量级模型(用于意图分类)
- """
- self._llm_service = None
- self._use_small_llm = use_small_llm
- self._subgraph_keywords = {
- "contact": ["通讯录", "联系人", "contact", "email", "邮件", "邮箱"],
- "dictionary": ["词典", "单词", "翻译", "dictionary", "translate", "生词"],
- "news_analysis": ["资讯", "新闻", "分析", "news", "report", "热点"],
- "research": ["研究", "深度分析", "报告", "引用", "溯源", "research", "analyze", "report"]
- }
-
- def _get_llm_service(self):
- """懒加载 LLM 服务(避免循环导入)"""
- if self._llm_service is None:
- from backend.app.model_services.chat_services import get_chat_service, get_small_llm_service
- if self._use_small_llm:
- self._llm_service = get_small_llm_service()
- else:
- self._llm_service = get_chat_service()
- return self._llm_service
-
- async def reason(
- self,
- query: str,
- context: Optional[Dict[str, Any]] = None
- ) -> ReasoningResult:
- """
- 推理意图,决定下一步动作
-
- Args:
- query: 用户查询
- context: 上下文信息(可能包含已检索文档、对话历史等)
-
- Returns:
- ReasoningResult
- """
- context = context or {}
- result = ReasoningResult(original_query=query)
-
- # 关键修复 1:检查是否已经有检索结果或子图结果,如果是,直接回答
- previous_actions = context.get("previous_actions", [])
- if "subgraph_completed" in previous_actions:
- result.action = ReasoningAction.DIRECT_RESPONSE
- result.confidence = 1.0
- result.reasoning = "子图已执行完成,直接回答"
- return result
-
- retrieved_docs = context.get("retrieved_docs", [])
- messages = context.get("messages", [])
-
- # 获取 RAG 相关状态
- previous_actions = context.get("previous_actions", [])
- rag_count = previous_actions.count("RETRIEVE_RAG")
- rag_attempts = context.get("rag_attempts", rag_count)
- rag_confidence = context.get("rag_confidence", 0.0)
- retrieved_docs = context.get("retrieved_docs", [])
- web_search_count = previous_actions.count("web_search")
-
- # 检查 RAG 是否多次失败(reasoning_history 中有失败的 RAG 记录)
- rag_history = context.get("reasoning_history", [])
- rag_fail_count = sum(
- 1 for h in rag_history
- if h.get("action") in ("RETRIEVE_RAG", "RE_RETRIEVE_RAG") and h.get("confidence", 1.0) == 0.0
- )
-
- # 如果有检索文档,根据置信度判断下一步
- if retrieved_docs and len(retrieved_docs) > 0:
- if rag_confidence >= 0.6:
- # 置信度足够高,直接回答
- result.action = ReasoningAction.DIRECT_RESPONSE
- result.confidence = 0.95
- result.reasoning = f"已获取检索文档,置信度={rag_confidence:.2f},直接回答"
- return result
- elif rag_attempts >= 2 or rag_fail_count >= 2:
- # 尝试次数已够或多次失败,放弃 RAG,转向联网搜索
- result.action = ReasoningAction.WEB_SEARCH
- result.confidence = 0.8
- result.reasoning = f"RAG 置信度={rag_confidence:.2f} < 0.6,且已尝试 {rag_attempts} 次,转向联网搜索"
- result.metadata["need_web_search"] = True
- result.metadata["search_query"] = query
- return result
- else:
- # 置信度不够但还有尝试机会,再查一次
- result.action = ReasoningAction.RETRIEVE_RAG
- result.confidence = 0.8
- result.reasoning = f"已获取检索文档但置信度={rag_confidence:.2f} < 0.6,可再尝试一次"
- result.retrieval_config.need_retrieval = True
- result.retrieval_config.retrieval_query = query
- return result
-
- # 如果 RAG 已多次失败且无文档,直接回答(基于常识)
- if rag_fail_count >= 2:
- result.action = ReasoningAction.DIRECT_RESPONSE
- result.confidence = 0.7
- result.reasoning = f"RAG 已尝试 {rag_fail_count} 次均失败,知识库无相关内容,直接基于常识回答"
- return result
-
- # 如果 web search 已执行过,直接回答
- if web_search_count >= 1:
- result.action = ReasoningAction.DIRECT_RESPONSE
- result.confidence = 0.95
- result.reasoning = "已获取联网搜索结果,直接回答"
- return result
-
- # 策略1:尝试使用 LLM 推理
- try:
- llm_result = await self._reason_with_llm(query, context)
- if llm_result.confidence >= 0.6: # 置信度足够高,直接返回
- return llm_result
- except Exception as e:
- print(f"[ReactReasoner] LLM 推理失败: {e}, 回退到规则")
-
- # 策略2:LLM 失败或置信度低,使用规则匹配
- return self._reason_with_rules(query, context)
-
- async def _reason_with_llm(
- self,
- query: str,
- context: Dict[str, Any]
- ) -> ReasoningResult:
- """使用 LLM 进行推理"""
- prompt = self._build_reasoning_prompt(query, context)
- llm = self._get_llm_service()
-
- response = await llm.ainvoke(prompt)
- return self._parse_llm_response(response.content, query)
-
- def _build_reasoning_prompt(self, query: str, context: Dict[str, Any]) -> str:
- """
- 构建推理提示词(优化版)
-
- 改进点:
- 1. 更清晰的指令和格式要求
- 2. 明确要求纯 JSON 输出,不要 markdown
- 3. 更好的示例和决策规则
- """
- # 构建上下文描述
- context_parts = []
- if context.get("retrieved_docs"):
- context_parts.append(f"- 已检索文档: {len(context['retrieved_docs'])} 条")
- rag_confidence = context.get("rag_confidence")
- if rag_confidence is not None:
- context_parts.append(f"- RAG 置信度: {rag_confidence:.2f}")
- rag_attempts = context.get("rag_attempts", 0)
- if rag_attempts:
- context_parts.append(f"- RAG 尝试次数: {rag_attempts}")
- previous_actions = context.get("previous_actions", [])
- if previous_actions:
- context_parts.append(f"- 历史动作: {previous_actions}")
-
- context_str = "\n".join(context_parts) if context_parts else "无"
-
- return f"""你是一个决策控制器。你需要根据当前状态决定下一步操作。
-
-【格式要求】
-你必须严格输出 JSON 格式,不要加任何 Markdown 代码块标记(如 ```json)。
-仅输出纯 JSON 字符串,不要有其他解释文字。
-
-【可用动作】
-1. DIRECT_RESPONSE - 直接回答(已有足够信息,不需要额外工具)
-2. RETRIEVE_RAG - 检索知识库(需要查询相关知识)
-3. RE_RETRIEVE_RAG - 重新检索(已有结果不够,需要再次尝试)
-4. WEB_SEARCH - 联网搜索(需要最新资讯或知识库没有的内容)
-5. ROUTE_SUBGRAPH - 路由到子图(通讯录/词典/资讯分析)
-6. CLARIFY - 澄清问题(问题不明确,需要用户补充)
-
-【动作参数说明】
-每个动作需要的参数:
-- RETRIEVE_RAG: {{"retrieval_query": "优化后的检索查询字符串"}}
-- RE_RETRIEVE_RAG: {{"retrieval_query": "优化后的检索查询字符串"}}
-- WEB_SEARCH: {{"search_query": "优化后的搜索查询字符串"}}
-- ROUTE_SUBGRAPH: {{"target_subgraph": "contact|dictionary|news_analysis"}}
-- DIRECT_RESPONSE/CLARIFY: {{}}(无需参数)
-
-【决策规则】
-1. 如果 RAG 置信度 >= 0.6 且有检索文档,使用 DIRECT_RESPONSE
-2. 如果 RAG 置信度 < 0.6 且尝试次数 < 2,使用 RETRIEVE_RAG/RE_RETRIEVE_RAG
-3. 如果 RAG 置信度 < 0.6 且尝试次数 >= 2,使用 WEB_SEARCH
-4. 如果已执行过联网搜索,使用 DIRECT_RESPONSE
-5. 如果问题涉及通讯录/词典/资讯分析,使用 ROUTE_SUBGRAPH
-6. 如果问题不明确,使用 CLARIFY
-
-【输出格式】
-{{
- "action": "动作名称(大写)",
- "confidence": 0.85,
- "reasoning": "简要说明决策理由",
- "target_subgraph": "contact|dictionary|news_analysis|null",
- "retrieval_query": "优化后的检索查询(可选)",
- "search_query": "优化后的搜索查询(可选)"
-}}
-
-【重要提示】
-- target_subgraph 仅在 action=ROUTE_SUBGRAPH 时提供,否则设为 null 或不包含
-- retrieval_query 仅在 action=RETRIEVE_RAG/RE_RETRIEVE_RAG 时提供
-- search_query 仅在 action=WEB_SEARCH 时提供
-- confidence 是你对当前决策的信心(0.0-1.0)
-
-【当前状态】
-用户查询: {query}
-当前上下文:
-{context_str}
-
-【现在开始】
-请根据以上信息,输出你的决策 JSON:"""
-
- def _parse_llm_response(self, response: str, original_query: str) -> ReasoningResult:
- """
- 解析 LLM 响应(优化版)
-
- 使用统一的 JSON 解析器,支持多种格式
- """
- result = ReasoningResult(original_query=original_query)
-
- # 使用新的 JSON 解析器
- parse_result = extract_and_parse_json(response)
-
- if not parse_result.success or not parse_result.data:
- # 解析失败,使用规则推理降级
- result.action = ReasoningAction.UNKNOWN
- result.confidence = 0.0
- result.reasoning = f"LLM 响应解析失败: {parse_result.error or '未知错误'}"
- return result
-
- data = parse_result.data
-
- # 安全地提取字段
- action_str = safe_get_str(data, "action", "UNKNOWN")
- confidence = safe_get_float(data, "confidence", 0.5)
- reasoning = safe_get_str(data, "reasoning", "")
- target_subgraph = safe_get_str(data, "target_subgraph", None)
- retrieval_query = safe_get_str(data, "retrieval_query", original_query)
- search_query = safe_get_str(data, "search_query", original_query)
-
- # 转换为枚举
- try:
- result.action = ReasoningAction[action_str]
- except (KeyError, ValueError):
- result.action = ReasoningAction.UNKNOWN
-
- result.confidence = confidence
- result.reasoning = reasoning
-
- # 处理子图路由
- if result.action == ReasoningAction.ROUTE_SUBGRAPH and target_subgraph:
- result.retrieval_config.target_subgraph = target_subgraph
- result.metadata["target_subgraph"] = target_subgraph
-
- # 处理检索查询
- if result.action in (ReasoningAction.RETRIEVE_RAG, ReasoningAction.RE_RETRIEVE_RAG):
- result.retrieval_config.need_retrieval = True
- result.retrieval_config.need_re_retrieval = (result.action == ReasoningAction.RE_RETRIEVE_RAG)
- result.retrieval_config.retrieval_query = retrieval_query
-
- # 处理联网搜索
- if result.action == ReasoningAction.WEB_SEARCH:
- result.metadata["need_web_search"] = True
- result.metadata["search_query"] = search_query
-
- return result
-
- def _reason_with_rules(
- self,
- query: str,
- context: Dict[str, Any]
- ) -> ReasoningResult:
- """基于规则的降级推理"""
- result = ReasoningResult(original_query=query)
- query_lower = query.lower()
-
- # 1. 检查子图路由(最高优先级)
- for subgraph_name, keywords in self._subgraph_keywords.items():
- if any(kw in query_lower for kw in keywords):
- result.action = ReasoningAction.ROUTE_SUBGRAPH
- result.confidence = 0.85
- result.reasoning = f"关键词匹配: {subgraph_name} 子图"
- result.retrieval_config.target_subgraph = subgraph_name
- result.metadata["target_subgraph"] = subgraph_name
- return result
-
- # 2. 检查是否需要联网搜索(谨慎触发)
- # 只有用户明确要求搜索才触发
- web_search_keywords = ["搜索", "搜索一下", "帮我搜", "search for", "web search", "搜索资料"]
- has_web_search = any(kw in query_lower for kw in web_search_keywords)
-
- if has_web_search:
- result.action = ReasoningAction.WEB_SEARCH
- result.confidence = 0.9
- result.reasoning = "用户明确要求联网搜索"
- result.metadata["need_web_search"] = True
- result.metadata["search_query"] = query
- return result
-
- # 3. 检查是否需要重新检索
- re_retrieve_keywords = ["再", "重新", "更多", "不够", "其他", "没找到", "找不到", "不对", "another", "again", "more"]
- has_re_retrieve = any(kw in query_lower for kw in re_retrieve_keywords)
- has_docs = context.get("retrieved_docs") and len(context["retrieved_docs"]) > 0
-
- if has_re_retrieve or (has_docs and len(context["retrieved_docs"]) < 2):
- result.action = ReasoningAction.RE_RETRIEVE_RAG
- result.confidence = 0.8 if has_re_retrieve else 0.65
- result.reasoning = "需要重新检索更多/更好结果"
- result.retrieval_config.need_retrieval = True
- result.retrieval_config.need_re_retrieval = True
- result.retrieval_config.retrieval_query = query
- return result
-
- # 3. 检查是否需要 RAG 检索
- retrieve_keywords = ["什么", "怎么", "如何", "为什么", "哪", "谁", "介绍", "解释", "说明", "资料", "文档", "查询", "搜索", "what", "how", "why", "where", "who", "tell me", "explain", "about", "information"]
- has_retrieve = any(kw in query_lower for kw in retrieve_keywords)
-
- if has_retrieve or len(query.strip()) > 5:
- result.action = ReasoningAction.RETRIEVE_RAG
- result.confidence = 0.8 if has_retrieve else 0.6
- result.reasoning = "需要查询知识库"
- result.retrieval_config.need_retrieval = True
- result.retrieval_config.retrieval_query = query
- return result
-
- # 4. 检查直接回答
- direct_keywords = ["你好", "您好", "hi", "hello", "hey", "早上好", "晚上好", "下午好", "嗨", "谢谢", "感谢", "多谢", "thanks", "thank you", "再见", "拜拜", "goodbye", "回见"]
- if any(kw in query_lower for kw in direct_keywords):
- result.action = ReasoningAction.DIRECT_RESPONSE
- result.confidence = 0.9
- result.reasoning = "直接回答(问候/感谢/道别)"
- return result
-
- # 5. 检查是否需要澄清
- if len(query.strip()) < 3 or any(q in query for q in ["?", "?", "哪个", "哪些", "什么意思", "请", "能详细"]):
- result.action = ReasoningAction.CLARIFY
- result.confidence = 0.7
- result.reasoning = "需要澄清问题"
- result.next_hints = ["请提供更多细节", "您想了解什么方面的内容?", "能否具体说明一下?"]
- return result
-
- # 6. 默认直接回答
- result.action = ReasoningAction.DIRECT_RESPONSE
- result.confidence = 0.5
- result.reasoning = "默认直接回答模式"
- return result
-
-
-# ========== 3. 便捷函数(保持与旧代码兼容) ==========
-
-# 全局推理器实例(懒加载)
-_reasoner: Optional[ReactIntentReasoner] = None
-_small_reasoner: Optional[ReactIntentReasoner] = None
-
-
-def _get_reasoner(use_small_llm: bool = True) -> ReactIntentReasoner:
- """
- 获取推理器实例
-
- Args:
- use_small_llm: 是否使用轻量级模型
-
- Returns:
- ReactIntentReasoner 实例
- """
- global _reasoner, _small_reasoner
- if use_small_llm:
- if _small_reasoner is None:
- _small_reasoner = ReactIntentReasoner(use_small_llm=True)
- return _small_reasoner
- else:
- if _reasoner is None:
- _reasoner = ReactIntentReasoner(use_small_llm=False)
- return _reasoner
-
-
-async def react_reason_async(
- query: str,
- context: Optional[Dict[str, Any]] = None,
- use_small_llm: bool = True
-) -> ReasoningResult:
- """
- 便捷函数:异步 React 推理(推荐使用)
-
- Args:
- query: 用户查询
- context: 上下文
- use_small_llm: 是否使用轻量级模型
-
- Returns:
- ReasoningResult
- """
- reasoner = _get_reasoner(use_small_llm=use_small_llm)
- return await reasoner.reason(query, context)
-
-
-def react_reason(
- query: str,
- context: Optional[Dict[str, Any]] = None,
- use_small_llm: bool = False
-) -> ReasoningResult:
- """
- 便捷函数:同步 React 推理(保持向后兼容)
-
- 注意:内部会运行事件循环,建议在异步环境中使用 react_reason_async
-
- Args:
- query: 用户查询
- context: 上下文
- use_small_llm: 是否使用轻量级模型
-
- Returns:
- ReasoningResult
- """
- import asyncio
-
- try:
- # 尝试获取现有事件循环
- loop = asyncio.get_event_loop()
- if loop.is_running():
- # 已经在运行的循环中,创建任务
- # 注意:这里不能真正等待,会导致死锁
- # 降级到规则推理
- print(f"[ReactReasoner] 检测到运行中的事件循环,使用规则推理")
- reasoner = _get_reasoner(use_small_llm=use_small_llm)
- return reasoner._reason_with_rules(query, context or {})
- except RuntimeError:
- pass
-
- # 创建新的事件循环
- loop = asyncio.new_event_loop()
- try:
- asyncio.set_event_loop(loop)
- return loop.run_until_complete(react_reason_async(query, context, use_small_llm=use_small_llm))
- finally:
- loop.close()
- loop.close()
-
-
-def get_route_by_reasoning(result: ReasoningResult) -> str:
- """
- 根据推理结果获取路由字符串(与旧代码兼容)
-
- Args:
- result: ReasoningResult
-
- Returns:
- str: 路由标识
- """
- action_to_route = {
- ReasoningAction.DIRECT_RESPONSE: "direct_response",
- ReasoningAction.RETRIEVE_RAG: "retrieve_rag",
- ReasoningAction.RE_RETRIEVE_RAG: "re_retrieve_rag",
- ReasoningAction.WEB_SEARCH: "web_search",
- ReasoningAction.CLARIFY: "clarify",
- ReasoningAction.ROUTE_SUBGRAPH: result.metadata.get("target_subgraph", "unknown_subgraph"),
- ReasoningAction.UNKNOWN: "unknown",
- }
- return action_to_route.get(result.action, "unknown")
-
-
-# ========== 4. 导出 ==========
-
-__all__ = [
- "ReasoningAction",
- "RetrievalConfig",
- "ReasoningResult",
- "ReactIntentReasoner",
- "react_reason",
- "react_reason_async",
- "get_route_by_reasoning"
-]
diff --git a/backend/app/deprecated/json_parser.py b/backend/app/deprecated/json_parser.py
deleted file mode 100644
index c2be1b5..0000000
--- a/backend/app/deprecated/json_parser.py
+++ /dev/null
@@ -1,203 +0,0 @@
-"""
-统一的 JSON 解析工具,保证 LLM JSON 输出的稳定性
-
-处理各种边界情况:
-1. 纯 JSON 字符串
-2. JSON 在 markdown 代码块中
-3. JSON 在文本中间
-4. JSON 有多余的逗号
-5. JSON 有尾随内容
-"""
-import re
-import json
-from typing import TypeVar, Type, Dict, Any, Optional
-from dataclasses import dataclass
-from json import JSONDecodeError
-
-T = TypeVar('T')
-
-
-@dataclass
-class ParseResult:
- """JSON 解析结果"""
- success: bool
- data: Optional[Dict[str, Any]] = None
- error: Optional[str] = None
- raw_response: str = ""
-
-
-def extract_and_parse_json(
- response: str,
- schema: Optional[Dict[str, Any]] = None
-) -> ParseResult:
- """
- 从 LLM 响应中提取并解析 JSON,使用多种策略处理边界情况
-
- Args:
- response: LLM 的原始响应
- schema: 可选的 JSON Schema(预留,暂未使用)
-
- Returns:
- ParseResult: 解析结果
- """
- result = ParseResult(raw_response=response, success=False)
-
- # 前置清理
- cleaned = response.strip()
- if not cleaned:
- result.error = "响应为空"
- return result
-
- # 策略1:尝试直接解析完整响应
- try:
- data = json.loads(cleaned)
- result.data = data
- result.success = True
- return result
- except JSONDecodeError:
- pass
-
- # 策略2:尝试匹配 markdown 代码块(优先)
- codeblock_patterns = [
- r'```(?:json)?\s*([\s\S]*?)\s*```', # ```json ... ```
- r'```([\s\S]*?)```', # ``` ... ```
- ]
-
- for pattern in codeblock_patterns:
- match = re.search(pattern, cleaned)
- if match:
- json_str = match.group(1).strip()
- if json_str:
- try:
- data = json.loads(json_str)
- result.data = data
- result.success = True
- return result
- except JSONDecodeError:
- continue
-
- # 策略3:提取最外层的完整 {} 块(处理嵌套)
- json_match = _extract_outermost_json(cleaned)
- if json_match:
- try:
- data = json.loads(json_match)
- result.data = data
- result.success = True
- return result
- except JSONDecodeError:
- pass
-
- # 策略4:尝试修复常见问题
- try:
- # 去除多余的尾随逗号
- fixed = re.sub(r',\s*([}\]])', r'\1', cleaned)
- # 提取第一个 { 到最后一个 } 的内容
- first_brace = fixed.find('{')
- last_brace = fixed.rfind('}')
- if first_brace != -1 and last_brace != -1 and first_brace < last_brace:
- json_str = fixed[first_brace:last_brace+1]
- data = json.loads(json_str)
- result.data = data
- result.success = True
- return result
- except Exception:
- pass
-
- # 所有策略都失败
- result.error = f"无法从响应中提取有效 JSON: {cleaned[:200]}..."
- return result
-
-
-def _extract_outermost_json(text: str) -> Optional[str]:
- """
- 提取最外层的完整 JSON 块(处理嵌套)
-
- 使用栈方法,正确处理嵌套的 {}
- """
- stack = []
- start_idx = -1
-
- for i, char in enumerate(text):
- if char == '{':
- if not stack:
- start_idx = i
- stack.append('{')
- elif char == '}':
- if stack:
- stack.pop()
- if not stack and start_idx != -1:
- # 找到完整的外层块
- return text[start_idx:i+1]
-
- return None
-
-
-def parse_json_to_dataclass(
- response: str,
- dataclass_type: Type[T],
- default_factory: callable
-) -> T:
- """
- 解析 JSON 并转换为 dataclass 实例,失败时返回默认值
-
- Args:
- response: LLM 响应
- dataclass_type: 目标 dataclass 类型
- default_factory: 生成默认值的工厂函数
-
- Returns:
- T: dataclass 实例
- """
- parse_result = extract_and_parse_json(response)
-
- if not parse_result.success or not parse_result.data:
- return default_factory()
-
- try:
- return dataclass_type(**parse_result.data)
- except (TypeError, ValueError) as e:
- # 字段不匹配时尝试降级
- return default_factory()
-
-
-def safe_get(data: Dict[str, Any], key: str, default: Any = None) -> Any:
- """安全地从字典中获取值"""
- if not data or not isinstance(data, dict):
- return default
- return data.get(key, default)
-
-
-def safe_get_bool(data: Dict[str, Any], key: str, default: bool = False) -> bool:
- """安全地获取布尔值"""
- value = safe_get(data, key, default)
- if isinstance(value, bool):
- return value
- if isinstance(value, str):
- return value.lower() in ('true', '1', 'yes', 'on')
- if isinstance(value, (int, float)):
- return bool(value)
- return default
-
-
-def safe_get_float(data: Dict[str, Any], key: str, default: float = 0.0) -> float:
- """安全地获取浮点值"""
- value = safe_get(data, key, default)
- try:
- return float(value)
- except (TypeError, ValueError):
- return default
-
-
-def safe_get_int(data: Dict[str, Any], key: str, default: int = 0) -> int:
- """安全地获取整数值"""
- value = safe_get(data, key, default)
- try:
- return int(value)
- except (TypeError, ValueError):
- return default
-
-
-def safe_get_str(data: Dict[str, Any], key: str, default: str = "") -> str:
- """安全地获取字符串值"""
- value = safe_get(data, key, default)
- return str(value) if value is not None else default
diff --git a/backend/app/deprecated/llm_call.py b/backend/app/deprecated/llm_call.py
deleted file mode 100644
index a6fb141..0000000
--- a/backend/app/deprecated/llm_call.py
+++ /dev/null
@@ -1,214 +0,0 @@
-"""
-LLM 调用节点模块
-负责调用大语言模型并处理响应
-"""
-
-import time
-from typing import Any, Dict
-from langchain_core.language_models import BaseChatModel
-from langchain_core.messages import AIMessage
-
-# 本地模块
-from ...main_graph.state import MainGraphState
-from ...agent.prompts import create_system_prompt
-from ...utils.logging import log_state_change
-from backend.app.logger import debug, info, error
-
-
-def create_dynamic_llm_call_node(chat_services: Dict[str, BaseChatModel], tools: list):
- """
- 工厂函数:创建动态 LLM 调用节点(根据 state.current_model 选择模型)
-
- Args:
- chat_services: 模型名称 -> ChatModel 实例 的字典
- tools: 工具列表(llm_call 不使用工具,只负责回答)
-
- Returns:
- 异步节点函数
- """
- # llm_call 节点不使用工具,只负责生成回答
- # 直接使用原始模型,不绑定工具
- models = chat_services
-
- # 预构建 prompt(不带工具描述)
- prompt = create_system_prompt()
-
- from langchain_core.runnables.config import RunnableConfig
-
- async def call_llm(state: MainGraphState, config: RunnableConfig) -> Dict[str, Any]:
- """
- LLM 调用节点(动态选择模型)
-
- Args:
- state: 当前对话状态
- config: LangChain/LangGraph 自动注入的配置,包含 callbacks 等信息
-
- Returns:
- 更新后的状态字典
- """
- log_state_change("llm_call", state, "进入")
-
- memory_context = getattr(state, "memory_context", "暂无用户信息")
- start_time = time.time()
-
- # 关键修复:如果 state.final_result 已经存在(比如子图执行完),直接返回
- if state.final_result:
- info(f"[llm_call] 检测到已有最终结果,直接返回: {state.final_result[:100]}...")
- elapsed_time = time.time() - start_time
- return {
- "final_result": state.final_result,
- "success": True,
- "current_phase": "done",
- "llm_calls": getattr(state, 'llm_calls', 0) + 1,
- "last_elapsed_time": elapsed_time,
- "turns_since_last_summary": getattr(state, 'turns_since_last_summary', 0) + 1,
- }
-
- # 动态选择模型
- model_name = getattr(state, "current_model", "")
- if not model_name or model_name not in models:
- # 回退到第一个可用模型
- fallback_name = next(iter(models.keys()))
- info(f"[llm_call] 模型 '{model_name}' 不可用,回退到 '{fallback_name}'")
- model_name = fallback_name
-
- llm = models[model_name]
- info(f"[llm_call] 使用模型(无工具): {model_name}")
-
- try:
- # 添加上下文到消息
- messages_with_context = list(state.messages)
- info(f"[llm_call] 原始消息数量: {len(messages_with_context)}")
- for i, msg in enumerate(messages_with_context):
- msg_type = getattr(msg, 'type', 'unknown')
- msg_content = getattr(msg, 'content', '')[:100] if hasattr(msg, 'content') else str(msg)[:100]
- info(f"[llm_call] msg[{i}] type={msg_type}, content={repr(msg_content)}")
-
- if state.rag_context:
- from langchain_core.messages import SystemMessage
- rag_system_msg = SystemMessage(content=f"以下是检索到的相关信息:\n{state.rag_context}")
- inserted = False
- for i, msg in enumerate(messages_with_context):
- if msg.type == "human":
- messages_with_context.insert(i, rag_system_msg)
- inserted = True
- break
- if not inserted:
- messages_with_context.insert(0, rag_system_msg)
- info(f"[llm_call] RAG上下文已添加,长度: {len(state.rag_context)}")
-
- # 恢复为:手动进行 astream,并将所有的 chunk 拼接成最终的 response 返回。
- # LangGraph 会自动监听这期间产生的所有 token。
- chain = prompt | llm
- chunks = []
- info(f"[llm_call] 开始调用 LLM astream...")
- async for chunk in chain.astream(
- {
- "messages": messages_with_context,
- "memory_context": memory_context
- },
- config=config
- ):
- chunks.append(chunk)
-
- info(f"[llm_call] LLM astream 完成,共收到 {len(chunks)} 个 chunks,info:{chunks[0].content[:50]}...{chunks[-1].content[:50]}")
-
- # 将所有 chunk 合并成最终的 AIMessage
- if chunks:
- response = chunks[0].content
- for chunk in chunks[1:]:
- response = response + chunk.content
- # 将所有 chunk 合并成最终的 AIMessage
- if chunks:
- response = chunks[0]
- for chunk in chunks[1:]:
- response = response + chunk
- else:
- response = AIMessage(content="")
- info(f"[llm_call] ⚠️ 警告: 没有收到任何 chunks!")
-
- elapsed_time = time.time() - start_time
-
- # 提取 token 用量(兼容不同 LLM 提供商的元数据格式)
- token_usage = {}
- input_tokens = 0
- output_tokens = 0
-
- # 尝试从 response_metadata 中提取
- if hasattr(response, 'response_metadata') and response.response_metadata:
- meta = response.response_metadata
- if 'token_usage' in meta:
- token_usage = meta['token_usage']
- elif 'usage' in meta:
- token_usage = meta['usage']
-
- # 尝试从 additional_kwargs 中提取
- if not token_usage and hasattr(response, 'additional_kwargs'):
- add_kwargs = response.additional_kwargs
- if 'llm_output' in add_kwargs and 'token_usage' in add_kwargs['llm_output']:
- token_usage = add_kwargs['llm_output']['token_usage']
-
- # 提取具体的 token 数值
- if token_usage:
- input_tokens = token_usage.get('prompt_tokens', token_usage.get('input_tokens', 0))
- output_tokens = token_usage.get('completion_tokens', token_usage.get('output_tokens', 0))
-
- # 打印 LLM 的完整输出
- debug("\n" + "="*80)
- debug(f"📥 [LLM输出] 模型: {model_name} 返回的完整响应:")
- debug(f" 消息类型: {response.type.upper()}")
- debug(f" 内容长度: {len(str(response.content))} 字符")
- debug("-"*80)
- debug(f"{response.content}")
-
- # 打印响应统计信息
- info(f"⏱️ [LLM统计] 调用耗时: {elapsed_time:.2f}秒")
- info(f"📊 [LLM统计] Token用量: 输入={input_tokens}, 输出={output_tokens}, 总计={input_tokens + output_tokens}")
- if token_usage:
- debug(f"📋 [LLM统计] 详细用量: {token_usage}")
- debug("="*80 + "\n")
-
- result = {
- "messages": [response],
- "llm_calls": getattr(state, 'llm_calls', 0) + 1,
- "last_token_usage": token_usage,
- "last_elapsed_time": elapsed_time,
- "turns_since_last_summary": getattr(state, 'turns_since_last_summary', 0) + 1,
- "final_result": response.content,
- "success": True,
- "current_phase": "done",
- "current_model": model_name # 记录实际使用的模型
- }
-
- log_state_change("llm_call", state, "离开")
- return result
-
- except Exception as e:
- elapsed_time = time.time() - start_time
- error(f"\n❌ [LLM错误] 模型 {model_name} 调用失败 (耗时: {elapsed_time:.2f}秒)")
- error(f" 错误类型: {type(e).__name__}")
- error(f" 错误信息: {str(e)}")
- import traceback
- error(f"📋 堆栈: {traceback.format_exc()}")
- debug("="*80 + "\n")
-
- # 返回一个友好的错误消息
- error_response = AIMessage(
- content="抱歉,模型暂时无法响应,可能是网络超时或服务繁忙,请稍后再试。"
- )
- error_result = {
- "messages": [error_response],
- "llm_calls": getattr(state, 'llm_calls', 0),
- "last_token_usage": {},
- "last_elapsed_time": elapsed_time,
- "turns_since_last_summary": getattr(state, 'turns_since_last_summary', 0) + 1,
- "final_result": "抱歉,模型暂时无法响应,可能是网络超时或服务繁忙,请稍后再试。",
- "success": False,
- "current_phase": "done",
- "current_model": model_name
- }
-
- log_state_change("llm_call", state, "离开(异常)")
- return error_result
-
- return call_llm
diff --git a/backend/app/deprecated/main_graph_builder.old.py b/backend/app/deprecated/main_graph_builder.old.py
deleted file mode 100644
index 922ac83..0000000
--- a/backend/app/deprecated/main_graph_builder.old.py
+++ /dev/null
@@ -1,232 +0,0 @@
-"""
-主图构建器 - 构建整合后的完整主图
-"""
-
-from langgraph.graph import StateGraph, START, END
-from typing import Dict, Any
-
-from .state import MainGraphState
-from .nodes.reasoning import react_reason_node
-from .nodes.web_search import web_search_node
-from .nodes.error_handling import error_handling_node
-from .nodes.routing import init_state_node, route_by_reasoning, should_summarize
-from .nodes.hybrid_router import (
- hybrid_router_node,
- route_from_hybrid_decision,
- check_fast_path_success,
-)
-from .nodes.fast_paths import (
- fast_chitchat_node,
- fast_rag_node,
- fast_tool_node,
-)
-from .nodes.llm_call import create_dynamic_llm_call_node
-from .nodes.rag_nodes import rag_retrieve_node
-from .nodes.retrieve_memory import create_retrieve_memory_node
-from .nodes.memory_trigger import memory_trigger_node, set_mem0_client
-from .nodes.summarize import create_summarize_node
-from .nodes.finalize import finalize_node
-from backend.app.subgraphs.contact import build_contact_subgraph
-from backend.app.subgraphs.dictionary import build_dictionary_subgraph
-from backend.app.subgraphs.news_analysis import build_news_analysis_subgraph
-from backend.app.logger import info
-
-from .subgraph_wrapper import create_subgraph_nodes
-
-
-# ========== 主图构建 ==========
-
-def build_react_main_graph(
- chat_services: dict,
- tools=None,
- mem0_client=None,
- use_hybrid_router: bool = True
-) -> StateGraph:
- """
- 构建整合后的完整主图(支持混合路由 + 动态模型选择)
-
- Args:
- chat_services: 模型名称 -> ChatModel 实例 的字典
- tools: 工具列表
- mem0_client: Mem0 客户端实例
- use_hybrid_router: 是否使用混合路由(快速路径 + React 循环)
-
- Returns:
- StateGraph: 构建好的图
- """
- # 创建图
- graph = StateGraph(MainGraphState)
-
- # 设置全局 mem0_client
- if mem0_client:
- set_mem0_client(mem0_client)
-
- # ========== 创建节点 ==========
-
- # LLM 调用节点
- llm_node = create_dynamic_llm_call_node(chat_services, tools or [])
-
- # 记忆节点
- retrieve_memory_node = None
- summarize_node = None
- if mem0_client:
- retrieve_memory_node = create_retrieve_memory_node(mem0_client)
- summarize_node = create_summarize_node(mem0_client)
-
- # 子图节点
- contact_graph = build_contact_subgraph()
- dictionary_graph = build_dictionary_subgraph()
- news_analysis_graph = build_news_analysis_subgraph()
- subgraph_nodes = create_subgraph_nodes(
- contact_graph, dictionary_graph, news_analysis_graph
- )
-
- # ========== 添加节点到图 ==========
-
- # 阶段 1: 记忆检索
- if retrieve_memory_node:
- graph.add_node("retrieve_memory", retrieve_memory_node)
- graph.add_node("memory_trigger", memory_trigger_node)
-
- # 阶段 2: 初始化
- graph.add_node("init_state", init_state_node)
-
- # 阶段 3: 混合路由(可选)
- if use_hybrid_router:
- graph.add_node("hybrid_router", hybrid_router_node)
- graph.add_node("fast_chitchat", fast_chitchat_node)
- graph.add_node("fast_rag", fast_rag_node)
- graph.add_node("fast_tool", fast_tool_node)
-
- # 阶段 4: React 循环推理(始终保留)
- graph.add_node("react_reason", react_reason_node)
- graph.add_node("rag_retrieve", rag_retrieve_node)
- graph.add_node("web_search", web_search_node)
- graph.add_node("handle_error", error_handling_node)
-
- if llm_node is not None:
- graph.add_node("llm_call", llm_node)
-
- # 子图节点
- for node_name, node_func in subgraph_nodes.items():
- graph.add_node(node_name, node_func)
-
- # 阶段 5: 完成处理
- if summarize_node:
- graph.add_node("summarize", summarize_node)
- graph.add_node("finalize", finalize_node)
-
- # ========== 添加边 ==========
-
- # 阶段 1: 记忆检索
- _add_memory_edges(graph, retrieve_memory_node)
-
- # 阶段 2: 初始化
- graph.add_edge("memory_trigger", "init_state")
-
- # 阶段 3: 路由分支
- _add_routing_edges(graph, use_hybrid_router, llm_node)
-
- # 阶段 4: React 循环边
- _add_react_loop_edges(graph, subgraph_nodes)
-
- # 阶段 5: 完成阶段
- _add_finalize_edges(graph, llm_node, summarize_node)
-
- info(f"✅ [图构建] 整合后的完整主图构建完成(混合路由: {use_hybrid_router})")
-
- return graph
-
-
-def _add_memory_edges(graph: StateGraph, retrieve_memory_node) -> None:
- """添加记忆检索阶段的边"""
- if retrieve_memory_node:
- graph.add_edge(START, "retrieve_memory")
- graph.add_edge("retrieve_memory", "memory_trigger")
- else:
- graph.add_edge(START, "memory_trigger")
-
-
-def _add_routing_edges(graph: StateGraph, use_hybrid_router: bool, llm_node) -> None:
- """添加路由阶段的边"""
- if use_hybrid_router:
- graph.add_edge("init_state", "hybrid_router")
-
- # 混合路由条件分支
- graph.add_conditional_edges(
- "hybrid_router",
- route_from_hybrid_decision,
- {
- "fast_chitchat": "fast_chitchat",
- "fast_rag": "fast_rag",
- "fast_tool": "fast_tool",
- "react_loop": "react_reason"
- }
- )
-
- # 快速路径的完成检查(fast_rag 失败直接走 react_reason)
- for fast_node in ["fast_chitchat", "fast_rag", "fast_tool"]:
- graph.add_conditional_edges(
- fast_node,
- check_fast_path_success,
- {
- "llm_call": "llm_call",
- "escalate": "react_reason"
- }
- )
-
- info(f"✅ [图构建] 混合路由模式已启用")
- else:
- graph.add_edge("init_state", "react_reason")
- info(f"✅ [图构建] 纯 React 模式")
-
-
-def _add_react_loop_edges(graph: StateGraph, subgraph_nodes: Dict[str, Any]) -> None:
- """添加 React 循环阶段的边"""
- subgraph_names = list(subgraph_nodes.keys())
-
- # React 推理的条件分支
- graph.add_conditional_edges(
- "react_reason",
- route_by_reasoning,
- {
- "rag_retrieve": "rag_retrieve",
- "web_search": "web_search",
- **{name: name for name in subgraph_names},
- "handle_error": "handle_error",
- "llm_call": "llm_call"
- }
- )
-
- # RAG 检索后回到 react_reason,由意图识别决定下一步
- graph.add_edge("rag_retrieve", "react_reason")
-
- # 循环边(回到 react_reason)
- loop_back_nodes = ["web_search", "handle_error"] + subgraph_names
- for node_name in loop_back_nodes:
- graph.add_edge(node_name, "react_reason")
-
-
-def _add_finalize_edges(graph: StateGraph, llm_node, summarize_node) -> None:
- """添加完成阶段的边"""
- if llm_node is not None:
- if summarize_node:
- graph.add_conditional_edges(
- "llm_call",
- should_summarize,
- {
- "summarize": "summarize",
- "finalize": "finalize"
- }
- )
- graph.add_edge("summarize", "finalize")
- else:
- graph.add_edge("llm_call", "finalize")
-
- graph.add_edge("finalize", END)
-
-
-# ========== 导出 ==========
-__all__ = [
- "build_react_main_graph",
-]
diff --git a/backend/app/deprecated/main_graph_tools/__init__.py b/backend/app/deprecated/main_graph_tools/__init__.py
deleted file mode 100644
index 3244249..0000000
--- a/backend/app/deprecated/main_graph_tools/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""主图工具"""
-from .graph_tools import AVAILABLE_TOOLS, TOOLS_BY_NAME
-from .subgraph_tools import SUBGRAPH_TOOLS, SUBGRAPH_TOOLS_BY_NAME
-
-__all__ = [
- "AVAILABLE_TOOLS",
- "TOOLS_BY_NAME",
- "SUBGRAPH_TOOLS",
- "SUBGRAPH_TOOLS_BY_NAME",
-]
diff --git a/backend/app/deprecated/main_graph_tools/common_tools.py b/backend/app/deprecated/main_graph_tools/common_tools.py
deleted file mode 100644
index e62e6e7..0000000
--- a/backend/app/deprecated/main_graph_tools/common_tools.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""
-公共工具模块 - 联网搜索、可视化图表等公共功能
-Common Tools Module - Web search, visualization, etc.
-"""
-
-from langchain_core.tools import tool
-from typing import Optional
-
-
-@tool
-def web_search_tool(query: str, max_results: int = 5) -> str:
- """
- 联网搜索工具 - 无需 API Key,使用 DuckDuckGo 免费搜索
-
- Args:
- query: 搜索关键词或问题
- max_results: 返回结果数量,默认 5 条
-
- Returns:
- 格式化的搜索结果,包含引用溯源
- """
- try:
- from backend.app.core import web_search
- return web_search(query, max_results)
- except Exception as e:
- return f"联网搜索出错:{str(e)}"
-
-
-@tool
-def generate_chart_tool(data_text: str, chart_type: str = "bar") -> str:
- """
- 可视化图表工具 - 生成 Mermaid 格式图表
-
- Args:
- data_text: 图表数据文本,格式:标题,标签1:值1,标签2:值2,...
- 示例:月度销售额,1月:100,2月:150,3月:200
- chart_type: 图表类型,可选:bar(柱状图)、line(折线图)、pie(饼图)
-
- Returns:
- 格式化的图表输出(Mermaid 格式)
- """
- try:
- from backend.app.core import generate_chart
- return generate_chart(data_text, chart_type)
- except Exception as e:
- return f"生成图表出错:{str(e)}\n\n请使用格式:标题,标签1:值1,标签2:值2,..."
-
-
-# 公共工具列表
-COMMON_TOOLS = [
- web_search_tool,
- generate_chart_tool
-]
-
-COMMON_TOOLS_BY_NAME = {tool.name: tool for tool in COMMON_TOOLS}
diff --git a/backend/app/deprecated/main_graph_tools/graph_tools.py b/backend/app/deprecated/main_graph_tools/graph_tools.py
deleted file mode 100644
index ca8cc0d..0000000
--- a/backend/app/deprecated/main_graph_tools/graph_tools.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""
-工具定义模块 - 子图工具 + RAG 工具 + 公共工具
-Subgraph Tools + RAG Tools + Common Tools
-"""
-
-# 子图工具
-from .subgraph_tools import (
- SUBGRAPH_TOOLS,
- SUBGRAPH_TOOLS_BY_NAME,
- dictionary_tool,
- news_analysis_tool,
- contact_tool
-)
-
-# 公共工具
-from .common_tools import (
- COMMON_TOOLS,
- COMMON_TOOLS_BY_NAME,
- web_search_tool,
- generate_chart_tool
-)
-
-# 工具列表和映射(全局常量)
-AVAILABLE_TOOLS = SUBGRAPH_TOOLS.copy() + COMMON_TOOLS.copy()
-TOOLS_BY_NAME = {**SUBGRAPH_TOOLS_BY_NAME, **COMMON_TOOLS_BY_NAME}
diff --git a/backend/app/deprecated/main_graph_tools/subgraph_tools.py b/backend/app/deprecated/main_graph_tools/subgraph_tools.py
deleted file mode 100644
index 51fc3de..0000000
--- a/backend/app/deprecated/main_graph_tools/subgraph_tools.py
+++ /dev/null
@@ -1,193 +0,0 @@
-"""
-子图工具模块 - 将三个子图包装成 LangChain 工具
-Subgraph Tools Module - Wrap three subgraphs as LangChain tools
-"""
-
-from langchain_core.tools import tool
-from typing import Optional
-
-
-# ============== 词典子图工具 ==============
-@tool
-def dictionary_tool(query: str, action: Optional[str] = None) -> str:
- """
- 词典/翻译工具 - 查询单词、翻译文本、提取术语、获取每日一词
-
- Args:
- query: 用户查询内容(单词、句子、文本等)
- action: 可选,指定操作类型("query" 查询单词,"translate" 翻译,
- "extract" 提取术语,"daily" 每日一词,不指定则自动识别)
-
- Returns:
- 格式化的结果文本
- """
- try:
- from backend.app.subgraphs.dictionary import (
- DictionaryState,
- DictionaryAction,
- parse_intent,
- format_result
- )
- from backend.app.subgraphs.dictionary.nodes import (
- query_word, translate_text, extract_terms, get_daily_word
- )
-
- # 创建初始状态
- state = DictionaryState(user_query=query, user_id="default")
-
- # 处理 action
- if action == "query":
- state.action = DictionaryAction.QUERY
- state.action_params = {"word": query}
- state = query_word(state)
- elif action == "translate":
- state.action = DictionaryAction.TRANSLATE
- state.source_text = query
- state = translate_text(state)
- elif action == "daily":
- state.action = DictionaryAction.DAILY_WORD
- state = get_daily_word(state)
- elif action == "extract":
- state.action = DictionaryAction.EXTRACT
- state.action_params = {"text": query}
- state = extract_terms(state)
- else:
- # 自动解析意图
- state = parse_intent(state)
- if state.action == DictionaryAction.QUERY:
- state = query_word(state)
- elif state.action == DictionaryAction.TRANSLATE:
- state = translate_text(state)
- elif state.action == DictionaryAction.DAILY_WORD:
- state = get_daily_word(state)
- elif state.action == DictionaryAction.EXTRACT:
- state = extract_terms(state)
-
- # 格式化结果
- state = format_result(state)
-
- return state.final_result or "操作完成"
-
- except Exception as e:
- return f"词典工具执行出错:{str(e)}"
-
-
-# ============== 资讯分析子图工具 ==============
-@tool
-def news_analysis_tool(query: str, action: Optional[str] = None) -> str:
- """
- 资讯分析工具 - 查询新闻、分析URL、提取关键词、生成报告
-
- Args:
- query: 用户查询内容(关键词、URL、文本等)
- action: 可选,指定操作类型("query" 查询新闻,"analyze" 分析URL,
- "keywords" 提取关键词,"report" 生成报告,不指定则自动识别)
-
- Returns:
- 格式化的结果文本
- """
- try:
- from backend.app.subgraphs.news_analysis import (
- NewsAnalysisState,
- NewsAction,
- parse_intent,
- format_result
- )
- from backend.app.subgraphs.news_analysis.nodes import (
- query_news, analyze_url, extract_keywords, generate_report
- )
-
- # 创建初始状态
- state = NewsAnalysisState(user_query=query, user_id="default")
-
- # 处理 action
- if action == "query":
- state.action = NewsAction.QUERY_NEWS
- state = query_news(state)
- elif action == "analyze":
- state.action = NewsAction.ANALYZE_URL
- state.custom_urls = [query]
- state = analyze_url(state)
- elif action == "keywords":
- state.action = NewsAction.EXTRACT_KEYWORDS
- state = extract_keywords(state)
- elif action == "report":
- state.action = NewsAction.GENERATE_REPORT
- state = generate_report(state)
- else:
- # 自动解析意图
- state = parse_intent(state)
- if state.action == NewsAction.QUERY_NEWS:
- state = query_news(state)
- elif state.action == NewsAction.ANALYZE_URL:
- state.custom_urls = [query]
- state = analyze_url(state)
- elif state.action == NewsAction.EXTRACT_KEYWORDS:
- state = extract_keywords(state)
- elif state.action == NewsAction.GENERATE_REPORT:
- state = generate_report(state)
-
- # 格式化结果
- state = format_result(state)
-
- return state.final_result or "操作完成"
-
- except Exception as e:
- return f"资讯分析工具执行出错:{str(e)}"
-
-
-# ============== 通讯录子图工具 ==============
-@tool
-def contact_tool(query: str, action: Optional[str] = None) -> str:
- """
- 通讯录工具 - 查询联系人、添加联系人、管理通讯录
-
- Args:
- query: 用户查询内容(姓名、电话、信息等)
- action: 可选,指定操作类型(不指定则自动识别)
-
- Returns:
- 格式化的结果文本
- """
- try:
- from backend.app.subgraphs.contact import (
- ContactState,
- ContactAction,
- parse_intent,
- format_result
- )
- from backend.app.subgraphs.contact.nodes import (
- query_contact, add_contact, list_contacts
- )
-
- # 创建初始状态
- state = ContactState(user_query=query, user_id="default")
-
- # 自动解析意图
- state = parse_intent(state)
-
- # 根据解析结果执行操作
- if state.action == ContactAction.QUERY:
- state = query_contact(state)
- elif state.action == ContactAction.ADD:
- state = add_contact(state)
- elif state.action == ContactAction.LIST:
- state = list_contacts(state)
-
- # 格式化结果
- state = format_result(state)
-
- return state.final_result or "操作完成"
-
- except Exception as e:
- return f"通讯录工具执行出错:{str(e)}"
-
-
-# ============== 工具列表 ==============
-SUBGRAPH_TOOLS = [
- dictionary_tool,
- news_analysis_tool,
- contact_tool
-]
-
-SUBGRAPH_TOOLS_BY_NAME = {tool.name: tool for tool in SUBGRAPH_TOOLS}
diff --git a/backend/app/deprecated/rag_nodes.py b/backend/app/deprecated/rag_nodes.py
deleted file mode 100644
index a88856a..0000000
--- a/backend/app/deprecated/rag_nodes.py
+++ /dev/null
@@ -1,269 +0,0 @@
-"""
-RAG 检索节点模块
-包含:RAG 检索、置信度判断、重检索等节点
-"""
-
-import time
-import asyncio
-from typing import Optional
-from datetime import datetime
-from langchain_core.runnables.config import RunnableConfig
-
-from ...main_graph.state import MainGraphState, ErrorRecord, ErrorSeverity
-from ...main_graph.utils.retry_utils import RAG_RETRY_CONFIG
-from backend.app.logger import info, debug
-from ...model_services import get_small_llm_service
-from ._utils import dispatch_custom_event, make_react_event
-
-
-# 置信度阈值配置
-RAG_CONFIDENCE_THRESHOLD = 0.6 # 低于此值认为检索不相关
-
-# 全局 pipeline 实例
-_rag_pipeline = None
-
-
-def _get_rag_pipeline():
- """获取 RAG Pipeline 实例"""
- global _rag_pipeline
- if _rag_pipeline is None:
- from backend.app.rag.pipeline import RAGPipeline
- _rag_pipeline = RAGPipeline(
- num_queries=3,
- rerank_top_n=5,
- use_rerank=True,
- return_parent_docs=True,
- )
- return _rag_pipeline
-
-
-def _get_rag_tool() -> Optional[callable]:
- """获取 RAG 工具"""
- from backend.app.main_graph.utils.rag_initializer import get_rag_tool
- return get_rag_tool()
-
-
-# ========== RAG 检索核心逻辑 ==========
-async def _rag_retrieve_core(state: MainGraphState, pipeline) -> MainGraphState:
- info(f"[RAG Core] _rag_retrieve_core 开始")
- retrieval_query = state.user_query
-
- # 优先使用推理结果中的优化查询 - 从新的结构化字段获取
- reasoning_result = state.react_reasoning.reasoning_result
- if reasoning_result and hasattr(reasoning_result, "retrieval_config"):
- cfg = reasoning_result.retrieval_config
- if cfg and cfg.retrieval_query:
- retrieval_query = cfg.retrieval_query
-
- info(f"[RAG Core] 使用检索查询: {retrieval_query[:50]}...")
- # 直接调用 pipeline 获取文档和上下文
- info(f"[RAG Core] 调用 pipeline.aretrieve")
- documents = await pipeline.aretrieve(retrieval_query)
- info(f"[RAG Core] pipeline.aretrieve 返回,得到 {len(documents)} 个文档")
- info(f"[RAG Core] 调用 pipeline.format_context")
- rag_context = pipeline.format_context(documents)
- info(f"[RAG Core] pipeline.format_context 返回")
-
- info(f"[RAG Core] 获取到 rag_context: {type(rag_context)}, 长度={len(rag_context) if rag_context else 0}")
- info(f"[RAG Core] 获取到 rag_docs: {len(documents)} 个文档")
-
- # 更新状态
- state.rag_context = rag_context
- state.rag_docs = documents # 保存文档用于置信度评估
- state.rag_retrieved = bool(documents) # 有文档才算检索成功
- state.rag_attempts = getattr(state, 'rag_attempts', 0) + 1
- # 移除对 debug_info 的依赖,不再保存 rag_scores
-
- info(f"[RAG Core] _rag_retrieve_core 结束")
- return state
-
-
-# ========== RAG 检索节点 ==========
-async def rag_retrieve_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- info(f"[RAG] rag_retrieve_node 开始")
- state.current_phase = "rag_retrieving"
- start_time = time.time()
-
- info(f"[RAG] 调用 _get_rag_pipeline")
- pipeline = _get_rag_pipeline()
-
- await dispatch_custom_event(
- "react_reasoning",
- make_react_event(state.reasoning_step, "rag_retrieve_start", 1.0, "开始执行 RAG 检索..."),
- config
- )
-
- try:
- info(f"[RAG] 调用 _rag_retrieve_core")
- state = await _rag_retrieve_core(state, pipeline)
- info(f"[RAG] _rag_retrieve_core 返回")
-
- # 评估置信度
- info(f"[RAG] 调用 _evaluate_rag_confidence")
- confidence = await _evaluate_rag_confidence(state)
- state.rag_confidence = confidence
-
- info(f"[RAG] 检索完成,置信度={confidence:.2f},RAG尝试次数={state.rag_attempts}")
-
- state.reasoning_history.append({
- "step": state.reasoning_step,
- "action": "RETRIEVE_RAG",
- "confidence": confidence,
- "reasoning": f"RAG 检索完成,置信度={confidence:.2f}",
- "timestamp": datetime.now().isoformat()
- })
-
- await dispatch_custom_event(
- "react_reasoning",
- make_react_event(state.reasoning_step, "rag_retrieve_complete", confidence,
- f"RAG 检索完成,置信度={confidence:.2f}"),
- config
- )
-
- except Exception as e:
- info(f"[RAG] 检索失败: {e}", exc_info=True)
- state.rag_confidence = 0.0
- state.rag_retrieved = False
-
- info(f"[RAG] rag_retrieve_node 结束")
- return state
-
-
-async def _evaluate_rag_confidence(state: MainGraphState) -> float:
- """评估 RAG 检索结果置信度(综合向量相似度 + 重排分数 + 小模型判断)"""
- query = state.user_query or ""
- rag_context = state.rag_context or ""
-
- if not rag_context:
- return 0.0
-
- # 方式1: 向量相似度(从 rag_docs 中获取)
- embedding_score = _get_embedding_similarity(state)
- info(f"[RAG Confidence] 向量相似度={embedding_score:.3f}")
-
- # 方式2: 重排序分数(从 rag_docs 中获取)
- rerank_score = _get_rerank_score(state)
- info(f"[RAG Confidence] 重排分数={rerank_score:.3f}")
-
- # 方式3: 小模型判断
- llm_score = await _get_llm_score(state)
- info(f"[RAG Confidence] LLM评估={llm_score:.3f}")
-
- # 综合得分(加权平均)
- # 向量相似度权重 0.3,重排权重 0.3,LLM 权重 0.4
- final_score = embedding_score * 0.3 + rerank_score * 0.3 + llm_score * 0.4
- info(f"[RAG Confidence] 综合置信度={final_score:.3f} (embedding={embedding_score:.3f}*0.3 + rerank={rerank_score:.3f}*0.3 + llm={llm_score:.3f}*0.4)")
-
- return final_score
-
-
-def _get_embedding_similarity(state: MainGraphState) -> float:
- """从 rag_docs 中获取向量相似度分数(不再从 debug_info 获取)"""
- # 降级:从 rag_docs 中获取
- rag_docs = getattr(state, "rag_docs", [])
- scores = []
- for doc in rag_docs:
- if isinstance(doc, dict):
- score = doc.get("score", 0.0)
- elif hasattr(doc, "metadata"):
- score = doc.metadata.get("embedding_score", doc.metadata.get("score", 0.0))
- else:
- continue
- if score > 1.0:
- score = min(score / 10.0, 1.0)
- scores.append(score)
-
- return max(scores) if scores else 0.0
-
-
-def _get_rerank_score(state: MainGraphState) -> float:
- """从 rag_docs 中获取重排序分数(不再从 debug_info 获取)"""
- # 降级:从 rag_docs 中获取
- rag_docs = getattr(state, "rag_docs", [])
- scores = []
- for doc in rag_docs:
- if isinstance(doc, dict):
- score = doc.get("rerank_score", 0.0)
- elif hasattr(doc, "metadata"):
- score = doc.metadata.get("rerank_score", 0.0)
- else:
- continue
- if score > 0:
- scores.append(score)
-
- return max(scores) if scores else 0.0
-
-
-async def _get_llm_score(state: MainGraphState) -> float:
- """使用小模型评估检索结果相关性"""
- query = state.user_query or ""
- rag_context = state.rag_context or ""
-
- try:
- llm = get_small_llm_service()
- prompt = f"""评估以下检索结果与用户问题的相关性,返回 0.0-1.0 的分数:
-- 1.0 = 完全相关,能直接回答问题
-- 0.5 = 部分相关,有一定参考价值
-- 0.0 = 完全不相关,无法回答问题
-
-用户问题:{query}
-
-检索结果:{rag_context[:1500]}
-
-只返回一个数字:"""
-
- response = await llm.ainvoke(prompt)
- content = response.content.strip()
-
- import re
- match = re.search(r'(\d+\.?\d*)', content)
- if match:
- score = float(match.group(1))
- return max(0.0, min(1.0, score))
-
- except Exception as e:
- info(f"[RAG Confidence] LLM评估失败: {e}")
-
- return 0.5 # 默认中等置信度
-
-
-# ========== 置信度判断节点 ==========
-def check_rag_confidence(state: MainGraphState) -> str:
- """
- 根据 RAG 置信度判断下一步
-
- Returns:
- "high_confidence" - 高置信度(>=0.6),可直接生成回答
- "low_confidence" - 低置信度(<0.6),需要联网搜索
- "no_rag" - 无检索结果,需要联网搜索
- """
- rag_attempts = getattr(state, 'rag_attempts', 0)
- rag_confidence = getattr(state, 'rag_confidence', 0.0)
-
- info(f"[Confidence Check] rag_attempts={rag_attempts}, rag_confidence={rag_confidence:.2f}")
-
- # 情况1: 没有检索结果
- if not getattr(state, 'rag_retrieved', False) or not state.rag_context:
- info("[Confidence Check] 无检索结果,走联网")
- return "no_rag"
-
- # 情况2: 置信度低于阈值
- if rag_confidence < RAG_CONFIDENCE_THRESHOLD:
- if rag_attempts >= 2:
- info(f"[Confidence Check] 置信度={rag_confidence:.2f}<{RAG_CONFIDENCE_THRESHOLD},且RAG尝试{rag_attempts}次,走联网")
- return "low_confidence"
- else:
- info(f"[Confidence Check] 置信度={rag_confidence:.2f}<{RAG_CONFIDENCE_THRESHOLD},可再尝试RAG一次")
- return "retry_rag"
-
- # 情况3: 高置信度
- info(f"[Confidence Check] 高置信度={rag_confidence:.2f}>={RAG_CONFIDENCE_THRESHOLD},直接生成回答")
- return "high_confidence"
-
-
-# ========== 导出 ==========
-__all__ = [
- "rag_retrieve_node",
- "check_rag_confidence",
- "RAG_CONFIDENCE_THRESHOLD",
-]
diff --git a/backend/app/deprecated/reasoning.py b/backend/app/deprecated/reasoning.py
deleted file mode 100644
index ccf231d..0000000
--- a/backend/app/deprecated/reasoning.py
+++ /dev/null
@@ -1,120 +0,0 @@
-"""
-React 推理节点
-使用 intent.py 进行意图推理
-"""
-
-from typing import Optional
-from datetime import datetime
-from langchain_core.runnables.config import RunnableConfig
-
-from backend.app.core.intent import react_reason_async, ReasoningResult, ReasoningAction
-from ..state import MainGraphState
-from backend.app.logger import info
-from ._utils import dispatch_custom_event, make_react_event
-
-
-async def react_reason_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- """React 模式推理节点:判断下一步做什么"""
- state.current_phase = "react_reasoning"
- state.reasoning_step += 1
-
- info(f"[推理] 第 {state.reasoning_step} 次推理开始")
-
- # ==================================================
- # 优化:如果是第一次推理,检查 hybrid_router 的结果!
- # 避免重复推理!
- # ==================================================
- if state.reasoning_step == 1 and state.hybrid_router.decision and state.hybrid_router.decision.reasoning_result:
- # 有保存的推理结果,直接复用!
- decision = state.hybrid_router.decision
- result: ReasoningResult = decision.reasoning_result
-
- info(f"[推理] 第1次推理,复用 hybrid_router 结果: action={result.action.name}, confidence={result.confidence}")
- if result.reasoning:
- info(f"[推理] 推理过程: {result.reasoning}")
-
- # 记录推理历史
- state.reasoning_history.append({
- "step": state.reasoning_step,
- "action": result.action.name,
- "confidence": result.confidence,
- "reasoning": result.reasoning,
- "timestamp": datetime.now().isoformat()
- })
-
- # 更新状态
- state.react_reasoning.last_reasoning = {
- "action": result.action.name,
- "confidence": result.confidence,
- "reasoning": result.reasoning
- }
- state.react_reasoning.reasoning_result = result
- state.last_action = result.action.name
-
- # 发送推理事件
- await dispatch_custom_event(
- "react_reasoning",
- make_react_event(
- state.reasoning_step,
- result.action.name,
- result.confidence,
- result.reasoning
- ),
- config
- )
-
- return state
-
- # ==================================================
- # 原来的逻辑(第二次推理或没有保存结果时使用)
- # ==================================================
-
- # 步骤1: 准备上下文
- context = {
- "retrieved_docs": state.rag_docs,
- "rag_confidence": getattr(state, "rag_confidence", 0.0),
- "rag_attempts": getattr(state, "rag_attempts", 0),
- "previous_actions": [h.get("action") for h in state.reasoning_history],
- "reasoning_history": state.reasoning_history,
- "messages": state.messages,
- "errors": state.errors
- }
-
- # 步骤2: 执行推理
- result: ReasoningResult = await react_reason_async(state.user_query, context)
-
- info(f"[推理] 推理结果: action={result.action.name}, confidence={result.confidence}")
- if result.reasoning:
- info(f"[推理] 推理过程: {result.reasoning}")
-
- # 步骤3: 记录推理历史
- state.reasoning_history.append({
- "step": state.reasoning_step,
- "action": result.action.name,
- "confidence": result.confidence,
- "reasoning": result.reasoning,
- "timestamp": datetime.now().isoformat()
- })
-
- # 步骤4: 更新状态 - 只使用新的结构化字段
- state.react_reasoning.last_reasoning = {
- "action": result.action.name,
- "confidence": result.confidence,
- "reasoning": result.reasoning
- }
- state.react_reasoning.reasoning_result = result
- state.last_action = result.action.name
-
- # 步骤5: 发送推理事件
- await dispatch_custom_event(
- "react_reasoning",
- make_react_event(
- state.reasoning_step,
- result.action.name,
- result.confidence,
- result.reasoning
- ),
- config
- )
-
- return state
diff --git a/backend/app/deprecated/routing.py b/backend/app/deprecated/routing.py
deleted file mode 100644
index 7f9926d..0000000
--- a/backend/app/deprecated/routing.py
+++ /dev/null
@@ -1,220 +0,0 @@
-"""
-路由与初始化模块
-包含状态初始化节点和条件路由函数
-
-三层统一循环防护:
-1. 全局步数硬上限(reasoning_step > max_steps)
-2. 路由模式检测(A→B→A→B 交替循环)
-3. 状态停滞检测(连续相同动作)
-"""
-
-from datetime import datetime
-
-from backend.app.core.intent import get_route_by_reasoning, ReasoningAction
-from ...main_graph.state import (
- MainGraphState,
- CurrentAction,
- ReactReasoningState,
- HybridRouterState,
- FastPathState
-)
-from backend.app.logger import info
-
-
-# ========== 初始化状态节点 ==========
-def init_state_node(state: MainGraphState) -> MainGraphState:
- """
- 初始化状态节点:在流程开始时设置初始值
-
- 重置策略:
- - 持久化字段(如 messages、turns_since_last_summary)不重置
- - 临时字段(如 rag_context、final_result)重置为初始值
- """
- # 持久化字段保留原样
- # - messages
- # - turns_since_last_summary
- # - user_id
-
- # ========== 重置临时字段 ==========
-
- # 主图控制字段
- state.user_query = ""
- state.current_action = CurrentAction.NONE
- state.current_model = ""
- state.intent_confidence = 0.0
-
- # React 推理专用字段
- state.reasoning_step = 0
- state.last_action = ""
- state.reasoning_history = []
-
- # RAG 相关字段
- state.rag_context = ""
- state.rag_retrieved = False
- state.rag_docs = []
- state.rag_confidence = 0.0
- state.rag_attempts = 0
-
- # 联网搜索相关字段
- state.web_search_results = []
-
- # 错误处理字段
- state.errors = []
- state.current_error = None
- state.retry_action = None
- state.error_message = ""
-
- # 子图结果字段
- state.news_result = None
- state.dictionary_result = None
- state.contact_result = None
-
- # 执行状态
- state.current_phase = "initializing"
- state.final_result = ""
- state.success = False
-
- # 元数据
- state.start_time = None
- state.end_time = None
-
- # 结构化状态
- state.react_reasoning = ReactReasoningState()
- state.hybrid_router = HybridRouterState()
- state.fast_path = FastPathState()
-
- # 统计字段
- state.llm_calls = 0
- state.last_token_usage = {}
- state.last_elapsed_time = 0.0
- state.memory_context = ""
-
- # 向后兼容字段
- state.debug_info = {}
-
- # 设置初始值
- state.current_phase = "initializing"
- state.reasoning_step = 0
- state.start_time = datetime.now().isoformat()
-
- # 从 messages 中提取 user_query(如果没有的话)
- if not state.user_query and state.messages:
- last_msg = state.messages[-1]
- state.user_query = getattr(last_msg, "content", str(last_msg))
-
- return state
-
-
-# ========== 条件路由函数 ==========
-def route_by_reasoning(state: MainGraphState) -> str:
- """
- 根据推理结果决定下一步路由,带三层统一循环防护
-
- 核心逻辑:
- 1. DIRECT_RESPONSE → 直接返回 llm_call
- 2. 子图完成/已有结果 → 直接返回 llm_call
- 3. 步数超限 → 直接返回 llm_call
- 4. 其他 → 正常路由
- """
- # 获取历史动作
- previous_actions = [h.get("action") for h in state.reasoning_history]
-
- info(f"[条件路由] step={state.reasoning_step}, phase={state.current_phase}, history={previous_actions}")
-
- # ========== 获取推理结果 - 从新的结构化字段获取 ==========
- reasoning_result = state.react_reasoning.reasoning_result
- latest_action = reasoning_result.action.name if reasoning_result else None
-
- # ========== 核心检查:DIRECT_RESPONSE 优先 ==========
- # 从 reasoning_result 检查(最新)
- if latest_action == "DIRECT_RESPONSE":
- info(f"[条件路由] 推理结果为 DIRECT_RESPONSE,直接去 llm_call")
- return "llm_call"
-
- # 备用:从历史记录检查
- if previous_actions and previous_actions[-1] == "DIRECT_RESPONSE":
- info(f"[条件路由] 历史记录最新动作为 DIRECT_RESPONSE,直接去 llm_call")
- return "llm_call"
-
- # ========== 子图完成/已有结果 ==========
- if "subgraph_completed" in previous_actions or state.final_result:
- info("[条件路由] 子图已完成或已有结果,直接终止")
- return "llm_call"
-
- # ========== 步数超限 ==========
- if state.reasoning_step > state.max_steps:
- info(f"[条件路由] 步数超限 ({state.reasoning_step}/{state.max_steps}),强制终止")
- return "llm_call"
-
- # ========== 特殊阶段快速通道 ==========
- if state.current_phase in ("max_steps_exceeded", "finalizing", "done"):
- return "llm_call"
- if state.current_phase == "error_handling" or state.current_error:
- return "handle_error"
-
- # ========== 无推理结果,默认终止 ==========
- if not reasoning_result:
- info("[条件路由] 无推理结果,默认去 llm_call")
- return "llm_call"
-
- # ========== 计算目标路由 ==========
- route = get_route_by_reasoning(reasoning_result)
-
- route_mapping = {
- "direct_response": "llm_call",
- "retrieve_rag": "rag_retrieve",
- "re_retrieve_rag": "rag_retrieve",
- "web_search": "web_search",
- "clarify": "llm_call",
- "call_tool": "llm_call",
- "contact": "contact_subgraph",
- "dictionary": "dictionary_subgraph",
- "news_analysis": "news_analysis_subgraph",
- }
- target = route_mapping.get(route, "llm_call")
-
- # ========== RAG 次数硬限制 ==========
- rag_attempts = getattr(state, 'rag_attempts', 0)
- if target == "rag_retrieve" and rag_attempts >= 2:
- info(f"[条件路由] RAG已尝试{rag_attempts}次,强制走联网搜索")
- target = "web_search"
-
- # ========== 循环防护检测 ==========
- # 1. 路由模式检测(A→B→A→B 交替)
- if len(previous_actions) >= 4:
- if (previous_actions[-4] == previous_actions[-2]
- and previous_actions[-3] == previous_actions[-1]
- and previous_actions[-2] != previous_actions[-1]):
- info(f"[条件路由] 检测到路由循环: {previous_actions[-4:]},强制终止")
- return "llm_call"
-
- # 2. 状态停滞检测(连续相同动作 TODO:本来应该是2)
- if len(previous_actions) >= 3 and previous_actions[-1] == previous_actions[-2] and previous_actions[-2] == previous_actions[-3]:
- info(f"[条件路由] 连续相同动作 '{previous_actions[-1]}',强制终止")
- return "llm_call"
-
- # ========== 智能优化 ==========
- if target == "rag_retrieve" and (state.rag_docs or state.rag_context):
- info("[条件路由] RAG 结果已存在,跳过检索")
- return "llm_call"
-
- info(f"[条件路由] 动作={latest_action}, 目标={target}")
- return target
-
-
-# ========== 完成阶段条件路由函数 ==========
-
-def should_summarize(state: MainGraphState) -> str:
- """
- 检查是否需要总结对话(对话足够长时)
-
- Args:
- state: 当前图状态
-
- Returns:
- "summarize" 或 "finalize"
- """
- if state.turns_since_last_summary >= 5: # 每5轮对话总结一次
- return "summarize"
- else:
- return "finalize"
diff --git a/backend/app/deprecated/state.old.py b/backend/app/deprecated/state.old.py
deleted file mode 100644
index a6aad6b..0000000
--- a/backend/app/deprecated/state.old.py
+++ /dev/null
@@ -1,148 +0,0 @@
-"""
-主图状态定义 - React 模式增强版
-Main Graph State Definition - React Mode Enhanced
-
-字段分类说明:
-- 持久化字段:跨轮次保留,不重置
-- 临时字段:每轮对话开始时重置
-"""
-
-from enum import Enum, auto
-from typing import Optional, Dict, Any, Annotated, Sequence, TypedDict, List
-from dataclasses import dataclass, field
-from langgraph.graph import add_messages
-from langchain_core.messages import BaseMessage
-
-
-# ========== 枚举类型 ==========
-class CurrentAction(Enum):
- """主图当前操作类型"""
- NONE = auto()
- GENERAL_CHAT = auto()
- NEWS_ANALYSIS = auto()
- DICTIONARY = auto()
- CONTACT = auto()
-
-
-class ErrorSeverity(Enum):
- """错误严重程度"""
- INFO = auto() # 信息级别,继续执行
- WARNING = auto() # 警告级别,可以重试
- ERROR = auto() # 错误级别,需要处理
- FATAL = auto() # 致命错误,终止执行
-
-
-@dataclass
-class ErrorRecord:
- """错误记录"""
- error_type: str
- error_message: str
- severity: ErrorSeverity = ErrorSeverity.ERROR
- source: str = "" # 来源:哪个节点/子图/工具
- timestamp: str = ""
- retry_count: int = 0 # 已重试次数
- max_retries: int = 3 # 最大重试次数
- context: Dict[str, Any] = field(default_factory=dict) # 错误上下文
-
-
-@dataclass
-class ReactReasoningState:
- """React 推理状态"""
- last_reasoning: Optional[Dict[str, Any]] = None
- reasoning_result: Optional[Any] = None # 实际类型是 ReasoningResult
-
-
-@dataclass
-class HybridRouterState:
- """混合路由状态"""
- decision: Optional[Any] = None # 实际类型是 HybridRouterResult
- start_time: Optional[str] = None
-
-
-@dataclass
-class FastPathState:
- """快速路径状态"""
- chitchat_success: bool = False
- rag_success: bool = False
- tool_success: bool = False
- failed: bool = False
- fail_reason: str = ""
-
-
-@dataclass
-class MainGraphState:
- """
- 主图状态定义
-
- 字段分类:
- - 持久化字段:跨轮次保留,不重置
- - 临时字段:每轮对话开始时重置
- """
-
- # ==================================================
- # 持久化字段(每轮保留)
- # ==================================================
-
- messages: Annotated[Sequence[BaseMessage], add_messages] = field(default_factory=list)
- turns_since_last_summary: int = 0 # 距离上次总结的轮数
- user_id: str = ""
-
- # ==================================================
- # 临时字段(每轮重置)
- # ==================================================
-
- # 主图控制字段
- user_query: str = ""
- current_action: CurrentAction = CurrentAction.NONE
- current_model: str = "" # 本次请求使用的模型
- intent_confidence: float = 0.0
-
- # React 推理专用字段
- reasoning_step: int = 0
- max_steps: int = 10 # 避免过长循环
- last_action: str = ""
- reasoning_history: List[Dict[str, Any]] = field(default_factory=list)
-
- # RAG 相关字段
- rag_context: str = ""
- rag_retrieved: bool = False
- rag_docs: List[Dict[str, Any]] = field(default_factory=list)
- rag_confidence: float = 0.0 # RAG 检索置信度 (0.0-1.0)
- rag_attempts: int = 0 # RAG 检索次数统计
-
- # 联网搜索相关字段
- web_search_results: List[str] = field(default_factory=list)
-
- # 错误处理字段
- errors: List[ErrorRecord] = field(default_factory=list)
- current_error: Optional[ErrorRecord] = None
- retry_action: Optional[str] = None
- error_message: str = ""
-
- # 子图结果字段
- news_result: Optional[Dict[str, Any]] = None
- dictionary_result: Optional[Dict[str, Any]] = None
- contact_result: Optional[Dict[str, Any]] = None
-
- # 执行状态
- current_phase: str = "init"
- final_result: str = ""
- success: bool = False
-
- # 元数据
- start_time: Optional[str] = None
- end_time: Optional[str] = None
-
- # 结构化状态
- react_reasoning: ReactReasoningState = field(default_factory=ReactReasoningState)
- hybrid_router: HybridRouterState = field(default_factory=HybridRouterState)
- fast_path: FastPathState = field(default_factory=FastPathState)
-
- # 统计字段(用于反馈)
- llm_calls: int = 0
- last_token_usage: Dict[str, Any] = field(default_factory=dict)
- last_elapsed_time: float = 0.0
- memory_context: str = "" # 记忆检索结果
-
- # 向后兼容(保留但不推荐使用)
- debug_info: Dict[str, Any] = field(default_factory=dict)
diff --git a/backend/app/deprecated/tool_call.py b/backend/app/deprecated/tool_call.py
deleted file mode 100644
index b990615..0000000
--- a/backend/app/deprecated/tool_call.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""
-工具执行节点模块
-负责执行 AI 调用的工具函数
-"""
-
-import asyncio
-from typing import Any, Dict
-from langchain_core.messages import AIMessage, ToolMessage
-from ...main_graph.config import get_stream_writer
-
-# 本地模块
-from ...main_graph.state import MainGraphState
-from ...utils.logging import log_state_change
-from backend.app.logger import debug, info
-
-def create_tool_call_node(tools_by_name: Dict[str, Any]):
- """
- 工厂函数:创建工具执行节点
-
- Args:
- tools_by_name: 名称到工具函数的映射字典
-
- Returns:
- 异步节点函数
- """
-
- from langchain_core.runnables.config import RunnableConfig
-
- async def call_tools(state: MainGraphState, config: RunnableConfig) -> Dict[str, Any]:
- """
- 工具执行节点(异步方法)
-
- Args:
- state: 当前对话状态
- config: 运行时配置
-
- Returns:
- 包含 ToolMessage 的状态更新
- """
- log_state_change("tool_node", state, "进入")
-
- last_message = state.messages[-1]
- if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
- log_state_change("tool_node", state, "离开(无工具调用)")
- return {"messages": []}
-
- results = []
- loop = asyncio.get_event_loop()
-
- info(f"🛠️ [工具调用] 准备执行 {len(last_message.tool_calls)} 个工具")
-
- for tool_call in last_message.tool_calls:
- tool_name = tool_call["name"]
- tool_args = tool_call["args"]
- tool_id = tool_call["id"]
- tool_func = tools_by_name.get(tool_name)
-
- debug(f" ├─ 调用工具: {tool_name} 参数: {tool_args}")
-
- if tool_func is None:
- err_msg = f"Tool {tool_name} not found"
- debug(f" └─ ❌ {err_msg}")
- results.append(ToolMessage(content=err_msg, tool_call_id=tool_id))
- continue
-
- # 获取流式写入器并发送工具调用开始事件
- writer = get_stream_writer()
- writer({"type": "custom", "data": {"type": "tool_start", "tool": tool_name}})
-
- try:
- # 修复闭包问题:将变量作为默认参数传入 lambda
- # 如果工具支持异步 (ainvoke),优先使用异步调用
- if hasattr(tool_func, 'ainvoke'):
- observation = await tool_func.ainvoke(tool_args)
- else:
- observation = await loop.run_in_executor(
- None,
- lambda args=tool_args: tool_func.invoke(args)
- )
-
- result_preview = str(observation).replace("\n", " ")
- debug(f" └─ ✅ 结果: {result_preview}")
- results.append(ToolMessage(content=str(observation), tool_call_id=tool_id))
-
- # 发送工具调用完成事件
- writer({"type": "custom", "data": {"type": "tool_end", "tool": tool_name, "success": True}})
- except Exception as e:
- debug(f" └─ ❌ 异常: {e}")
- results.append(ToolMessage(content=f"Error: {e}", tool_call_id=tool_id))
-
- # 发送工具调用失败事件
- writer({"type": "custom", "data": {"type": "tool_end", "tool": tool_name, "success": False, "error": str(e)}})
-
- info(f"🛠️ [工具调用] 执行完成,返回 {len(results)} 条 ToolMessage")
-
- result = {"messages": results}
- log_state_change("tool_node", state, "离开")
- return result
-
- return call_tools
diff --git a/backend/app/deprecated/web_search.py b/backend/app/deprecated/web_search.py
deleted file mode 100644
index bba819e..0000000
--- a/backend/app/deprecated/web_search.py
+++ /dev/null
@@ -1,116 +0,0 @@
-"""
-联网搜索节点 - 执行搜索并将结果保存到状态
-"""
-
-from typing import Optional
-from datetime import datetime
-from langchain_core.runnables.config import RunnableConfig
-
-from ...main_graph.state import MainGraphState, ErrorRecord, ErrorSeverity
-from backend.app.logger import info
-
-
-async def web_search_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- """
- 联网搜索节点:执行搜索并将结果保存到状态
- """
- state.current_phase = "web_searching"
-
- # 发送开始事件
- if config:
- try:
- from langchain_core.callbacks.manager import adispatch_custom_event
- callbacks = config.get("callbacks")
- if callbacks:
- await adispatch_custom_event(
- "react_reasoning",
- {
- "step": state.reasoning_step,
- "action": "web_search_start",
- "confidence": 1.0,
- "reasoning": "开始执行联网搜索..."
- },
- callbacks=callbacks
- )
- except Exception as e:
- info(f"[web_search_node] 无法发送开始事件: {e}")
-
- # 获取搜索查询 - 从新的结构化字段获取
- reasoning_result = state.react_reasoning.reasoning_result
- search_query = reasoning_result.metadata.get("search_query", state.user_query) if reasoning_result else state.user_query
-
- try:
- from backend.app.core import web_search
-
- print(f"[WebSearch] 搜索: {search_query}")
- search_result = web_search(search_query, max_results=5)
-
- # 保存搜索结果到状态
- if not hasattr(state, "web_search_results"):
- state.web_search_results = []
- state.web_search_results.append(search_result)
-
- # 将搜索结果添加到 rag_context,供 LLM 使用
- if state.rag_context:
- state.rag_context = f"{state.rag_context}\n\n---\n\n## 🌐 联网搜索结果:\n{search_result}"
- else:
- state.rag_context = f"## 🌐 联网搜索结果:\n{search_result}"
-
- state.success = True
- print(f"[WebSearch] 搜索完成")
-
- # 发送完成事件
- if config:
- try:
- from langchain_core.callbacks.manager import adispatch_custom_event
- callbacks = config.get("callbacks")
- if callbacks:
- await adispatch_custom_event(
- "react_reasoning",
- {
- "step": state.reasoning_step,
- "action": "web_search_complete",
- "confidence": 1.0,
- "reasoning": f"联网搜索完成,找到 {len(search_result) if isinstance(search_result, list) else 1} 条结果"
- },
- callbacks=callbacks
- )
- except Exception as e:
- info(f"[web_search_node] 无法发送完成事件: {e}")
-
- except Exception as e:
- error_record = ErrorRecord(
- error_type="WebSearchError",
- error_message=str(e),
- severity=ErrorSeverity.WARNING,
- source="web_search_node",
- timestamp=datetime.now().isoformat(),
- retry_count=0,
- max_retries=2,
- context={"search_query": search_query}
- )
- state.errors.append(error_record)
- state.current_error = error_record
- state.current_phase = "error_handling"
- state.success = False
-
- # 发送错误事件
- if config:
- try:
- from langchain_core.callbacks.manager import adispatch_custom_event
- callbacks = config.get("callbacks")
- if callbacks:
- await adispatch_custom_event(
- "react_reasoning",
- {
- "step": state.reasoning_step,
- "action": "web_search_error",
- "confidence": 1.0,
- "reasoning": f"联网搜索失败: {str(e)}"
- },
- callbacks=callbacks
- )
- except Exception as e:
- info(f"[web_search_node] 无法发送错误事件: {e}")
-
- return state
diff --git a/backend/app/main_graph/main_graph_builder.py b/backend/app/main_graph/main_graph_builder.py
index 766d8d4..f99445f 100644
--- a/backend/app/main_graph/main_graph_builder.py
+++ b/backend/app/main_graph/main_graph_builder.py
@@ -7,8 +7,7 @@ from langgraph.graph import StateGraph, START, END
from backend.app.main_graph.state import AgentState
from backend.app.main_graph.nodes.memory_trigger import memory_trigger_node, set_mem0_client
from backend.app.main_graph.nodes.agent import create_agent_node
-from backend.app.logger import info, warning
-from backend.app.tools import ALL_TOOLS
+from backend.app.logger import info
def build_agent_graph(
@@ -27,9 +26,6 @@ def build_agent_graph(
Returns:
构建好的 StateGraph(未编译)
"""
- # 获取主模型
- primary_model = chat_services.get("primary", next(iter(chat_services.values())))
-
# ========== 设置全局客户端 ==========
if mem0_client:
set_mem0_client(mem0_client)
@@ -51,9 +47,8 @@ def build_agent_graph(
except Exception as e:
info(f"[Graph Builder] 记忆节点初始化失败: {e}")
- # ========== 3. Agent 节点(包含完整 ReAct 循环) ==========
- llm_with_tools = primary_model.bind_tools(ALL_TOOLS)
- agent_node_fn = create_agent_node(llm_with_tools, primary_model)
+ # ========== 3. Agent 节点(包含完整 ReAct 循环,支持动态模型切换) ==========
+ agent_node_fn = create_agent_node(chat_services)
# ========== 4. 完成节点 ==========
async def finalize_node_simple(state: AgentState):
diff --git a/backend/app/main_graph/nodes/agent.py b/backend/app/main_graph/nodes/agent.py
index e1c9ecd..b60971c 100644
--- a/backend/app/main_graph/nodes/agent.py
+++ b/backend/app/main_graph/nodes/agent.py
@@ -1,120 +1,151 @@
"""
-Agent 节点:完整的 ReAct 循环 + 流式 Tool Calling 拼接
-完全参考指南实现!
+Agent 节点 - 简化版本
+直接定义 agent_node 函数,支持动态模型切换和循环检测
"""
+import hashlib
from typing import Dict, Any, Optional, List
-from langchain_core.messages import SystemMessage, AIMessage, AIMessageChunk, ToolMessage
from langchain_core.runnables.config import RunnableConfig
+from langchain_core.messages import AIMessage, AIMessageChunk, SystemMessage, ToolMessage
+
from backend.app.main_graph.state import AgentState
-from backend.app.logger import info, warning, error
-from backend.app.agent.stream_context import get_stream_queue
+from backend.app.logger import info, error
from backend.app.tools import ALL_TOOLS
+from backend.app.agent.stream_context import get_stream_queue
+from backend.app.agent.prompts import SYSTEM_PROMPT
-# 系统提示词(从 main_graph_builder.py 搬过来)
-SYSTEM_PROMPT = """你是一个智能助手,可以使用多种工具完成复杂任务。你必须用中文回复。
-
-## 核心工具与能力
-你可以使用以下工具(函数),但只能在真正需要时调用,禁止无意义的测试调用或重复调用:
-1. rag_search – 从内部知识库中检索文档,输入为优化后的查询字符串。
-2. web_search – 联网搜索获取最新信息,输入为搜索关键词。
-3. contact_lookup – 查询企业通讯录,输入姓名、部门或邮箱等。
-4. dictionary_lookup – 翻译单词、查询词典或提取术语。
-5. news_analysis – 获取或分析新闻资讯。
-
-## 工作流程(ReAct 决策闭环)
-你必须严格按照思考 → 行动 → 观察的闭环来处理每个请求,具体规则如下:
-
-### 1. 初始决策
-- 如果用户的问题很明确且你已有足够内部知识,可以直接回答,无需调用任何工具。
-- 如果需要外部信息,请按以下优先级选择工具:
- - 优先使用 rag_search。
- - 若第一次 rag_search 返回的结果不相关或质量低,你可以改写查询关键词再次调用 rag_search(最多重复一次)。
- - 如果两次 rag_search 均无法获得满意信息,或者用户明确要求实时资讯,则必须切换为 web_search。
-- 遇到通讯录、词典、新闻类明确需求,直接调用对应的专用工具。
-
-### 2. 观察与反思
-- 每次工具调用返回结果后,你必须先评估结果质量(内容是否相关、是否充分)。
-- 如果信息不足,根据上述规则决定下一步行动;如果信息足够,则直接生成最终答案,绝不再调用任何工具。
-- 在整个过程中,禁止使用工具返回的信息直接重复或编造来源,必须如实标注。
-
-### 3. 结束条件
-当你认为已经拥有足够信息回答用户时,输出最终回复并停止调用工具。若连续调用工具超过 5 轮仍未解决,也必须基于当前收集到的信息给出最佳回答并说明局限性。
-
-## 回答规范
-1. 来源标注:回答开头用方括号注明信息来源,如多处来源按使用顺序列出:
- - 知识库:【知识库:相关文档主题】
- - 联网搜索:【联网搜索:来源网站或摘要】
-2. 思维链:对于需要复杂推理的问题,请将推理过程放在 ... 标签内,并置于回答最前面(来源标注之前)。
-3. 内容要求:回答应重点突出、条理清晰,优先结合用户背景信息进行个性化;若无任何可靠依据,如实说明“暂时无法回答”。
-
-## 特别注意
-- 不要向用户暴露任何工具调用的技术细节(如参数、函数名)。
-- 如果用户只是闲聊、问候或道别,直接友好回复,严禁调用任何工具。
-- 所有联网搜索必须以获取帮助用户为目的,不得搜索无关内容。
-
-现在,请遵循以上规则处理用户的每一次输入。记住:思考 → 行动 → 观察 → 直到完成。"""
+def _normalize_args(args: dict) -> str:
+ """标准化工具参数用于比较"""
+ return str(sorted(args.items()))
-def create_agent_node(llm_with_tools, llm):
- """创建 Agent 节点函数,完整 ReAct 循环"""
+def _is_similar_result(results: List[str], threshold: float = 0.8) -> bool:
+ """检测结果是否相似(简单实现:长度相似+部分内容重复)"""
+ if len(results) < 2:
+ return False
+
+ latest = results[-1]
+ prev = results[-2]
+
+ # 长度差异太大,不算相似
+ if len(latest) == 0 or len(prev) == 0:
+ return len(latest) == len(prev)
+
+ len_ratio = min(len(latest), len(prev)) / max(len(latest), len(prev))
+ if len_ratio < 0.5:
+ return False
+
+ # 检查内容重复度(简单:前100字符)
+ common_len = 0
+ for a, b in zip(latest[:100], prev[:100]):
+ if a == b:
+ common_len += 1
+ else:
+ break
+
+ return (common_len / 100) > threshold
+
+
+def _should_stop_for_loop(tool_calls: List[dict], tool_results: List[str]) -> bool:
+ """
+ 检测是否应该停止(循环检测)
+
+ 条件:连续2次调用相同工具 + 参数相似 + 结果相似
+ """
+ if len(tool_calls) < 2:
+ return False
+
+ # 检查最近的工具调用是否相同
+ last_tc = tool_calls[-1]
+ prev_tc = tool_calls[-2]
+
+ if last_tc["name"] != prev_tc["name"]:
+ return False
+
+ # 参数是否相似
+ last_args = _normalize_args(last_tc["args"])
+ prev_args = _normalize_args(prev_tc["args"])
+
+ if last_args != prev_args:
+ return False
+
+ # 结果是否相似
+ if len(tool_results) >= 2:
+ return _is_similar_result(tool_results[-2:])
+
+ return False
+
+
+def create_agent_node(chat_services: dict):
+ """
+ 创建 Agent 节点 - 支持动态模型切换
+
+ 简化设计:
+ - 直接返回 async 函数,无需工厂包装
+ - 从 config 中获取模型名称,运行时动态切换
+ """
async def agent_node(state: AgentState, config: Optional[RunnableConfig] = None) -> Dict[str, Any]:
- """
- Agent 节点:完整的 ReAct 循环,带流式 token 和工具调用事件
- 兼容流式和非流式两种情况!
-
- Args:
- state: 当前状态
- config: 运行配置
-
- Returns:
- 状态更新字典
- """
- # 获取队列
+ """Agent 节点:完整的 ReAct 循环"""
queue = get_stream_queue()
is_streaming = queue is not None
- # 获取当前步数
+ # 获取步数
current_step = getattr(state, "current_step", 0)
max_steps = getattr(state, "max_steps", 10)
info(f"[Agent] 从第 {current_step} 步开始,最大步数: {max_steps},流式: {is_streaming}")
- # 组装完整消息
- messages = [SystemMessage(content=SYSTEM_PROMPT)] + list(state.messages)
- turn = current_step # 轮次从当前步数开始
+ # 动态获取模型
+ model_name = "primary"
+ if config:
+ configurable = config.get("configurable", {})
+ model_name = configurable.get("model", "primary")
+
+ llm = chat_services.get(model_name)
+ if llm is None:
+ llm = next(iter(chat_services.values()))
+ info(f"[Agent] 模型 '{model_name}' 不可用,使用 '{type(llm).__name__}'")
+
+ llm_with_tools = llm.bind_tools(ALL_TOOLS)
+
+ # 获取记忆上下文
+ memory_context = getattr(state, "memory_context", "暂无用户背景信息")
+
+ # 组装消息(注入记忆上下文到提示词)
+ prompt_with_memory = SYSTEM_PROMPT.format(memory_context=memory_context)
+ messages = [SystemMessage(content=prompt_with_memory)] + list(state.messages)
+ turn = current_step
try:
while turn < max_steps:
turn += 1
info(f"[Agent] 第 {turn} 轮思考")
- # 告诉前端:新的一轮开始(如果流式)
if is_streaming:
- await queue.put({
- "type": "node_start",
- "node": "agent",
- })
+ await queue.put({"type": "node_start", "node": "agent"})
- # 选择 LLM
+ # 选择 LLM(最后一轮不带工具)
if turn >= max_steps:
- info(f"[Agent] 达到步数上限,用不带工具的 LLM")
current_llm = llm.bind_tools([])
+ info(f"[Agent] 达到步数上限,使用无工具模型")
else:
current_llm = llm_with_tools
- # 初始化变量
+ # 初始化
full_content = ""
full_reasoning_content = ""
- pending_tool_calls = {} # key: index, value: {id, name, args_str}
+ pending_tool_calls = {}
final_tool_calls = []
- # 只有流式的时候用 astream,非流式直接用 ainvoke 更快!
+ # 循环检测:记录历史调用
+ tool_call_history: List[dict] = []
+ tool_result_history: List[str] = []
+
+ # 调用 LLM
if is_streaming:
async for chunk in current_llm.astream(messages):
if isinstance(chunk, AIMessageChunk):
- # 1. 处理文本 token
if chunk.content:
full_content += chunk.content
await queue.put({
@@ -123,29 +154,23 @@ def create_agent_node(llm_with_tools, llm):
"token": chunk.content,
"reasoning_token": ""
})
-
- # 2. 处理 reasoning token
+
if hasattr(chunk, 'additional_kwargs') and chunk.additional_kwargs:
- reasoning_content = chunk.additional_kwargs.get("reasoning_content", "")
- if reasoning_content:
- full_reasoning_content += reasoning_content
+ reasoning = chunk.additional_kwargs.get("reasoning_content", "")
+ if reasoning:
+ full_reasoning_content += reasoning
await queue.put({
"type": "llm_token",
"node": "agent",
"token": "",
- "reasoning_token": reasoning_content
+ "reasoning_token": reasoning
})
- # 3. 流式 Tool Calling 拼接逻辑(核心!用 tool_call_chunks!)
if hasattr(chunk, 'tool_call_chunks') and chunk.tool_call_chunks:
for tc_chunk in chunk.tool_call_chunks:
idx = tc_chunk.get("index", 0)
if idx not in pending_tool_calls:
- pending_tool_calls[idx] = {
- "id": "",
- "name": "",
- "args": "" # 初始化为字符串
- }
+ pending_tool_calls[idx] = {"id": "", "name": "", "args": ""}
if tc_chunk.get("id"):
pending_tool_calls[idx]["id"] += tc_chunk["id"]
@@ -159,57 +184,48 @@ def create_agent_node(llm_with_tools, llm):
import json
pending_tool_calls[idx]["args"] += json.dumps(args_val)
else:
- # 非流式,直接 ainvoke
result = await current_llm.ainvoke(messages)
full_content = result.content if result.content else ""
if hasattr(result, 'tool_calls') and result.tool_calls:
final_tool_calls = result.tool_calls
- if hasattr(result, 'additional_kwargs') and result.additional_kwargs:
+ if hasattr(result, 'additional_kwargs'):
full_reasoning_content = result.additional_kwargs.get("reasoning_content", "")
- # 流式调用结束后,整理最终的 tool_calls(只在流式时处理 pending!)
+ # 整理工具调用
if is_streaming:
for idx in sorted(pending_tool_calls.keys()):
tc_data = pending_tool_calls[idx]
- if tc_data["name"]: # 只有有名字的才是有效工具调用
- # 解析参数字符串
+ if tc_data["name"]:
args = {}
if tc_data["args"]:
try:
import json
args = json.loads(tc_data["args"])
except Exception as e:
- info(f"[Agent] Failed to parse args JSON: {e}, raw: {tc_data['args']}")
+ info(f"[Agent] 解析参数失败: {e}")
final_tool_calls.append({
"id": tc_data["id"],
"name": tc_data["name"],
"args": args
})
- # 判断是否有工具调用
+ # 执行工具
if final_tool_calls:
info(f"[Agent] 第 {turn} 轮:调用 {len(final_tool_calls)} 个工具")
-
- # 执行工具调用
new_messages = []
+
for tc in final_tool_calls:
tool_name = tc["name"]
tool_args = tc["args"]
tool_id = tc["id"]
- # 发送工具开始事件(如果流式)
if is_streaming:
await queue.put({
"type": "custom",
- "data": {
- "type": "tool_start",
- "tool": tool_name,
- "args": tool_args,
- "id": tool_id
- }
+ "data": {"type": "tool_start", "tool": tool_name, "args": tool_args, "id": tool_id}
})
- # 找到并执行对应工具
+ # 查找并执行工具
tool_result = ""
tool_found = False
for tool in ALL_TOOLS:
@@ -225,36 +241,32 @@ def create_agent_node(llm_with_tools, llm):
if not tool_found:
tool_result = f"未找到工具: {tool_name}"
- # 发送工具结束事件(如果流式)
if is_streaming:
await queue.put({
"type": "custom",
- "data": {
- "type": "tool_end",
- "tool": tool_name,
- "id": tool_id,
- "result": str(tool_result)
- }
+ "data": {"type": "tool_end", "tool": tool_name, "id": tool_id, "result": str(tool_result)}
})
- # 构造 ToolMessage
- tool_msg = ToolMessage(
- content=str(tool_result),
- tool_call_id=tool_id,
- name=tool_name
- )
- new_messages.append(tool_msg)
+ # 记录历史(用于循环检测)
+ tool_call_history.append({"name": tool_name, "args": tool_args})
+ tool_result_history.append(str(tool_result))
+
+ new_messages.append(ToolMessage(content=str(tool_result), tool_call_id=tool_id, name=tool_name))
+
+ # 循环检测:相同工具 + 相似参数 + 相似结果 → 终止
+ if _should_stop_for_loop(tool_call_history, tool_result_history):
+ info(f"[Agent] ⚠️ 检测到循环,强制终止")
+ # 添加一条终止消息
+ messages.append(AIMessage(content="[系统] 检测到工具调用循环,已终止。"))
+ break
- # 添加到 messages,继续下一轮
messages.extend(new_messages)
continue
-
else:
- # 没有工具调用,最终输出(不需要发 final_answer,因为 llm_token 已经发了)
info(f"[Agent] 第 {turn} 轮:完成,无工具调用")
break
- # 构建完整的 AIMessage 用于状态更新
+ # 构建响应
response_kwargs = {"content": full_content}
if final_tool_calls:
response_kwargs["tool_calls"] = final_tool_calls
@@ -262,7 +274,6 @@ def create_agent_node(llm_with_tools, llm):
if full_reasoning_content:
response.additional_kwargs["reasoning_content"] = full_reasoning_content
- # 返回状态更新
return {
"messages": [response],
"current_step": turn,
@@ -273,12 +284,8 @@ def create_agent_node(llm_with_tools, llm):
error(f"[Agent] ❌ 第 {turn} 轮出错: {e}")
import traceback
error(f"[Agent] 堆栈: {traceback.format_exc()}")
- # 发送错误事件(如果流式)
if is_streaming:
- await queue.put({
- "type": "error",
- "message": str(e)
- })
+ await queue.put({"type": "error", "message": str(e)})
raise
return agent_node
diff --git a/backend/app/main_graph/subgraph_wrapper.py b/backend/app/main_graph/subgraph_wrapper.py
deleted file mode 100644
index 3fef666..0000000
--- a/backend/app/main_graph/subgraph_wrapper.py
+++ /dev/null
@@ -1,150 +0,0 @@
-"""
-子图包装器 - 为子图添加错误处理和事件追踪
-"""
-
-from typing import Dict, Any, Optional
-from datetime import datetime
-
-from langchain_core.runnables.config import RunnableConfig
-
-from .state import MainGraphState, ErrorRecord, ErrorSeverity
-from backend.app.logger import info
-from .nodes._utils import dispatch_custom_event, make_react_event
-
-
-def wrap_subgraph_for_error_handling(subgraph, name: str):
- """
- 包装子图,使其错误能传递给主图
-
- Args:
- subgraph: 编译好的子图
- name: 子图名称(用于错误标识)
-
- Returns: 包装后的节点函数
- """
- async def wrapped_node(state: MainGraphState, config: Optional[RunnableConfig] = None) -> MainGraphState:
- # 发送子图开始事件
- try:
- await dispatch_custom_event(
- "react_reasoning",
- make_react_event(
- state.reasoning_step,
- f"{name}_subgraph_start",
- 1.0,
- f"开始执行 {name} 子图..."
- ),
- config
- )
- except Exception as e:
- info(f"[{name}_subgraph] 无法发送开始事件: {e}")
-
- try:
- # 调用子图(异步,传 config)
- result = await subgraph.ainvoke(state, config=config)
-
- # 更新主图状态
- subgraph_result = None
- if name == "contact":
- state.contact_result = result
- subgraph_result = result.get("final_result", "")
- elif name == "dictionary":
- state.dictionary_result = result
- subgraph_result = result.get("final_result", "")
- elif name == "news_analysis":
- state.news_result = result
- subgraph_result = result.get("final_result", "")
-
- # 设置最终结果
- if subgraph_result:
- state.final_result = subgraph_result
- else:
- state.final_result = "子图执行完成"
-
- # 标记成功
- state.success = True
- state.current_phase = "done"
- state.reasoning_history.append({
- "step": state.reasoning_step,
- "action": "subgraph_completed",
- "confidence": 1.0,
- "reasoning": f"{name}子图执行完成",
- "timestamp": datetime.now().isoformat()
- })
-
- # 发送子图完成事件
- try:
- await dispatch_custom_event(
- "react_reasoning",
- make_react_event(
- state.reasoning_step,
- f"{name}_subgraph_complete",
- 1.0,
- f"{name} 子图执行完成"
- ),
- config
- )
- except Exception as e:
- info(f"[{name}_subgraph] 无法发送完成事件: {e}")
-
- return state
-
- except Exception as e:
- # 捕获子图错误,传递给主图
- error_record = ErrorRecord(
- error_type=f"{name}SubgraphError",
- error_message=str(e),
- severity=ErrorSeverity.WARNING,
- source=f"{name}_subgraph",
- timestamp=datetime.now().isoformat(),
- retry_count=0,
- max_retries=1,
- context={"user_query": state.user_query}
- )
- state.errors.append(error_record)
- state.current_error = error_record
- state.current_phase = "error_handling"
- state.success = False
-
- # 发送子图错误事件
- try:
- await dispatch_custom_event(
- "react_reasoning",
- make_react_event(
- state.reasoning_step,
- f"{name}_subgraph_error",
- 1.0,
- f"{name} 子图执行失败: {str(e)}"
- ),
- config
- )
- except Exception as e:
- info(f"[{name}_subgraph] 无法发送错误事件: {e}")
-
- return state
-
- return wrapped_node
-
-
-def create_subgraph_nodes(contact_graph, dictionary_graph, news_analysis_graph) -> Dict[str, Any]:
- """
- 创建所有子图节点的字典
-
- Args:
- contact_graph: 联系人子图
- dictionary_graph: 词典子图
- news_analysis_graph: 新闻分析子图
-
- Returns:
- 子图节点字典 {name: wrapped_node}
- """
- return {
- "contact_subgraph": wrap_subgraph_for_error_handling(
- contact_graph.compile(), "contact"
- ),
- "dictionary_subgraph": wrap_subgraph_for_error_handling(
- dictionary_graph.compile(), "dictionary"
- ),
- "news_analysis_subgraph": wrap_subgraph_for_error_handling(
- news_analysis_graph.compile(), "news_analysis"
- ),
- }
diff --git a/backend/app/rag/pipeline.py b/backend/app/rag/pipeline.py
index b6f8727..b8f2b41 100644
--- a/backend/app/rag/pipeline.py
+++ b/backend/app/rag/pipeline.py
@@ -1,21 +1,33 @@
"""
RAG 检索流水线
-流程: 检索子文档 → 重排 → 获取父文档 → 返回
+流程: 检索子文档 → 重排 → 获取父文档 → 置信度评估 → 返回
"""
import asyncio
+import re
+from dataclasses import dataclass
from typing import List
from langchain_core.documents import Document
from langchain_core.language_models import BaseLanguageModel
from backend.app.logger import info, warning
-from ..model_services import get_rerank_service, get_small_llm_service
+from ..model_services import get_small_llm_service
from ..rag.rerank import create_document_reranker
from ..rag.query_transform import MultiQueryGenerator
from ..rag.fusion import reciprocal_rank_fusion
from ..rag.retriever import create_parent_hybrid_retriever
+@dataclass
+class RAGResult:
+ """RAG 检索结果(包含置信度)"""
+ content: str # 格式化后的上下文
+ documents: List[Document] # 原始文档
+ confidence: float # 综合置信度 0.0-1.0
+ scores: dict # 各维度分数 {embedding, rerank, llm, final}
+ is_useful: bool # 是否可用(confidence >= 0.6)
+
+
class RAGPipeline:
def __init__(
self,
@@ -26,6 +38,7 @@ class RAGPipeline:
collection_name: str = "rag_documents",
use_rerank: bool = True,
return_parent_docs: bool = True,
+ confidence_threshold: float = 0.6,
):
self.retriever = retriever or create_parent_hybrid_retriever(
collection_name=collection_name, search_k=rerank_top_n * 4
@@ -34,8 +47,9 @@ class RAGPipeline:
self.rerank_top_n = rerank_top_n
self.use_rerank = use_rerank
self.return_parent_docs = return_parent_docs
- self._last_docs = [] # 保存最后一次检索的文档
- self._last_scores = [] # 保存最后一次检索的分数
+ self.confidence_threshold = confidence_threshold
+ self._last_docs: List[Document] = []
+ self._last_scores: List[dict] = []
if llm == "default_small":
try:
@@ -47,62 +61,188 @@ class RAGPipeline:
self.query_generator = MultiQueryGenerator(self.llm, num_queries) if self.llm else None
self.reranker = create_document_reranker() if use_rerank else None
- info(f"[Pipeline] init: rerank={use_rerank}, return_parent={return_parent_docs}")
+ info(f"[Pipeline] init: rerank={use_rerank}, return_parent={return_parent_docs}, threshold={confidence_threshold}")
@property
def last_docs(self) -> List[Document]:
- """获取最后一次检索的文档"""
return self._last_docs
@property
def last_scores(self) -> List[dict]:
- """获取最后一次检索的分数信息"""
return self._last_scores
async def aretrieve(self, query: str) -> List[Document]:
- info(f"[Pipeline] aretrieve 开始: query={query[:50]}...")
- # Step 1: 检索
- info(f"[Pipeline] Step 1: 调用 _retrieve")
- child_docs = await self._retrieve(query)
- info(f"[Pipeline] Step 1 完成: 检索到 {len(child_docs)} 个子文档")
- # 调试:打印子文档长度
- for i, doc in enumerate(child_docs[:5]):
- content_len = len(doc.page_content)
- info(f"[Pipeline] 子文档[{i}] 长度={content_len}字符")
+ """原接口,保持向后兼容"""
+ docs = await self._do_retrieve(query)
+ self._last_docs = docs
+ self._last_scores = self._extract_scores(docs)
+ return docs
- # Step 1.5: 向量初筛(进入重排前先过滤)
+ async def aretrieve_with_confidence(self, query: str, original_query: str = "") -> RAGResult:
+ """
+ 带置信度评估的检索
+
+ Args:
+ query: 检索查询
+ original_query: 原始用户问题(用于置信度评估)
+
+ Returns:
+ RAGResult: 包含内容和置信度的结构化结果
+ """
+ info(f"[Pipeline] aretrieve_with_confidence: query={query[:50]}...")
+
+ # 1. 执行检索
+ docs = await self._do_retrieve(query)
+ self._last_docs = docs
+ self._last_scores = self._extract_scores(docs)
+
+ # 2. 格式化内容
+ content = self.format_context(docs)
+
+ if not docs or not content:
+ info(f"[Pipeline] 无检索结果,置信度=0")
+ return RAGResult(
+ content="",
+ documents=[],
+ confidence=0.0,
+ scores={"embedding": 0.0, "rerank": 0.0, "llm": 0.0, "final": 0.0},
+ is_useful=False
+ )
+
+ # 3. 评估置信度(三维度)
+ scores = await self._evaluate_confidence(
+ query=original_query or query,
+ docs=docs,
+ content=content
+ )
+
+ confidence = scores["final"]
+ is_useful = confidence >= self.confidence_threshold
+
+ info(f"[Pipeline] 置信度评估完成: confidence={confidence:.3f}, is_useful={is_useful}")
+
+ return RAGResult(
+ content=content,
+ documents=docs,
+ confidence=confidence,
+ scores=scores,
+ is_useful=is_useful
+ )
+
+ async def _do_retrieve(self, query: str) -> List[Document]:
+ """执行检索流程"""
+ # Step 1: 检索
+ child_docs = await self._retrieve(query)
+
+ # Step 1.5: 向量初筛
vector_top_n = 20
- info(f"[Pipeline] Step 1.5: 向量初筛: 取前 {vector_top_n} 个(当前 {len(child_docs)} 个)")
if len(child_docs) > vector_top_n:
child_docs = child_docs[:vector_top_n]
- info(f"[Pipeline] Step 1.5 完成: 向量初筛后 {len(child_docs)} 个")
# Step 2: 重排
- info(f"[Pipeline] Step 2: 开始重排")
if self.reranker:
try:
child_docs = self.reranker.compress_documents(child_docs, query, self.rerank_top_n)
- info(f"[Pipeline] Step 2 完成: 重排后 {len(child_docs)} 个")
except Exception as e:
warning(f"[Pipeline] 重排失败: {e}")
child_docs = child_docs[:self.rerank_top_n]
- else:
- info(f"[Pipeline] Step 2 跳过: 未启用 reranker")
# Step 3: 获取父文档
- info(f"[Pipeline] Step 3: 开始获取父文档")
if self.return_parent_docs:
- parent_docs = await self._get_parents(child_docs)
- info(f"[Pipeline] Step 3 完成: 获取到 {len(parent_docs)} 个父文档")
- # 保存分数信息到 last_scores 供外部访问
- self._last_scores = self._extract_scores(parent_docs)
- info(f"[Pipeline] aretrieve 结束: 返回父文档")
- return parent_docs
-
- self._last_scores = self._extract_scores(child_docs)
- info(f"[Pipeline] aretrieve 结束: 返回子文档")
+ return await self._get_parents(child_docs)
return child_docs
+ async def _evaluate_confidence(
+ self,
+ query: str,
+ docs: List[Document],
+ content: str
+ ) -> dict:
+ """
+ 三维度置信度评估
+
+ Returns:
+ {
+ "embedding": float, # 向量相似度 (0-1)
+ "rerank": float, # 重排分数 (0-1)
+ "llm": float, # LLM判断 (0-1)
+ "final": float # 综合分数 (0-1)
+ }
+ """
+ scores = {"embedding": 0.0, "rerank": 0.0, "llm": 0.5, "final": 0.0}
+
+ # 1. 向量相似度
+ if docs:
+ embedding_scores = []
+ for doc in docs:
+ score = doc.metadata.get("embedding_score", doc.metadata.get("score", 0.0))
+ # 归一化(如果分数 > 1)
+ if score > 1.0:
+ score = min(score / 10.0, 1.0)
+ embedding_scores.append(score)
+ scores["embedding"] = max(embedding_scores) if embedding_scores else 0.0
+
+ info(f"[Confidence] embedding={scores['embedding']:.3f}")
+
+ # 2. 重排分数
+ if docs:
+ rerank_scores = []
+ for doc in docs:
+ score = doc.metadata.get("rerank_score", 0.0)
+ # 归一化(假设满分 10)
+ if score > 1.0:
+ score = min(score / 10.0, 1.0)
+ rerank_scores.append(score)
+ scores["rerank"] = max(rerank_scores) if rerank_scores else 0.0
+
+ info(f"[Confidence] rerank={scores['rerank']:.3f}")
+
+ # 3. LLM 判断
+ if self.llm and content:
+ llm_score = await self._get_llm_confidence(query, content)
+ scores["llm"] = llm_score
+ info(f"[Confidence] llm={scores['llm']:.3f}")
+
+ # 4. 综合得分(加权平均)
+ scores["final"] = (
+ scores["embedding"] * 0.25 +
+ scores["rerank"] * 0.25 +
+ scores["llm"] * 0.50
+ )
+
+ info(f"[Confidence] final={scores['final']:.3f}")
+
+ return scores
+
+ async def _get_llm_confidence(self, query: str, context: str) -> float:
+ """使用 LLM 评估检索结果相关性"""
+ try:
+ prompt = f"""评估以下检索结果与用户问题的相关性,返回 0.0-1.0 的分数:
+- 1.0 = 完全相关,能直接回答问题
+- 0.7 = 高度相关,有很大参考价值
+- 0.5 = 部分相关,有一定参考价值
+- 0.3 = 低度相关,参考价值有限
+- 0.0 = 完全不相关,无法回答问题
+
+用户问题:{query}
+
+检索结果:{context[:1500]}
+
+只返回一个数字(0.0-1.0):"""
+
+ response = await self.llm.ainvoke(prompt)
+ content = response.content.strip()
+
+ match = re.search(r'(\d+\.?\d*)', content)
+ if match:
+ score = float(match.group(1))
+ return max(0.0, min(1.0, score))
+
+ except Exception as e:
+ info(f"[Confidence] LLM评估失败: {e}")
+
+ return 0.5 # 默认中等置信度
+
def _extract_scores(self, docs: List[Document]) -> List[dict]:
"""提取文档的分数信息"""
scores = []
@@ -114,84 +254,54 @@ class RAGPipeline:
return scores
async def _retrieve(self, query: str) -> List[Document]:
- info(f"[Pipeline] _retrieve 开始: query={query[:50]}...")
if self.query_generator:
- info(f"[Pipeline] _retrieve: 调用 query_generator.agenerate")
queries = await self.query_generator.agenerate(query)
queries = [query] + [q for q in queries if q != query]
- info(f"[Pipeline] _retrieve: 生成 {len(queries)} 个查询: {queries}")
- info(f"[Pipeline] _retrieve: 开始 asyncio.gather 并行检索")
doc_lists = await asyncio.gather(*[self.retriever.ainvoke(q) for q in queries])
- info(f"[Pipeline] _retrieve: asyncio.gather 完成,得到 {len(doc_lists)} 组结果")
- info(f"[Pipeline] _retrieve: 开始 reciprocal_rank_fusion")
- result = reciprocal_rank_fusion(doc_lists)
- info(f"[Pipeline] _retrieve: RRF 完成,得到 {len(result)} 个文档")
- info(f"[Pipeline] _retrieve 结束")
- return result
- info(f"[Pipeline] _retrieve: query_generator 未启用,直接单次检索")
- result = await self.retriever.ainvoke(query)
- info(f"[Pipeline] _retrieve 结束")
- return result
+ return reciprocal_rank_fusion(doc_lists)
+ return await self.retriever.ainvoke(query)
async def _get_parents(self, child_docs: List[Document]) -> List[Document]:
- info(f"[Pipeline] _get_parents 开始: {len(child_docs)} 个子文档")
- # 收集 parent_id 和对应的分数
- parent_map = {} # parent_id -> (embedding_score, rerank_score)
-
+ parent_map = {}
for doc in child_docs:
pid = doc.metadata.get("parent_id")
if pid and pid not in parent_map:
- # embedding 分数
embedding_score = doc.metadata.get("score", 0.0)
- # rerank 分数(如果有的话)
rerank_score = doc.metadata.get("rerank_score", 0.0)
parent_map[pid] = (embedding_score, rerank_score)
- info(f"[Pipeline] _get_parents: 收集到 {len(parent_map)} 个 unique parent_id")
if not parent_map:
- warning("[Pipeline] 未找到 parent_id,返回子文档")
return child_docs
try:
- info(f"[Pipeline] _get_parents: 调用 create_docstore")
from backend.rag_core import create_docstore
docstore, _ = create_docstore()
- info(f"[Pipeline] _get_parents: 调用 docstore.amget")
- parent_docs =await docstore.amget(list(parent_map.keys()))
- info(f"[Pipeline] _get_parents: docstore.amget 返回 {len(parent_docs)} 个结果")
+ parent_docs = await docstore.amget(list(parent_map.keys()))
- # 构建结果,保持分数信息
result = []
for doc in parent_docs:
if doc:
pid = doc.metadata.get("id")
scores = parent_map.get(pid, (0.0, 0.0))
- # 将分数添加到 metadata 中
doc.metadata["embedding_score"] = scores[0]
doc.metadata["rerank_score"] = scores[1]
- result.append((doc, scores[0] + scores[1] * 2)) # 综合分数,rerank 权重更高
+ # 综合分数,rerank 权重更高
+ result.append((doc, scores[0] + scores[1] * 2))
result.sort(key=lambda x: x[1], reverse=True)
- docs = [d for d, _ in result]
- info(f"[Pipeline] _get_parents: 最终得到 {len(docs)} 个父文档")
- info(f"[Pipeline] _get_parents 结束")
- return docs
+ return [d for d, _ in result]
except Exception as e:
- warning(f"[Pipeline] 获取父文档失败: {e}", exc_info=True)
+ warning(f"[Pipeline] 获取父文档失败: {e}")
return child_docs
def format_context(self, documents: List[Document]) -> str:
- info(f"[Pipeline] format_context 开始: {len(documents)} 个文档")
if not documents:
- info(f"[Pipeline] format_context: 无文档,返回空字符串")
return ""
parts = []
for i, doc in enumerate(documents, 1):
source = doc.metadata.get("source", "未知来源")
parts.append(f"【资料 {i}】来源:{source}\n{doc.page_content}\n---\n")
- result = "\n".join(parts)
- info(f"[Pipeline] format_context 结束: 结果长度={len(result)} 字符")
- return result
+ return "\n".join(parts)
def create_rag_pipeline(**kwargs) -> RAGPipeline:
diff --git a/backend/app/tools/__init__.py b/backend/app/tools/__init__.py
index 59b72e5..74dfebe 100644
--- a/backend/app/tools/__init__.py
+++ b/backend/app/tools/__init__.py
@@ -1,18 +1,16 @@
"""
-Agent Tools - 封装所有功能为 @tool 函数
+Agent Tools - 所有工具统一定义
"""
from langchain_core.tools import tool
-from typing import Optional
from backend.app.logger import info
+# ========== RAG ==========
-# ========== RAG Pipeline(复用现有)
_rag_pipeline = None
def _get_rag_pipeline():
- """获取 RAG Pipeline 实例(复用 rag_nodes.py 的逻辑)"""
global _rag_pipeline
if _rag_pipeline is None:
from backend.app.rag.pipeline import RAGPipeline
@@ -28,161 +26,100 @@ def _get_rag_pipeline():
@tool
async def rag_search(query: str) -> str:
"""
- 检索知识库获取相关信息。
-
- 当用户询问关于系统、业务、文档相关的问题时使用此工具。
-
- Args:
- query: 用户的问题或搜索关键词
-
+ 检索知识库获取相关信息
+
Returns:
- 检索到的相关文档内容
+ 包含检索结果和置信度的结构化回复,格式:
+ - 内容:检索到的相关信息
+ - 置信度评估:基于向量相似度、重排分数、LLM判断的综合评分
"""
- info(f"[RAG Tool] 开始检索: {query[:50]}...")
-
+ info(f"[Tool] rag_search: {query[:30]}...")
try:
pipeline = _get_rag_pipeline()
- documents = await pipeline.aretrieve(query)
- rag_context = pipeline.format_context(documents)
-
- info(f"[RAG Tool] 检索完成,得到 {len(documents)} 个文档")
-
- if rag_context:
- return rag_context
- else:
- return "知识库中没有找到相关内容。"
-
- except Exception as e:
- info(f"[RAG Tool] 检索失败: {e}")
- return f"知识库检索失败: {str(e)}"
+ # 使用带置信度的检索
+ result = await pipeline.aretrieve_with_confidence(query, original_query=query)
+ if not result.content:
+ return "【RAG检索结果】\n未在知识库中找到相关内容。\n置信度:0.0\n建议:可尝试联网搜索获取信息。"
+
+ # 构建包含置信度的回复
+ confidence_desc = "高"
+ if result.confidence < 0.4:
+ confidence_desc = "低"
+ elif result.confidence < 0.6:
+ confidence_desc = "中"
+
+ response = f"""【RAG检索结果】
+{result.content}
+
+【置信度评估】
+- 综合置信度:{result.confidence:.2f}({confidence_desc})
+- 向量相似度:{result.scores['embedding']:.2f}
+- 重排分数:{result.scores['rerank']:.2f}
+- LLM评估:{result.scores['llm']:.2f}
+
+{'✅ 检索结果可信,可直接使用' if result.is_useful else '⚠️ 检索结果置信度较低,可能需要联网搜索补充'}"""
+
+ info(f"[Tool] rag_search 完成: confidence={result.confidence:.3f}, is_useful={result.is_useful}")
+ return response
+
+ except Exception as e:
+ info(f"[Tool] rag_search 失败: {e}")
+ return f"【RAG检索失败】\n错误:{str(e)}\n建议:请稍后重试或使用联网搜索"
+
+
+# ========== 联网搜索 ==========
@tool
def web_search(query: str) -> str:
- """
- 联网搜索获取最新信息。
-
- 当用户询问实时新闻、热点事件、最新资讯或知识库中没有的内容时使用此工具。
-
- Args:
- query: 搜索关键词
-
- Returns:
- 搜索结果摘要
- """
- info(f"[WebSearch Tool] 开始搜索: {query[:50]}...")
-
+ """联网搜索获取最新信息"""
+ info(f"[Tool] web_search: {query[:30]}...")
try:
- from backend.app.core import web_search as core_web_search
- search_result = core_web_search(query, max_results=5)
-
- info(f"[WebSearch Tool] 搜索完成")
- return search_result
-
+ from backend.app.core.web_search import web_search as search_fn
+ return search_fn(query, max_results=5)
except Exception as e:
- info(f"[WebSearch Tool] 搜索失败: {e}")
+ info(f"[Tool] web_search 失败: {e}")
return f"联网搜索失败: {str(e)}"
-# ====== 子图工具封装器
-async def _invoke_subgraph(subgraph_builder, query: str, state_class) -> str:
- """
- 通用子图调用函数
-
- Args:
- subgraph_builder: 子图构建函数
- query: 用户查询
- state_class: 子图状态类
-
- Returns:
- 子图执行结果
- """
+# ========== 子图工具 ==========
+
+async def _call_subgraph(builder_fn, state_cls, query: str) -> str:
+ """通用子图调用"""
try:
- graph = subgraph_builder()
- compiled_graph = graph.compile()
-
- # 构造初始状态
- initial_state = state_class(user_query=query)
-
- # 调用子图
- result = await compiled_graph.ainvoke(initial_state)
-
- # 返回结果
- return result.get("final_result", "子图执行完成")
-
+ graph = builder_fn().compile()
+ state = state_cls(user_query=query)
+ result = await graph.ainvoke(state)
+ return result.get("final_result", "执行完成")
except Exception as e:
- info(f"[Subgraph Tool] 执行失败: {e}")
+ info(f"[Tool] 子图调用失败: {e}")
return f"执行失败: {str(e)}"
@tool
async def contact_lookup(query: str) -> str:
- """
- 查询通讯录信息。
-
- 当用户询问联系人、邮箱、联系方式、发送邮件时使用此工具。
-
- Args:
- query: 用户查询,描述需要的操作
-
- Returns:
- 联系人信息或操作结果
- """
- info(f"[Contact Tool] 查询: {query[:50]}...")
-
+ """查询通讯录"""
from backend.app.subgraphs.contact.graph import build_contact_subgraph
from backend.app.subgraphs.contact.state import ContactState
-
- return await _invoke_subgraph(build_contact_subgraph, query, ContactState)
+ return await _call_subgraph(build_contact_subgraph, ContactState, query)
@tool
async def dictionary_lookup(word: str) -> str:
- """
- 查询词典,获取单词释义、翻译等。
-
- 当用户询问单词、翻译、生词时使用此工具。
-
- Args:
- word: 需要查询的单词或短语
-
- Returns:
- 单词释义和翻译
- """
- info(f"[Dictionary Tool] 查询: {word}")
-
+ """查询词典/翻译"""
from backend.app.subgraphs.dictionary.graph import build_dictionary_subgraph
from backend.app.subgraphs.dictionary.state import DictionaryState
-
- return await _invoke_subgraph(build_dictionary_subgraph, word, DictionaryState)
+ return await _call_subgraph(build_dictionary_subgraph, DictionaryState, word)
@tool
async def news_analysis(topic: str) -> str:
- """
- 分析热点新闻和资讯。
-
- 当用户询问新闻分析、热点解读时使用此工具。
-
- Args:
- topic: 新闻主题或关键词
-
- Returns:
- 新闻分析结果
- """
- info(f"[NewsAnalysis Tool] 分析: {topic}")
-
+ """分析新闻热点"""
from backend.app.subgraphs.news_analysis.graph import build_news_analysis_subgraph
from backend.app.subgraphs.news_analysis.state import NewsAnalysisState
-
- return await _invoke_subgraph(build_news_analysis_subgraph, topic, NewsAnalysisState)
+ return await _call_subgraph(build_news_analysis_subgraph, NewsAnalysisState, topic)
-# ====== 导出所有工具
-ALL_TOOLS = [
- rag_search,
- web_search,
- contact_lookup,
- dictionary_lookup,
- news_analysis,
-]
+# ========== 导出 ==========
+
+ALL_TOOLS = [rag_search, web_search, contact_lookup, dictionary_lookup, news_analysis]