源码解读:手写一个最小 ReAct Agent
理解 Agent 最好的方式,就是亲手写一个最小的。本文用一个不到 100 行的 TypeScript 实现,把 ReAct(Thought → Action → Observation 循环)讲透:核心循环、工具调用、停止条件、错误恢复。读完你就知道 LangChain/LangGraph 那些 Agent 框架内部在做什么。
本文要点
- 主题聚焦:AI Agent、TypeScript
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
用框架(LangChain、LangGraph、AI SDK)搭 Agent 很方便,但也容易"会用而不懂"。理解 Agent 最彻底的方式,是自己从零写一个最小的——你会发现 ReAct Agent 的核心,其实就是一个不到 100 行的循环。
这篇带你手写一个,并讲清每一步。
一、ReAct 的本质:一个循环
ReAct(Reason + Act)的核心就这么点东西:
循环 {
向模型发送: system(规则+工具) + 历史(thought/action/observation)
模型返回: 下一步的 thought 和 action(调哪个工具、传什么参数)
如果模型说"完成了" → 结束
否则: 执行 action(工具),把结果作为 observation 加进历史
继续
}
就这。所有 Agent 框架的复杂度,都是在这个循环外面包"状态管理、并行、人在回路、可观测"等工程能力。核心循环本身极简。
二、定义工具
Agent 的能力来自工具。先定义两个最简单的:
const tools = [
{
type: "function" as const,
function: {
name: "search",
description: "Search the web for factual information. Use when you don't know something.",
parameters: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
},
{
type: "function" as const,
function: {
name: "calculate",
description: "Evaluate a math expression, e.g. '12 * (3+4)'. Use for exact arithmetic.",
parameters: {
type: "object",
properties: { expr: { type: "string" } },
required: ["expr"],
},
},
},
];
// 工具的实际实现
const handlers: Record<string, (args: any) => Promise<string>> = {
search: async ({ query }) => `[模拟搜索结果] 关于 "${query}" 的信息...`,
calculate: async ({ expr }) => String(Function(`return (${expr})`)()),
};
三、核心循环(这就是 Agent 本体)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function reactAgent(userMessage: string, maxSteps = 10) {
const messages: any[] = [{ role: "user", content: userMessage }];
for (let step = 0; step < maxSteps; step++) {
// 1) 让模型想下一步
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
system: "你是一个会使用工具的助手。一步步推理。需要信息就 search,需要计算就 calculate。完成就给出最终答案。",
tools,
messages,
});
// 2) 把模型这轮的输出加入历史
messages.push({ role: "assistant", content: response.content });
// 3) 模型没调工具 → 它给出最终答案,结束
if (response.stop_reason === "end_turn") {
const text = response.content.find((c: any) => c.type === "text");
return text?.text ?? "(no answer)";
}
// 4) 模型调了工具 → 执行,把结果作为 observation 回灌
if (response.stop_reason === "tool_use") {
const toolUses = response.content.filter((c: any) => c.type === "tool_use");
const toolResults = [];
for (const tu of toolUses) {
const handler = handlers[tu.name];
const result = handler
? await handler(tu.input).catch((e) => `tool error: ${e.message}`)
: `unknown tool: ${tu.name}`;
toolResults.push({ type: "tool_result", tool_use_id: tu.id, content: result });
}
messages.push({ role: "user", content: toolResults });
}
}
return "(达到最大步数,未完成)";
}
这就是一个能跑的 ReAct Agent,核心不到 50 行。 跑一下:
await reactAgent("2024 年中国 GDP 总量是多少万亿美元?换算成人民币是多少(按 7.2 汇率)?");
// 模型会: search(GDP) → observation → calculate(换算) → observation → 给最终答案
四、看懂这几十行,就懂了所有 Agent 框架
对照这个实现,去理解框架:
- LangGraph 的 StateGraph:把这个循环画成图,nodes 是"模型一步",edge 是循环回来,state 是
messages。 - AI SDK 的
generateText({ tools, maxSteps }):就是把这个循环封装成maxSteps参数。 - 各种 Agent 类:本质都是这个循环 + 不同程度的状态/并行/可观测封装。
框架的价值在外围工程(并行、人在回路、trace、状态持久化),不在核心循环。一旦你看穿"Agent 就是个带工具调用的循环",框架就不再是黑盒,你能在合适的时候绕过框架自己写。
五、必须自己加的工程护栏
这个最小实现是为了讲原理,生产用还差几道关:
- maxSteps(已加):防死循环。没有上限的 Agent 是烧钱炸弹。
- 错误恢复:工具失败时,
catch把可读错误回灌(tool error: ...),模型能自行换路——而不是抛异常中断。 - 工具参数校验:执行前用 schema 校验
tu.input,不合法的回灌错误让模型重试。 - 超时:每个工具调用设超时,别卡死循环。
- trace:每一步的 thought/action/observation 落日志(调试和生产都靠它)。
加上这五样,这个 50 行的 Agent 就具备生产雏形了。
六、从这个起点扩展
理解了核心,向上扩展都是叠加:
- 多工具 / 并行调用:模型一次返回多个 tool_use,循环里并行执行、一起回灌(代码里已经支持)。
- 记忆:在
messages外加长期记忆,每轮检索注入。 - RAG:加一个
retrieve_docs工具,让 Agent 自主决定何时检索。 - 多 Agent:把工具换成"调用另一个 Agent",就是多 Agent 协作。
- Plan-Execute:循环前先让模型出计划,按计划执行,就是 Plan-Execute 范式。
这些扩展,本质上都还是在这个核心循环上加东西。
小结
ReAct Agent 的核心,是一个"模型决定下一步 → 执行工具 → 把结果喂回去"的循环,本体不到 50 行。所有 Agent 框架的复杂度都在外围(状态、并行、可观测、护栏),核心极其简单。
亲手写一遍这个最小实现,是理解 Agent 最快的路径。你会明白:Agent 不神秘,它就是"LLM + 一个循环 + 工具"。带着这个认知去用 LangGraph、AI SDK,你不再是被框架牵着走,而是知道每一步在干什么、什么时候该用框架、什么时候自己写更清爽。
Related