ReAct 架构
深入理解 ReAct 架构原理,掌握使用预构建代理和自定义实现的方法
📚 学习目标
学完这篇文章后,你将能够:
- 理解 ReAct (Reasoning and Acting) 的核心设计理念
- 使用
createAgent快速构建智能代理 - 手动构建自定义的 ReAct 循环以获得更细粒度的控制
- 实现具备工具调用能力的流式对话代理
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- ReAct 论文(Optionally)的基本概念:LLM 在执行任务时交替进行思考(Reasoning)和行动(Acting)
1 什么是 ReAct?
ReAct = Reasoning(推理)+ Acting(行动)。
它解决了纯 LLM 的两个核心问题:
- 幻觉:LLM 可能会编造事实,而 ReAct 允许它通过工具获取真实信息。
- 错误传播:单次推理容易出错,ReAct 允许它观察行动结果并修正计划。
执行流程循环
交互时序(ReAct 循环)
下面用时序图更直观地展示“推理→行动→观察”的往复过程:
💡 理解要点
- ReAct 的关键不是“调用工具”,而是 让模型在每一步都有机会纠错。
- 只要工具输出改变了上下文,就会触发下一轮推理。
2 快速开始:预构建代理
LangChain v1 提供了 createAgent 函数(取代已废弃的 createReactAgent),可以一键创建标准的 ReAct 代理。
⚠️ 迁移提示:
createReactAgent(来自@langchain/langgraph/prebuilt)已在 LangGraph v1 中被废弃。请使用createAgent(来自langchain)。主要变化:
- 导入路径:
@langchain/langgraph/prebuilt→langchain- 函数名:
createReactAgent→createAgent- 参数
llm→model(支持字符串简写,如"gpt-4o")- 参数
prompt→systemPrompt- 新增
middleware系统,替代preModelHook/postModelHook
代码示例
import { createAgent } from 'langchain';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
// 1. 定义工具
const magicTool = tool(
async ({ input }) => {
return `Magic result for ${input}`;
},
{
name: 'magic_function',
description: 'A magic tool that does something cool',
schema: z.object({ input: z.string() }),
},
);
// 2. 创建代理
const agent = createAgent({
model: 'gpt-4o', // 支持字符串简写或模型实例
tools: [magicTool],
systemPrompt: 'You are a helpful assistant.', // 原 prompt,现改名为 systemPrompt
});
// 3. 运行
const result = await agent.invoke({
messages: [{ role: 'user', content: "Use the magic tool on 'hello'" }],
});
console.log(result.messages.at(-1).content);代码解析:
tool(...)的schema决定了 LLM 能不能稳定地构造正确参数。createAgent把"工具调用 + 循环"封装成了一个可直接invoke/stream的 runnable,底层基于 LangGraph 运行时。model参数支持字符串简写(如"gpt-4o"、"claude-sonnet-4-5-20250929"),也支持传入模型实例。- 如果你需要更复杂的控制流(人机交互、子图隔离、并行),就要用自定义图。
使用 Middleware 增强代理
createAgent 的核心新特性是 middleware 系统,通过可组合的中间件实现动态提示词、对话摘要、人机审批等场景:
import {
createAgent,
summarizationMiddleware,
humanInTheLoopMiddleware,
} from 'langchain';
const agent = createAgent({
model: 'claude-sonnet-4-5-20250929',
tools: [readEmail, sendEmail],
middleware: [
// 对话过长时自动摘要
summarizationMiddleware({
model: 'claude-sonnet-4-5-20250929',
trigger: { tokens: 1000 },
}),
// 敏感操作需人工审批
humanInTheLoopMiddleware({
interruptOn: {
sendEmail: { allowedDecisions: ['approve', 'edit', 'reject'] },
},
}),
],
});Middleware 钩子一览:
| 钩子 | 执行时机 | 典型用途 |
|---|---|---|
beforeAgent | 代理启动前 | 加载记忆、验证输入 |
beforeModel | 每次 LLM 调用前 | 动态提示词、裁剪消息 |
wrapModelCall | 包裹 LLM 调用 | 拦截/修改请求与响应 |
wrapToolCall | 包裹工具调用 | 工具错误处理 |
afterModel | 每次 LLM 响应后 | 输出验证、护栏 |
afterAgent | 代理完成后 | 保存结果、清理 |
3 预构建 vs 自定义:怎么选?
| 方案 | 优点 | 适用场景 | 代价 |
|---|---|---|---|
createAgent | 上手快、代码少、middleware 可组合 | 原型、单一 agent、标准 ReAct、带中间件扩展 | 超复杂控制流受限 |
| 自定义 Graph | 控制力最强 | 审批流、复杂分支、子图隔离、多 agent | 代码更多,需要设计 state |
4 进阶:自定义 ReAct 实现
为了完全掌控 ReAct 循环(例如添加人工审批、修改记忆逻辑),我们需要手动构建 Graph。
核心组件
- Agent Node:负责调用 LLM 进行推理。
- Tools Node:负责执行 LLM 请求调用的工具。
- Routers:决定是继续调用工具还是结束对话。
实现步骤
import { StateGraph, START, END } from '@langchain/langgraph';
import { MessagesAnnotation } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
// ... 定义 tool 和 llmWithTools ...
// 1. 定义节点
const agentNode = async (state) => {
const response = await llmWithTools.invoke(state.messages);
return { messages: [response] };
};
// 工具执行节点(LangGraph 预置)
const toolsRunner = new ToolNode(tools);
const toolsNode = async (state: typeof ReactState.State) => {
const result = await toolsRunner.invoke(state);
return {
...result,
toolCallCount: state.toolCallCount + 1,
stage: 'observation_complete',
};
};
// 2. 定义路由
const shouldContinue = (state) => {
const lastMessage = state.messages[state.messages.length - 1];
// 如果 LLM 想要调用工具 -> tools
if (lastMessage.tool_calls?.length) {
return 'tools';
}
// 否则 -> 结束
return END;
};
// 3. 构建 Graph
const workflow = new StateGraph(MessagesAnnotation)
.addNode('agent', agentNode)
.addNode('tools', toolsNode)
.addEdge(START, 'agent')
.addConditionalEdges('agent', shouldContinue)
.addEdge('tools', 'agent'); // 行动后回到代理继续思考
const app = workflow.compile();💡 提示:递归限制
一旦你有循环边(tools -> agent),就建议在调用时设置 recursionLimit,避免模型卡在"反复调用工具"的坏循环里。
更细粒度的 ReAct 循环(带状态控制)
当你需要限制工具调用次数、记录阶段信息或输出可观察的执行轨迹时,可以扩展状态:
import {
Annotation,
StateGraph,
START,
END,
messagesStateReducer,
} from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { BaseMessage } from '@langchain/core/messages';
const ReactState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
toolCallCount: Annotation<number>({
reducer: (state, update) => update ?? state,
default: () => 0,
}),
maxToolCalls: Annotation<number>({
reducer: (state, update) => update ?? state,
default: () => 3,
}),
stage: Annotation<string>({
reducer: (state, update) => update ?? state,
default: () => 'reasoning',
}),
});
const agentNode = async (state: typeof ReactState.State) => {
const response = await llmWithTools.invoke(state.messages);
return { messages: [response], stage: 'reasoning_complete' };
};
const toolsNode = new ToolNode(tools);
const shouldContinue = (state: typeof ReactState.State) => {
const last = state.messages[state.messages.length - 1];
if (state.toolCallCount >= state.maxToolCalls) return END;
if (last.tool_calls?.length) return 'tools';
return END;
};
const app = new StateGraph(ReactState)
.addNode('agent', agentNode)
.addNode('tools', toolsNode)
.addEdge(START, 'agent')
.addConditionalEdges('agent', shouldContinue)
.addEdge('tools', 'agent')
.compile();💡 使用建议
- 用
toolCallCount控制最坏情况下的循环次数。 - 用
stage记录执行阶段,便于调试和可视化。
简化的错误处理与重试
真实场景中工具可能失败,你可以用“错误消息 + 路由重试”来兜底:
import { END } from '@langchain/langgraph';
import { ToolMessage } from '@langchain/core/messages';
const MAX_RETRY = 2;
const safeToolsNode = async (state: typeof ReactState.State) => {
try {
return await toolsNode.invoke(state);
} catch (error) {
return {
messages: [
new ToolMessage({
content: `错误:${(error as Error).message}`,
tool_call_id: 'tool_error',
}),
],
};
}
};
const shouldRetry = (state: typeof ReactState.State) => {
const last = state.messages[state.messages.length - 1];
const isError =
typeof last?.content === 'string' && last.content.startsWith('错误:');
if (isError && state.toolCallCount < MAX_RETRY) return 'agent';
if (isError) return END;
return 'agent';
};
const appWithRetry = new StateGraph(ReactState)
.addNode('agent', agentNode)
.addNode('tools', safeToolsNode)
.addEdge(START, 'agent')
.addConditionalEdges('agent', shouldContinue)
.addConditionalEdges('tools', shouldRetry)
.compile();⚠️ 注意
错误处理建议只保留 1-2 次重试,避免无限自循环。
5 把 ReAct 做成“能用的应用”:流式输出 + 记忆
最贴近真实产品的组合通常是:
- 流式输出:用户能边看边等
- 持久化记忆:页面刷新/服务重启后仍能继续对话
你不需要在这里一次学完,先记住两件事:
💡 练习题
-
思考题:在 ReAct 循环中,如果 LLM 连续多次调用工具但无法解决问题,会导致什么后果?如何防止这种情况?
点击查看答案
可能进入“工具调用死循环”,导致性能浪费与响应超时。可通过
recursionLimit或状态中的maxToolCalls限制循环次数。 -
操作题:使用
createAgent创建一个getCurrentTime工具,询问“现在几点了?”。点击查看答案
import { createAgent } from 'langchain'; import { tool } from '@langchain/core/tools'; import { z } from 'zod'; const getCurrentTime = tool(async () => new Date().toISOString(), { name: 'get_current_time', description: '获取当前时间', schema: z.object({}), }); const agent = createAgent({ model: 'gpt-4o', tools: [getCurrentTime], systemPrompt: '你是一个乐于助人的助手。', }); const result = await agent.invoke({ messages: [{ role: 'user', content: '现在几点了?' }], }); -
思考题:为什么要把工具调用与推理分成两个节点(agent/tools)?
点击查看答案
分离节点可以让每个阶段职责清晰:推理节点只负责“决定是否调用工具”,工具节点只负责“执行与返回结果”。这样便于调试、插入安全检查,并在需要时替换工具执行逻辑。
-
操作题:为自定义 ReAct 循环加入
maxToolCalls限制,避免无限重试。点击查看答案
在状态中添加
toolCallCount与maxToolCalls,在shouldContinue中判断:if (state.toolCallCount >= state.maxToolCalls) return END; -
操作题:模拟一次工具失败,并让系统自动重试 1 次。
点击查看答案
在工具节点中捕获异常并返回
ToolMessage,然后在路由中检测“错误:”前缀并触发一次重试即可。
📚 参考资源
官方文档
✅ 总结
本章要点:
- ReAct 是构建智能代理的基石模式。
createAgent(LangChain v1)适合快速原型开发,通过 middleware 系统提供强大的可扩展性。- 自定义 Graph 实现提供了无限的扩展能力(如添加 Human-In-The-Loop)。
下一步:当一个 Agent 不够用时,我们需要多代理系统。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。