AI Agent 三大核心架构精讲:ReAct / CodeAct / Plan-and-Execute 原理 + 手撕源码
文章目录
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(观察)
^ |
+------- 循环直到完成 ----------------+
- Thought(思考):大模型输出推理逻辑,规划需要调用什么工具、调用顺序
- Action(行动):输出工具调用指令,执行具体操作
- 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 的短板
- 对模型代码生成能力要求高:2022 年早期大模型代码能力不足,所以当时优先流行 ReAct;现代代码大模型下 CodeAct 效果更好
- 代码块提取依赖正则:需要从模型输出中解析代码块,格式不稳定时可能失败
- 安全风险:代码执行必须放在沙箱中,生产环境绝不能直接
eval
三、Plan-and-Execute 原理剖析
3.1 核心思想
Plan-and-Execute = Plan(规划)+ Execute(执行),核心思想是:把复杂任务先拆解为子任务 DAG(有向无环图),再按依赖关系有序执行。
ReAct: 边走边看,下一步做什么取决于上一步结果
Plan-and-Execute: 先画好地图,再按图索骥
3.2 适用场景
Plan-and-Execute 最适合标准化、长链路任务,特点是:
- 任务步骤可预知:可以先规划好再执行
- 步骤间有依赖关系:后面的步骤依赖前面的结果
- 需要监控和重试:每步可独立监控,失败可单步重试
- 需要可控性:知道当前进度、哪些步骤已完成
典型场景:
- 批量文档解析流水线(下载 → 解析 → 提取 → 入库 → 通知)
- 反诈风险研判的多维度分析(身份核验 → 行为分析 → 关系图谱 → 风险评估)
- 财务报表生成(数据采集 → 清洗 → 计算 → 汇总 → 导出)
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 = 42
第 2 轮: 最终答案: 15 + 27 = 42
统计: 2 轮循环, 1 次工具调用
=== 测试用例 2: 小明有 50 元,买书买笔... ===
第 1 轮: multiply(3, 12) = 36
第 2 轮: multiply(2, 5) = 10
第 3 轮: add(36, 10) = 46
第 4 轮: subtract(50, 46) = 4
第 5 轮: 最终答案: 小明还剩 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:
- 记忆系统:短期记忆(对话历史)+ 长期记忆(向量数据库)
- RAG(检索增强生成):接入外部知识库,让 Agent 能回答私有数据问题
- 多轮自我反思:Agent 执行完任务后,自我评估结果质量,自动修正
- 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
更多推荐



所有评论(0)