feat: 完成极简 LangGraph 架构迁移,添加 Baosi API 支持
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Failing after 6m36s

主要变更:
- 迁移到极简 LangGraph 标准架构(START → init_state → 记忆 → Agent ⇄ Tools → finalize → END)
- 添加 Baosi API 支持,配置 ops4.7 模型
- 保留本地模型作为默认首选,Baosi 作为备选
- 新架构使用 LangGraph 原生 ToolNode 和 bind_tools
- 移除旧的混合路由、JSON 解析等复杂逻辑
- 把旧代码移到 deprecated/ 目录
- 添加新的 Agent 节点和 Tools 模块
- 添加测试脚本验证新架构
- 所有测试通过 ✓
This commit is contained in:
2026-05-07 00:48:17 +08:00
parent 5e762da740
commit 22fdb625a4
23 changed files with 1232 additions and 494 deletions

View File

@@ -0,0 +1,226 @@
"""
快速路径节点模块
包含闲聊、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",
]

View File

@@ -0,0 +1,60 @@
"""
完成事件节点模块
负责发送完成事件包含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

View File

@@ -0,0 +1,215 @@
"""
混合路由节点模块 - 前置路由决策
负责决定走快速路径还是 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",
]

View File

@@ -0,0 +1,547 @@
"""
意图理解与推理模块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}, 回退到规则")
# 策略2LLM 失败或置信度低,使用规则匹配
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"
]

View File

@@ -0,0 +1,203 @@
"""
统一的 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

View File

@@ -0,0 +1,232 @@
"""
主图构建器 - 构建整合后的完整主图
"""
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",
]

View File

@@ -0,0 +1,120 @@
"""
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

View File

@@ -0,0 +1,148 @@
"""
主图状态定义 - 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)