
1. 项目概述从面试题看智能体开发的核心最近在技术社区和面试复盘里经常看到一个高频问题“ReAct框架的核心循环是什么消息格式怎么设计” 这看似是一个具体的面试题实则精准地切入了当前AI应用开发特别是智能体Agent构建领域最核心的实践难题。无论是想深入理解ReAct原理的开发者还是正在设计自己Agent系统的架构师这个问题都像一把钥匙能打开通往构建可靠、可解释AI工作流的大门。ReActReasoning Acting不是一个具体的软件包而是一种让大语言模型LLM与外部工具和环境进行交互的范式框架。它的核心价值在于将模型的“思考”推理和“行动”执行解耦并形成闭环从而解决传统提示工程中模型容易“胡思乱想”或“无法执行”的痛点。当你被问到“核心循环”和“消息格式”时面试官真正想考察的是你是否理解一个自治智能系统如何持续、稳定地运转以及如何设计通信协议来保证这种运转的可靠性。这背后涉及思维链CoT、工具调用Tool Calling、智能体状态机等一连串的关键概念。接下来我将以一个多年全栈开发和AI应用架构者的视角彻底拆解这个问题。我不会只给你教科书式的定义而是结合真实的开发场景、常见的坑以及我们团队在构建商业Agent时的设计取舍把ReAct框架里里外外讲透。无论你是正在准备面试还是打算亲手实现一个ReAct智能体这篇文章都能提供从理论到实操的完整路径。2. ReAct框架的核心循环不只是“思考-行动”那么简单很多人对ReAct循环的第一印象是“Think - Act - Observe”的简单重复。这个理解没错但过于简化在实际工程中会处处碰壁。一个健壮的ReAct核心循环更像一个精心设计的状态机它需要处理异常、管理上下文、并决定何时终止。2.1 标准循环的深度拆解一个完整的ReAct迭代步骤通常包含以下四个阶段而不仅仅是两个阶段一推理Reasoning这是循环的起点。模型基于当前的任务描述、历史交互记录包括之前的思考、行动和观察结果以及可用的工具列表进行下一步的“思考”。这个思考的输出不是直接的动作而是一个推理轨迹。例如“用户想查询北京明天的天气。我需要一个能查询天气的工具。我手头有‘get_weather’工具。因此我下一步应该调用这个工具。”关键点推理步骤必须“自包含”。即使脱离后续的观察这段文字本身也能让人理解智能体为什么做出这个决定。这是实现可解释性的基础。阶段二行动Acting根据推理步骤的结论智能体格式化一个具体的动作请求。这通常是一个结构化数据比如{ action: get_weather, action_input: {city: 北京, date: 2023-10-27} }这个阶段的核心是工具调用。智能体需要从注册的工具库中精准选择匹配的工具并生成符合该工具参数要求的输入。阶段三观察Observing将行动指令发送给对应的工具执行器Tool Executor后获取执行结果。这个结果可能是成功的输出如“北京明天晴15-22℃”也可能是明确的错误如“工具调用超时”、“参数‘date’格式无效”。观察结果必须被原样或经过适当格式化后反馈给智能体。阶段四整合与判断Integrate Judge这是最容易被忽略但至关重要的环节。智能体接收到观察结果后需要整合历史将本次循环的推理行动观察三元组追加到交互历史中。判断终止条件分析当前状态判断任务是否完成。判断逻辑可以是模型自主判断让LLM根据当前结果和任务目标输出“Final Answer: ...”或“任务未完成继续”。预定义规则例如当行动是“final_answer”工具时或当循环次数达到上限如10次时强制终止。结果验证检查观察结果是否直接满足了用户的查询意图。只有完成了这四个阶段一个循环才算结束并决定是开启下一个循环还是输出最终答案。2.2 循环中的状态管理与上下文控制核心循环的稳定运行极度依赖良好的状态管理。这里有两个核心挑战1. 上下文窗口的消耗与优化每一次循环都会在对话历史中新增三段内容Think, Act, Observe。对于长任务历史上下文会迅速膨胀可能超出模型的上下文窗口限制。常见的解决方案有选择性记忆不保存完整的原始历史而是定期用LLM对之前的交互进行摘要Summarize只保留摘要和最近几次的完整循环。关键信息提取从历史观察中提取出关键的事实数据如查询到的温度、股票价格、数据库ID以结构化形式单独维护一个“事实池”在每次推理时注入而非传递全部冗长历史。分层循环将大任务分解为子任务每个子任务内部运行一个ReAct循环其历史在子任务结束后被清除只将子任务的结果传递给父任务。2. 错误处理与循环恢复工具执行可能失败模型推理也可能跑偏。核心循环必须具备容错能力。工具错误当观察结果是错误信息时应在下一次推理中明确告知模型“上次调用‘get_weather’失败原因为‘网络超时’。请重试或调整策略。” 这需要将错误信息格式化为模型可理解的文本。推理死循环智能体可能陷入重复调用无效工具或执行无效操作的循环。必须设置“最大循环次数”作为安全阀。一旦触发循环终止并尝试降级处理如直接提示用户或转交人工。检查点Checkpoint对于耗时极长的任务可以考虑将当前完整的状态包括历史、工具输出等序列化保存。在中断后可以从检查点恢复循环而不是从头开始。3. 消息格式设计智能体与环境的通信协议如果说核心循环是智能体的“发动机”那么消息格式就是确保发动机各部件协同工作的“标准化油路和电路”。设计不当的消息格式会导致解析错误、信息丢失或模型理解混乱。3.1 核心消息类型与结构在一个ReAct系统中通常存在三种角色的消息交互用户User、智能体Agent和工具Tool。我们需要为它们之间的通信设计格式。1. 用户与智能体的交互消息这通常沿用聊天模型的常见格式但为了支持智能体需要增强。{ role: user, content: 查询北京明天天气并告诉我是否需要带伞。 }content字段就是用户的自然语言指令。2. 智能体内部的“推理-行动”消息这是ReAct的核心。一种清晰的做法是将“思考”和“行动”放在同一个消息里但用特殊标记分隔方便解析。{ role: assistant, content: Thought: 用户需要北京的天气和伞的建议。我需要先获取天气信息。Action: get_weather\nAction Input: {\city\: \北京\, \date\: \2023-10-27\} }或者更结构化的方式是将动作部分分离{ role: assistant, content: Thought: 用户需要北京的天气和伞的建议。我需要先获取天气信息。, tool_calls: [ { id: call_001, type: function, function: { name: get_weather, arguments: {\city\: \北京\, \date\: \2023-10-27\} } } ] }第二种格式与OpenAI的Function Calling API格式兼容是现代Agent框架如LangChain、LlamaIndex更常用的方式因为它能被SDK更好地解析和处理。3. 工具执行结果返回消息工具执行完成后需要将结果格式化后返回给智能体作为“观察”。{ role: tool, content: 北京明天2023-10-27天气为多云转晴气温15-22℃降水概率10%风力2-3级。, tool_call_id: call_001 // 关联到具体的工具调用 }如果执行失败{ role: tool, content: Error: Failed to call weather API. Network timeout., tool_call_id: call_001 }3.2 历史上下文的组织与传递智能体在每一步推理时都需要看到完整的历史交互记录。如何组织这个历史列表至关重要。一个典型的、准备送入LLM的上下文列表可能长这样messages [ {role: system, content: 你是一个有帮助的助手可以调用工具。请遵循‘思考-行动’的格式。}, {role: user, content: 查询北京明天天气并告诉我是否需要带伞。}, {role: assistant, content: Thought: 用户需要... Action: get_weather..., tool_calls: [...]}, {role: tool, content: 北京明天...降水概率10%..., tool_call_id: call_001}, {role: assistant, content: Thought: 天气显示降水概率很低只有10%。通常不需要带伞。我可以直接给出最终答案了。, tool_calls: [...]} # 可能调用final_answer工具 ]设计要点System Prompt必须清晰定义行为规范包括输出格式如必须包含“Thought:”和“Action:”、可用工具列表及其描述。这是引导模型行为的第一道指令。顺序性消息列表必须严格保持交互的时间顺序。任何错位都会导致模型逻辑混乱。工具描述的注入除了在System Prompt中说明更精细的做法是在每次模型调用前动态地将当前可用的工具函数签名名称、描述、参数JSON Schema以某种格式如XML标签插入到上下文中帮助模型更准确地选择工具。3.3 实操心得消息格式设计的坑与技巧坑1模型不遵循指定格式你设计了完美的“Thought/Action”格式但模型有时会直接输出答案或者格式错乱。解决方案在System Prompt中强化格式要求使用明确的指令如“你必须严格按照以下格式输出\nThought: [你的思考过程]\nAction: [工具名]\nAction Input: [工具输入]”。可以加入“这是强制要求”等强调词。提供少样本示例Few-shot在System Prompt或初始消息中提供1-2个完整的、格式正确的交互示例。这是最有效的方法之一。输出后处理与重试编写一个解析器如果解析失败如找不到“Action:”标签则捕获该异常将错误信息如“你的输出格式不正确请确保包含‘Action:’”连同原始历史一起重新提交给模型要求其修正。这构成了一个小的自我修正循环。坑2工具参数解析错误模型输出了Action: get_weather但Action Input是一个非JSON字符串如“北京明天”导致工具无法解析。解决方案在工具描述中明确参数格式在给模型的工具描述里写明“参数必须是一个JSON对象例如{city: 城市名, date: YYYY-MM-DD}”。使用支持JSON模式的LLM优先选用在训练中强化了JSON输出能力的模型或使用其JSON Mode API。后处理与规范化编写一个健壮的参数解析层。如果输入不是合法JSON尝试用启发式方法提取关键信息如用正则匹配城市和日期再组装成JSON。如果失败则将解析错误作为“观察”反馈给模型让其重试。技巧为最终答案设计特殊工具如何优雅地结束循环一个常见模式是定义一个名为final_answer的工具。当模型认为任务完成时就调用这个工具并将答案作为输入。{ action: final_answer, action_input: {answer: 北京明天降水概率很低不需要带伞。} }系统检测到这个特殊工具被调用时就终止循环并将action_input[“answer”]返回给用户。这比让模型输出“Final Answer: ...”文本更易于程序化处理。4. 从零构建一个简易ReAct智能体代码实操理解了原理和设计我们动手实现一个简单的命令行天气查询ReAct智能体。我们将使用OpenAI的Chat Completions API模拟LLM和Requests库模拟工具执行。4.1 环境准备与工具定义首先定义我们的“工具库”。在这个例子中我们只有一个工具get_weather。import json import requests from typing import Dict, Any # 模拟的工具函数 def get_weather(city: str, date: str) - str: 模拟查询天气的工具。 在实际应用中这里会调用真实的天气API。 # 这里我们模拟一个API响应 weather_data { 北京: {2023-10-27: 晴15-22℃降水概率10%}, 上海: {2023-10-27: 多云18-25℃降水概率20%}, } try: return weather_data.get(city, {}).get(date, f未找到{city}在{date}的天气信息。) except Exception as e: return f查询天气时出错{str(e)} # 工具注册表将工具函数与其元数据关联 TOOLS [ { type: function, function: { name: get_weather, description: 根据城市和日期查询天气预报。, parameters: { type: object, properties: { city: {type: string, description: 城市名称例如北京}, date: {type: string, description: 日期格式为YYYY-MM-DD例如2023-10-27}, }, required: [city, date], }, }, } ] # 一个特殊的“最终答案”工具 FINAL_ANSWER_TOOL { type: function, function: { name: final_answer, description: 当你已经得出最终答案需要结束任务时调用此工具。, parameters: { type: object, properties: { answer: {type: string, description: 给用户的最终答案。} }, required: [answer], }, }, } ALL_TOOLS TOOLS [FINAL_ANSWER_TOOL]4.2 核心循环引擎的实现接下来是实现ReAct循环的主逻辑。我们将模拟LLM的响应。class SimpleReActAgent: def __init__(self, tools): self.tools {tool[function][name]: tool for tool in tools} self.messages [] # 维护完整的对话历史 def _call_llm(self, messages, tools): 模拟LLM调用。在实际中这里会替换为真实的OpenAI/Anthropic等API调用。 我们根据消息历史硬编码一个合理的响应来模拟。 # 这是一个极其简化的模拟真实场景需要调用API。 last_user_msg [m for m in messages if m[role] user][-1][content] if 天气 in last_user_msg: # 模拟第一次调用决定调用天气工具 return { role: assistant, content: 用户想查询天气信息。我需要使用get_weather工具来获取数据。, tool_calls: [ { id: call_sim_1, type: function, function: { name: get_weather, arguments: json.dumps({city: 北京, date: 2023-10-27}) } } ] } else: # 假设已经收到了天气信息现在给出最终答案 return { role: assistant, content: 我已经获取了天气信息可以给出最终答案了。, tool_calls: [ { id: call_sim_final, type: function, function: { name: final_answer, arguments: json.dumps({answer: 北京明天晴天气温舒适降水概率低不需要带伞。}) } } ] } def _execute_tool(self, tool_call): 执行工具调用。 tool_name tool_call[function][name] try: arguments json.loads(tool_call[function][arguments]) except json.JSONDecodeError: return f错误工具参数不是有效的JSON格式。 if tool_name get_weather: result get_weather(**arguments) elif tool_name final_answer: # 遇到最终答案工具直接返回结果并标记任务结束 return {type: final_answer, result: arguments.get(answer)} else: result f错误未知工具 {tool_name}。 return result def run(self, user_query: str, max_turns: int 5): 运行ReAct循环处理用户查询。 print(f用户: {user_query}) # 初始化消息历史加入系统指令 self.messages [ {role: system, content: 你是一个有帮助的助手可以调用工具。请根据需要使用工具并在得到足够信息后使用final_answer工具结束对话。} ] self.messages.append({role: user, content: user_query}) for turn in range(max_turns): print(f\n--- 第{turn1}轮循环 ---) # 1. 推理与行动调用LLM print(智能体思考中...) llm_response self._call_llm(self.messages, self.tools) self.messages.append(llm_response) print(f智能体: {llm_response[content]}) # 检查是否有工具调用 if not llm_response.get(tool_calls): print(无工具调用循环结束。) break # 2. 执行所有被调用的工具 for tool_call in llm_response[tool_calls]: tool_name tool_call[function][name] print(f执行工具: {tool_name}({tool_call[function][arguments]})) observation self._execute_tool(tool_call) # 处理最终答案 if isinstance(observation, dict) and observation[type] final_answer: print(f\n任务完成最终答案: {observation[result]}) return observation[result] # 3. 观察将工具结果加入历史 tool_message { role: tool, content: str(observation), tool_call_id: tool_call[id] } self.messages.append(tool_message) print(f工具结果: {observation}) print(f\n达到最大循环次数({max_turns})任务未完成。) return 抱歉我未能在限定步骤内完成此任务。 # 运行智能体 if __name__ __main__: agent SimpleReActAgent(ALL_TOOLS) result agent.run(北京明天天气怎么样需要带伞吗)这个简易实现清晰地展示了ReAct循环的骨架维护消息历史、调用LLM、解析并执行工具调用、处理结果并决定下一步。在真实项目中_call_llm函数会被替换为真实的模型API调用并且需要加入更复杂的错误处理和上下文管理逻辑。5. 常见问题排查与高级优化策略在实际开发中你会遇到比示例复杂得多的情况。下面是一些典型问题及其解决思路。5.1 模型不调用工具或调用错误工具症状LLM总是直接生成文本回答拒绝调用工具或者频繁调用错误的工具。排查与解决检查System Prompt确保指令清晰、强硬地要求模型调用工具。例如“你必须通过调用上述工具来回答问题。禁止直接给出答案。”优化工具描述工具的名称和描述要精准、无歧义。描述应明确工具的用途、输入和输出。使用模型能理解的动词开头如“查询...”、“计算...”、“搜索...”。提供Few-shot示例这是最有效的方法之一。在System Prompt中提供1-2个完整的、从用户问题到工具调用再到最终答案的示例。调整温度Temperature过高的温度可能导致输出随机性太大不遵循格式。对于工具调用任务通常使用较低的温度如0.1或0.2以获得更确定性的输出。使用强制JSON模式如果所用API支持如OpenAI的response_format: { “type”: “json_object” }强制模型以JSON格式输出便于解析。5.2 上下文过长导致性能下降或遗忘症状任务执行到后期模型似乎“忘记”了早期的指令或关键信息API调用速度变慢成本增加。排查与解决实施摘要策略每经过3-5轮循环或当历史token数接近阈值时触发一次摘要。将当前任务目标、已完成的步骤和关键结果用一个小模型如GPT-3.5-turbo或专用摘要提示词进行总结然后用摘要替换掉大部分旧历史。提取关键事实维护一个独立于对话历史的“知识库”或“事实列表”。每次工具返回重要数据如数字、ID、名称就提取出来存入这个列表。在每次推理时将这个列表作为附加信息注入系统提示或用户提示中。使用具有长上下文窗口的模型优先选择支持128K甚至更长上下文的模型。但需注意长上下文下的注意力机制可能使模型对中间部分信息关注度下降并非越长越好。任务分解对于复杂任务先让一个“规划器”模型将任务分解为清晰的子步骤序列。然后为每个子步骤启动一个独立的ReAct循环每个循环只关注当前子步骤的上下文完成后将结果传递给下一个循环。5.3 工具执行失败或超时症状工具返回错误或长时间无响应导致整个循环卡住。排查与解决设置超时与重试为每个工具调用设置合理的网络超时如5秒。如果超时自动重试1-2次。重试失败后将明确的错误信息如“工具X调用超时”返回给模型让它尝试备用方案。参数验证与清洗在工具执行前对模型生成的输入参数进行预验证。检查类型、范围、必填字段等。对于明显的错误如日期格式不对可以尝试自动校正而不是直接传给工具。实现工具熔断如果某个工具在短时间内连续失败多次暂时将其标记为“不可用”并从本次会话的可用工具列表中移除。在反馈给模型的错误信息中说明“工具X暂时不可用请尝试其他方法”。设计降级策略对于关键工具准备一个简化版或缓存版的备用工具。当主工具失败时自动切换。5.4 循环无法终止或提前终止症状智能体陷入无限循环反复执行相同或无效操作或者任务未完成就过早调用了final_answer。排查与解决强制最大循环次数这是最基本的安全网。根据任务复杂度设置一个上限如10-20次。检测循环模式在代码中记录最近几次的动作输入对。如果检测到完全相同的模式重复出现超过N次如3次则中断循环并反馈“检测到可能循环请重新评估策略”。强化终止条件判断除了依赖模型调用final_answer还可以在每一轮结束后用一个独立的“判断器”模型或规则评估当前观察结果是否已满足用户意图。如果满足则主动终止并输出。优化最终答案工具的描述在final_answer的工具描述中强调其使用条件例如“仅当你确信已获得足够信息并能直接、完整回答用户初始问题时才调用此工具”。构建一个生产级的ReAct智能体远不止实现基本循环。它涉及提示工程、上下文管理、错误恢复、性能监控等一系列工程挑战。理解“核心循环”和“消息格式”这两个基础问题为你搭建更复杂、更可靠的智能体系统打下了坚实的地基。真正的挑战和乐趣在于如何在这个基础框架上根据具体的业务场景进行精细化的调整、优化和扩展。