mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1781 字
5 分钟
React 设计模式
2026-09-09

第 11 章 · ReAct Agent 实现 & create_react_agent 复盘#

核心目标:吃透 ReAct 循环的每一步决策,理解 create_react_agent 这个”黑盒”内部到底做了什么,并能手写一遍等价的 Graph。


目录#

  1. ReAct 是什么
  2. 用 create_react_agent 快速创建 Agent
  3. 给 Agent 加系统提示词
  4. 流式输出 Agent 的执行过程
  5. 手动实现 ReAct Graph(加深理解)
  6. 把 ReAct Agent 封装进项目
  7. 章节小结

1. ReAct 是什么#

ReAct = Reasoning(推理)+ Acting(行动)。

普通的 LLM 调用是”一问一答”,模型一次性给出结果;而 ReAct Agent 是一个循环:模型先思考需要什么信息、要不要调工具,调用工具拿到结果后再思考”信息够不够”,不够就继续调,够了才输出最终答案。

1.1 完整循环流程#

flowchart TD A[用户提问 HumanMessage] --> B[LLM 思考:需要什么信息?要不要用工具?] B --> C{是否需要调用工具?} C -->|需要 tool_calls| D[调用工具 ToolNode 执行] D --> E[工具返回结果 ToolMessage] E --> F[LLM 再思考:结果够了吗?] F --> C C -->|信息已足够| G[生成最终回答 AIMessage]

这个循环可以跑多轮,直到 LLM 自己判断信息充分、不再发起工具调用为止。多步推理场景(例如”先查天气,再根据天气和时间给建议”)就是在循环里串起多次工具调用。

1.2 消息链路(ReAct 的核心)#

一次完整执行后,state["messages"] 里的消息顺序固定为:

顺序消息类型含义
1HumanMessage用户问题
2AIMessage(带 tool_calls)LLM 决定调用哪个工具、传什么参数
3ToolMessage工具执行结果,回灌给模型
4AIMessage(带 tool_calls)信息不够,继续调下一个工具(可能多轮)
5ToolMessage第二个工具结果
……思考 ↔ 行动循环
末尾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/ch11
touch scripts/ch11/test_react_agent.py

2.2 完整代码 scripts/ch11/test_react_agent.py#

import asyncio
from langchain_core.messages import HumanMessage
from langgraph.prebuilt import create_react_agent
from app.ai.llm import llm
from 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.py

2.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, HumanMessage
from langgraph.prebuilt import create_react_agent
from app.ai.llm import llm
from 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 HumanMessage
from langgraph.prebuilt import create_react_agent
from app.ai.llm import llm
from 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_messages reducer 保证新消息自动追加(而不是覆盖)。
  • 两个节点:
    • chat 节点:把消息喂给绑定了工具的 LLM,由模型决定”直接回答”还是”发起 tool_calls”;
    • tools 节点:ToolNode,统一执行模型请求的工具调用。
  • 条件边:chat 之后用 tools_condition 判断——有 tool_calls 就去 tools,没有就 END;tools 执行完固定回到 chat,形成循环。

5.2 完整代码 scripts/ch11/manual_react_graph.py#

import asyncio
from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from app.ai.llm import llm
from 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)
# 入口 -> chat
graph_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")
# 编译得到可运行的 graph
manual_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 手写图结构#

flowchart LR START([START]) --> chat[chat 节点<br/>LLM 推理] chat -->|有 tool_calls| tools[tools 节点<br/>ToolNode 执行] tools -->|ToolMessage 回灌| chat chat -->|无 tool_calls| END([END])

5.4 与 create_react_agent 的对照结论#

环节create_react_agent 内部手写实现
状态内置 messages + add_messages自定义 ReactState
模型节点内置 agent 节点,自动 bind_toolschat_node 手动 bind
工具节点内置 ToolNode同样用 ToolNode
路由内置条件边判断 tool_callstools_condition / 自写 should_continue
回环tools → agenttools → chat

两者核心逻辑完全一致:create_react_agent 只是把这套固定写法封装成了一行 API。需要自定义节点、状态字段或路由策略时,就回到手写 StateGraph 的方式。


6. 把 ReAct Agent 封装进项目#

6.1 Agent 服务层:app/ai/graphs/react_agent.py#

from langchain_core.messages import HumanMessage
from langgraph.prebuilt import create_react_agent
from app.ai.llm import llm
from 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.content

6.2 路由层:在 app/routers/chat.py 增加 Agent 接口#

from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from 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 FastAPI
from 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 知识脉络#

flowchart TD A[ReAct = Reasoning + Acting 循环] --> B[create_react_agent 一行创建] B --> C[state_modifier 注入系统提示] B --> D[astream_events 流式观察全过程] A --> E[手写 StateGraph: chat ↔ tools 条件回环] E -->|本质相同| B B --> F[封装 AgentService + FastAPI/SSE 接口落地]

7.2 要点清单#

  1. ReAct 核心循环:思考 → 行动(调工具)→ 观察结果 → 再思考,循环直到模型不再发起 tool_calls,输出最终答案;
  2. 消息链是本质:HumanMessage → AIMessage(tool_calls) → ToolMessage → … → AIMessage(content),Agent 并不神秘;
  3. create_react_agent(llm, tools) 一行即可得到完整 Agent,get_graph().draw_mermaid() 可查看内部图结构;
  4. 系统提示词通过 state_modifier 注入,支持字符串(固定人设)和函数(动态改写消息)两种形式;
  5. astream_events(version="v2") 提供 on_chat_model_start/stream/end、on_tool_start/end 等事件,可还原完整思考过程并驱动前端 UI;
  6. 手写 ReAct Graph = StateGraph + ToolNode + tools_condition 条件边 + tools→chat 回环,与高级封装逻辑等价,需要定制时走这条路;
  7. 工程落地:Agent 全局编译一次,封装到 AgentService,对上提供普通接口和 SSE 流式接口两套 API。

7.3 自测问题#

  • 不看代码画出 ReAct 的循环图,并说出每一步对应哪种消息类型?
  • 模型在哪一步决定”结束循环”?判断条件是什么?
  • add_messages reducer 起什么作用?如果去掉会怎样?
  • tools_condition 的两个分支分别走向哪里?如何用自定义函数替代?
  • astream_events 中如何区分”模型发起工具调用”和”模型输出最终答案”?
  • 为什么 react_agent 要在模块级全局构建一次,而不是每个请求新建? #(注:内容由AI生成)
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

React 设计模式
https://mizuki-master-1vt.pages.dev/posts/react_设计模式/
作者
汪凝~
发布于
2026-09-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录