Skip to content

ReAct Agent

要点

  • 前五篇分别学了 StateGraph 的节点与边、Reducer 的状态合并、条件边的循环控制、Checkpointer 的状态持久化
  • 在写 Agent 之前,先搞清楚一个根本问题:Agent 和普通程序的区别是什么
  • Agent 的能力边界由工具决定,内容创作智能体也是如此
  • ReAct Agent 的核心结构是 callModel → callTools → callModel
  • LangGraph 的 ReAct 结构本质上是一个由条件边控制的模型-工具循环
  • Checkpointer 让同一个创作线程能连续改大纲、草稿和审核意见

内容

1. 前五篇的零件,这一篇组装

前五篇分别学了 StateGraph 的节点与边、Reducer 的状态合并、条件边的循环控制、Checkpointer 的状态持久化。

每一篇解决一个具体问题,但始终没有放在一起。

这一篇就是「回报点」——把所有零件组装成一个完整的 ReAct Agent

构建完成后你会发现,核心图定义只有三行。之前五篇铺垫的知识点,在这三行里全部用上了。

2. 什么是 Agent:模型自己决定下一步做什么

在写 Agent 之前,先搞清楚一个根本问题:Agent 和普通程序的区别是什么?

普通程序的执行路径是你写死的。用户输入 → 调 API → 拼结果 → 返回。每一步做什么、做几次、什么时候停下来,全由代码决定。

Agent 不一样。你把工具交给模型,模型自己决定:

  • 调不调工具——用户说「你好」,不需要调工具,直接回复
  • 调哪个工具——用户说「先查资料」,模型选择 search_references 而不是 estimate_reading_time
  • 传什么参数——模型从用户的自然语言中提取 topic: "LangGraph"
  • 调几次——用户说「查资料,再估算 1500 字读多久」,模型可以一次调两个工具
  • 什么时候停——拿到所有结果后,模型决定不再调工具,生成最终回复

这就是「自主性」。你不写 if...else 来分发请求,模型根据上下文自己做判断。

ReAct(Reasoning + Acting)是实现这种自主性最经典的模式。它的循环很简单:

txt
// react-loop.txt
用户: 帮我写一篇 LangGraph 入门文章,先查资料。

  [思考] 用户要写文章,我需要先检索参考资料

  [行动] 调用 search_references({ topic: "LangGraph 入门" })

  [观察] 工具返回:官方文档、实践案例、常见误区

  [思考] 已经拿到资料,可以整理大纲并继续生成草稿

  [行动] 生成最终回复

AI: 我先按「为什么需要 LangGraph → 核心概念 → 最小示例」组织一版大纲。

对应到 LangGraph,就是第四篇讲过的条件边循环:

  • callModel 节点——模型思考 + 决定行动(调工具还是直接回复)
  • callTools 节点——执行工具,把结果放回消息列表(观察)
  • 条件边——有工具调用就继续循环,没有就结束

我对照了 LangGraph 官方 Quickstart 和 Graph API。官方示例里的 ReAct Agent 通常会把模型调用和工具执行拆成两个节点,再用条件边判断「最后一条 AI 消息里有没有 tool calls」。有 tool calls 就去工具节点,工具结果写回 messages 后再回到模型;没有 tool calls 就到 END

这套结构的工作原理可以压成一张小表:

步骤图里的节点状态里发生什么
模型判断callModel追加一条 AI 消息,里面可能包含工具调用请求
工具执行callTools追加工具结果消息
是否继续条件边有工具调用就回到工具/模型循环,没有就结束

对于内容创作智能体,这意味着模型不是直接「凭空写完」。它可以先决定查资料,再根据资料估算阅读时间,最后生成大纲或草稿。图结构负责把这个循环约束住,工具 schema 负责把每次行动的参数收紧。

3. 准备工具:给 Agent 装上能力

Agent 的能力边界由工具决定。内容创作智能体可以先准备三个工具:检索参考资料、估算阅读时间、建议发布时间。

typescript
// tools.ts
import { tool } from '@langchain/core/tools'

import * as z from 'zod'

// 检索参考资料

const searchReferences = tool(
  async ({ topic }) => {
    const data: Record<string, string[]> = {
      'LangGraph 入门': ['官方文档', 'StateGraph 示例', '人工审核工作流案例'],

      LangGraph: ['官方文档', 'StateGraph 示例', '人工审核工作流案例'],

      ReAct: ['Reasoning + Acting 模式', '工具调用循环', '停止条件'],

      Checkpointer: ['线程状态保存', '中断恢复', '历史快照回溯'],
    }

    return data[topic]?.join('、') ?? `暂无 ${topic} 的资料`
  },

  {
    name: 'search_references',

    description: '为技术文章检索参考资料',

    schema: z.object({
      topic: z.string().describe('文章主题或概念名'),
    }),
  },
)

// 估算阅读时间

const estimateReadingTime = tool(
  async ({ wordCount }) => {
    const minutes = Math.ceil(wordCount / 500)

    return `约 ${minutes} 分钟读完`
  },

  {
    name: 'estimate_reading_time',

    description: '根据字数估算文章阅读时间',

    schema: z.object({
      wordCount: z.number().describe('文章预计字数'),
    }),
  },
)

// 建议发布时间

const suggestPublishSlot = tool(
  async () => {
    return '建议工作日 09:30 或 20:30 发布,便于读者通勤或晚间阅读。'
  },

  {
    name: 'suggest_publish_slot',

    description: '给内容发布建议一个时间窗口',

    schema: z.object({}),
  },
)

const tools = [searchReferences, estimateReadingTime, suggestPublishSlot]

每个工具的结构都一样:一个执行函数 + 一个描述对象。namedescription 是给模型看的——模型根据这些信息决定什么时候调用哪个工具。schema 用 Zod 定义参数结构,LangChain 会自动把它转成模型需要的 JSON Schema 格式。

4. 构建 ReAct Agent

4.1 状态、模型、节点

typescript
// agent-core.ts
import {
  StateGraph,
  StateSchema,
  MessagesValue,
  START,
  END,
} from '@langchain/langgraph'

import type { GraphNode, ConditionalEdgeRouter } from '@langchain/langgraph'

import { InMemorySaver } from '@langchain/langgraph'

import { ChatOpenAI } from '@langchain/openai'

// 状态:只需要一个消息列表

// 用户输入、模型回复、工具调用请求、工具执行结果——全部是消息

const State = new StateSchema({
  messages: MessagesValue,
})

// 模型:绑定工具后,模型在推理时会自动考虑是否调用它们

const model = new ChatOpenAI({ model: 'gpt-4.1-mini' }).bindTools(tools)

// 节点 1:调用模型

// 把所有消息(含历史对话和工具结果)发给模型,拿回回复

const callModel: GraphNode<typeof State> = async (state) => {
  const response = await model.invoke(state.messages)

  return { messages: [response] }
}

// 节点 2:执行工具

// 从模型回复中取出工具调用请求,执行后把结果放回消息列表

const toolsByName = Object.fromEntries(tools.map((t) => [t.name, t]))

const callTools: GraphNode<typeof State> = async (state) => {
  const lastMsg = state.messages.at(-1)!

  const toolCalls = lastMsg.tool_calls ?? []

  // 多个工具调用并行执行

  const results = await Promise.all(
    toolCalls.map(async (tc) => {
      try {
        const result = await toolsByName[tc.name].invoke(tc.args)

        return { role: 'tool' as const, content: result, tool_call_id: tc.id }
      } catch (err) {
        // 工具失败时把错误信息返回给模型,让它自己决定怎么应对

        const errorMsg = err instanceof Error ? err.message : String(err)

        return {
          role: 'tool' as const,

          content: `工具执行失败: ${errorMsg}`,

          tool_call_id: tc.id,
        }
      }
    }),
  )

  return { messages: results }
}

4.2 路由和组装

typescript
// build-graph.ts
// 路由:模型回复里有工具调用就继续循环,没有就结束

const shouldContinue: ConditionalEdgeRouter<typeof State, 'callTools'> = (
  state,
) => {
  const lastMsg = state.messages.at(-1)!

  if (lastMsg.tool_calls && lastMsg.tool_calls.length > 0) {
    return 'callTools'
  }

  return END
}

// 组装图

const graph = new StateGraph(State)

  .addNode('callModel', callModel)

  .addNode('callTools', callTools)

  .addEdge(START, 'callModel') // 入口

  .addConditionalEdges('callModel', shouldContinue, ['callTools']) // 条件分支

  .addEdge('callTools', 'callModel') // 循环回路

  .compile({
    checkpointer: new InMemorySaver(), // 支持多轮对话
  })

三行图定义,就是整个 ReAct Agent 的控制流。对照第四篇的条件边循环:

  • START → callModel:收到消息后,先让模型思考
  • callModel → shouldContinue → callTools:模型决定调工具,执行它
  • callTools → callModel:工具结果回到模型,继续思考

模型决定不再调工具时,shouldContinue 返回 END,循环结束。

5. 运行:观察 Agent 的自主决策

5.1 Agent 自己选择工具

typescript
// single-turn.ts
const config = { configurable: { thread_id: 'test-001' } }

const result = await graph.invoke(
  {
    messages: [
      {
        role: 'user',
        content:
          '帮我写一篇 LangGraph 入门文章,先查资料,再估算 1500 字读多久。',
      },
    ],
  },

  config,
)

for (const msg of result.messages) {
  const type = msg.getType()

  if (type === 'human') {
    console.log(`[用户] ${msg.content}`)
  } else if (type === 'ai' && msg.tool_calls?.length) {
    console.log(
      `[模型决策] 调用工具: ${msg.tool_calls.map((tc) => `${tc.name}(${JSON.stringify(tc.args)})`).join(', ')}`,
    )
  } else if (type === 'tool') {
    console.log(`[工具结果] ${msg.content}`)
  } else {
    console.log(`[模型回复] ${msg.content}`)
  }
}

输出:

txt
// output.txt
[用户] 帮我写一篇 LangGraph 入门文章,先查资料,再估算 1500 字读多久。

[模型决策] 调用工具: search_references({"topic":"LangGraph"}), estimate_reading_time({"wordCount":1500})

[工具结果] 官方文档、StateGraph 示例、人工审核工作流案例

[工具结果] 约 3 分钟读完

[模型回复] 我会把这篇文章控制在约 1500 字,预计 3 分钟读完。结构建议是:先讲 LangGraph 解决的问题,再讲 StateGraph 示例,最后补一个人工审核工作流案例。

注意看 [模型决策] 那一行:你没有写任何代码告诉模型「先查资料再估算阅读时间」。模型自己分析了用户的请求,判断需要调两个工具,一次性发出了两个调用请求。这就是第二节说的「自主性」——控制流不是你写死的,是模型推理出来的。

如果用户改问「你好」,模型会判断不需要任何工具,直接回复。你不需要为此写一行额外的代码。

5.2 多轮对话:Checkpointer 的效果

typescript
// multi-turn.ts
const config = { configurable: { thread_id: 'test-002' } }

// 第一轮

await graph.invoke(
  { messages: [{ role: 'user', content: '这篇文章先写 LangGraph 入门。' }] },

  config,
)

// 第二轮:同一个 thread_id,上下文自动累积

const r2 = await graph.invoke(
  { messages: [{ role: 'user', content: '帮我估算 2000 字读多久。' }] },

  config,
)

console.log(r2.messages.at(-1)?.content)

// → 约 4 分钟读完。

// 第三轮:Agent 记得之前的对话

const r3 = await graph.invoke(
  { messages: [{ role: 'user', content: '前面我问了你什么?' }] },

  config,
)

console.log(r3.messages.at(-1)?.content)

// → 你先说这篇文章写 LangGraph 入门,然后让我估算 2000 字的阅读时间。

多轮对话的能力来自第五篇的 Checkpointer。同一个 thread_id 下,每次 invoke 时 Checkpointer 先加载上一次的状态,新消息通过 MessagesValue 追加到历史后面。你只需要传本轮新增的消息,历史对话由 Checkpointer 自动管理。

6. 添加系统提示:给 Agent 一个创作边界

裸的 ReAct Agent 已经能跑了。生产环境最常见的定制是加一个系统提示,控制 Agent 的创作边界和输出风格。

最简单的方式——在第一轮调用时把系统消息放在消息列表最前面:

typescript
// system-prompt.ts
const config = { configurable: { thread_id: 'content-agent-001' } }

const result = await graph.invoke(
  {
    messages: [
      {
        role: 'system',

        content: `你是一个内容创作智能体。

规则:

1. 先确认文章目标读者和发布场景

2. 写作时优先给出大纲、草稿、审核建议

3. 如果资料不足,先说明缺口,不要编造案例`,
      },

      { role: 'user', content: '帮我写一篇 LangGraph 入门文章。' },
    ],
  },

  config,
)

console.log(result.messages.at(-1)?.content)

// → 我先确认这篇文章面向初学者还是有 LangChain 基础的读者,再给出大纲和第一版草稿。

如果你把系统消息也放进 messages,同一个 thread_id 下后续轮次通常会继续带着它,所以很多场景里不用每轮都重复传。

如果系统提示需要动态变化(比如根据用户身份调整),更好的做法是在 callModel 节点里注入:

typescript
// dynamic-system-prompt.ts
const callModel: GraphNode<typeof State> = async (state, config) => {
  const userId = config?.configurable?.user_id ?? 'anonymous'

  // 每次调模型时在消息最前面插入系统提示

  const systemMessage = {
    role: 'system' as const,

    content: `你是用户 ${userId} 的内容创作智能体。先确认目标读者,再组织大纲和草稿。`,
  }

  const response = await model.invoke([systemMessage, ...state.messages])

  return { messages: [response] }
}

7. 生产环境:createReactAgent

前面手动构建了整个 ReAct Agent,是为了理解内部结构。但在实际开发中,LangGraph 提供了一个开箱即用的工具函数 createReactAgent一行代码就能创建标准的 ReAct Agent

typescript
// create-react-agent.ts
import { createReactAgent } from '@langchain/langgraph/prebuilt'

import { ChatOpenAI } from '@langchain/openai'

import { InMemorySaver } from '@langchain/langgraph'

const model = new ChatOpenAI({ model: 'gpt-4.1-mini' })

// 一行搞定:模型 + 工具 + Checkpointer

const agent = createReactAgent({
  model,

  tools: [searchReferences, estimateReadingTime, suggestPublishSlot],

  checkpointer: new InMemorySaver(),
})

// 用法和手动构建的完全一样

const result = await agent.invoke(
  { messages: [{ role: 'user', content: '帮我写一篇 LangGraph 入门文章。' }] },

  { configurable: { thread_id: 'quick-001' } },
)

createReactAgent 内部做的事和我们手动构建的完全一样:定义 callModel 节点、callTools 节点、条件边循环、编译图。它是一层封装,不是黑魔法。

什么时候用 createReactAgent,什么时候手动构建?

  • 标准的「模型 + 工具」场景 → 用 createReactAgent,省时间
  • 需要在循环中插入额外步骤(安全检查、日志记录、人工审批)→ 手动构建,因为你需要控制节点和边的结构

理解了本文的手动构建过程,你就知道 createReactAgent 省略了什么,也知道在需要定制时从哪里下手。

8. 总结

这一篇把前五篇的知识点串在了一起:

知识点来自在 ReAct Agent 中的作用
StateSchema + MessagesValue第二篇定义状态,消息自动追加合并
条件边 + 循环第四篇callModel → callTools → callModel 的 ReAct 循环
InMemorySaver + thread_id第五篇多轮对话的状态持久化
GraphNode + ConditionalEdgeRouter第二、四篇节点和路由函数的类型约束

Agent 的核心不是代码量,而是一个设计决策:把「下一步做什么」的决定权交给模型。 你定义好工具、接好循环,模型在循环里自主推理、自主行动、自主判断何时停下。这就是从「程序」到「Agent」的跨越。

下一篇讲流式输出——让 Agent 的回复像打字一样逐字出现,而不是等所有工具执行完才一次性返回。

基于 MIT 协议开源