第 11 章 · ReAct Agent 实现 & create_react_agent 复盘
核心目标:吃透 ReAct 循环的每一步决策,理解
create_react_agent这个”黑盒”内部到底做了什么,并能手写一遍等价的 Graph。
目录
- ReAct 是什么
- 用 create_react_agent 快速创建 Agent
- 给 Agent 加系统提示词
- 流式输出 Agent 的执行过程
- 手动实现 ReAct Graph(加深理解)
- 把 ReAct Agent 封装进项目
- 章节小结
1. ReAct 是什么
ReAct = Reasoning(推理)+ Acting(行动)。
普通的 LLM 调用是”一问一答”,模型一次性给出结果;而 ReAct Agent 是一个循环:模型先思考需要什么信息、要不要调工具,调用工具拿到结果后再思考”信息够不够”,不够就继续调,够了才输出最终答案。
1.1 完整循环流程
这个循环可以跑多轮,直到 LLM 自己判断信息充分、不再发起工具调用为止。多步推理场景(例如”先查天气,再根据天气和时间给建议”)就是在循环里串起多次工具调用。
1.2 消息链路(ReAct 的核心)
一次完整执行后,state["messages"] 里的消息顺序固定为:
| 顺序 | 消息类型 | 含义 |
|---|---|---|
| 1 | HumanMessage | 用户问题 |
| 2 | AIMessage(带 tool_calls) | LLM 决定调用哪个工具、传什么参数 |
| 3 | ToolMessage | 工具执行结果,回灌给模型 |
| 4 | AIMessage(带 tool_calls) | 信息不够,继续调下一个工具(可能多轮) |
| 5 | ToolMessage | 第二个工具结果 |
| … | … | 思考 ↔ 行动循环 |
| 末尾 | AIMessage(带 content,无 tool_calls) | 最终回答 |
Agent 不是魔法:本质就是”模型决策 → 执行工具 → 结果回灌 → 再决策”这条消息链不断追加,直到最后一条 AIMessage 不再带
tool_calls。
第 08 章手写过一次单轮 tool call,ReAct Agent 做的事就是把这个循环封装完整,并支持多轮。LangGraph 1.1 提供了高级封装 create_react_agent,传入 llm 和 tools 就能直接得到一个内部 Graph 已搭好的 Agent。
2. 用 create_react_agent 快速创建 Agent
2.1 新建文件
mkdir -p scripts/ch11touch scripts/ch11/test_react_agent.py2.2 完整代码 scripts/ch11/test_react_agent.py
import asyncio
from langchain_core.messages import HumanMessagefrom langgraph.prebuilt import create_react_agent
from app.ai.llm import llmfrom app.ai.tools import get_weather, calculate, get_current_time
# 工具列表:Agent 能自主决定调用其中的哪几个tools = [get_weather, calculate, get_current_time]
# 一行代码创建完整的 ReAct Agent# create_react_agent 内部已经帮我们搭好了 LangGraph 的图结构agent = create_react_agent(llm, tools)
# 打印 Agent 内部的 Graph 结构(Mermaid 文本),直观看到它搭了什么print("ReAct Agent 内部 Graph 结构:")print(agent.get_graph().draw_mermaid())
async def run_agent(question: str): print(f"\n{'=' * 50}") print(f"用户问题:{question}") print("-" * 50)
result = await agent.ainvoke({ "messages": [HumanMessage(content=question)] })
# 遍历所有消息,复盘 Agent 的完整思考过程 for msg in result["messages"]: msg_type = type(msg).__name__
if hasattr(msg, "tool_calls") and msg.tool_calls: print(f"[{msg_type}] 决定调用工具:{[tc['name'] for tc in msg.tool_calls]}") for tc in msg.tool_calls: print(f" 工具名:{tc['name']},参数:{tc['args']}") elif msg_type == "ToolMessage": print(f"[{msg_type}] 工具返回:{msg.content}") elif msg_type == "AIMessage" and msg.content: print(f"[{msg_type}] AI 回复:{msg.content}")
print(f"\n最终答案:{result['messages'][-1].content}")
async def main(): # 场景一:单工具调用(实时天气) await run_agent("武汉现在天气怎么样?") # 场景二:数学计算(走 calculate 工具,而不是让模型心算) await run_agent("帮我算一下 (256 + 144) / 20 等于多少") # 场景三:不需要工具,模型直接回答 await run_agent("Python 的 list 和 tuple 有什么区别?")
asyncio.run(main())2.3 运行
uv run python scripts/ch11/test_react_agent.py2.4 观察要点
- 前两个问题会出现
AIMessage(tool_calls) → ToolMessage → AIMessage(content)的完整链路; - 第三个问题模型判断无需工具,消息链里只有
HumanMessage → AIMessage; agent.get_graph().draw_mermaid()打印出的结构和第 5 节手写的图是同一套逻辑:agent 节点 ↔ tools 节点循环,无条件可调时直接结束。
3. 给 Agent 加系统提示词
默认创建的 Agent 没有系统提示词,模型按默认行为行事。通过 state_modifier 可以注入系统提示,控制行为风格(语言、简洁度、工具使用倾向等)。
在 scripts/ch11/test_react_agent.py 中追加:
from langchain_core.messages import SystemMessage, HumanMessagefrom langgraph.prebuilt import create_react_agent
from app.ai.llm import llmfrom app.ai.tools import get_weather, calculate, get_current_time
tools = [get_weather, calculate, get_current_time]
# 方式一:直接传字符串,最常用agent_with_system = create_react_agent( llm, tools, state_modifier="你是一个智能助手,在回答问题时优先使用工具获取实时信息。回答要简洁,用中文回复。",)
# 方式二:传一个可调用函数,动态改写消息列表(更灵活)def add_system_message(messages): system = SystemMessage(content="你是一个智能助手,擅长使用工具解决问题。") # 在每轮对话最前面插入 SystemMessage return [system] + messages
agent_with_modifier = create_react_agent( llm, tools, state_modifier=add_system_message,)
async def test_with_system(): result = await agent_with_system.ainvoke({ "messages": [HumanMessage(content="武汉天气怎么样,适合今天出门运动吗?")] }) # 这类问题通常会先调 get_weather,再结合结果给建议 print("带系统提示词的回答:", result["messages"][-1].content)
asyncio.run(test_with_system())两种方式对比:
| 方式 | 入参形式 | 适用场景 |
|---|---|---|
| 方式一 | 固定字符串 | 人设/风格固定,绝大多数场景 |
| 方式二 | 函数 messages -> messages | 需要按上下文动态插入提示、裁剪历史、注入变量 |
4. 流式输出 Agent 的执行过程
create_react_agent 支持 astream_events,可以实时监听”开始思考 → 调工具 → 工具返回 → 继续思考 → 输出答案”的每一个事件,非常适合做前端”思考过程”展示。
新建 scripts/ch11/test_agent_stream.py:
import asyncio
from langchain_core.messages import HumanMessagefrom langgraph.prebuilt import create_react_agent
from app.ai.llm import llmfrom app.ai.tools import get_weather, calculate, get_current_time
tools = [get_weather, calculate, get_current_time]
agent = create_react_agent( llm, tools, state_modifier="你是一个智能助手,使用中文回复,回答要简洁。",)
async def stream_agent(question: str): print(f"\n问题:{question}") print("-" * 40)
# astream_events(v2) 按时间顺序吐出执行过程中的全部事件 async for event in agent.astream_events( {"messages": [HumanMessage(content=question)]}, version="v2", ): event_name = event["event"] event_data = event.get("data", {})
if event_name == "on_chat_model_start": # 模型开始一轮推理 print("🤔 AI 开始思考...")
elif event_name == "on_chat_model_stream": # 模型 token 级流式输出,逐字打印 chunk = event_data.get("chunk") if chunk and hasattr(chunk, "content") and chunk.content: print(chunk.content, end="", flush=True)
elif event_name == "on_tool_start": # 工具开始执行 print(f"\n🔧 调用工具:{event_data.get('name')},参数:{event_data.get('input')}")
elif event_name == "on_tool_end": # 工具执行结束,拿到返回值 print(f"📦 工具返回:{event_data.get('output')}")
elif event_name == "on_chat_model_end": # 本轮模型输出结束(可能是发起 tool_call,也可能是最终答案) print("\n✅ 本轮模型输出结束\n")
async def main(): await stream_agent("现在几点了?武汉天气怎么样,帮我判断要不要带伞")
asyncio.run(main())运行:
uv run python scripts/ch11/test_agent_stream.py关键事件速查
| 事件名 | 触发时机 | 典型用途 |
|---|---|---|
on_chat_model_start | 模型开始一轮推理 | 前端显示”思考中” |
on_chat_model_stream | 逐个 token 生成 | 打字机效果 |
on_tool_start | 工具被调用 | 展示”正在调用 xx 工具” |
on_tool_end | 工具返回结果 | 展示中间观察结果 |
on_chat_model_end | 一轮模型输出结束 | 分段、换行 |
多轮工具调用时,
on_chat_model_start → on_tool_start → on_tool_end → on_chat_model_start ...会重复出现,直到最后一轮模型直接流式输出最终答案,这正是 ReAct 循环在事件流上的体现。
5. 手动实现 ReAct Graph(加深理解)
create_react_agent 是黑盒,手动用 StateGraph 实现一遍等价结构,才能真正看懂它内部。
5.1 核心设计
- State:只维护一个
messages列表,用add_messagesreducer 保证新消息自动追加(而不是覆盖)。 - 两个节点:
chat节点:把消息喂给绑定了工具的 LLM,由模型决定”直接回答”还是”发起 tool_calls”;tools节点:ToolNode,统一执行模型请求的工具调用。
- 条件边:
chat之后用tools_condition判断——有tool_calls就去tools,没有就END;tools执行完固定回到chat,形成循环。
5.2 完整代码 scripts/ch11/manual_react_graph.py
import asynciofrom typing import Annotatedfrom typing_extensions import TypedDict
from langchain_core.messages import HumanMessagefrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langgraph.prebuilt import ToolNode, tools_condition
from app.ai.llm import llmfrom app.ai.tools import get_weather, calculate, get_current_time
tools = [get_weather, calculate, get_current_time]
# 1. 定义状态:messages 借助 add_messages 自动累加class ReactState(TypedDict): messages: Annotated[list, add_messages]
# 2. chat 节点:调用绑定工具后的 LLM,产出 AIMessage(可能带 tool_calls)def chat_node(state: ReactState): llm_with_tools = llm.bind_tools(tools) ai_message = llm_with_tools.invoke(state["messages"]) return {"messages": [ai_message]}
# 3. tools 节点:ToolNode 自动读取上一条 AIMessage 里的 tool_calls 并执行tool_node = ToolNode(tools)
# 4. 搭图graph_builder = StateGraph(ReactState)graph_builder.add_node("chat", chat_node)graph_builder.add_node("tools", tool_node)
# 入口 -> chatgraph_builder.add_edge(START, "chat")
# chat 之后条件路由:# AIMessage 带 tool_calls -> 去 "tools"# AIMessage 不带 tool_calls -> END(输出最终答案)graph_builder.add_conditional_edges("chat", tools_condition)
# 工具执行完 -> 回到 chat,让模型基于 ToolMessage 继续推理graph_builder.add_edge("tools", "chat")
# 编译得到可运行的 graphmanual_agent = graph_builder.compile()
# 5.(可选)不使用 tools_condition,手写条件路由,逻辑完全等价def should_continue(state: ReactState) -> str: last_message = state["messages"][-1] if getattr(last_message, "tool_calls", None): return "tools" # 模型请求调工具 return END # 模型给出最终回答,结束循环
# 用自定义路由替换内置条件边:# graph_builder.add_conditional_edges("chat", should_continue, {"tools": "tools", END: END})
async def main(): # 打印手写图结构,和 create_react_agent 打印的结构对照 print(manual_agent.get_graph().draw_mermaid())
result = await manual_agent.ainvoke({ "messages": [HumanMessage(content="帮我算一下 (256 + 144) / 20,再告诉我现在几点")] }) for msg in result["messages"]: print(f"[{type(msg).__name__}] {msg.content if msg.content else msg.tool_calls}")
asyncio.run(main())5.3 手写图结构
5.4 与 create_react_agent 的对照结论
| 环节 | create_react_agent 内部 | 手写实现 |
|---|---|---|
| 状态 | 内置 messages + add_messages | 自定义 ReactState |
| 模型节点 | 内置 agent 节点,自动 bind_tools | chat_node 手动 bind |
| 工具节点 | 内置 ToolNode | 同样用 ToolNode |
| 路由 | 内置条件边判断 tool_calls | tools_condition / 自写 should_continue |
| 回环 | tools → agent | tools → chat |
两者核心逻辑完全一致:create_react_agent 只是把这套固定写法封装成了一行 API。需要自定义节点、状态字段或路由策略时,就回到手写 StateGraph 的方式。
6. 把 ReAct Agent 封装进项目
6.1 Agent 服务层:app/ai/graphs/react_agent.py
from langchain_core.messages import HumanMessagefrom langgraph.prebuilt import create_react_agent
from app.ai.llm import llmfrom app.ai.tools import get_weather, calculate, get_current_time
# 全局只构建一次 Agent(编译图有开销,避免每次请求重建)tools = [get_weather, calculate, get_current_time]
SYSTEM_PROMPT = ( "你是一个智能助手,优先使用工具获取实时信息和完成精确计算," "使用中文简洁回复。")
react_agent = create_react_agent( llm, tools, state_modifier=SYSTEM_PROMPT,)
class AgentService: """对外暴露的 Agent 服务:普通调用 + 流式调用"""
@staticmethod async def chat(question: str) -> str: """一次性返回最终答案""" result = await react_agent.ainvoke({ "messages": [HumanMessage(content=question)] }) return result["messages"][-1].content
@staticmethod async def stream_chat(question: str): """异步生成器:只把模型的文本 token 逐块吐出,工具过程可按需透传""" async for event in react_agent.astream_events( {"messages": [HumanMessage(content=question)]}, version="v2", ): if event["event"] == "on_chat_model_stream": chunk = event["data"].get("chunk") if chunk and getattr(chunk, "content", None): yield chunk.content6.2 路由层:在 app/routers/chat.py 增加 Agent 接口
from fastapi import APIRouterfrom fastapi.responses import StreamingResponsefrom pydantic import BaseModel
from app.ai.graphs.react_agent import AgentService
router = APIRouter(prefix="/chat", tags=["chat"])
class ChatRequest(BaseModel): question: str
@router.post("/agent")async def agent_chat(req: ChatRequest): """普通接口:等 Agent 跑完整个 ReAct 循环后一次性返回""" answer = await AgentService.chat(req.question) return {"question": req.question, "answer": answer}
@router.post("/agent/stream")async def agent_stream(req: ChatRequest): """SSE 流式接口:边推理边返回 token"""
async def event_generator(): async for token in AgentService.stream_chat(req.question): # SSE 协议:每条消息以 data: 开头,两个换行结束 yield f"data: {token}\n\n" yield "data: [DONE]\n\n"
return StreamingResponse( event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, )确保路由在应用入口(如 app/main.py)中已注册:
from fastapi import FastAPIfrom app.routers import chat
app = FastAPI(title="AI Agent Demo")app.include_router(chat.router)6.3 验证
启动服务后在 Swagger UI(/docs)测试:
POST /chat/agent:输入需要工具的问题(如”深圳现在天气怎么样”),返回完整答案;POST /chat/agent/stream:用curl -N或前端EventSource观察分块到达的流式响应。
curl -N -X POST "http://localhost:8000/chat/agent/stream" \ -H "Content-Type: application/json" \ -d '{"question": "现在几点了?(128+72)*3 等于多少?"}'7. 章节小结
7.1 知识脉络
7.2 要点清单
- ReAct 核心循环:思考 → 行动(调工具)→ 观察结果 → 再思考,循环直到模型不再发起
tool_calls,输出最终答案; - 消息链是本质:
HumanMessage → AIMessage(tool_calls) → ToolMessage → … → AIMessage(content),Agent 并不神秘; create_react_agent(llm, tools)一行即可得到完整 Agent,get_graph().draw_mermaid()可查看内部图结构;- 系统提示词通过
state_modifier注入,支持字符串(固定人设)和函数(动态改写消息)两种形式; astream_events(version="v2")提供on_chat_model_start/stream/end、on_tool_start/end等事件,可还原完整思考过程并驱动前端 UI;- 手写 ReAct Graph = StateGraph + ToolNode + tools_condition 条件边 + tools→chat 回环,与高级封装逻辑等价,需要定制时走这条路;
- 工程落地:Agent 全局编译一次,封装到
AgentService,对上提供普通接口和 SSE 流式接口两套 API。
7.3 自测问题
- 不看代码画出 ReAct 的循环图,并说出每一步对应哪种消息类型?
- 模型在哪一步决定”结束循环”?判断条件是什么?
-
add_messagesreducer 起什么作用?如果去掉会怎样? -
tools_condition的两个分支分别走向哪里?如何用自定义函数替代? -
astream_events中如何区分”模型发起工具调用”和”模型输出最终答案”? - 为什么
react_agent要在模块级全局构建一次,而不是每个请求新建? #(注:内容由AI生成)
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时





