agent_old.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. """
  2. 基于 StateGraph 的、具备上下文感知能力的 React Agent 核心实现
  3. """
  4. import logging
  5. import json
  6. import pandas as pd
  7. from typing import List, Optional, Dict, Any, Tuple
  8. from contextlib import AsyncExitStack
  9. from langchain_openai import ChatOpenAI
  10. from langchain_core.messages import HumanMessage, ToolMessage, BaseMessage
  11. from langgraph.graph import StateGraph, END
  12. from langgraph.prebuilt import ToolNode
  13. from redis.asyncio import Redis
  14. try:
  15. from langgraph.checkpoint.redis import AsyncRedisSaver
  16. except ImportError:
  17. AsyncRedisSaver = None
  18. # 从新模块导入配置、状态和工具
  19. from . import config
  20. from .state import AgentState
  21. from .sql_tools import sql_tools
  22. logger = logging.getLogger(__name__)
  23. class CustomReactAgent:
  24. """
  25. 一个使用 StateGraph 构建的、具备上下文感知和持久化能力的 Agent。
  26. """
  27. def __init__(self):
  28. """私有构造函数,请使用 create() 类方法来创建实例。"""
  29. self.llm = None
  30. self.tools = None
  31. self.agent_executor = None
  32. self.checkpointer = None
  33. self._exit_stack = None
  34. @classmethod
  35. async def create(cls):
  36. """异步工厂方法,创建并初始化 CustomReactAgent 实例。"""
  37. instance = cls()
  38. await instance._async_init()
  39. return instance
  40. async def _async_init(self):
  41. """异步初始化所有组件。"""
  42. logger.info("🚀 开始初始化 CustomReactAgent...")
  43. # 1. 初始化 LLM
  44. self.llm = ChatOpenAI(
  45. api_key=config.QWEN_API_KEY,
  46. base_url=config.QWEN_BASE_URL,
  47. model=config.QWEN_MODEL,
  48. temperature=0.1,
  49. model_kwargs={
  50. "extra_body": {
  51. "enable_thinking": False,
  52. "misc": {
  53. "ensure_ascii": False
  54. }
  55. }
  56. }
  57. )
  58. logger.info(f" LLM 已初始化,模型: {config.QWEN_MODEL}")
  59. # 2. 绑定工具
  60. self.tools = sql_tools
  61. self.llm_with_tools = self.llm.bind_tools(self.tools)
  62. logger.info(f" 已绑定 {len(self.tools)} 个工具。")
  63. # 3. 初始化 Redis Checkpointer
  64. if config.REDIS_ENABLED and AsyncRedisSaver is not None:
  65. try:
  66. self._exit_stack = AsyncExitStack()
  67. checkpointer_manager = AsyncRedisSaver.from_conn_string(config.REDIS_URL)
  68. self.checkpointer = await self._exit_stack.enter_async_context(checkpointer_manager)
  69. await self.checkpointer.asetup()
  70. logger.info(f" AsyncRedisSaver 持久化已启用: {config.REDIS_URL}")
  71. except Exception as e:
  72. logger.error(f" ❌ RedisSaver 初始化失败: {e}", exc_info=True)
  73. if self._exit_stack:
  74. await self._exit_stack.aclose()
  75. self.checkpointer = None
  76. else:
  77. logger.warning(" Redis 持久化功能已禁用。")
  78. # 4. 构建 StateGraph
  79. self.agent_executor = self._create_graph()
  80. logger.info(" StateGraph 已构建并编译。")
  81. logger.info("✅ CustomReactAgent 初始化完成。")
  82. async def close(self):
  83. """清理资源,关闭 Redis 连接。"""
  84. if self._exit_stack:
  85. await self._exit_stack.aclose()
  86. self._exit_stack = None
  87. self.checkpointer = None
  88. logger.info("✅ RedisSaver 资源已通过 AsyncExitStack 释放。")
  89. def _create_graph(self):
  90. """定义并编译 StateGraph。"""
  91. builder = StateGraph(AgentState)
  92. # 定义节点
  93. builder.add_node("agent", self._agent_node)
  94. builder.add_node("prepare_tool_input", self._prepare_tool_input_node)
  95. builder.add_node("tools", ToolNode(self.tools))
  96. builder.add_node("update_state_after_tool", self._update_state_after_tool_node)
  97. builder.add_node("format_final_response", self._format_final_response_node)
  98. # 定义边
  99. builder.set_entry_point("agent")
  100. builder.add_conditional_edges(
  101. "agent",
  102. self._should_continue,
  103. {
  104. "continue": "prepare_tool_input",
  105. "end": "format_final_response"
  106. }
  107. )
  108. builder.add_edge("prepare_tool_input", "tools")
  109. builder.add_edge("tools", "update_state_after_tool")
  110. builder.add_edge("update_state_after_tool", "agent")
  111. builder.add_edge("format_final_response", END)
  112. # 编译图,并传入 checkpointer
  113. return builder.compile(checkpointer=self.checkpointer)
  114. def _should_continue(self, state: AgentState) -> str:
  115. """判断是继续调用工具还是结束。"""
  116. last_message = state["messages"][-1]
  117. if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
  118. return "end"
  119. return "continue"
  120. def _agent_node(self, state: AgentState) -> Dict[str, Any]:
  121. """Agent 节点:调用 LLM 进行思考和决策。"""
  122. logger.info(f"🧠 [Node] agent - Thread: {state['thread_id']}")
  123. messages_for_llm = list(state["messages"])
  124. if state.get("suggested_next_step"):
  125. instruction = f"基于之前的步骤,强烈建议你下一步执行 '{state['suggested_next_step']}' 操作。"
  126. # 为了避免污染历史,可以考虑不同的注入方式,但这里为了简单直接添加
  127. messages_for_llm.append(HumanMessage(content=instruction, name="system_instruction"))
  128. response = self.llm_with_tools.invoke(messages_for_llm)
  129. logger.info(f" LLM 返回: {response.pretty_print()}")
  130. return {"messages": [response]}
  131. def _prepare_tool_input_node(self, state: AgentState) -> Dict[str, Any]:
  132. """信息组装节点:为需要上下文的工具注入历史消息。"""
  133. logger.info(f"🛠️ [Node] prepare_tool_input - Thread: {state['thread_id']}")
  134. last_message = state["messages"][-1]
  135. if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
  136. return {}
  137. # 创建一个新的 AIMessage 来替换,避免直接修改 state 中的对象
  138. new_tool_calls = []
  139. for tool_call in last_message.tool_calls:
  140. if tool_call["name"] == "generate_sql":
  141. logger.info(" 检测到 generate_sql 调用,注入可序列化的历史消息。")
  142. # 复制一份以避免修改原始 tool_call
  143. modified_args = tool_call["args"].copy()
  144. # 将消息对象列表转换为可序列化的字典列表
  145. serializable_history = []
  146. for msg in state["messages"]:
  147. serializable_history.append({
  148. "type": msg.type,
  149. "content": msg.content
  150. })
  151. modified_args["history_messages"] = serializable_history
  152. new_tool_calls.append({
  153. "name": tool_call["name"],
  154. "args": modified_args,
  155. "id": tool_call["id"],
  156. })
  157. else:
  158. new_tool_calls.append(tool_call)
  159. # 用包含修改后参数的新消息替换掉原来的
  160. last_message.tool_calls = new_tool_calls
  161. return {"messages": [last_message]}
  162. def _update_state_after_tool_node(self, state: AgentState) -> Dict[str, Any]:
  163. """流程建议与错误处理节点:在工具执行后更新状态。"""
  164. logger.info(f"📝 [Node] update_state_after_tool - Thread: {state['thread_id']}")
  165. last_tool_message = state['messages'][-1]
  166. tool_name = last_tool_message.name
  167. tool_output = last_tool_message.content
  168. next_step = None
  169. if tool_name == 'generate_sql':
  170. if "失败" in tool_output or "无法生成" in tool_output:
  171. next_step = 'answer_with_common_sense'
  172. logger.warning(f" generate_sql 失败,建议下一步: {next_step}")
  173. else:
  174. next_step = 'valid_sql'
  175. logger.info(f" generate_sql 成功,建议下一步: {next_step}")
  176. elif tool_name == 'valid_sql':
  177. if "失败" in tool_output:
  178. next_step = 'analyze_validation_error'
  179. logger.warning(f" valid_sql 失败,建议下一步: {next_step}")
  180. else:
  181. next_step = 'run_sql'
  182. logger.info(f" valid_sql 成功,建议下一步: {next_step}")
  183. elif tool_name == 'run_sql':
  184. next_step = 'summarize_final_answer'
  185. logger.info(f" run_sql 执行完毕,建议下一步: {next_step}")
  186. return {"suggested_next_step": next_step}
  187. def _format_final_response_node(self, state: AgentState) -> Dict[str, Any]:
  188. """最终输出格式化节点(当前为占位符)。"""
  189. logger.info(f"🎨 [Node] format_final_response - Thread: {state['thread_id']} - 准备格式化最终输出...")
  190. # 这里可以添加一个标记,表示这是格式化后的输出
  191. last_message = state['messages'][-1]
  192. formatted_content = f"[Formatted Output]\n{last_message.content}"
  193. last_message.content = formatted_content
  194. return {"messages": [last_message]}
  195. async def chat(self, message: str, user_id: str, thread_id: Optional[str] = None) -> Dict[str, Any]:
  196. """
  197. 处理用户聊天请求。
  198. """
  199. if not thread_id:
  200. thread_id = f"{user_id}:{pd.Timestamp.now().strftime('%Y%m%d%H%M%S%f')}"
  201. logger.info(f"🆕 新建会话,Thread ID: {thread_id}")
  202. config = {"configurable": {"thread_id": thread_id}}
  203. # 定义输入
  204. inputs = {
  205. "messages": [HumanMessage(content=message)],
  206. "user_id": user_id,
  207. "thread_id": thread_id,
  208. "suggested_next_step": None, # 初始化建议
  209. }
  210. final_state = None
  211. try:
  212. logger.info(f"🔄 开始处理 - Thread: {thread_id}, User: {user_id}, Message: '{message}'")
  213. # 使用 ainvoke 来执行完整的图流程
  214. final_state = await self.agent_executor.ainvoke(inputs, config)
  215. if final_state and final_state.get("messages"):
  216. answer = final_state["messages"][-1].content
  217. logger.info(f"✅ 处理完成 - Thread: {thread_id}, Final Answer: '{answer}'")
  218. return {"success": True, "answer": answer, "thread_id": thread_id}
  219. else:
  220. logger.error(f"❌ 处理异常结束,最终状态为空 - Thread: {thread_id}")
  221. return {"success": False, "error": "Agent failed to produce a final answer.", "thread_id": thread_id}
  222. except Exception as e:
  223. logger.error(f"❌ 处理过程中发生严重错误 - Thread: {thread_id}: {e}", exc_info=True)
  224. return {"success": False, "error": str(e), "thread_id": thread_id}
  225. async def get_conversation_history(self, thread_id: str) -> List[Dict[str, Any]]:
  226. """从 checkpointer 获取指定线程的对话历史。"""
  227. if not self.checkpointer:
  228. return []
  229. config = {"configurable": {"thread_id": thread_id}}
  230. conversation_state = await self.checkpointer.get(config)
  231. if not conversation_state:
  232. return []
  233. history = []
  234. for msg in conversation_state['values'].get('messages', []):
  235. if isinstance(msg, HumanMessage):
  236. role = "human"
  237. elif isinstance(msg, ToolMessage):
  238. role = "tool"
  239. else: # AIMessage
  240. role = "ai"
  241. history.append({
  242. "type": role,
  243. "content": msg.content,
  244. "tool_calls": getattr(msg, 'tool_calls', None)
  245. })
  246. return history