三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从硬编码到智能体:ReAct Agent Loop架构重构实战指南

从硬编码到智能体:ReAct Agent Loop架构重构实战指南

1. 项目概述:从“死”代码到“活”智能的进化

如果你正在开发一个AI对话应用,并且已经实现了基础的聊天功能,那么你很可能正面临一个关键的架构瓶颈:工具调用。早期的实现,往往简单粗暴——在代码里写死一堆if-else或者switch-case语句,根据用户输入的特定关键词,去匹配并执行对应的函数。比如,用户说“查一下北京的天气”,你的后端代码里可能就有一个if (msg.includes('天气')) { callWeatherAPI('北京') }。这种做法,我们称之为“硬编码路由”。在功能简单、场景固定的初期,它确实能跑起来,但稍微复杂一点,比如用户说“明天上海会不会下雨?”或者“帮我看看纽约和伦敦的天气对比”,这套系统就立刻捉襟见肘,代码会迅速膨胀成一团难以维护的“意大利面条”。

这正是“从硬编码路由到 ReAct Agent Loop”这个重构主题的核心。它描述的是一次根本性的范式升级:将AI从一个被动的、按固定路径执行的“命令响应器”,转变为一个主动的、能思考、会使用工具的“智能体”。ReAct(Reasoning + Acting)和Agent Loop(智能体循环)是当前构建实用AI应用最炙手可热的设计模式。简单来说,它让AI拥有了“大脑”和“手”。大脑负责理解用户意图、规划步骤(Reasoning),手则负责调用外部工具、API或函数来执行动作(Acting),并根据执行结果再次思考,形成一个“思考-行动-观察”的闭环,直到完成任务。

这次重构的价值,远不止于代码变得“优雅”。它意味着你的AI应用获得了真正的“泛化能力”。用户不再需要记忆固定的命令格式,可以用自然、多变的方式表达需求;你可以轻松地接入新的工具(如查股票、订机票、控制智能家居),而无需大规模修改核心对话逻辑;系统的可维护性和可扩展性将得到质的提升。接下来,我将以一个资深全栈开发者的视角,带你完整走一遍这次重构的实战路径,涵盖设计思路、核心实现、避坑经验,以及如何应对那些面试官最爱问的“React面试题”式深度问题。

2. 核心架构解析:为何ReAct是破局关键

2.1 硬编码路由的“七宗罪”

在深入新架构之前,我们必须彻底诊断旧方案的病症。硬编码路由的缺陷是系统性的:

  1. 意图识别脆弱:完全依赖关键词匹配,无法处理同义表达、省略和复杂句式。“播放周杰伦的晴天”、“我想听晴天这首歌,周杰伦唱的”、“来点周杰伦的晴天下雨”对于AI来说可能是完全不同的句子,需要写无数个includes来覆盖,漏网之鱼极多。
  2. 逻辑与执行强耦合:业务逻辑(判断要做什么)和工具执行(具体怎么做)死死绑在一起。想增加一个“播放MV”的功能,你不仅要加新的工具函数,还得在那一大坨路由逻辑里插入新的判断分支,违反了单一职责原则。
  3. 上下文感知为零:无法进行多轮对话和基于历史上下文的决策。用户问“那首歌的歌手是谁?”,硬编码系统根本不知道“那首歌”指代的是什么,因为它没有维持对话状态和上下文关联的能力。
  4. 扩展性是灾难:每增加一个新工具或API,路由判断的复杂度几乎呈指数增长。想象一下有20个工具,用户输入一句话,你要按什么顺序、用什么规则去匹配?代码最终会变成无人敢动的“祖传屎山”。
  5. 错误处理僵化:工具调用失败后的回退、重试或替代方案,很难在分散的if-else中优雅实现。通常只能返回一个笼统的“出错了”。
  6. 无法处理复合请求:对于“查一下天气然后推荐附近的餐厅”这类需要多个工具顺序执行的请求,硬编码方案几乎无法实现,除非你为每一种可能的组合都预先写好代码。
  7. 开发和调试效率低下:任何改动都可能引发意想不到的副作用,测试用例难以编写,调试时需要追踪散落在各处的逻辑分支。

2.2 ReAct范式:为AI装上“思考-行动”的循环引擎

ReAct范式完美地回应了上述所有痛点。它的核心是一个循环流程:

思考(Reason)-> 行动(Act)-> 观察(Observe)-> 再思考(Reason)...

在这个循环中,AI模型(通常是LLM)扮演“大脑”的角色。它接收用户的请求和当前的上下文(包括历史对话和之前工具执行的结果),然后输出一个结构化的“下一步指令”。这个指令通常包含两部分:一个是“想法”(Thought),用自然语言描述当前的推理和计划;另一个是“动作”(Action),是一个标准化的调用,比如{“tool_name”: “get_weather”, “input”: {“city”: “上海”}}

一个独立的“执行器”(Executor)会解析这个动作,调用对应的工具函数,获取结果(Observation),然后将这个结果连同之前的“想法”一起,作为新的上下文喂回给AI“大脑”,开启下一轮循环。直到AI认为任务完成,输出最终的答案(Final Answer)。

这种架构带来了革命性的优势:

  • 意图理解泛化:将意图识别的重任交给了LLM。LLM凭借其强大的自然语言理解能力,可以从千变万化的用户表达中,精准抽取出需要调用哪个工具、以及传入什么参数。你不再需要写任何规则。
  • 解耦与清晰:工具变成了独立的、可插拔的模块。它们只需要按照统一的接口(函数名、参数格式)实现,并在一个“工具清单”中注册。AI大脑负责调度,执行器负责调用,职责清晰。
  • 上下文感知与状态维持:整个ReAct循环的过程(Thought, Action, Observation)都被记录在会话上下文中。这使得AI能记住之前做了什么、结果如何,从而进行连贯的多轮对话和复杂规划。
  • 强大的可扩展性:新增一个工具,只需要实现函数并在清单中注册,AI大脑就能在合适的时机学会使用它。无需修改核心路由逻辑。
  • 优雅的错误处理与规划:如果工具调用失败,观察结果会是错误信息,AI大脑可以“思考”这个错误,决定重试、换一种方式或向用户澄清。对于复合任务,AI可以自主规划步骤(“我需要先查天气,再根据天气推荐穿衣”)。

2.3 Agent Loop:从单次调用到持续会话的智能体

ReAct是一个基础模式,而Agent Loop则是将其产品化、工程化的完整架构。一个成熟的Agent Loop系统通常包含以下核心组件:

  1. Orchestrator(编排器):系统的总控中心。它管理用户会话,初始化Agent,并驱动每一次的ReAct循环。它负责维护会话状态、处理超时、管理循环次数以防无限循环。
  2. Agent(智能体):核心决策单元。它封装了LLM和ReAct逻辑。给定一个任务和上下文,它产出Thought和Action。
  3. Tools Registry(工具注册中心):一个集中式的工具目录。每个工具都有清晰的名称、描述、参数Schema(通常用JSON Schema描述)。这个目录会在每次调用时动态提供给LLM,让它知道“手头有哪些工具可用”。
  4. Executor(执行器):负责安全、可靠地执行Agent发出的Action。它会验证参数,调用对应的工具函数,处理异常,并格式化返回结果。
  5. Memory(记忆模块):负责存储和检索会话历史、工具调用结果等。可以是简单的短期会话记忆,也可以是能进行向量检索的长期记忆,用于实现“记住用户偏好”等高级功能。
  6. Prompt Manager(提示词管理器):管理给LLM的系统提示词(System Prompt)。这部分至关重要,它定义了Agent的角色、目标、思考格式以及工具使用的规则。一个精心设计的提示词是Agent表现好坏的决定性因素之一。

3. 重构实战:一步步构建你的ReAct Agent系统

理论讲完,我们进入实战。假设我们有一个简单的AI聊天后端,目前硬编码了get_weather(查天气)和search_web(搜索网页)两个功能。现在我们要将其重构为基于ReAct Agent Loop的系统。

3.1 第一步:定义清晰统一的工具接口

工具接口是系统解耦的基石。我们首先定义每个工具的标准格式。

# 工具基础类 class BaseTool: name: str # 工具唯一名称,如 “get_weather” description: str # 给LLM看的工具描述,至关重要 parameters_schema: dict # JSON Schema,定义输入参数 async def execute(self, input_args: dict) -> str: """执行工具,返回结果字符串""" raise NotImplementedError # 具体工具实现示例:天气查询 class WeatherTool(BaseTool): def __init__(self): self.name = “get_weather” self.description = “获取指定城市的当前天气情况。如果用户未明确城市,需要主动询问。” self.parameters_schema = { “type”: “object”, “properties”: { “city”: {“type”: “string”, “description”: “城市名称,如‘北京’、‘New York’”} }, “required”: [“city”] } self.api_key = os.getenv(“WEATHER_API_KEY”) async def execute(self, input_args: dict) -> str: city = input_args.get(“city”) if not city: return “错误:缺少必要参数‘city’。” # 调用真实天气API try: # 这里模拟API调用 # async with aiohttp.ClientSession() as session: ... return f“{city}的天气是晴天,25摄氏度。” except Exception as e: return f“调用天气API失败:{str(e)}”

关键点

  • description字段要写得详细、准确,这是LLM理解工具用途的唯一依据。好的描述应包含用途、输入要求和输出示例。
  • parameters_schema使用JSON Schema,它能被LLM很好地理解,也能用于执行前的前置验证。
  • execute方法返回字符串,这个字符串会成为Observation,被反馈给LLM。因此,结果要信息丰富、格式清晰。

3.2 第二步:构建工具注册与执行中心

我们需要一个中心化的地方来管理所有工具。

class ToolRegistry: def __init__(self): self._tools: Dict[str, BaseTool] = {} def register(self, tool: BaseTool): if tool.name in self._tools: raise ValueError(f“工具 {tool.name} 已注册”) self._tools[tool.name] = tool def get_tool(self, name: str) -> Optional[BaseTool]: return self._tools.get(name) def get_tools_description_for_llm(self) -> str: """生成供LLM使用的工具描述文本""" descriptions = [] for name, tool in self._tools.items(): desc = f“- {name}: {tool.description} 参数格式:{json.dumps(tool.parameters_schema, ensure_ascii=False)}” descriptions.append(desc) return “\n”.join(descriptions) class ToolExecutor: def __init__(self, registry: ToolRegistry): self.registry = registry async def execute(self, action_name: str, action_input: dict) -> str: tool = self.registry.get_tool(action_name) if not tool: return f“错误:未知工具 ‘{action_name}’。可用工具有:{list(self.registry._tools.keys())}” # 可选:根据schema验证action_input # validate_schema(action_input, tool.parameters_schema) try: result = await tool.execute(action_input) return result except Exception as e: return f“执行工具‘{action_name}’时发生内部错误:{str(e)}”

关键点

  • ToolRegistry是工具目录,方便动态增删工具。
  • get_tools_description_for_llm方法生成的文本,将被拼接到给LLM的系统提示词中,这是实现工具调用的“魔法”所在。
  • ToolExecutor负责实际的调用和错误处理,将底层异常转化为给LLM的友好观察信息。

3.3 第三步:设计Agent核心与ReAct提示工程

这是最核心也最需要技巧的部分。我们需要设计一个提示词模板,引导LLM按照ReAct格式进行思考。

# 系统提示词模板 REACT_SYSTEM_PROMPT_TEMPLATE = “”” 你是一个专业的AI助手,可以使用工具来帮助用户解决问题。 你必须遵循以下格式进行思考: 当前对话历史: {history} 当前目标:{user_input} 你可以使用的工具: {tools_description} 你必须按以下格式回应: Thought: 在这里分析用户目标,决定是否需要使用工具,以及使用哪个工具。 Action: 如果需要工具,则输出工具名称和输入参数。格式必须是严格的JSON:{{“tool”: “工具名”, “input”: {{“参数1”: “值1”, …}}}} Final Answer: 如果不需要工具或任务已完成,则直接输出最终答案给用户。 注意: 1. Action和Final Answer二选一。 2. Action中的JSON必须严格符合工具的参数格式。 3. 如果工具返回的结果不完整或需要进一步处理,继续思考。 “”” class ReActAgent: def __init__(self, llm_client, tool_registry: ToolRegistry): self.llm = llm_client # 例如OpenAI, Anthropic, 或本地LLM的客户端 self.tool_registry = tool_registry async def run_step(self, user_input: str, conversation_history: list) -> dict: # 1. 构建提示词 tools_desc = self.tool_registry.get_tools_description_for_llm() prompt = REACT_SYSTEM_PROMPT_TEMPLATE.format( history=conversation_history, user_input=user_input, tools_description=tools_desc ) # 2. 调用LLM llm_response = await self.llm.chat_completion( messages=[{“role”: “system”, “content”: prompt}], temperature=0.1 # 低温度保证输出格式稳定 ) content = llm_response[“choices”][0][“message”][“content”] # 3. 解析LLM响应 result = {“thought”: “”, “action”: None, “final_answer”: None} lines = content.strip().split(‘\n’) for line in lines: if line.startswith(‘Thought:’): result[“thought”] = line.replace(‘Thought:’, ‘’).strip() elif line.startswith(‘Action:’): action_str = line.replace(‘Action:’, ‘’).strip() try: result[“action”] = json.loads(action_str) except json.JSONDecodeError: result[“action”] = {“error”: f“无法解析Action JSON: {action_str}”} elif line.startswith(‘Final Answer:’): result[“final_answer”] = line.replace(‘Final Answer:’, ‘’).strip() return result

提示工程要点

  • 格式强制:在提示词中明确要求Thought:Action:Final Answer:的格式,并强调JSON的严格性。这能极大提高LLM输出的结构化程度。
  • 提供上下文:将对话历史{history}和工具描述{tools_description}动态注入提示词。
  • 低温度(Temperature):在Agent推理步骤使用较低的温度值(如0.1),以获得更确定、更符合格式的输出。
  • 错误处理:在解析LLM响应时,要做好JSON解析失败的异常处理,防止格式错误导致系统崩溃。

3.4 第四步:实现编排器与主循环

最后,我们需要一个“导演”来串联整个流程。

class AgentOrchestrator: def __init__(self, agent: ReActAgent, executor: ToolExecutor): self.agent = agent self.executor = executor self.max_iterations = 10 # 防止无限循环 async def run(self, user_input: str, session_memory: list) -> dict: """运行一次完整的Agent交互,可能包含多轮ReAct循环""" current_history = session_memory.copy() full_trace = [] # 记录完整的思考-行动轨迹,用于调试 for i in range(self.max_iterations): # 1. Agent思考并决定行动 step_result = await self.agent.run_step(user_input, current_history) full_trace.append({“iteration”: i, “step”: step_result}) # 2. 检查是否已有最终答案 if step_result[“final_answer”] is not None: current_history.append({“role”: “assistant”, “content”: step_result[“final_answer”]}) return { “final_response”: step_result[“final_answer”], “trace”: full_trace, “updated_memory”: current_history } # 3. 执行工具调用 if step_result[“action”] and “tool” in step_result[“action”]: action = step_result[“action”] tool_name = action[“tool”] tool_input = action.get(“input”, {}) observation = await self.executor.execute(tool_name, tool_input) # 将本次思考、行动、观察加入历史,供下一轮参考 current_history.append({ “role”: “assistant”, “content”: f“Thought: {step_result[‘thought’]}\nAction: {json.dumps(action)}” }) current_history.append({ “role”: “user”, # 将观察视为“环境”的反馈 “content”: f“Observation: {observation}” }) full_trace[-1][“observation”] = observation else: # 如果没有Action也没有Final Answer,可能是LLM格式错误,中断循环 error_msg = “Agent未输出有效的Action或Final Answer。” current_history.append({“role”: “assistant”, “content”: error_msg}) return { “final_response”: “系统处理出现异常,请稍后再试。”, “trace”: full_trace, “updated_memory”: current_history } # 循环超过最大次数 return { “final_response”: “任务处理超时,可能过于复杂。”, “trace”: full_trace, “updated_memory”: current_history }

编排器核心逻辑

  • 循环控制:设置max_iterations是必须的,防止Agent陷入死循环。
  • 历史管理:巧妙地将Thought/ActionObservation以特定格式加入对话历史,模拟了ReAct论文中的“轨迹”,让LLM在下一轮能基于完整上下文进行推理。
  • 轨迹记录full_trace对于调试和优化Agent行为至关重要,你可以看到AI每一步的“心理活动”和行动结果。

4. 高级优化与生产级考量

基础框架搭建完成后,要投入生产环境,还需要解决一系列工程挑战。

4.1 工具描述的优化艺术

工具描述的质量直接决定LLM能否正确调用。差的描述会导致LLM不理解、用错工具或参数。

  • 反面例子“search: 搜索工具”。这几乎没用。
  • 正面例子“web_search: 使用搜索引擎获取最新的网络信息。当用户询问实时信息、新闻、未知知识或需要最新资料时使用此工具。输入参数为一个JSON对象,包含必填的‘query’字段(搜索关键词字符串)。例如,对于‘今天有什么科技新闻’,输入应为 {‘query’: ‘科技新闻 今日’}。”
  • 技巧:在描述中说明使用场景输入输出示例,甚至常见错误(如“城市名需为中文”)。

4.2 处理复杂请求与规划能力

简单的ReAct能处理单步工具调用。但对于“查天气并推荐穿搭”这类多步任务,需要增强Agent的规划能力。

  • 子目标分解:在提示词中鼓励LLM进行任务分解。例如,在系统提示中加入:“对于复杂任务,你可以将其分解为多个子目标,并一步步完成。在Thought部分,清晰地列出你的计划。”
  • 使用更强大的模型:GPT-4、Claude-3等模型在复杂规划和逻辑推理上远强于GPT-3.5。对于复杂Agent,模型能力是瓶颈。
  • 引入规划专用工具:可以创建一个plan_task的虚拟工具,让LLM先输出一个完整的步骤计划(JSON格式),再由编排器按计划逐步执行。这相当于将“规划”和“执行”分离。

4.3 记忆与上下文管理

当对话轮次变多,上下文长度会爆炸,需要智能管理。

  • 摘要式记忆:在对话轮次达到一定数量后,用一个单独的LLM调用对之前的对话历史进行总结,将冗长的历史压缩成一段摘要,作为新的“背景”放入后续上下文。这能有效节省Token,并保留核心信息。
  • 向量检索记忆:将历史对话中的关键信息(如用户偏好、事实信息)存入向量数据库。当用户提到相关话题时,通过检索召回这些信息注入上下文。这实现了“长期记忆”。
  • 结构化会话状态:除了自然语言历史,维护一个结构化的会话状态对象。例如,{“current_topic”: “旅游”, “discussed_cities”: [“北京”, “上海”]}。这个状态可以被工具读取和修改,也为Agent提供了更清晰的上下文。

4.4 稳定性与监控

  • 结构化输出强制(Function Calling):相比于让LLM输出文本我们再解析JSON,更可靠的方式是使用LLM原生的“函数调用”(OpenAI)或“工具调用”(Anthropic)功能。这本质上是让LLM直接输出结构化的数据,格式错误率极低。我们的ReActAgent.run_step可以重构为利用此功能。
  • 超时与重试:对LLM调用和工具调用都要设置超时。对于可重试的错误(如网络波动),实现指数退避的重试机制。
  • 全面的日志与追踪:记录每一次LLM请求/响应、工具调用输入/输出、完整的ReAct轨迹。这对于排查诡异问题(如Agent陷入循环)和优化提示词不可或缺。full_trace就是为此而生。
  • 看门狗(Watchdog):监控循环次数、单个工具调用耗时等指标。当指标异常时,主动终止会话并给出友好错误提示。

5. 常见陷阱与实战调试心得

在实际重构和运维中,我踩过不少坑,也积累了一些心得。

5.1 LLM不按格式输出

这是最常见的问题。LLM可能忽略你的格式要求,直接输出自然语言答案。

  • 对策1:强化提示词。在系统提示的开头和结尾都强调格式,使用“你必须”、“严格遵循”等强指令性词语。给出更清晰的示例(Few-shot Prompting)。
  • 对策2:使用更低温度的模型。如前述,降低temperature值。
  • 对策3:后处理与降级。在解析失败时,可以尝试用正则表达式从自然语言回复中提取工具名和参数,或者直接将其作为Final Answer返回给用户,并记录日志告警。这保证了用户体验不中断。
  • 对策4:转向Function Calling。这是终极解决方案,能从根本上杜绝格式问题。

5.2 Agent陷入无效循环

Agent可能在一个步骤里来回摇摆,或者反复调用同一个工具得不到进展。

  • 对策1:在上下文中加入“禁止循环”指令。例如:“注意,你已经使用过X工具并得到了Y结果,请不要再重复相同的操作。”
  • 对策2:在编排器中检测循环。检查最近N步的Action历史,如果出现重复模式,则中断循环,并让LLM反思问题所在或直接向用户求助。
  • 对策3:优化工具描述和结果。有时候循环是因为工具返回的结果模糊或错误,导致LLM无法做出正确判断。确保工具返回的信息明确、可操作。

5.3 工具调用安全与成本

工具可能执行写数据库、发邮件、支付等危险或高成本操作。

  • 对策1:权限分级。为工具标注风险等级(如read_only,write_low,write_high)。在编排器层面,根据用户会话的权限级别,动态过滤可用的工具清单。
  • 对策2:用户确认。对于高风险操作,设计一个confirm_action工具。当Agent尝试调用高风险工具时,先调用此工具,由它向用户界面发送一个确认请求,待用户确认后,再执行原操作。
  • 对策3:设置预算与限额。对调用外部API的工具(如生成图片、深度搜索),设置单次会话或单用户的调用次数/成本上限。

5.4 性能与延迟

ReAct涉及多轮LLM调用,延迟可能比直接聊天高一个数量级。

  • 对策1:流式输出(Streaming)。在Agent思考(Thought)时,就可以将“思考中…”的信息流式返回给前端,提升用户体验感知。
  • 对策2:缓存。对常见、结果变化不频繁的工具调用(如“北京天气”),可以将结果缓存一段时间。甚至可以对LLM在相同上下文下的推理结果进行缓存。
  • 对策3:并行化。如果Agent规划出的多个步骤之间没有依赖关系,可以在安全的前提下尝试并行执行工具调用。

从硬编码路由到ReAct Agent Loop的重构,是一次从“机械执行”到“智能协作”的思维转变。初期投入确实更大,你需要设计架构、编写提示词、处理各种边界情况。但一旦跑通,其带来的灵活性、扩展性和用户体验的提升是革命性的。你的AI应用将真正成为一个能理解、会思考、可行动的智能体。这个架构也是当前通向更复杂AI应用(如AutoGPT、CrewAI等多智能体系统)的必经之路。在实际操作中,建议从一个核心工具开始,逐步迭代,不断完善提示词和工具生态,最终你会拥有一套强大而优雅的AI能力中台。

← 返回列表