为什么还需要 ReAct?
上一篇里,我们已经有了一个最小可用的 SimpleAgent。 它能理解用户输入,也能结合上下文生成回答。
但很快,小王就遇到了一个很现实的问题:
老板:"这个智能体现在能聊天了,但如果用户问'今天天气怎么样'、'最新新闻是什么'、'帮我算一个表达式',它还是不太行。能不能让它真的做点事?"
这句话点中了纯 LLM Agent 的三个典型短板:
- 知识不是实时的模型训练数据有截止时间,不知道今天刚发生的事。
- 遇到未知内容可能幻觉它不一定会诚实地说"不知道",有时会编一个看起来像真的答案。
- 它不能直接行动 它会生成文本,但不会自己查搜索、调用 API、访问数据库、执行运算。
也就是说,SimpleAgent 更像一个"会说话的助手",但还不像一个"会做事的智能体"。
ReAct 正是为这个问题设计的。
第一章:ReAct 到底是什么?
1.1 ReAct 的全称
ReAct 来自这两个词:
text
Reasoning + Acting
也就是:
- Reasoning:先思考
- Acting:再行动
它不是让模型拿到问题后直接吐最终答案,而是让模型先做中间推理,再调用工具,再根据结果继续思考,直到答案足够可靠。
1.2 ReAct 的核心闭环
ReAct 最经典的结构是:
这里面最重要的是三种状态:
Thought当前知道什么?还缺什么?下一步最合理的动作是什么?Action调用一个工具,或者宣布任务结束。Observation工具返回的结果,它会进入下一轮推理。
1.3 一个最直观的例子
假设用户问:
华为最新发布的手机是什么型号?售价多少?
纯 LLM 容易直接"猜"一个答案。 而 ReAct 会更像这样:
text
第 1 轮
Thought: 我需要先确认华为最近发布了什么手机
Action: Search[华为最新发布手机型号]
Observation: 华为发布了 Mate 70 系列,包括 Mate 70、Mate 70 Pro、Mate 70 Pro+
第 2 轮
Thought: 已经知道型号了,现在需要查售价
Action: Search[华为 Mate 70 售价]
Observation: Mate 70 起售价 5499 元,Mate 70 Pro 起售价 6499 元,Mate 70 Pro+ 起售价 8499 元
第 3 轮
Thought: 信息已经足够,可以回答用户了
Action: Finish[华为最新发布的是 Mate 70 系列,......]
ReAct 的价值,不只是"多查了一次",而是它把"思考 -> 执行 -> 根据结果继续调整"的过程显式化了。
第二章:什么时候适合用 ReAct?
ReAct 特别适合这些任务:
- 实时信息问答
- 检索增强问答
- 带计算能力的助手
- 需要多步工具调用的任务
- 用户目标明确,但路径不固定的场景
例如:
- "今天北京天气怎么样?"
- "帮我查一下某家公司最新融资消息"
- "计算
(15 + 27) * 3 - 8的结果" - "先搜索一个技术方案,再基于结果给建议"
不太适合直接上 ReAct 的情况也有:
- 纯文案生成
- 不需要任何工具
- 路径很固定
- 对延迟特别敏感
你可以这样简单判断:
第三章:把读者当小白,我们先从零搭一个能跑的项目
如果你看到文章里的 @hello-agents/shared 会困惑,这是正常的。 因为那是当前项目里已经封装好的共享模块,但如果你自己是第一次做,当然不知道里面是什么。
所以这一篇我们不偷懒,直接从零开始搭一个最小项目。
3.1 项目结构
建议你先创建这样一个目录:
text
react-agent-demo/
├── package.json
├── tsconfig.json
├── .env
└── src/
├── shared/
│ └── llm.ts
├── react/
│ ├── prompt.ts
│ ├── parser.ts
│ ├── tools.ts
│ └── agent.ts
└── index.ts
3.2 package.json
先初始化项目:
json
{
"name": "react-agent-demo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc"
},
"dependencies": {
"dotenv": "^16.6.1",
"openai": "^4.104.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.0"
}
}
然后安装依赖:
bash
npm install
3.3 tsconfig.json
json
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3.4 .env
先准备模型配置:
bash
LLM_API_KEY="你的模型 API Key"
LLM_MODEL_ID="gpt-4o-mini"
LLM_BASE_URL="https://api.openai.com/v1"
如果你不是用 OpenAI,而是别的兼容 OpenAI API 的服务,只要改这三个变量就行。
第四章:先写共享层 llm.ts
这就是之前文章里提到但没展开的"共享代码"。
我们先创建 src/shared/llm.ts 这个文件,内容如下:
typescript
// src/shared/llm.ts
import "dotenv/config";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
export interface LLMOptions {
model?: string;
apiKey?: string;
baseUrl?: string;
timeout?: number;
}
export class HelloAgentsLLM {
private readonly client: OpenAI;
private readonly model: string;
constructor(options: LLMOptions = {}) {
const apiKey = options.apiKey ?? process.env.LLM_API_KEY;
const baseUrl = options.baseUrl ?? process.env.LLM_BASE_URL;
if (!apiKey) {
throw new Error("缺少 LLM_API_KEY,请检查 .env 配置");
}
this.model = options.model ?? process.env.LLM_MODEL_ID ?? "gpt-4o-mini";
this.client = new OpenAI({
apiKey,
baseURL: baseUrl,
timeout: options.timeout ?? 60_000,
});
}
async think(
messages: ChatCompletionMessageParam[],
temperature = 0
): Promise<string> {
const response = await this.client.chat.completions.create({
model: this.model,
messages,
temperature,
});
return response.choices[0]?.message?.content ?? "";
}
}
4.1 这段代码到底是干什么的?
如果你是小白,可以把它简单理解成:
HelloAgentsLLM就是我们自己的"大模型调用器"- 它负责从
.env里读取配置 - 帮我们统一调用 OpenAI 兼容接口
- 以后所有 Agent 都通过它来访问模型
也就是说,后面文章里凡是出现 HelloAgentsLLM,你现在已经知道它是什么了,不再是一个神秘黑盒。
第五章:设计 Prompt 协议
ReAct 能不能稳定跑,提示词设计比很多人想象中更重要。
因为模型必须知道:
- 当前有哪些工具
- 输出必须长什么样
- 每次只能执行一步
- 什么情况下可以结束
创建 src/react/prompt.ts:
typescript
// src/react/prompt.ts
export const REACT_PROMPT_TEMPLATE = `
你是一个具备推理和行动能力的 AI 助手。你可以通过思考分析问题,然后调用合适的工具来获取信息,最终给出准确的答案。
## 可用工具
{tools}
## 工作流程
请严格按照以下格式进行回应,每次只能执行一个步骤:
Thought: 分析当前问题,思考需要什么信息或采取什么行动。
Action: 选择一个行动,格式必须是以下之一:
- \`ToolName[参数]\` - 调用指定工具
- \`Finish[最终答案]\` - 当你有足够信息给出最终答案时
## 重要规则
1. 每次回应必须包含 Thought 和 Action 两部分
2. 工具调用格式必须严格遵循:工具名[参数]
3. 只有当你确信有足够信息回答问题时,才使用 Finish
4. 如果工具返回的信息不够,请继续推理并调用工具
## 当前任务
Question: {question}
## 执行历史
{history}
现在开始你的推理和行动:
`;
5.1 为什么这段 Prompt 很关键?
这不是随便写一段提示词,而是在告诉模型:
- 你现在不是普通聊天模式
- 你必须按 ReAct 协议输出
- 你不能跳步
- 你可以使用工具
如果这一层写得不清楚,后面的代码再完整,也会经常跑偏。
第六章:实现输出解析器
有了 Prompt,还不够。 因为模型输出的是字符串,程序需要把它解析成结构化数据。
创建 src/react/parser.ts:
typescript
// src/react/parser.ts
export interface ParsedOutput {
thought: string | null;
action: string | null;
}
export interface ParsedAction {
toolName: string;
toolInput: string;
}
export function parseOutput(text: string): ParsedOutput {
const thoughtMatch = text.match(
/Thought:\s*([\s\S]*?)(?=\nAction:|$)/i
);
const actionMatch = text.match(
/Action:\s*(Finish\[[\s\S]*?\]|\w+\[[^\n\]]*\])/i
);
return {
thought: thoughtMatch?.[1]?.trim() || null,
action: actionMatch?.[1]?.trim() || null,
};
}
export function parseAction(actionText: string): ParsedAction | null {
const match = actionText.match(/(\w+)\[([^\n]*)\]/);
if (!match) {
return null;
}
return {
toolName: match[1],
toolInput: match[2].trim(),
};
}
export function isFinishAction(action: string): boolean {
return action.startsWith("Finish");
}
6.1 小白最容易忽略的一点
很多人第一次做 Agent 会以为:
"LLM 都能输出了,我直接把字符串拿来用不就好了?"
但其实不行。因为你的程序必须知道:
- 当前输出是不是合法的
Thought - 当前输出是不是合法的
Action - 这轮到底该结束还是继续
所以解析器不是"锦上添花",而是 ReAct 的关键基础设施。
第七章:实现工具系统
ReAct 的第二个核心,就是工具。
创建 src/react/tools.ts:
typescript
// src/react/tools.ts
export interface Tool {
name: string;
description: string;
execute: (input: string) => Promise<string>;
}
export class ToolRegistry {
private tools: Map<string, Tool> = new Map();
register(tool: Tool): void {
this.tools.set(tool.name, tool);
console.log(`✅ 工具 '${tool.name}' 已注册`);
}
getToolsDescription(): string {
const descriptions: string[] = [];
this.tools.forEach((tool) => {
descriptions.push(`- ${tool.name}: ${tool.description}`);
});
return descriptions.join("\n") || "暂无可用工具";
}
async execute(toolName: string, input: string): Promise<string> {
const tool = this.tools.get(toolName);
if (!tool) {
return `错误:未找到工具 '${toolName}'`;
}
try {
return await tool.execute(input);
} catch (error) {
return `工具执行失败:${error instanceof Error ? error.message : String(error)}`;
}
}
hasTool(name: string): boolean {
return this.tools.has(name);
}
}
7.1 为什么这里不用复杂设计?
因为我们现在是教学版实现,目标是先把 ReAct 跑起来。这个版本已经足够表达 3 件重要的事情:
- 工具有名字和描述
- 模型需要先知道有哪些工具
- 程序要能根据工具名真正去执行
第八章:实现 ReActAgent 主循环
现在终于来到核心部分:ReActAgent 本体。
创建 src/react/agent.ts:
typescript
// src/react/agent.ts
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
import { HelloAgentsLLM } from "../shared/llm";
import { ToolRegistry } from "./tools";
import { parseOutput, parseAction, isFinishAction } from "./parser";
import { REACT_PROMPT_TEMPLATE } from "./prompt";
export interface ReActAgentConfig {
name: string;
maxSteps: number;
}
export class ReActAgent {
private readonly llm: HelloAgentsLLM;
private readonly tools: ToolRegistry;
private readonly config: ReActAgentConfig;
private history: string[] = [];
constructor(
llm: HelloAgentsLLM,
tools: ToolRegistry,
config: Partial<ReActAgentConfig> = {}
) {
this.llm = llm;
this.tools = tools;
this.config = {
name: "ReActAgent",
maxSteps: 5,
...config,
};
}
async run(question: string): Promise<string> {
this.history = [];
console.log(`\n🤖 ${this.config.name} 开始处理问题: ${question}\n`);
for (let step = 0; step < this.config.maxSteps; step++) {
console.log(`--- 第 ${step + 1}/${this.config.maxSteps} 步 ---`);
const prompt = this.buildPrompt(question);
const messages: ChatCompletionMessageParam[] = [
{ role: "user", content: prompt },
];
const response = await this.llm.think(messages, 0);
if (!response) {
console.log("❌ LLM 未返回有效响应");
break;
}
const { thought, action } = parseOutput(response);
if (thought) {
console.log(`💭 Thought: ${thought}`);
}
if (!action) {
console.log("❌ 无法解析 Action");
break;
}
console.log(`🎬 Action: ${action}`);
if (isFinishAction(action)) {
const parsed = parseAction(action);
const answer = parsed?.toolInput || response;
console.log(`\n✅ 任务完成,最终答案: ${answer}`);
return answer;
}
const parsedAction = parseAction(action);
if (!parsedAction) {
console.log("❌ 无法解析工具调用");
break;
}
const observation = await this.tools.execute(
parsedAction.toolName,
parsedAction.toolInput
);
console.log(`👁️ Observation: ${observation}`);
this.history.push(`Thought: ${thought || ""}`);
this.history.push(`Action: ${action}`);
this.history.push(`Observation: ${observation}`);
}
return "抱歉,我无法在限定步数内完成这个任务。";
}
private buildPrompt(question: string): string {
const historyStr = this.history.length > 0
? this.history.join("\n")
: "无";
return REACT_PROMPT_TEMPLATE
.replace("{tools}", this.tools.getToolsDescription())
.replace("{question}", question)
.replace("{history}", historyStr);
}
getHistory(): string[] {
return [...this.history];
}
}
8.1 这段代码到底在做什么?
你可以把它理解成一个严格循环:
- 构建 Prompt
- 调用 LLM
- 解析
Thought和Action - 如果是
Finish,直接结束 - 如果是工具调用,就执行工具
- 把
Observation写进历史 - 进入下一轮
这就是 ReAct 的本质。
8.2 为什么一定要有 maxSteps?
为了防止死循环。
如果模型:
- 一直不 Finish
- 总是重复调用同一个工具
- 一直拿不到足够信息
那系统不能无限跑下去。 maxSteps 就是最基础的安全阀。
第九章:写两个最简单的工具
为了让读者不依赖额外服务,我们先写两个能本地跑的工具:
CalculatorMockSearch
直接把它们写在 src/index.ts 里也可以。
typescript
// src/index.ts
import "dotenv/config";
import { HelloAgentsLLM } from "./shared/llm";
import { ReActAgent } from "./react/agent";
import { ToolRegistry, Tool } from "./react/tools";
class CalculatorTool implements Tool {
name = "Calculator";
description = "数学计算工具,支持基本表达式计算";
async execute(expression: string): Promise<string> {
try {
const sanitized = expression.replace(/[^0-9+\-*/().]/g, "");
const result = Function(`"use strict"; return (${sanitized})`)();
return `计算结果:${result}`;
} catch (error) {
return `计算错误:${error instanceof Error ? error.message : String(error)}`;
}
}
}
class MockSearchTool implements Tool {
name = "Search";
description = "模拟搜索工具,用于演示查询实时信息";
async execute(query: string): Promise<string> {
const knowledgeBase: Record<string, string> = {
"华为最新发布手机型号": "华为最新发布的是 Mate 70 系列。",
"华为 Mate 70 售价": "Mate 70 起售价 5499 元,Mate 70 Pro 起售价 6499 元。",
"北京今天天气": "北京今天晴,最高温 24 度,最低温 12 度。",
};
for (const key of Object.keys(knowledgeBase)) {
if (query.includes(key)) {
return knowledgeBase[key];
}
}
return `没有找到与"${query}"相关的信息。`;
}
}
async function main() {
const llm = new HelloAgentsLLM();
const tools = new ToolRegistry();
tools.register(new CalculatorTool());
tools.register(new MockSearchTool());
const agent = new ReActAgent(llm, tools, {
name: "ReAct 演示助手",
maxSteps: 5,
});
const questions = [
"计算 (15 + 27) * 3 - 8 的结果",
"华为最新发布的手机是什么型号?售价多少?",
];
for (const question of questions) {
console.log("=".repeat(60));
const answer = await agent.run(question);
console.log(`📝 最终回答: ${answer}\n`);
}
}
main().catch(console.error);
9.1 为什么先用 MockSearch?
因为如果一上来就接真实搜索 API,新手很容易被:
- API Key
- 网络问题
- 第三方 SDK
- 返回格式解析
这些问题打断学习节奏。
先用本地模拟工具,更容易把注意力放在 ReAct 主循环本身。
等跑通之后,再升级成真实搜索工具就很自然了。
第十章:如果你想换成真实搜索工具,可以怎么做?
当你本地跑通之后,可以把 MockSearchTool 换成真实搜索工具。
例如:
typescript
class SearchTool implements Tool {
name = "Search";
description = "网页搜索引擎,用于查询实时信息、新闻、事实等";
async execute(query: string): Promise<string> {
const { getJson } = await import("serpapi");
const results = await getJson({
engine: "google",
q: query,
api_key: process.env.SERPAPI_API_KEY,
hl: "zh-cn",
gl: "cn",
});
if (results.answer_box?.answer) {
return results.answer_box.answer;
}
if (results.organic_results?.length > 0) {
return results.organic_results
.slice(0, 3)
.map((r: any, i: number) => `[${i + 1}] ${r.title}\n${r.snippet}`)
.join("\n\n");
}
return `未找到关于 '${query}' 的相关信息`;
}
}
然后在 .env 里增加:
bash
SERPAPI_API_KEY="你的 serpapi key"
这时候你的 ReActAgent 就从"教学版"变成了"可联网版"。
第十一章:完整运行流程长什么样?
你执行:
bash
npm run dev
终端里会看到类似过程:
text
--- 第 1/5 步 ---
Thought: 我需要先计算表达式
Action: Calculator[(15 + 27) * 3 - 8]
Observation: 计算结果:118
--- 第 2/5 步 ---
Thought: 已经拿到结果,可以直接回答
Action: Finish[(15 + 27) * 3 - 8 的结果是 118]
这时候你就已经真正跑通了一个最小可用的 ReActAgent。
总结
这一篇真正重要的,不只是"ReAct 有个 Thought-Action-Observation 循环",而是你要理解它把 Agent 往前推进了一步:
它让智能体从"只会回答问题",变成"会先想一下,再做一下,再根据结果继续想"的系统。
你现在应该已经掌握这些关键点:
- ReAct 解决的是纯 LLM 无法可靠获取外部信息和执行动作的问题。
- 它的核心机制是
Thought -> Action -> Observation。 - 一个从零实现的 ReActAgent 至少需要
llm.ts、Prompt、解析器、工具系统和主循环。 - 你现在可以自己从零搭一个最小 ReActAgent。
如果说第一篇让我们理解了什么是 Agent,那么这一篇就是第一次真正让 Agent "动起来"。
下篇预告
ReAct 已经让智能体具备了"边想边做"的能力。但它还有一个明显问题:
它通常是"做完就结束",不太会回头检查自己做得好不好。
所以下一篇,我们会进入另一个非常重要的范式:ReflectionAgent。
也就是让智能体学会:
到那时,Agent 就不只是"能做事",而是开始具备"自我改进"的能力。