文章目录

AI Agent 三大核心架构精讲:ReAct / CodeAct / Plan-and-Execute 原理 + 手撕源码


前言:从"调用工具"到"自主决策",Agent 架构的三次进化

2022 年 10 月,一篇名为《ReAct: Synergizing Reasoning and Acting in Language Models》的论文横空出世,首次将推理(Reasoning)行动(Acting) 融合为一个闭环,Agent 从此不再是"只会调用工具的机器人",而是能边思考边行动的智能体。

两年后,Apple 机器学习团队提出 CodeAct,把"调用预定义工具"升级为"直接生成代码并执行",突破了工具数量的天花板。与此同时,LangChain 社区将 Plan-and-Execute 模式封装为 DAG 调度引擎,让长链路任务变得可控、可监控、可重试。

直至今日,这三种模式,依旧构成了现代 AI Agent 架构的三大基石。本文将从论文原理 + 完整源码 + 实战拆解三个维度,以加减乘除计算器为统一案例,带你彻底搞懂它们。之所以选择计算器而不是复杂业务,是为了让你聚焦 Agent 的循环逻辑本身,不被业务代码干扰。一旦吃透核心循环,无论是反诈风险研判、报表运算还是文档解析流水线,都是换壳而已。


一、ReAct 完整原理剖析

1.1 Agent 的"原始痛点":为什么需要 ReAct

在 ReAct 出现之前,大模型解决复杂问题有两种主流方案,但各有致命缺陷:

方案 做法 致命缺陷
纯推理(CoT 思维链) 让模型一步步思考 只会脑补,无法联网/调用工具,知识过时易幻觉
纯工具调用(如早期 WebGPT) 让模型搜索/调用 API 只会无脑搜索,没有逻辑规划,无法串联多轮结果

论文经典案例:问模型"除了 Apple Remote,还有什么设备能控制它管理的设备?"

  • 纯模型:直接输出"iPad"——纯幻觉,完全错误
  • 纯 CoT:思考文本变长,但无外部数据支撑,推导不出正确答案
  • 纯搜索:只会搜"Apple Remote",无法关联多条搜索结果
  • ReAct:第一轮搜索 Apple Remote 基础信息 → 发现配套软件 Front Row → 第二轮搜索 Front Row → 第三轮搜索 Front Row software → 观测维基百科结果,发现键盘也可控制 → 得出正确答案

这就是 ReAct 的核心价值:把推理和行动串成一个闭环,让模型在思考中行动,在行动中思考。

1.2 标准循环三段式(论文核心结构)

ReAct 的每一轮交互固定包含三个部分,多轮循环直到模型认为不需要工具、可以直接输出答案:

Thought(思考)→ Action(行动)→ Observation(观察)
    ^                                    |
    +------- 循环直到完成 ----------------+
  1. Thought(思考):大模型输出推理逻辑,规划需要调用什么工具、调用顺序
  2. Action(行动):输出工具调用指令,执行具体操作
  3. Observation(观察):外部工具执行后的返回结果,追加到上下文,送入下一轮模型推理

1.3 原生论文实现 vs 现代 Function Calling(关键时间线)

这是一个非常重要的知识点,很多人都搞混了:

维度 原生 ReAct(2022.10) 现代 Function Calling 版
时间 2022 年 10 月 6 日 2023 年 6 月 13 日(OpenAI 发布 Function Calling)
实现方式 纯文本提示词约束模型输出固定格式 利用 Function Calling 的 JSON Schema 结构化输出
解析方式 字符串暴力分割 Thought/Action 直接取 tool_calls 字段
稳定性 模型服从度差时格式混乱,解析失败率高 结构化输出,解析准确率接近 100%
代表实现 LangChain 早期版本 LangChain 现代版、本文实战代码

一句话总结:原版 ReAct 论文发表时还没有 Function Calling,所以只能靠提示词工程约束模型输出。现代实现直接利用 Function Calling 的 tool_calls 机制,稳定性和准确率大幅提升。


二、CodeAct 完整原理剖析

2.1 来源与核心思想

CodeAct(Code + Acting) 由 Apple 机器学习团队提出,Apple 的 Menas 产品底层就采用了这个思路。其核心是让模型自主编写代码来解决开放式任务,而非调用预定义工具。

概念区分:市面上还有更广义的"Code Agent"概念(泛指一切能生成代码的 Agent),与 Apple 论文中定义的 CodeAct 并不完全等同。本文讨论的 CodeAct 特指 Apple 论文中的"Code as Action"模式。

核心改进:把 ReAct 中"调用预定义工具"替换为大模型直接生成完整代码,在安全沙箱中运行

ReAct 的 Action:  调用 calculator({operation: "add", a: 1, b: 2})
CodeAct 的 Action: 生成并执行一段 JavaScript 代码 → console.log(1 + 2)

2.2 CodeAct 四大核心优势

优势 1:无限制动作空间

ReAct 必须提前手动定义全部工具,想加一个 avg 计算就得写一个新工具。CodeAct 直接用编程语言标准库,for 循环、if 判断、map 遍历,想写什么写什么,无需额外封装。

优势 2:工具自由组合,大幅减少交互轮次

这是最核心的优势。看一个对比:

ReAct 做复合计算(小明买书买笔):

第 1 轮: 调用 calculator (3 × 12 = 36)
第 2 轮: 调用 calculator (2 × 5 = 10)
第 3 轮: 调用 calculator (36 + 10 = 46)
第 4 轮: 调用 calculator (50 - 46 = 4)
→ 4 次工具调用,5 轮交互

CodeAct 做同样的计算

const bookCost = multiply(3, 12);        // 36
const penCost = multiply(2, 5);          // 10
const totalCost = add(bookCost, penCost); // 46
const remaining = subtract(50, totalCost); // 4
console.log(`小明还剩 ${remaining}`);
console.log(`花费占比: ${(totalCost / 50 * 100).toFixed(1)}%`);

1 次代码执行,2 轮交互,效率提升 4 倍

优势 3:自带纠错能力

代码运行报错 → 错误信息作为 Observation 传入下一轮 → 模型自动修复代码。这个闭环和程序员写代码、跑测试、修 bug 的流程一模一样。

优势 4:表达能力更强

支持循环、条件判断、绘图、数据处理等复杂逻辑。比如"计算数组 [12,24,36,48,60] 的总和与平均值",ReAct 需要多次调用或专门写一个工具,CodeAct 一段 for 循环搞定。

2.3 CodeAct 的短板

  1. 对模型代码生成能力要求高:2022 年早期大模型代码能力不足,所以当时优先流行 ReAct;现代代码大模型下 CodeAct 效果更好
  2. 代码块提取依赖正则:需要从模型输出中解析代码块,格式不稳定时可能失败
  3. 安全风险:代码执行必须放在沙箱中,生产环境绝不能直接 eval

三、Plan-and-Execute 原理剖析

3.1 核心思想

Plan-and-Execute = Plan(规划)+ Execute(执行),核心思想是:把复杂任务先拆解为子任务 DAG(有向无环图),再按依赖关系有序执行

ReAct:             边走边看,下一步做什么取决于上一步结果
Plan-and-Execute:  先画好地图,再按图索骥

3.2 适用场景

Plan-and-Execute 最适合标准化、长链路任务,特点是:

  1. 任务步骤可预知:可以先规划好再执行
  2. 步骤间有依赖关系:后面的步骤依赖前面的结果
  3. 需要监控和重试:每步可独立监控,失败可单步重试
  4. 需要可控性:知道当前进度、哪些步骤已完成

典型场景

  • 批量文档解析流水线(下载 → 解析 → 提取 → 入库 → 通知)
  • 反诈风险研判的多维度分析(身份核验 → 行为分析 → 关系图谱 → 风险评估)
  • 财务报表生成(数据采集 → 清洗 → 计算 → 汇总 → 导出)

3.3 DAG 调度模式

Plan-and-Execute 的核心是 DAG(有向无环图)调度,以"小明买书买笔"为例:

step1: 3 x 12 = 36 --+
step2: 2 x 5 = 10  --+--> step3: 36 + 10 = 46 --> step4: 50 - 46 = 4

关键设计:step1 和 step2 无依赖 → 可并行执行;step3 依赖 step1 和 step2 → 等两者都完成;step4 依赖 step3 → 顺序执行。

说明:本文实现为静态规划版本(一次性生成完整 DAG,全程不修改任务)。进阶方案支持执行失败或中途发现新信息时,调用 LLM 重新调整任务 DAG,即动态 Plan-and-Execute——这也是原始论文的核心思路:先规划,执行过程中允许动态修正计划。


四、ReAct 完整源码手撕

以下为 ReAct Agent 的完整 TypeScript 实现。项目结构按文件夹组织,共 5 个文件。

4.1 项目结构

react-agent/
  - package.json          # 项目配置与依赖
  - tsconfig.json         # TypeScript 编译配置
  - .env.example          # 环境变量模板
  - src/
      - llm.ts            # LLM 封装层(对接 DeepSeek API)
      - tools.ts          # 工具定义层(计算器 + 注册中心)
      - agent.ts          # Agent 核心循环(Thought->Action->Observation)
      - index.ts          # 入口文件 + 测试用例

4.2 项目配置

package.json —— 核心依赖:openai(LLM 调用)、zod(参数校验)、zod-to-json-schema(生成工具 Schema):

{
  "name": "react-agent",
  "version": "1.0.0",
  "description": "ReAct (Reasoning + Acting) Agent 手撕实现",
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts"
  },
  "dependencies": {
    "openai": "^4.73.0",
    "zod": "^3.23.8",
    "zod-to-json-schema": "^3.23.5",
    "dotenv": "^16.4.5"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.5.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

.env.example

DEEPSEEK_API_KEY=your-api-key-here
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
DEEPSEEK_MODEL=deepseek-chat

4.3 LLM 封装层(src/llm.ts

这是整个 Agent 的"大脑接口",负责和 DeepSeek API 通信。关键设计:temperature: 0 保证确定性,tool_choice: "auto" 让模型自主判断是否需要调用工具。

import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";

export interface LLMConfig {
  apiKey: string;
  baseURL: string;
  model: string;
}

/** 创建 OpenAI 兼容的 LLM 客户端(支持 DeepSeek 等任意兼容接口) */
export function createLLMClient(config: LLMConfig): OpenAI {
  return new OpenAI({
    apiKey: config.apiKey,
    baseURL: config.baseURL,
  });
}

/**
 * 封装一次 LLM 调用
 * @param client   OpenAI 客户端
 * @param model    模型名称
 * @param messages 对话历史
 * @param tools    工具定义(OpenAI Function Calling 格式)
 * @returns LLM 返回的消息
 */
export async function chat(
  client: OpenAI,
  model: string,
  messages: ChatCompletionMessageParam[],
  tools?: OpenAI.Chat.Completions.ChatCompletionTool[],
): Promise<OpenAI.Chat.Completions.ChatCompletionMessage> {
  const response = await client.chat.completions.create({
    model,
    messages,
    tools,                          // ← 关键:传入工具定义
    temperature: 0,                 // 工具调用场景必须保证确定性
    tool_choice: tools ? "auto" : undefined,  // 让模型自己决定是否调用工具
  });

  const choice = response.choices[0];
  if (!choice) {
    throw new Error("LLM 返回了空响应");
  }
  return choice.message;
}

设计要点

  • temperature: 0:工具调用场景必须保证确定性,不能用随机性
  • tool_choice: "auto":让模型自主判断是否需要调用工具,而不是强制调用
  • 类比 Java 后端:相当于 RestTemplate 封装 HTTP 调用,只是这里封装的是 LLM 调用

4.4 工具定义层(src/tools.ts

每个工具是 Zod 参数 Schema + execute 执行逻辑 + 注册到 ToolRegistry 的高内聚模块。

import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import type OpenAI from "openai";

// ============================================================
// 工具类型定义
// ============================================================

export interface ToolExecutionContext {
  signal?: AbortSignal;
}

export interface ToolDefinition {
  name: string;
  description: string;
  parameters: z.ZodObject<any>;
  execute: (args: Record<string, any>, ctx: ToolExecutionContext) => Promise<string>;
  toOpenAITool: () => OpenAI.Chat.Completions.ChatCompletionTool;
}

// ============================================================
// 工具工厂函数
// ============================================================

/**
 * 创建一个工具定义
 * 参考 GitHub: @lamemind/react-agent-ts 中的 DynamicTool 模式
 */
export function createTool(config: {
  name: string;
  description: string;
  parameters: z.ZodObject<any>;
  execute: (args: Record<string, any>, ctx: ToolExecutionContext) => Promise<string>;
}): ToolDefinition {
  return {
    name: config.name,
    description: config.description,
    parameters: config.parameters,
    execute: config.execute,
    toOpenAITool() {
      return {
        type: "function" as const,
        function: {
          name: this.name,
          description: this.description,
          parameters: zodToJsonSchema(this.parameters),
        },
      };
    },
  };
}

// ============================================================
// 计算器工具:加减乘除
// ============================================================

/**
 * 完整的四则运算工具
 * - Zod schema 定义参数结构,模型必须按这个格式传参
 * - execute 做实际计算,返回字符串结果
 * - 支持 add、subtract、multiply、divide 四种运算
 */
export const calculatorTool = createTool({
  name: "calculator",
  description:
    "执行四则运算。支持加(add)、减(subtract)、乘(multiply)、除(divide)四种运算。" +
    "当你需要进行数学计算时,请使用此工具。",
  parameters: z.object({
    operation: z
      .enum(["add", "subtract", "multiply", "divide"])
      .describe("运算类型:add=加, subtract=减, multiply=乘, divide=除"),
    a: z.number().describe("第一个操作数"),
    b: z.number().describe("第二个操作数"),
  }),
  async execute(args, _ctx) {
    const { operation, a, b } = args as {
      operation: "add" | "subtract" | "multiply" | "divide";
      a: number;
      b: number;
    };

    let result: number;
    switch (operation) {
      case "add":
        result = a + b;
        break;
      case "subtract":
        result = a - b;
        break;
      case "multiply":
        result = a * b;
        break;
      case "divide":
        if (b === 0) {
          return "错误:除数不能为零";
        }
        result = a / b;
        break;
      default:
        return `错误:不支持的运算类型 '${operation}'`;
    }

    return `${a} ${getOperationSymbol(operation)} ${b} = ${result}`;
  },
});

function getOperationSymbol(op: string): string {
  switch (op) {
    case "add": return "+";
    case "subtract": return "-";
    case "multiply": return "×";
    case "divide": return "÷";
    default: return "?";
  }
}

// ============================================================
// 工具注册表:统一管理所有工具
// 类比 Java 后端:一个 Map<String, Service> 的注册中心
// ============================================================

export class ToolRegistry {
  private tools: Map<string, ToolDefinition> = new Map();

  register(tool: ToolDefinition): void {
    this.tools.set(tool.name, tool);
  }

  get(name: string): ToolDefinition | undefined {
    return this.tools.get(name);
  }

  /** 获取所有工具的 OpenAI Function Calling 格式 */
  toOpenAITools(): OpenAI.Chat.Completions.ChatCompletionTool[] {
    return Array.from(this.tools.values()).map((t) => t.toOpenAITool());
  }

  /** 列出所有工具名称 */
  listNames(): string[] {
    return Array.from(this.tools.keys());
  }
}

设计要点

  • Zod 校验z.object({...}) 定义参数结构,自动校验模型传入的参数是否合法
  • zod-to-json-schema:将 Zod Schema 自动转为 OpenAI Function Calling 需要的 JSON Schema
  • ToolRegistry:一个 Map<String, ToolDefinition> 的注册中心,类比 Java 后端的服务注册中心
  • createTool 工厂函数:参考 LangChain 的 DynamicStructuredTool 设计,让工具定义更简洁

4.5 Agent 核心循环(src/agent.ts)—— 重中之重

一个 while 循环驱动"思考→行动→观察"的闭环:

import type OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
import { chat } from "./llm.js";
import type { ToolRegistry } from "./tools.js";

// ============================================================
// 类型定义
// ============================================================

export interface ReActResult {
  /** 最终答案 */
  answer: string;
  /** 执行了多少轮循环 */
  turns: number;
  /** 调用了多少次工具 */
  toolCalls: number;
  /** 完整的对话历史(用于调试和日志) */
  history: ChatCompletionMessageParam[];
}

export interface ReActConfig {
  client: OpenAI;
  model: string;
  tools: ToolRegistry;
  /** 最大循环轮次(防止死循环) */
  maxTurns?: number;
  /** 系统提示词 */
  systemPrompt?: string;
  /** 是否打印详细日志 */
  verbose?: boolean;
}

// ============================================================
// ReAct Agent 核心
// ============================================================

/**
 * ReAct Agent
 *
 * 核心流程(while 循环):
 * 1. Thought(思考): 模型分析当前状态,决定下一步
 * 2. Action(行动): 需要工具则调用,不需要则直接输出答案
 * 3. Observation(观察): 工具结果注入上下文 → 回到步骤 1
 */
export class ReActAgent {
  private client: OpenAI;
  private model: string;
  private tools: ToolRegistry;
  private maxTurns: number;
  private systemPrompt: string;
  private verbose: boolean;

  constructor(config: ReActConfig) {
    this.client = config.client;
    this.model = config.model;
    this.tools = config.tools;
    this.maxTurns = config.maxTurns ?? 10;
    this.verbose = config.verbose ?? false;
    this.systemPrompt =
      config.systemPrompt ??
      `你是一个能够使用工具的 AI 助手。

你需要按照 ReAct(Reasoning + Acting)模式工作:
1. 思考(Thought):分析用户问题,判断是否需要使用工具
2. 行动(Action):如果需要工具,调用对应工具;如果不需要,直接回答
3. 观察(Observation):根据工具返回结果,继续思考或给出最终答案

重要规则:
- 当你能直接给出答案时,不要再调用工具
- 每次工具调用的结果会作为 Observation 反馈给你
- 你的回答应该清晰、准确,使用中文`;
  }

  /**
   * 执行 Agent
   * @param question 用户问题
   * @returns 执行结果
   */
  async run(question: string): Promise<ReActResult> {
    // 初始化对话历史
    const messages: ChatCompletionMessageParam[] = [
      { role: "system", content: this.systemPrompt },
      { role: "user", content: question },
    ];

    let turnCount = 0;
    let toolCallCount = 0;
    const openAITools = this.tools.toOpenAITools();

    this.log(`\n ReAct Agent 启动`);
    this.log(` 用户问题: ${question}`);
    this.log(` 可用工具: ${this.tools.listNames().join(", ")}`);

    // ============================================================
    // 核心循环:ReAct 的 while 循环
    // ============================================================
    while (turnCount < this.maxTurns) {
      turnCount++;
      this.log(`\n 第 ${turnCount} 轮循环`);

      // Step 1: Thought(思考 & 决策)
      // 调用 LLM,让模型决定是调用工具还是直接回答
      const response = await chat(
        this.client,
        this.model,
        messages,
        openAITools
      );

      // 如果模型返回了 tool_calls,说明它需要调用工具
      if (response.tool_calls && response.tool_calls.length > 0) {
        this.log(
          `Thought: 模型决定调用 ${response.tool_calls.length} 个工具`
        );

        // 将 assistant 消息(包含 tool_calls)加入对话历史
        messages.push({
          role: "assistant",
          content: response.content,
          tool_calls: response.tool_calls,
        });

        // Step 2: Action(执行工具调用)
        for (const toolCall of response.tool_calls) {
          toolCallCount++;
          const toolName = toolCall.function.name;
          const rawArgs = toolCall.function.arguments;

          this.log(` Action: 调用工具 '${toolName}'`);
          this.log(` 参数: ${rawArgs}`);

          const tool = this.tools.get(toolName);
          if (!tool) {
            const errorMsg = `错误:未找到工具 '${toolName}'`;
            messages.push({
              role: "tool",
              tool_call_id: toolCall.id,
              content: errorMsg,
            });
            this.log(`${errorMsg}`);
            continue;
          }

          // 解析参数并执行
          try {
            const parsedArgs = JSON.parse(rawArgs);
            // Zod 校验参数
            const validatedArgs = tool.parameters.parse(parsedArgs);
            // 执行工具
            const result = await tool.execute(validatedArgs, {});

            this.log(`结果: ${result}`);

            // Step 3: Observation(将工具返回结果注入上下文)
            messages.push({
              role: "tool",
              tool_call_id: toolCall.id,
              content: result,
            });
          } catch (error) {
            const errorMsg = `工具执行错误: ${
              error instanceof Error ? error.message : String(error)
            }`;
            messages.push({
              role: "tool",
              tool_call_id: toolCall.id,
              content: errorMsg,
            });
            this.log(`${errorMsg}`);
          }
        }
      } else {
        // 模型没有调用工具,直接返回了答案
        const answer = response.content ?? "抱歉,我无法回答这个问题。";
        this.log(` 最终答案: ${answer}`);
        this.log(
          `\n 统计: ${turnCount} 轮循环, ${toolCallCount} 次工具调用`
        );

        return {
          answer,
          turns: turnCount,
          toolCalls: toolCallCount,
          history: messages,
        };
      }
    }

    // 达到最大轮次,强制结束
    const fallbackAnswer = "已达到最大执行轮次,无法完成任务。";
    this.log(` ${fallbackAnswer}`);
    return {
      answer: fallbackAnswer,
      turns: turnCount,
      toolCalls: toolCallCount,
      history: messages,
    };
  }

  private log(message: string): void {
    if (this.verbose) {
      console.log(message);
    }
  }
}

核心逻辑拆解

第 1 轮: Thought "需要计算器" → Action: calculator(add, 15, 27) → Observation: "15 + 27 = 42"
第 2 轮: Thought "已有答案" → 直接返回: "15 + 27 = 42"

循环终止条件:模型返回 finish_reason: stop(无 tool_calls)→ 直接输出答案;或达到 maxTurns(默认 10 轮)→ 强制终止,防止死循环。

关键设计决策while 而非递归(递归易栈溢出);tool_calls 为数组(支持并行工具调用);maxTurns 上限(防止模型陷入死循环)。

4.6 入口文件(src/index.ts

import "dotenv/config";
import { createLLMClient } from "./llm.js";
import { calculatorTool, ToolRegistry } from "./tools.js";
import { ReActAgent } from "./agent.js";

async function main() {
  // 1. 初始化 LLM 客户端(DeepSeek,兼容 OpenAI 协议)
  const client = createLLMClient({
    apiKey: process.env.DEEPSEEK_API_KEY!,
    baseURL: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com/v1",
    model: process.env.DEEPSEEK_MODEL || "deepseek-chat",
  });

  // 2. 注册工具:计算器(加减乘除)
  const toolRegistry = new ToolRegistry();
  toolRegistry.register(calculatorTool);

  // 3. 创建 ReAct Agent
  const agent = new ReActAgent({
    client,
    model: process.env.DEEPSEEK_MODEL || "deepseek-chat",
    tools: toolRegistry,
    maxTurns: 10,
    verbose: true,
  });

  // 4. 测试用例
  const testCases = [
    // 简单计算:单轮工具调用
    "请帮我计算 15 + 27 等于多少?",
    // 复合计算:需要多轮工具调用
    "请先计算 100 除以 4,然后用结果加上 30,最后告诉我结果",
    // 复杂计算:需要多轮工具调用
    "小明有 50 元,买了 3 本书每本 12 元,又买了 2 支笔每支 5 元,请问他还剩多少钱?",
  ];

  console.log("=".repeat(60));
  console.log("  ReAct Agent 演示 —— 计算器实战");
  console.log("=".repeat(60));

  for (const [index, question] of testCases.entries()) {
    console.log(`\n${"=".repeat(60)}`);
    console.log(`  测试用例 ${index + 1}: ${question}`);
    console.log("=".repeat(60));

    try {
      const result = await agent.run(question);
      console.log(`\n 最终答案: ${result.answer}`);
      console.log(
        ` 统计: ${result.turns} 轮循环, ${result.toolCalls} 次工具调用`
      );
    } catch (error) {
      console.error(` 执行失败:`, error);
    }
  }
}

main().catch(console.error);

4.7 运行效果

$ npx tsx src/index.ts

=== 测试用例 1: 15 + 27 ===1 轮: Thought → Action: calculator(add, 15, 27)15 + 27 = 422 轮: 最终答案: 15 + 27 = 42
统计: 2 轮循环, 1 次工具调用

=== 测试用例 2: 小明有 50 元,买书买笔... ===1 轮: multiply(3, 12) = 362 轮: multiply(2, 5) = 103 轮: add(36, 10) = 464 轮: subtract(50, 46) = 45 轮: 最终答案: 小明还剩 4 元
统计: 5 轮循环, 4 次工具调用

关键观察:复合计算需要 5 轮循环、4 次工具调用。每一步依赖上一步结果,模型必须等一轮结束才能开始下一轮——这是 ReAct 的固有特性,也是 CodeAct 要解决的痛点。


五、CodeAct 重要源码讲解

CodeAct 与 ReAct 最大的区别:不需要工具注册,核心是沙箱代码执行。以下重点讲解差异部分。

5.1 项目结构

codeact-agent/
  - package.json          # 依赖更少:无需 zod、zod-to-json-schema
  - tsconfig.json
  - .env.example
  - src/
      - llm.ts            # LLM 封装(纯文本,不走 Function Calling)
      - sandbox.ts        # 代码沙箱(核心新增)
      - agent.ts          # Agent 循环(代码提取 -> 沙箱执行 -> Observation)
      - index.ts

5.2 沙箱模块(src/sandbox.ts)—— CodeAct 的心脏

相当于 ReAct 中"工具注册表 + 工具执行"的合体,核心机制:劫持 console.log → 收集输出 → 隔离作用域执行 → 恢复 console

/**
 * 代码沙箱 —— CodeAct 的核心
 *
 * 设计理念(参考 Apple ML CodeAct 论文):
 * - ReAct 的 Action = 调用预定义工具
 * - CodeAct 的 Action = 执行一段代码
 * - 代码运行在沙箱中,输出捕获为 Observation
 *
 * 安全说明:
 * - 演示环境使用 AsyncFunction 简化实现
 * - 生产环境必须使用 isolated-vm / Docker 等真隔离方案
 * - 严禁在生产环境直接使用 eval / Function 构造器
 */

export interface SandboxResult {
  success: boolean;
  output: string;      // console.log 的输出
  error?: string;
  duration: number;    // 执行耗时(毫秒)
}

export class CodeSandbox {
  async run(code: string): Promise<SandboxResult> {
    const startTime = Date.now();
    const outputs: string[] = [];
    const originalLog = console.log;  // 保存原始 console.log

    try {
      // 1. 劫持 console.log,收集所有输出
      console.log = (...args: any[]) => {
        const formatted = args.map(arg =>
          typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)
        ).join(" ");
        outputs.push(formatted);
        originalLog("[沙箱]", formatted);
      };

      // 2. 准备沙箱上下文:预置加减乘除等函数
      const sandboxContext = {
        add: (a: number, b: number) => a + b,
        subtract: (a: number, b: number) => a - b,
        multiply: (a: number, b: number) => a * b,
        divide: (a: number, b: number) => {
          if (b === 0) throw new Error("除数不能为零");
          return a / b;
        },
        sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0),
        avg: (arr: number[]) =>
          arr.length === 0 ? 0 : arr.reduce((a, b) => a + b, 0) / arr.length,
        console: {
          log: (...args: any[]) => {
            const formatted = args.map(arg =>
              typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)
            ).join(" ");
            outputs.push(formatted);
            originalLog("[沙箱]", formatted);
          },
        },
      };

      // 3. 使用 AsyncFunction 在隔离作用域中执行代码
      const AsyncFunction = Object.getPrototypeOf(
        async function () {}
      ).constructor;

      const contextKeys = Object.keys(sandboxContext);
      const contextValues = Object.values(sandboxContext);
      const executor = new AsyncFunction(...contextKeys, `"use strict"; ${code}`);
      await executor(...contextValues);

      const duration = Date.now() - startTime;
      return { success: true, output: outputs.join("\n"), duration };
    } catch (error) {
      const duration = Date.now() - startTime;
      return {
        success: false,
        output: outputs.join("\n"),
        error: error instanceof Error ? error.message : String(error),
        duration,
      };
    } finally {
      console.log = originalLog;  // 4. 恢复原始 console.log
    }
  }
}

设计要点console.log 劫持收集输出作为 Observation;AsyncFunction 构造独立作用域避免变量污染;预置 add/subtract/multiply/divide/sum/avg 供模型直接调用。生产环境必须使用 isolated-vm 或 Docker 真隔离。

5.3 Agent 核心循环(src/agent.ts)—— 与 ReAct 的本质区别

CodeAct 的循环和 ReAct 有本质区别:不需要工具注册,只需要代码提取和沙箱执行

/**
 * CodeAct Agent 核心循环
 *
 * 与 ReAct 的核心区别:
 * - ReAct: 调用预定义工具(Function Calling)
 * - CodeAct: 生成代码 → 沙箱运行 → 输出 = Observation
 */
export class CodeActAgent {
  private client: OpenAI;
  private model: string;
  private maxTurns: number;
  private sandbox: CodeSandbox;

  async run(question: string): Promise<CodeActResult> {
    const messages: ChatCompletionMessageParam[] = [
      { role: "system", content: this.systemPrompt },
      { role: "user", content: question },
    ];

    let turnCount = 0;
    let codeExecutions = 0;

    while (turnCount < this.maxTurns) {
      turnCount++;

      // Step 1: 调用 LLM(纯文本,不走 Function Calling)
      const response = await chat(this.client, this.model, messages);

      // Step 2: 提取代码块
      const codeBlocks = extractCodeBlocks(response);

      if (codeBlocks.length > 0) {
        // 有代码块 → 送入沙箱执行
        messages.push({ role: "assistant", content: response });

        for (const code of codeBlocks) {
          codeExecutions++;
          const result = await this.sandbox.run(code);

          // Step 3: Observation → 将沙箱输出注入上下文
          messages.push({
            role: "user",
            content: `[代码执行结果]\n${result.output}\n\n请根据以上结果继续处理。`,
          });
        }
      } else {
        // 没有代码块 → 最终答案
        return { answer: response, turns: turnCount, codeExecutions, history: messages };
      }
    }
  }
}

/** 从 LLM 回复中提取 JavaScript 代码块 */
function extractCodeBlocks(text: string): string[] {
  const blocks: string[] = [];
  // 匹配 ```javascript ... ```格式
  const regex = /```(?:javascript|js)\n([\s\S]*?)```/g;
  let match;
  while ((match = regex.exec(text)) !== null) {
    blocks.push(match[1].trim());
  }
  return blocks;
}

关键 System Prompt 设计

你是一个 CodeAct Agent,能够通过编写和执行 JavaScript 代码来完成任务。

## 沙箱中可用的函数
- add(a, b) / subtract(a, b) / multiply(a, b) / divide(a, b)
- sum(arr) / avg(arr)
- console.log(...) → 输出结果

## 重要规则
1. 所有计算结果必须通过 console.log 输出
2. 复杂计算尽量写在一段代码中完成,减少交互轮次
3. 代码执行出错时,分析错误信息,修复后重新执行
4. 得到最终结果后,直接回复用户,不要再写代码块

与 ReAct 的核心区别:ReAct 走 Function Calling(tools + tool_calls),CodeAct 走纯文本(代码块 → 沙箱执行);ReAct 的工具调用是结构化 JSON,CodeAct 的代码执行是图灵完备的。


六、Plan-and-Execute 重要源码讲解

Plan-and-Execute 的核心是"两阶段":先规划(Planner),再执行(Executor)。以下重点讲解规划器和执行器的 DAG 调度逻辑。

6.1 项目结构

plan-and-execute-agent/
  - package.json          # 依赖最简:仅 openai + dotenv
  - tsconfig.json
  - .env.example
  - src/
      - llm.ts            # LLM 封装
      - planner.ts        # 规划器(LLM 拆解任务为 JSON 子任务 DAG)
      - executor.ts       # 执行器(Kahn 拓扑排序 + 引用解析 + 失败重试)
      - agent.ts          # Agent 核心(Phase 1 规划 + Phase 2 执行)
      - index.ts

6.2 规划器(src/planner.ts)—— 任务拆解引擎

使用 LLM 将用户问题拆解为 JSON 格式的子任务 DAG:

/**
 * 子任务定义
 */
export interface SubTask {
  id: string;                    // step1, step2, ...
  description: string;           // 任务描述
  dependsOn: string[];           // 依赖的任务 ID 列表
  operation: "add" | "subtract" | "multiply" | "divide";
  operandA: number | string;     // 可以是数字或引用(如 "$step1")
  operandB: number | string;
}

export interface Plan {
  question: string;
  tasks: SubTask[];
}

/**
 * Plan-and-Execute 规划器
 * 使用 LLM 将用户问题拆解为可执行的子任务 DAG
 */
export class Planner {
  private client: OpenAI;
  private model: string;

  async createPlan(question: string): Promise<Plan> {
    const systemPrompt = `你是一个任务规划专家。将用户的计算问题拆解为有序的子任务。

## 规则
1. 每个子任务是一个四则运算(add/subtract/multiply/divide)
2. 子任务按依赖关系排序,后面的任务可以引用前面任务的结果
3. 引用前面步骤的结果时,使用 $step1, $step2 等格式
4. 输出必须是合法的 JSON 格式

## 输出格式
{
  "tasks": [
    {
      "id": "step1",
      "description": "计算书的总价",
      "dependsOn": [],
      "operation": "multiply",
      "operandA": 3,
      "operandB": 12
    },
    {
      "id": "step3",
      "description": "计算总花费",
      "dependsOn": ["step1", "step2"],
      "operation": "add",
      "operandA": "$step1",
      "operandB": "$step2"
    }
  ]
}`;

    const messages = [
      { role: "system" as const, content: systemPrompt },
      { role: "user" as const, content: `请将以下问题拆解为子任务:\n${question}` },
    ];

    const response = await chat(this.client, this.model, messages);

    // 从 LLM 回复中提取 JSON
    const jsonMatch = response.match(/```json\s*([\s\S]*?)```/);
    const jsonStr = jsonMatch ? jsonMatch[1].trim() : response.trim();
    const parsed = JSON.parse(jsonStr);
    return { question, tasks: parsed.tasks };
  }
}

6.3 执行器(src/executor.ts)—— DAG 调度核心

这是 Plan-and-Execute 最核心的模块,实现了三个关键能力:拓扑排序(Kahn 算法)→ 引用解析($step1 替换)→ 失败重试

export interface StepResult {
  stepId: string;
  success: boolean;
  result: number;
  description: string;
  expression: string;   // 如 "3 × 12 = 36"
  error?: string;
  retries: number;
}

export interface ExecutionResult {
  success: boolean;
  steps: StepResult[];
  finalAnswer: string;
  log: string[];
}

export class Executor {
  private maxRetries: number;

  constructor(maxRetries: number = 3) {
    this.maxRetries = maxRetries;
  }

  /** 执行完整计划 */
  async execute(plan: Plan): Promise<ExecutionResult> {
    const log: string[] = [];
    const stepResults: Map<string, StepResult> = new Map();

    // Step 1: 拓扑排序 —— 按依赖关系确定执行顺序
    const executionOrder = this.topologicalSort(plan.tasks);
    log.push(`🔗 执行顺序: ${executionOrder.map((t) => t.id).join(" → ")}`);

    // Step 2: 按序执行每个子任务
    let allSuccess = true;
    for (const task of executionOrder) {
      // 解析 $step1 等引用 → 替换为实际值
      const a = this.resolveValue(task.operandA, stepResults);
      const b = this.resolveValue(task.operandB, stepResults);

      // 执行计算(带重试)
      const stepResult = await this.executeWithRetry(task, a, b, log);
      stepResults.set(task.id, stepResult);

      if (!stepResult.success) {
        allSuccess = false;
      }
    }

    // Step 3: 汇总结果
    const finalAnswer = this.buildFinalAnswer(plan, stepResults, log);
    return { success: allSuccess, steps: Array.from(stepResults.values()), finalAnswer, log };
  }

  /**
   * Kahn 算法拓扑排序
   * 时间复杂度 O(V + E),空间复杂度 O(V + E)
   */
  private topologicalSort(tasks: SubTask[]): SubTask[] {
    const inDegree = new Map<string, number>();
    const adjacency = new Map<string, string[]>();
    const taskMap = new Map<string, SubTask>();

    // 构建入度表和邻接表
    for (const task of tasks) {
      taskMap.set(task.id, task);
      if (!inDegree.has(task.id)) inDegree.set(task.id, 0);
      if (!adjacency.has(task.id)) adjacency.set(task.id, []);
    }

    for (const task of tasks) {
      for (const depId of task.dependsOn) {
        if (!adjacency.has(depId)) adjacency.set(depId, []);
        adjacency.get(depId)!.push(task.id);
        inDegree.set(task.id, (inDegree.get(task.id) ?? 0) + 1);
      }
    }

    // BFS:入度为 0 的节点先执行
    const queue: SubTask[] = [];
    for (const task of tasks) {
      if ((inDegree.get(task.id) ?? 0) === 0) {
        queue.push(task);
      }
    }

    const sorted: SubTask[] = [];
    while (queue.length > 0) {
      const current = queue.shift()!;
      sorted.push(current);
      for (const neighbor of adjacency.get(current.id) ?? []) {
        const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
        inDegree.set(neighbor, newDegree);
        if (newDegree === 0) {
          const neighborTask = taskMap.get(neighbor);
          if (neighborTask) queue.push(neighborTask);
        }
      }
    }

    if (sorted.length !== tasks.length) {
      throw new Error("DAG 中存在循环依赖,拓扑排序失败");
    }
    return sorted;
  }

  /**
   * 解析操作数:将 $step1 等引用替换为实际值
   */
  private resolveValue(
    operand: number | string,
    results: Map<string, StepResult>
  ): number {
    if (typeof operand === "number") return operand;

    // 检查是否是引用格式($step1, $step2 等)
    if (typeof operand === "string" && operand.startsWith("$")) {
      const refId = operand.slice(1);  // 去掉 $ 前缀
      const refResult = results.get(refId);
      if (!refResult) throw new Error(`引用的步骤 '${refId}' 未找到或尚未执行`);
      if (!refResult.success) throw new Error(`引用的步骤 '${refId}' 执行失败`);
      return refResult.result;
    }

    // 尝试解析为数字
    const parsed = Number(operand);
    if (isNaN(parsed)) throw new Error(`无法解析操作数: ${operand}`);
    return parsed;
  }

  /**
   * 带重试的步骤执行
   */
  private async executeWithRetry(
    task: SubTask,
    a: number,
    b: number,
    log: string[]
  ): Promise<StepResult> {
    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        const result = calculate(task.operation, a, b);
        const expression = `${a} ${getOpSymbol(task.operation)} ${b} = ${result}`;
        log.push(`  ${expression}`);
        return { stepId: task.id, success: true, result, description: task.description, expression, retries: attempt };
      } catch (error) {
        log.push(`${attempt + 1} 次尝试失败`);
        if (attempt < this.maxRetries - 1) {
          log.push(`   准备重试...`);
        }
      }
    }
    return { stepId: task.id, success: false, result: 0, description: task.description, expression: "", retries: this.maxRetries };
  }
}

/** 四则运算 */
function calculate(operation: string, a: number, b: number): number {
  switch (operation) {
    case "add": return a + b;
    case "subtract": return a - b;
    case "multiply": return a * b;
    case "divide":
      if (b === 0) throw new Error("除数不能为零");
      return a / b;
    default: throw new Error(`不支持的运算类型: ${operation}`);
  }
}

核心设计:Kahn 算法拓扑排序(BFS,O(V+E));$step1 引用解析实现步骤间数据传递;每步最多重试 3 次。

6.4 Agent 核心(src/agent.ts

export class PlanAndExecuteAgent {
  private planner: Planner;
  private executor: Executor;

  async run(question: string): Promise<PlanAndExecuteResult> {
    // Phase 1: Plan(规划)
    const plan = await this.planner.createPlan(question);

    // Phase 2: Execute(执行)
    const result = await this.executor.execute(plan);

    return { answer: result.finalAnswer, log: result.log, success: result.success };
  }
}

6.5 运行效果

$ npx tsx src/index.ts

=== 测试用例: 小明有 50 元,买书买笔... ===
Phase 1 规划: 4 个子任务
  step1: 3×12=36 → step2: 2×5=10 → step3: 36+10=46 → step4: 50-46=4
Phase 2 执行: step1  → step2  → step3 → step4 
最终结果: 4

七、三大模式全景对比

7.1 核心维度对比

维度 ReAct CodeAct Plan-and-Execute
全称 Reasoning + Acting Code + Acting Plan-and-Execute
决策方式 边走边看,每一步动态决策 代码即行动,一段代码搞定 先规划后执行,按图索骥
Action 形式 调用预定义工具函数(JSON) 生成完整代码,沙箱运行 拆解为子任务 DAG,有序执行
工具约束 必须提前定义所有工具 无限制,编程语言标准库 子任务定义在 Plan 中
多步骤计算 分多轮调用,交互次数多 单段代码嵌套完成,轮次少 按 DAG 依赖逐步执行
交互轮次 多(每步一轮) 少(批量处理) 中(规划 1 轮 + 执行 N 步)
模型要求 基础推理能力即可 强代码生成能力 强规划 + 执行能力
可控性 低(黑盒循环) 中(代码可审查) 高(每步可监控)
失败处理 整体重试,上下文丢失 修改代码重新执行 单步重试,上下文保留
适用场景 不确定路径的动态任务 批量数据处理、复杂运算 标准化长链路流水线

7.2 实战数据对比(同一计算题:小明买书买笔)

指标 ReAct CodeAct Plan-and-Execute
循环轮次 5 轮 2 轮 2 轮(1 规划 + 1 执行)
工具调用/代码执行 4 次工具调用 1 次代码执行 4 步子任务执行
核心依赖 openai + zod + zod-to-json-schema openai openai
扩展新运算 需要注册新工具 写代码即可 规划器自动适配

八、业务场景选型指南

8.1 ReAct 最佳场景:不确定路径的动态任务

反诈风险研判:无法提前确定需要调用多少个工具,需要根据多模态识别结果动态决定是否调取知识库、是否触发监护人预警。

用户举报 → 身份核验 → 发现异常 → 查询关系图谱 → 发现多个关联风险账号
→ 调取历史行为数据 → 风险评估 → 触发监护人预警

每一步的结果决定下一步做什么,路径不确定,这正是 ReAct 的优势。

8.2 CodeAct 最佳场景:批量数据处理

报表运算、复杂文件解析:不想为每一类数据处理单独开发工具,直接让模型生成代码处理。

用户上传 Excel → 模型生成 JS 代码 → 沙箱执行:读取数据、清洗、计算、汇总 → 输出结果

8.3 Plan-and-Execute 最佳场景:标准化长链路任务

批量文档解析流水线:先拆解完整子任务,有序执行,可控性更强,方便做任务监控、失败重试。

Plan: 下载文件 → 解析格式 → 提取文本 → 清洗数据 → 入库 → 通知用户
Execute: 按 DAG 依赖逐步执行,每步记录日志,失败自动重试

九、延伸拓展

9.1 从单模式到混合模式

实际生产环境中,很少有系统只用单一模式。常见的混合方案:

  • ReAct + CodeAct 混合:日常工具调用用 ReAct,遇到复杂计算时切换到 CodeAct
  • Plan-and-Execute + ReAct 混合:先用 Plan-and-Execute 规划整体流程,每个子任务内部用 ReAct 动态执行
  • 三层架构:Plan-and-Execute(顶层调度)→ ReAct(中层决策)→ CodeAct(底层计算)

9.2 基于三者可扩展的完整 Agent 系统

在三种模式的基础上,可以叠加以下能力构建完整 Agent:

  1. 记忆系统:短期记忆(对话历史)+ 长期记忆(向量数据库)
  2. RAG(检索增强生成):接入外部知识库,让 Agent 能回答私有数据问题
  3. 多轮自我反思:Agent 执行完任务后,自我评估结果质量,自动修正
  4. MCP 协议:统一标准化所有工具调用,兼容 ReAct / CodeAct / Plan-and-Execute 的工具层

9.3 其他值得关注的 Agent 架构

  • HuggingGPT:多模型分工调度,不同任务分配不同模型
  • AutoGPT:自主任务分解 + 执行 + 反思的完整闭环
  • MetaGPT:多 Agent 协作,模拟软件公司角色分工

十、总结

三种 Agent 模式并非互相替代,而是互补关系

  • ReAct 给你"边思考边行动"的灵活性,适合未知路径的探索。就像一个灵活应变的前线侦察兵,根据现场情况动态决策
  • CodeAct 给你"代码即行动"的效率,适合批量计算和复杂逻辑。就像一个高效的工程师,一段代码解决所有计算问题
  • Plan-and-Execute 给你"先规划后执行"的可控性,适合标准化长链路任务。就像一个靠谱的项目经理,先做计划,再按步骤推进

选型的核心原则:没有银弹,只有合适的场景。理解每种模式的本质,才能在正确的场景下做出正确的选择。


运行方式

cd react-agent  # 或 codeact-agent / plan-and-execute-agent
npm install
cp .env.example .env  # 填入 DeepSeek API Key
npx tsx src/index.ts

参考文献

  • ReAct 论文:Yao et al., “ReAct: Synergizing Reasoning and Acting in Language Models”, 2022.10
  • CodeAct 论文:Apple ML, “CodeAct: Code as Action in Language Model Agents”, 2024
  • Plan-and-Execute 架构参考:LangGraph 源码 github.com/langchain-ai/langgraph
  • E2B 代码沙箱:github.com/e2b-dev/e2b
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐