架构模式

ReAct 架构

深入理解 ReAct 架构原理,掌握使用预构建代理和自定义实现的方法

📚 学习目标

学完这篇文章后,你将能够:

  • 理解 ReAct (Reasoning and Acting) 的核心设计理念
  • 使用 createAgent 快速构建智能代理
  • 手动构建自定义的 ReAct 循环以获得更细粒度的控制
  • 实现具备工具调用能力的流式对话代理

前置知识

在开始学习之前,建议先阅读:

你需要了解:

  • ReAct 论文(Optionally)的基本概念:LLM 在执行任务时交替进行思考(Reasoning)和行动(Acting)

1 什么是 ReAct?

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

它解决了纯 LLM 的两个核心问题:

  1. 幻觉:LLM 可能会编造事实,而 ReAct 允许它通过工具获取真实信息。
  2. 错误传播:单次推理容易出错,ReAct 允许它观察行动结果并修正计划。

执行流程循环

交互时序(ReAct 循环)

下面用时序图更直观地展示“推理→行动→观察”的往复过程:

💡 理解要点

  • ReAct 的关键不是“调用工具”,而是 让模型在每一步都有机会纠错
  • 只要工具输出改变了上下文,就会触发下一轮推理。

2 快速开始:预构建代理

LangChain v1 提供了 createAgent 函数(取代已废弃的 createReactAgent),可以一键创建标准的 ReAct 代理。

⚠️ 迁移提示createReactAgent(来自 @langchain/langgraph/prebuilt)已在 LangGraph v1 中被废弃。请使用 createAgent(来自 langchain)。主要变化:

  • 导入路径:@langchain/langgraph/prebuiltlangchain
  • 函数名:createReactAgentcreateAgent
  • 参数 llmmodel(支持字符串简写,如 "gpt-4o"
  • 参数 promptsystemPrompt
  • 新增 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);

代码解析

  1. tool(...)schema 决定了 LLM 能不能稳定地构造正确参数。
  2. createAgent 把"工具调用 + 循环"封装成了一个可直接 invoke/stream 的 runnable,底层基于 LangGraph 运行时。
  3. model 参数支持字符串简写(如 "gpt-4o""claude-sonnet-4-5-20250929"),也支持传入模型实例。
  4. 如果你需要更复杂的控制流(人机交互、子图隔离、并行),就要用自定义图。

使用 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。

核心组件

  1. Agent Node:负责调用 LLM 进行推理。
  2. Tools Node:负责执行 LLM 请求调用的工具。
  3. 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 做成“能用的应用”:流式输出 + 记忆

最贴近真实产品的组合通常是:

  • 流式输出:用户能边看边等
  • 持久化记忆:页面刷新/服务重启后仍能继续对话

你不需要在这里一次学完,先记住两件事:

  1. 流式输出:用 stream / streamEvents(见 流式处理
  2. 记忆:编译时传 checkpointer,调用时带 thread_id(见 持久化

💡 练习题

  1. 思考题:在 ReAct 循环中,如果 LLM 连续多次调用工具但无法解决问题,会导致什么后果?如何防止这种情况?

    点击查看答案

    可能进入“工具调用死循环”,导致性能浪费与响应超时。可通过 recursionLimit 或状态中的 maxToolCalls 限制循环次数。

  2. 操作题:使用 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: '现在几点了?' }],
    });
  3. 思考题:为什么要把工具调用与推理分成两个节点(agent/tools)?

    点击查看答案

    分离节点可以让每个阶段职责清晰:推理节点只负责“决定是否调用工具”,工具节点只负责“执行与返回结果”。这样便于调试、插入安全检查,并在需要时替换工具执行逻辑。

  4. 操作题:为自定义 ReAct 循环加入 maxToolCalls 限制,避免无限重试。

    点击查看答案

    在状态中添加 toolCallCountmaxToolCalls,在 shouldContinue 中判断:

    if (state.toolCallCount >= state.maxToolCalls) return END;
  5. 操作题:模拟一次工具失败,并让系统自动重试 1 次。

    点击查看答案

    在工具节点中捕获异常并返回 ToolMessage,然后在路由中检测“错误:”前缀并触发一次重试即可。


📚 参考资源

官方文档


✅ 总结

本章要点

  • ReAct 是构建智能代理的基石模式。
  • createAgent(LangChain v1)适合快速原型开发,通过 middleware 系统提供强大的可扩展性。
  • 自定义 Graph 实现提供了无限的扩展能力(如添加 Human-In-The-Loop)。

下一步:当一个 Agent 不够用时,我们需要多代理系统

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page