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

@@ -1,232 +1,185 @@
"""
主图构建器 - 构建整合后的完整主图
极简 Agent 主图 - 回归 LangGraph 标准模式
架构:
START → [init_state] → [记忆] → [Agent] ⇄ [Tools] → [Finalize] → END
↑________↓
"""
from langgraph.graph import StateGraph, START, END
from typing import Dict, Any
from langgraph.prebuilt import ToolNode
from langchain_core.runnables.config import RunnableConfig
from typing import Dict, Any, Optional
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 .state import AgentState
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
from .nodes.agent import create_agent_node
from backend.app.tools import ALL_TOOLS
from backend.app.logger import info, warning
# ========== 主图构建 ==========
def build_react_main_graph(
def build_agent_graph(
chat_services: dict,
tools=None,
mem0_client=None,
use_hybrid_router: bool = True
max_steps: int = 10
) -> StateGraph:
"""
构建整合后的完整主图(支持混合路由 + 动态模型选择)
构建极简 Agent 图
Args:
chat_services: 模型名称 -> ChatModel 实例 的字典
tools: 工具列表
mem0_client: Mem0 客户端实例
use_hybrid_router: 是否使用混合路由(快速路径 + React 循环)
chat_services: 模型服务字典
mem0_client: 记忆客户端(可选)
max_steps: 最大步数限制
Returns:
StateGraph: 构建好的图
"""
# 创建图
graph = StateGraph(MainGraphState)
# 设置全局 mem0_client
graph = StateGraph(AgentState)
# ========== 设置全局客户端 ==========
if mem0_client:
set_mem0_client(mem0_client)
# ========== 创建节点 ==========
# ========== 创建核心节点 ==========
# LLM 调用节点
llm_node = create_dynamic_llm_call_node(chat_services, tools or [])
# 1. Agent 节点(绑定工具的 LLM
llm = chat_services.get("primary", list(chat_services.values())[0] if chat_services else None)
if llm is None:
raise ValueError("No LLM service provided")
# 记忆节点
llm_with_tools = llm.bind_tools(ALL_TOOLS)
agent_node = create_agent_node(llm_with_tools, llm)
# 2. Tool 节点LangGraph 内置)
tool_node = ToolNode(ALL_TOOLS)
# 3. 记忆/总结节点(保留现有)
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)
try:
from .nodes.retrieve_memory import create_retrieve_memory_node
retrieve_memory_node = create_retrieve_memory_node(mem0_client)
summarize_node = create_summarize_node(mem0_client)
except Exception as e:
info(f"[Graph Builder] 记忆节点初始化失败: {e}")
# 子图节点
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. 初始化节点(重置步数)
async def init_state_node(state: AgentState) -> Dict[str, Any]:
"""初始化状态:重置步数计数器"""
info("[Init State] 初始化状态,重置步数")
return {
"current_step": 0
}
# 阶段 1: 记忆检索
graph.add_node("init_state", init_state_node)
# 2. 记忆阶段
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. 核心 Agent 循环
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_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: 完成处理
# 4. 完成阶段
if summarize_node:
graph.add_node("summarize", summarize_node)
graph.add_node("finalize", finalize_node)
# 简单的完成节点
async def finalize_node_simple(state: AgentState, config: Optional[RunnableConfig] = None) -> Dict[str, Any]:
"""简单的完成节点,只发送完成事件"""
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)
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}")
return {}
graph.add_node("finalize", finalize_node_simple)
# ========== 添加边 ==========
# 阶段 1: 记忆检索
_add_memory_edges(graph, retrieve_memory_node)
# 1. 初始化
graph.add_edge(START, "init_state")
# 阶段 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:
"""添加记忆检索阶段的边"""
# 2. 记忆阶段
if retrieve_memory_node:
graph.add_edge(START, "retrieve_memory")
graph.add_edge("init_state", "retrieve_memory")
graph.add_edge("retrieve_memory", "memory_trigger")
else:
graph.add_edge(START, "memory_trigger")
graph.add_edge("init_state", "memory_trigger")
# 3. 进入 Agent
graph.add_edge("memory_trigger", "agent")
def _add_routing_edges(graph: StateGraph, use_hybrid_router: bool, llm_node) -> None:
"""添加路由阶段的边"""
if use_hybrid_router:
graph.add_edge("init_state", "hybrid_router")
# 4. 核心循环Agent ⇄ Tools
def should_continue(state: AgentState) -> str:
"""判断是继续调用工具还是结束"""
messages = state.messages
last_message = messages[-1] if messages else None
# 混合路由条件分支
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"
}
)
# 检查是否有 tool_calls
if last_message and hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
# 快速路径的完成检查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"
}
)
# 否则结束
return "finalize"
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,
"agent",
should_continue,
{
"rag_retrieve": "rag_retrieve",
"web_search": "web_search",
**{name: name for name in subgraph_names},
"handle_error": "handle_error",
"llm_call": "llm_call"
"tools": "tools",
"finalize": "finalize"
}
)
# RAG 检索后回到 react_reason由意图识别决定下一步
graph.add_edge("rag_retrieve", "react_reason")
# Tools 执行完回到 Agent
graph.add_edge("tools", "agent")
# 循环边(回到 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")
# 5. 完成阶段
if summarize_node:
def should_summarize(state: AgentState) -> str:
if state.turns_since_last_summary >= 5:
return "summarize"
return "finalize"
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")
# 总结逻辑暂简化:先 finalize
graph.add_edge("agent", "finalize")
else:
graph.add_edge("agent", "finalize")
graph.add_edge("finalize", END)
# ========== 导出 ==========
__all__ = [
"build_react_main_graph",
]
info("✅ [图构建] 极简 Agent 图构建完成")
return graph

View File

@@ -1,61 +1,21 @@
"""
主图节点模块导出
主图节点模块导出 - 极简架构
"""
# React 模式节点
from .reasoning import react_reason_node
from .web_search import web_search_node
from .error_handling import error_handling_node
from .routing import init_state_node, route_by_reasoning, should_summarize
from .llm_call import create_dynamic_llm_call_node
from .rag_nodes import rag_retrieve_node
# 记忆节点
from .retrieve_memory import create_retrieve_memory_node
from .memory_trigger import memory_trigger_node, set_mem0_client
from .summarize import create_summarize_node
from .finalize import finalize_node
# 混合路由节点
from .hybrid_router import (
hybrid_router_node,
route_from_hybrid_decision,
check_fast_path_success,
)
from .fast_paths import (
fast_chitchat_node,
fast_rag_node,
fast_tool_node,
)
# 通用工具
from ._utils import dispatch_custom_event, make_react_event
# 新架构节点
from .agent import create_agent_node
__all__ = [
# React 模式节点
"init_state_node",
"react_reason_node",
"web_search_node",
"error_handling_node",
"route_by_reasoning",
"should_summarize",
"create_dynamic_llm_call_node",
"rag_retrieve_node",
"rag_re_retrieve_node",
# 记忆节点
"create_retrieve_memory_node",
"memory_trigger_node",
"set_mem0_client",
"create_summarize_node",
"finalize_node",
# 混合路由节点
"hybrid_router_node",
"route_from_hybrid_decision",
"check_fast_path_success",
"fast_chitchat_node",
"fast_rag_node",
"fast_tool_node",
# 通用工具
"dispatch_custom_event",
"make_react_event",
# 新架构节点
"create_agent_node",
]

View File

@@ -0,0 +1,89 @@
"""Agent 节点:核心推理与工具调用"""
from typing import Dict, Any, Optional
from langchain_core.messages import SystemMessage
from langchain_core.runnables.config import RunnableConfig
from ..state import AgentState
from backend.app.logger import info, warning
# 系统提示词(从 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. 思维链:对于需要复杂推理的问题,请将推理过程放在 <think>...</think> 标签内,并置于回答最前面(来源标注之前)。
3. 内容要求:回答应重点突出、条理清晰,优先结合用户背景信息进行个性化;若无任何可靠依据,如实说明“暂时无法回答”。
## 特别注意
- 不要向用户暴露任何工具调用的技术细节(如参数、函数名)。
- 如果用户只是闲聊、问候或道别,直接友好回复,严禁调用任何工具。
- 所有联网搜索必须以获取帮助用户为目的,不得搜索无关内容。
现在,请遵循以上规则处理用户的每一次输入。记住:思考 → 行动 → 观察 → 直到完成。"""
def create_agent_node(llm_with_tools, llm):
"""创建 Agent 节点函数"""
async def agent_node(state: AgentState, config: Optional[RunnableConfig] = None) -> Dict[str, Any]:
"""
Agent 节点:调用带工具的 LLM处理步数限制
Args:
state: 当前状态
config: 运行配置
Returns:
状态更新字典
"""
info(f"[Agent] 第 {state.current_step} 步推理")
# 组装完整消息:系统提示 + 历史消息
full_messages = [SystemMessage(content=SYSTEM_PROMPT)] + state.messages
# 判断是否达到步数上限
if state.current_step >= state.max_steps:
info(f"[Agent] 达到步数上限 {state.max_steps},强制结束,不绑定工具")
llm_no_tools = llm.bind_tools([])
response = await llm_no_tools.ainvoke(full_messages)
else:
response = await llm_with_tools.ainvoke(full_messages)
# 返回状态更新(注意:不原地修改 state返回字典让 LangGraph 处理
return {
"messages": [response],
"current_step": state.current_step + 1,
"llm_calls": state.llm_calls + 1
}
return agent_node

View File

@@ -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",
]

View File

@@ -1,43 +1,42 @@
"""
完成事件节点模块
负责发送完成事件包含token使用情况和耗时信息
完成事件节点模块新架构版本
负责发送完成事件
"""
from typing import Any, Dict
from datetime import datetime
# 本地模块
from ...main_graph.state import MainGraphState
from ...utils.logging import log_state_change
from .state import AgentState
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]:
async def finalize_node(state: AgentState, 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"
}
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:
@@ -47,7 +46,7 @@ async def finalize_node(state: MainGraphState, config: RunnableConfig) -> Dict[s
"type": "done",
"token_usage": state.last_token_usage,
"elapsed_time": state.last_elapsed_time,
"final_result": state.final_result
"final_result": final_reply
}
})
info("🏁 [完成事件] 已发送完成事件")
@@ -55,6 +54,6 @@ async def finalize_node(state: MainGraphState, config: RunnableConfig) -> Dict[s
warning(f"⚠️ [完成事件] 发送完成事件失败 (非致命): {e}")
except Exception as e:
warning(f"⚠️ [完成事件] 处理失败 (非致命): {e}")
log_state_change("finalize", state, "离开")
return result
info("[Finalize] 离开完成节点")
return {}

View File

@@ -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",
]

View File

@@ -1,6 +1,6 @@
from typing import Any, Dict
from langchain_core.runnables.config import RunnableConfig
from ...main_graph.state import MainGraphState
from ..state import AgentState
from ...memory.mem0_client import Mem0Client
from backend.app.logger import info
@@ -14,7 +14,7 @@ def set_mem0_client(client: Mem0Client):
_mem0_client = client
async def memory_trigger_node(state: MainGraphState, config: RunnableConfig) -> Dict[str, Any]:
async def memory_trigger_node(state: AgentState, config: RunnableConfig) -> Dict[str, Any]:
"""检测用户消息中的记忆指令,若命中则主动调用 Mem0 存储"""
if _mem0_client is None:
return {}

View File

@@ -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

View File

@@ -6,7 +6,7 @@
from typing import Any, Dict
# 本地模块
from ...main_graph.state import MainGraphState
from ...main_graph.state import AgentState
from ...memory.mem0_client import Mem0Client
from ...utils.logging import log_state_change
from backend.app.logger import debug
@@ -25,7 +25,7 @@ def create_retrieve_memory_node(mem0_client: Mem0Client):
from langchain_core.runnables.config import RunnableConfig
async def retrieve_memory(state: MainGraphState, config: RunnableConfig) -> Dict[str, Any]:
async def retrieve_memory(state: AgentState, config: RunnableConfig) -> Dict[str, Any]:
"""
记忆检索节点 - 使用 Mem0

View File

@@ -6,7 +6,7 @@
from typing import Any, Dict
# 本地模块
from ...main_graph.state import MainGraphState
from ...main_graph.state import AgentState
from ...memory.mem0_client import Mem0Client
from ...utils.logging import log_state_change
from backend.app.logger import debug, info, error, warning
@@ -25,7 +25,7 @@ def create_summarize_node(mem0_client: Mem0Client):
from langchain_core.runnables.config import RunnableConfig
async def summarize_conversation(state: MainGraphState, config: RunnableConfig) -> Dict[str, Any]:
async def summarize_conversation(state: AgentState, config: RunnableConfig) -> Dict[str, Any]:
"""
记忆存储节点 - 使用 Mem0

View File

@@ -1,148 +1,37 @@
"""
主图状态定义 - React 模式增强版
Main Graph State Definition - React Mode Enhanced
极简 Agent 状态定义 - 只保留真正需要的字段
字段分类说明
- 持久化字段:跨轮次保留,不重置
- 临时字段:每轮对话开始时重置
保留的核心字段:
- messages: 对话历史LangGraph 必需)
- user_id: 用户标识
- 记忆相关turns_since_last_summary, memory_context
- 安全限制current_step, max_steps
- 统计llm_calls, last_token_usage, last_elapsed_time
"""
from enum import Enum, auto
from typing import Optional, Dict, Any, Annotated, Sequence, TypedDict, List
from typing import Annotated, Sequence, Optional, Dict, Any
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:
"""
主图状态定义
字段分类:
- 持久化字段:跨轮次保留,不重置
- 临时字段:每轮对话开始时重置
"""
# ==================================================
# 持久化字段(每轮保留)
# ==================================================
class AgentState:
"""Agent 状态"""
# ========== 核心持久化字段(必需) ==========
messages: Annotated[Sequence[BaseMessage], add_messages] = field(default_factory=list)
turns_since_last_summary: int = 0 # 距离上次总结的轮数
user_id: str = ""
# ==================================================
# 临时字段(每轮重置)
# ==================================================
# ========== 安全限制字段(防止无限循环) ==========
max_steps: int = 10
current_step: int = 0
# 主图控制字段
user_query: str = ""
current_action: CurrentAction = CurrentAction.NONE
current_model: str = "" # 本次请求使用的模型
intent_confidence: float = 0.0
# ========== 记忆相关字段(保留) ==========
turns_since_last_summary: int = 0
memory_context: str = ""
# 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)