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]