LLM——使用 LangGraph 构建 ReAct 智能体:多轮对话 + 工具调用 + 可视化流程图
本文将带大家构建一个具有 ReAct 能力(Reasoning + Acting)的多轮智能体,同时集成 LangChain 工具调用、OpenAI 或通义千问大模型、LangGraph 状态流程管理,并输出流程图,适用于旅行规划、业务辅助、智能客服等多种场景(文末附项目完整代码)。
一、什么是 ReAct 架构?
ReAct(Reason + Act)是一种大模型交互范式,强调以下流程的循环:
- Reasoning:模型分析用户输入和上下文,进行思考。
- Acting:模型选择并调用外部工具(API/函数)。
- Observation:观察工具执行结果,继续推理或输出答案。
它打破了单轮问答限制,使得模型可以多轮调用工具,直到获得满意的结果为止。
二、项目技术栈
技术组件 | 用途 |
---|---|
LangGraph | 构建状态图、流程管理 |
LangChain | 定义工具 / 管理消息历史 |
Qwen/OpenAI | 提供大模型 API 接口 |
ToolNode | 自动处理工具调用 |
Mermaid 图 | 可视化整个智能体调用流程 |
三、核心代码解析
1️⃣ 定义工具函数(Tools)
@tool()
def get_weather(location: str) -> str:"""Get the weather forecast for a given location.Args:location (str): The location for which to retrieve the weather forecast.Returns:str: The weather forecast for the specified location."""return f"The weather forecast for {location} is sunny and warm."@tool()
def get_travel_advise(destination: str, weather: str) -> str:"""Get travel advice for a given destination based on the weather conditions.Args:destination (str): The location for which to give the travel advice.weather (str): The weather at the destinationReturns:str: The travel advice for the specified destination"""return f"The travel advise for is city walk"
通过 @tool()
装饰器,工具可以被模型识别为“可调用动作”。在ReAct 中的 Action(Act) 阶段被触发。
2️⃣ 构建智能体代理(Agent)
def create_agent(llm, tools, system_message: str):"""创建一个代理。"""# 创建一个聊天提示模板prompt = ChatPromptTemplate.from_messages([SystemMessage("你是一个有帮助的AI助手,与其他助手合作。"" 使用提供的工具来推进问题的回答。"" 如果你不能完全回答,没关系,另一个拥有不同工具的助手"" 会接着你的位置继续帮助。执行你能做的以取得进展。"" 如果你或其他助手有最终答案或交付物,"" 在你的回答前加上FINAL ANSWER,以便团队知道停止。"" 你可以使用以下工具: {tool_names}。\n{system_message}",),# 消息占位符MessagesPlaceholder(variable_name="messages"),])# 传递系统消息参数prompt = prompt.partial(system_message=system_message)# 传递工具名称参数prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))# 绑定工具并返回提示模板return prompt | llm.bind_tools(tools)
该函数生成一个具有工具调用能力的 LLM 智能体,具备 ReAct 的 Reasoning 和 Action 触发逻辑。
3️⃣ 定义状态与路由器
class State(TypedDict):messages: Annotated[Sequence[BaseMessage], add_messages]def router(state) -> Literal["tools", "__end__", "chatbot"]:last = state["messages"][-1]if last.tool_calls:return "tools"elif "FINAL ANSWER" in last.content.upper():print("emerge FINAL ANSWER!!!")return "__end__"
其中,add_messages是LangGraph 提供的一个“合并函数(merge function)”,用于处理节点的增量返回。
messages 是消息的序列,每次节点执行后返回新消息时,系统会自动追加到这个字段中,这个过程就是通过 add_messages 完成的。
router()
函数起到 动态流程控制器的作用,是 LangGraph 中的核心条件跳转逻辑。
它实现了:
- 检测是否需要执行工具
- 检测是否已有最终答案(带有 “FINAL ANSWER”)
4️⃣ 构建 LangGraph 状态图
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", lambda state: {"messages": chat_agent.invoke(state)})
graph_builder.add_node("tools", ToolNode(tools))graph_builder.add_edge("tools", "chatbot")
graph_builder.add_conditional_edges("chatbot", router, {"tools": "tools", "__end__": END})
graph_builder.set_entry_point("chatbot")
graph = graph_builder.compile()
(1)lambda state: {"messages": chat_agent.invoke(state)}
是自定义的推理节点,这个节点表示 “由 LLM 进行一次推理”,自定义
- 输入:整个 state(包含历史 messages)
- 输出:新的 messages 追加
这里 chat_agent.invoke(state) 是我们绑定的 LLM agent,它会根据当前对话历史,决定是否回复内容或调用工具。
(2)ToolNode(tools)
是工具执行节点,这是 LangGraph 提供的内置封装,用于处理大模型产生的 tool_call。
ToolNode的执行逻辑是:当 ToolNode 接收到一个 AIMessage(包含 tool_calls),它会
- 读取 tool_calls 列表,执行对应工具函数(比如我们使用@tool()装饰的函数)
- 将执行结果转换成 ToolMessage(…) 实例
- ToolNode 返回 {“messages”: [ToolMessage]}
- LangGraph 查到 messages 的合并策略是 add_messages,自动执行:state[“messages”] += [ToolMessage]
这段代码声明了 ReAct 的循环路径:
chatbot → 判断是否需要工具 → tools → chatbot → ...
直到输出包含 FINAL ANSWER
,流程才终止。
5️⃣ 可视化输出:自动绘制 Mermaid 图
graph_png = graph.get_graph().draw_mermaid_png()
with open("ReAct(Travel).png", "wb") as f:f.write(graph_png)
这使得流程图可导出为 .png
文件,便于分析和设计系统。
6️⃣ 运行部分
events = graph.stream({"messages": [HumanMessage(content="Please give me a travel advice based on the weather in London.")]},{"recursion_limit": 10}
)for e in events:print(e)
这段代码启动一个基于消息历史的 ReAct 循环对话。模型会首先获取天气 → 然后再给出旅行建议 → 输出最终答案。
✅ 效果展示(终端日志)
{'chatbot': {'messages': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_5edea821994449b0bf3b28', 'function': {'arguments': '{"location": "London"}', 'name': 'get_weather'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 416, 'total_tokens': 433, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen2.5-72b-instruct', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--1ed5eb6a-880d-45dc-8fc7-1f7a8496f01b-0', tool_calls=[{'name': 'get_weather', 'args': {'location': 'London'}, 'id': 'call_5edea821994449b0bf3b28', 'type': 'tool_call'}], usage_metadata={'input_tokens': 416, 'output_tokens': 17, 'total_tokens': 433, 'input_token_details': {}, 'output_token_details': {}})}}
{'tools': {'messages': [ToolMessage(content='The weather forecast for London is sunny and warm.', name='get_weather', id='2f510f25-87ce-4ed8-a238-2deab5d18537', tool_call_id='call_5edea821994449b0bf3b28')]}}
{'chatbot': {'messages': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_58529302dae44a32a2a848', 'function': {'arguments': '{"destination": "London", "weather": "sunny and warm"}', 'name': 'get_travel_advise'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 30, 'prompt_tokens': 450, 'total_tokens': 480, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen2.5-72b-instruct', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--864cef64-2167-4e8f-865b-96e0421686df-0', tool_calls=[{'name': 'get_travel_advise', 'args': {'destination': 'London', 'weather': 'sunny and warm'}, 'id': 'call_58529302dae44a32a2a848', 'type': 'tool_call'}], usage_metadata={'input_tokens': 450, 'output_tokens': 30, 'total_tokens': 480, 'input_token_details': {}, 'output_token_details': {}})}}
{'tools': {'messages': [ToolMessage(content='The travel advise for is city walk', name='get_travel_advise', id='5a35f924-bdef-406e-a75f-799c3eb18b48', tool_call_id='call_58529302dae44a32a2a848')]}}
emerge FINAL ANSWER!!!
{'chatbot': {'messages': AIMessage(content='FINAL ANSWER: Based on the sunny and warm weather in London, the travel advice is to go for a city walk. Enjoy exploring the vibrant streets and attractions on foot!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 36, 'prompt_tokens': 495, 'total_tokens': 531, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen2.5-72b-instruct', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--1846b799-e725-4d23-99b1-b73be4ef9c6e-0', usage_metadata={'input_tokens': 495, 'output_tokens': 36, 'total_tokens': 531, 'input_token_details': {}, 'output_token_details': {}})}}
📈 最终生成流程图(ReAct 结构)
我们将会得到如下图所示结构(ReAct(Travel).png
):
🧩 总结:项目已实现的功能
能力 | 说明 |
---|---|
✅ 实现 ReAct 模式 | 基于 LangChain 工具 + LLM 推理 + LangGraph 状态循环 |
✅ 工具调用自动执行 | ToolNode 封装调用逻辑,自动执行 tool_calls |
✅ 动态流程管理 | router() 控制工具/结束状态跳转 |
✅ 状态可视化 | 支持导出 Mermaid 图,分析智能体推理路径 |
🔜 读者可进一步扩展
- ✅ 引入多个 agent,每个 agent 拥有独立工具集
- ✅ 增加 memory/history 持久化支持,实现更长上下文理解
- ✅ 接入真实 API(如天气 API、推荐 API)替代 mock 工具函数
- ✅ 接入 LangSmith 实现可视化追踪和调试
附项目源代码
from typing import Annotated, Sequence
from typing import Literalfrom langchain_core.messages import HumanMessage, SystemMessage, BaseMessage
# 导入聊天提示模板和消息占位符
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from typing_extensions import TypedDict# 定义一个函数,用于创建代理
def create_agent(llm, tools, system_message: str):"""创建一个代理。"""# 创建一个聊天提示模板prompt = ChatPromptTemplate.from_messages([SystemMessage("你是一个有帮助的AI助手,与其他助手合作。"" 使用提供的工具来推进问题的回答。"" 如果你不能完全回答,没关系,另一个拥有不同工具的助手"" 会接着你的位置继续帮助。执行你能做的以取得进展。"" 如果你或其他助手有最终答案或交付物,"" 在你的回答前加上FINAL ANSWER,以便团队知道停止。"" 你可以使用以下工具: {tool_names}。\n{system_message}",),# 消息占位符MessagesPlaceholder(variable_name="messages"),])# 传递系统消息参数prompt = prompt.partial(system_message=system_message)# 传递工具名称参数prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))# 绑定工具并返回提示模板return prompt | llm.bind_tools(tools)@tool()
def get_weather(location: str) -> str:"""Get the weather forecast for a given location.Args:location (str): The location for which to retrieve the weather forecast.Returns:str: The weather forecast for the specified location."""return f"The weather forecast for {location} is sunny and warm."@tool()
def get_travel_advise(destination: str, weather: str) -> str:"""Get travel advice for a given destination based on the weather conditions.Args:destination (str): The location for which to give the travel advice.weather (str): The weather at the destinationReturns:str: The travel advice for the specified destination"""return f"The travel advise for is city walk"tools = [get_weather, get_travel_advise]llm = ChatOpenAI(base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",api_key="your_secret_key",model="qwen2.5-72b-instruct")chat_agent = create_agent(llm, tools, system_message="You are a helpful assistant.")class State(TypedDict):messages: Annotated[Sequence[BaseMessage], add_messages]def router(state) -> Literal["tools", "__end__", "chatbot"]:last = state["messages"][-1]if last.tool_calls:return "tools"elif "FINAL ANSWER" in last.content.upper():print("emerge FINAL ANSWER!!!")return "__end__"graph_builder = StateGraph(State)
graph_builder.add_node("tools", ToolNode(tools))
graph_builder.add_node("chatbot", lambda state: {"messages": chat_agent.invoke(state)})
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_conditional_edges("chatbot", router, {"tools": "tools", "__end__": END}
)
graph_builder.set_entry_point("chatbot")
graph = graph_builder.compile()# 将生成的图片保存到文件
graph_png = graph.get_graph().draw_mermaid_png()
with open("ReAct(Travel).png", "wb") as f:f.write(graph_png)events = graph.stream({"messages": [HumanMessage(content="Please give me a travel advice based on the weather in London.")]},{"recursion_limit": 10}
)for e in events:print(e)