1. 项目概述为什么需要实现 ReActAgent 的循环如果你正在构建一个 AI 应用尤其是涉及到需要与外部工具、API 或环境交互的智能体Agent那么“思考-行动”循环就是你绕不开的核心机制。ReActReasoning Acting范式正是为了解决大模型在复杂任务中“拍脑袋”决策、缺乏规划与纠错能力而生的。简单来说它让 AI 像人一样先动脑思考Reasoning再动手执行Acting并根据执行结果调整下一步计划。在 SDK 开发的语境下实现一个 ReActAgent 意味着你将提供一个标准化的、可复用的“智能体引擎”。开发者只需注入一个大语言模型LLM和一组工具Tools就能立刻获得一个能自主拆解任务、调用工具、并从反馈中学习的智能体。这远比让开发者从零开始处理提示词工程、工具调用解析、状态管理要高效得多。本次我们就用 TypeScript 来从零构建这个核心循环你会看到如何将理论转化为一行行健壮、可测试的代码。2. 核心架构设计拆解 ReActAgent 的三大支柱一个完整的 ReActAgent 实现远不止一个while循环。我们需要一个清晰、解耦的架构来保证其扩展性和可维护性。核心可以抽象为三个支柱推理器Reasoning Engine、执行器Acting Engine和状态管理器State Manager。2.1 支柱一推理器 - 大脑的思考过程推理器的核心职责是分析当前状态包括任务目标、历史记录、可用工具并生成下一步的“思考”和“行动指令”。这通常通过精心设计的提示词Prompt和大语言模型LLM的补全Completion来实现。关键设计点提示词模板我们需要一个结构化的模板引导模型按照“Thought: ... Action: ... Action Input: ...”的格式输出。模板中需要动态插入可用工具列表名称、描述、参数、任务目标、之前的交互历史。输出解析LLM 的输出是自由文本我们必须将其解析为结构化的数据。这里强烈推荐使用像zod这样的模式验证库来定义并校验输出格式确保后续流程的稳定性。上下文管理如何将冗长的对话历史有效地喂给模型我们需要设计截断或总结策略防止超出模型的上下文窗口。注意不要试图让 LLM 一次性输出完美的 JSON。ReAct 的精髓在于让模型用自然语言“自言自语”地推理我们再从固定的格式中提取结构化指令。这比直接让模型输出 JSON 指令更稳定也更能激发模型的推理能力。2.2 支柱二执行器 - 身体的行动机制执行器负责将推理器生成的“行动指令”转化为实际的操作。这主要涉及工具Tool的查找与调用。关键设计点工具抽象每个工具应该是一个统一的接口例如包含name、description、schema参数 JSON Schema和execute方法。这样执行器可以统一管理和调用。动态工具注册Agent 应该支持在运行时动态地添加或移除工具以适应不同的任务场景。安全与沙箱对于执行可能具有副作用如写文件、调用网络 API的工具需要考虑安全机制比如参数校验、权限控制甚至在独立环境中运行。2.3 支柱三状态管理器 - 记忆与进程记录状态管理器维护着 Agent 执行任务的生命周期状态。它需要记录完整的“思考-行动-观察”循环历史并决定当前循环的输入是什么。关键设计点状态结构一个典型的状态对象应包含objective最终目标history历史记录数组每项包含 thought, action, actionInput, observation以及可能的intermediateStep当前步骤的临时数据。历史压缩随着任务进行历史会越来越长。状态管理器需要具备压缩能力例如只保留最近 N 条记录或将早期的详细记录总结成一句话以节省上下文空间。终止条件判断状态管理器需要能判断任务是否完成如模型输出了Final Answer:或是否陷入死循环如重复动作超过 N 次并触发循环结束。3. 分步实现从接口定义到完整循环有了架构蓝图我们开始动手编码。我们将采用自底向上的方式先定义核心类型和接口。3.1 步骤一定义核心类型与接口首先我们用 TypeScript 定义所有核心数据的形状。这能极大提升代码的可靠性和开发体验。// 定义工具接口。一个工具就像 Agent 可以使用的“函数”。 export interface Tool { name: string; description: string; // 用于提示词告诉模型这个工具是干嘛的 schema: z.ZodObjectany; // 使用 zod 定义参数模式用于验证和生成 JSON Schema execute: (args: any) Promisestring; // 执行方法返回观察结果字符串 } // 定义单步历史记录的结构 export interface ReActStep { thought: string; // 模型的思考过程 action: string; // 要执行的动作通常是工具名 actionInput: string; // 动作的输入通常是 JSON 字符串 observation: string; // 执行工具后观察到的结果 } // 定义 Agent 的当前状态 export interface ReActAgentState { objective: string; // 用户提出的最终目标 history: ReActStep[]; // 已完成的步骤历史 currentStep?: { // 当前正在进行的步骤可选 thought?: string; action?: string; actionInput?: string; }; } // 定义 LLM 包装器接口用于抽象不同的模型提供商 export interface LLMProvider { generate: (prompt: string) Promisestring; }3.2 步骤二实现推理器Reasoning Engine推理器是大脑。我们创建一个ReasoningEngine类它接收状态和工具列表生成下一步的指令。import { z } from zod; import { LLMProvider, ReActAgentState, Tool } from ./types; // 定义我们期望 LLM 输出的格式 const ReActOutputSchema z.object({ thought: z.string().describe(模型对当前情况和下一步的思考), action: z.string().describe(要执行的动作名称必须是提供的工具之一或是 Final Answer), actionInput: z.string().describe(动作的输入如果是 Final Answer这里就是答案内容) }); export class ReasoningEngine { constructor(private llm: LLMProvider) {} async generateNextStep( state: ReActAgentState, tools: Tool[] ): Promisez.infertypeof ReActOutputSchema { // 1. 构建提示词 const prompt this.buildPrompt(state, tools); // 2. 调用 LLM const rawOutput await this.llm.generate(prompt); // 3. 解析输出 return this.parseOutput(rawOutput); } private buildPrompt(state: ReActAgentState, tools: Tool[]): string { const toolList tools.map(t - ${t.name}: ${t.description}).join(\n); const history state.history.map(step Thought: ${step.thought}\nAction: ${step.action}\nAction Input: ${step.actionInput}\nObservation: ${step.observation} ).join(\n\n); return 你是一个智能助手需要通过思考和行动来完成一个任务。 你可以使用以下工具 ${toolList} 当任务完成时请使用 Action: Final Answer 来给出最终答案。 任务目标是${state.objective} ${history ? 以下是已经发生的历史\n${history}\n\n现在请开始你的下一步 : 请开始你的第一步} ; } private parseOutput(rawOutput: string): z.infertypeof ReActOutputSchema { // 简单的正则匹配来提取 Thought, Action, Action Input // 在实际项目中可以使用更稳健的解析器或依赖 LLM 的结构化输出功能如 OpenAI 的 function calling const thoughtMatch rawOutput.match(/Thought:\s*(.*?)(?\nAction:|$)/s); const actionMatch rawOutput.match(/Action:\s*(.*?)(?\nAction Input:|$)/s); const actionInputMatch rawOutput.match(/Action Input:\s*([\s\S]*?)(?\nThought:|\nAction:|$)/s); const result { thought: thoughtMatch ? thoughtMatch[1].trim() : , action: actionMatch ? actionMatch[1].trim() : , actionInput: actionInputMatch ? actionInputInputMatch[1].trim() : , }; // 使用 Zod 进行验证和类型安全转换 return ReActOutputSchema.parse(result); } }实操心得提示词Prompt是 ReAct 的灵魂。buildPrompt方法中的格式和措辞会极大影响 Agent 的表现。多花时间调整提示词比如明确要求模型“逐步思考”或示例一两个历史步骤的格式能显著提升输出的稳定性和质量。另外解析输出是脆弱的环节在生产环境中可以考虑使用支持 JSON 模式或函数调用的模型 API直接从源头获得结构化数据。3.3 步骤三实现执行器Acting Engine执行器是双手。它根据推理器给出的动作指令找到对应的工具并执行。export class ActingEngine { private tools: Mapstring, Tool new Map(); registerTool(tool: Tool) { this.tools.set(tool.name, tool); } async executeAction(action: string, actionInput: string): Promisestring { // 1. 检查是否为最终答案 if (action Final Answer) { return 任务完成。最终答案是${actionInput}; } // 2. 查找工具 const tool this.tools.get(action); if (!tool) { return 错误未知动作 ${action}。可用动作有${Array.from(this.tools.keys()).join(, )}; } // 3. 解析输入通常是 JSON 字符串并验证 let parsedInput: any; try { parsedInput JSON.parse(actionInput); } catch { // 如果解析失败可能输入本身就是普通字符串 parsedInput actionInput; } try { // 使用 Zod Schema 验证输入参数 const validatedInput tool.schema.parse(parsedInput); // 4. 执行工具 const observation await tool.execute(validatedInput); return observation; } catch (error) { if (error instanceof z.ZodError) { return 错误动作输入参数不合法。详情${error.errors.map(e ${e.path}: ${e.message}).join(; )}; } return 错误执行动作时发生异常。${error}; } } }注意事项工具执行是风险最高的部分。务必对actionInput进行严格的验证和清理防止注入攻击。tool.schema.parse()这一步至关重要。此外execute方法应该返回字符串格式的观察结果即使工具本身返回复杂对象也要序列化成描述性文字以便作为下一轮推理的输入。3.4 步骤四实现状态管理器与主循环现在我们将三个支柱组合起来形成完整的 ReAct 主循环。export class ReActAgent { private state: ReActAgentState; private reasoningEngine: ReasoningEngine; private actingEngine: ActingEngine; private maxIterations: number; constructor( objective: string, llm: LLMProvider, tools: Tool[], maxIterations: number 10 ) { this.state { objective, history: [] }; this.reasoningEngine new ReasoningEngine(llm); this.actingEngine new ActingEngine(); this.maxIterations maxIterations; // 注册所有工具 tools.forEach(tool this.actingEngine.registerTool(tool)); } async run(): PromiseReActAgentState { let iteration 0; while (iteration this.maxIterations) { iteration; console.log(\n 迭代第 ${iteration} 步 ); // 1. 推理生成下一步 const nextStep await this.reasoningEngine.generateNextStep(this.state, Array.from(this.actingEngine[tools].values())); // 注意这里访问了私有属性实际应通过getter console.log(思考: ${nextStep.thought}); console.log(行动: ${nextStep.action}); console.log(输入: ${nextStep.actionInput}); // 2. 执行运行动作获取观察结果 const observation await this.actingEngine.executeAction(nextStep.action, nextStep.actionInput); console.log(观察: ${observation}); // 3. 更新状态将本轮步骤加入历史 const step: ReActStep { thought: nextStep.thought, action: nextStep.action, actionInput: nextStep.actionInput, observation: observation }; this.state.history.push(step); // 4. 检查终止条件是否输出了最终答案 if (nextStep.action Final Answer) { console.log(\n✅ 任务完成); break; } // 可选检查是否陷入循环简单示例最近三步动作完全相同 if (this.hasRepeatedCycle(3)) { console.log(\n⚠️ 检测到可能陷入循环提前终止。); this.state.history.push({ thought: 检测到重复循环无法推进任务。, action: Final Answer, actionInput: 无法完成任务可能由于信息不足或工具限制。, observation: }); break; } } if (iteration this.maxIterations) { console.log(\n❌ 达到最大迭代次数${this.maxIterations}任务未完成。); } return this.state; } private hasRepeatedCycle(lookBack: number): boolean { const history this.state.history; if (history.length lookBack) return false; const recentActions history.slice(-lookBack).map(s s.action); return new Set(recentActions).size 1 recentActions[0] ! Final Answer; } }4. 实战演示构建一个查询天气的智能体理论说得再多不如跑个例子。我们来创建一个简单的天气查询 Agent。4.1 创建模拟工具首先我们创建两个工具一个搜索工具模拟网络搜索一个计算器工具。import { z } from zod; // 模拟搜索工具 const searchTool: Tool { name: Search, description: 用于在互联网上搜索信息。输入是一个搜索查询字符串。, schema: z.object({ query: z.string() }), async execute(args) { // 模拟网络延迟 await new Promise(resolve setTimeout(resolve, 100)); // 模拟返回搜索结果 const mockResults: Recordstring, string { 北京今天天气: 北京2023年10月27日晴气温5-15°C西北风2级。, 上海明天天气: 上海2023年10月28日多云转阴气温18-22°C东南风1级。, 圆周率: 圆周率π是一个数学常数约等于3.14159。, }; return mockResults[args.query] || 未找到关于${args.query}的信息。; } }; // 计算器工具 const calculatorTool: Tool { name: Calculator, description: 用于执行简单的数学计算。输入是一个数学表达式字符串如 2 3 * 4。, schema: z.object({ expression: z.string() }), async execute(args) { try { // 警告在生产环境中绝对不要使用 eval这里仅用于演示。 // 应使用安全的数学表达式解析库如 math.js const result Function(use strict; return (${args.expression}))(); return 计算结果${args.expression} ${result}; } catch { return 错误无法计算表达式 ${args.expression}。; } } };4.2 创建模拟 LLM 并运行 Agent为了演示我们创建一个模拟的 LLM它会根据提示词返回预设的 ReAct 格式输出。class MockLLM implements LLMProvider { async generate(prompt: string): Promisestring { // 这是一个非常简化的模拟实际应根据提示词动态生成 console.log(\n--- 模拟LLM接收到的提示词 ---); console.log(prompt.substring(0, 500) ...); // 打印前500字符 console.log(--- 结束 ---\n); // 模拟一个简单的 ReAct 推理过程 if (prompt.includes(北京今天天气)) { return Thought: 用户想知道北京的天气。我需要使用搜索工具来获取最新信息。 Action: Search Action Input: {query: 北京今天天气}; } else if (prompt.includes(气温5-15°C)) { return Thought: 我已经获得了北京的天气信息。用户问的是摄氏度但可能也想了解华氏度。我可以计算一下。 Action: Calculator Action Input: {expression: (5 15) / 2}; } else if (prompt.includes(10)) { return Thought: 平均气温是10°C。现在我可以给出最终答案了。 Action: Final Answer Action Input: 北京今天的天气是晴气温在5到15摄氏度之间平均气温约为10摄氏度。; } // 默认返回一个初始思考 return Thought: 我需要理解用户的任务并开始规划。 Action: Search Action Input: {query: 北京今天天气}; } } // 运行 Agent async function main() { const llm new MockLLM(); const tools [searchTool, calculatorTool]; const agent new ReActAgent( 告诉我北京今天的天气并计算一下平均气温是多少摄氏度。, llm, tools, 5 ); const finalState await agent.run(); console.log(\n 最终执行历史 ); finalState.history.forEach((step, i) { console.log([步骤${i1}] 思考: ${step.thought}); console.log( 行动: ${step.action} - ${step.actionInput}); console.log( 观察: ${step.observation}\n); }); } main();运行这段代码你会看到控制台输出完整的 ReAct 循环过程LLM 如何思考、选择工具、执行、观察并最终给出答案。5. 进阶优化与生产级考量上面的实现是一个教学用的最小可行产品MVP。要用于生产环境还需要考虑很多方面。5.1 性能优化减少 LLM 调用与上下文长度LLM 调用昂贵且慢。优化策略包括思维压缩不是将全部原始历史都塞进提示词。可以只保留最近几步的详细记录将更早的步骤总结成一句话例如“用户最初询问了天气然后我搜索了北京天气得到了气温信息。”流式输出对于需要长时间运行的 Agent可以考虑支持流式输出Thought让用户能实时看到 Agent 的“思考过程”提升体验。缓存对于相同的工具调用如相同的搜索查询可以将结果缓存起来避免重复调用和消耗 Token。5.2 稳定性增强错误处理与重试机制网络会波动工具会出错LLM 会胡言乱语。健壮的 Agent 需要工具调用重试对于因网络问题失败的工具调用可以实现指数退避重试。LLM 输出兜底当parseOutput失败时不应直接崩溃。可以尝试用更宽松的正则再解析一次或者将错误信息作为observation反馈给 LLM让它“纠正”自己的输出格式。超时控制为llm.generate和tool.execute设置超时防止单个步骤卡死整个 Agent。5.3 可观测性与调试当 Agent 行为不符合预期时如何调试详细日志记录每个循环的完整输入提示词和输出LLM 原始响应、解析结果、工具结果。状态快照允许将ReActAgentState序列化保存便于事后分析和复现问题。可视化工具可以考虑开发一个简单的 UI以时间线的方式展示Thought-Action-Observation的循环过程这对理解复杂任务至关重要。5.4 扩展性设计支持更复杂的 Agent 模式ReAct 是基础你可以在此基础上构建更强大的智能体多智能体协作你的ReActAgent实例本身可以作为一个“工具”被另一个“管理者”Agent 调用形成层级结构。集成向量数据库将历史记录或工具文档存入向量数据库让 LLM 在推理时能进行相关记忆检索RAG突破上下文窗口限制。支持规划Plan在循环开始前先让 LLM 生成一个高层次的任务分解计划Plan然后 ReAct 循环负责执行每个子步骤。实现一个生产级的 ReActAgent SDK 是一个持续迭代的过程。从今天这个简单的循环开始逐步加入上述的优化和特性你就能打造出一个真正强大、可靠的智能体基础设施。记住核心永远是那个简单的循环思考、行动、观察、再思考。代码只是让这个循环稳定、高效、可控地运转起来。