🌟 AI Agent 核心框架大揭秘:彻底搞懂 ReAct(面试重点中的重点!)
如果你正在准备 AI Agent 相关的面试,或者打算自己动手写一个 Agent,那么ReAct 框架绝对是你绕不开的“大山”。它不仅是目前最主流的 Agent 落地方式之一,更是面试官最爱考的核心概念。
今天我们就用最通俗易懂的语言、直观的例子和手把手的代码,带你彻底吃透 ReAct!
1. 💡 什么是 ReAct?(大白话解释)
ReAct 的全称是Reasoning + Acting(推理 + 行动)。
为了方便理解,你可以把它想象成一个**“边想边做”的模范学生**。
以前的大模型做题,喜欢“一口气把答案全写出来”(很容易脑补和胡说八道)。而 ReAct 要求大模型:
- 先在心里嘀咕一句:「我接下来要干什么?」(这叫推理 Thought)
- 真的去动手:比如去查资料、按计算器、调 API(这叫行动 Action)
- 看看结果:拿到真实的返回结果(这叫观察 Observation)
- 循环往复:根据刚才看到的结果,继续想下一步该干嘛,直到把题解出来(Final Answer)。
🌟 一句话总结:
ReAct = 把「思考过程」和「工具使用」写进同一条轨迹里,让大模型的推理过程可监督、可纠错、可复现。
2. ⚙️ 核心工作原理:神奇的闭环
大模型在单轮问答时,最致命的弱点就是容易“幻觉”(胡编乱造事实)。ReAct 通过“行动”把大模型锚定在外部真实的证据上。
它的标准工作流是一个死循环,直到得出最终结论:
- 用户问题 (Question)→\rightarrow→“北京明天天气如何?”
- 推理 (Thought)→\rightarrow→“我需要知道北京明天的天气,我应该调用天气搜索工具。”
- 行动 (Action)→\rightarrow→选择工具
search_weather,传入参数北京, 明天 - 观察 (Observation)→\rightarrow→外部工具返回:“北京明天晴,22度。”
- 推理 (Thought)→\rightarrow→“我已经拿到天气信息了,可以回答用户了。”
- 最终答案 (Final Answer)→\rightarrow→“北京明天是晴天,气温22度哦。”
3. 📝 经典的 Prompt 模板设计
要想让大模型乖乖听话,按这套流程走,我们需要给它设定严格的 Prompt 模板。下面是一个经典的业界模板(面试时可以口述核心结构):
你是一个可以调用外部工具来解决问题的智能助手。 你必须严格遵守以下格式进行多轮交互: Question: 你需要回答的用户输入问题 Thought: 仔细思考你下一步应该做什么 Action: 你要采取的行动,必须是 [{tool_names}] 中的一个 Action Input: 传入给该工具的参数 Observation: 工具执行后返回的真实结果 ... (上述 Thought/Action/Action Input/Observation 的循环可以重复多次) ... Thought: 我现在已经收集到了足够的信息,知道最终答案了 Final Answer: 针对原始问题的最终回答 开始! Question: {question} Thought: {optional_seed_thought}⚠️ 设计要点(面试踩坑提醒):
- 工具清单要写清:减少模型胡乱编造工具名的情况。
Observation绝不能让模型自己写:必须由我们的代码系统强制阻断模型生成,由真实工具执行后填入。- 给几个示例 (Few-shot):给模型演示 1~3 个完整的轨迹,能显著提高它对格式的遵从率。
4. 💻 代码实战:手写一个 ReAct 循环 (含保姆级注释)
面试官很喜欢让你手写或口述 ReAct 的代码逻辑。下面是一段极简但五脏俱全的 Python 代码逻辑:
fromtypingimportCallable,Dict,List,Tupledefparse_action(text:str)->Tuple[str,str]:""" 极简的解析器:从大模型输出的文本中提取出 Action 和参数。 (实际生产中建议使用 JSON 结构化输出或更稳健的正则表达式) """action=Noneaction_input=Noneforlineintext.splitlines():ifline.startswith("Action:"):action=line.split("Action:",1)[1].strip()ifline.startswith("Action Input:"):action_input=line.split("Action Input:",1)[1].strip()returnaction,action_inputdefreact_loop(question:str,tools:Dict[str,Callable],llm_call:Callable,max_steps:int=6)->str:""" 核心 ReAct 循环调度器 :param question: 用户的问题 :param tools: 字典格式的工具箱,key是工具名,value是执行函数 :param llm_call: 调用大模型的函数 :param max_steps: 为了防止死循环,设置最大尝试步数 """# 获取可用工具的名称列表tool_names="["+", ".join(tools.keys())+"]"# 记录历史交互轨迹:存入 Thought, Action, Observationhistory:List[Tuple[str,str,str]]=[]forstepinrange(max_steps):# 1. 组装当前的 Prompt(包含系统提示、历史轨迹和当前问题)prompt=build_prompt(question,history,tool_names)# 2. 调用大模型,获取模型的下一步打算out=llm_call(prompt)# 3. 检查是否得出了最终答案if"Final Answer:"inout:returnout.split("Final Answer:",1)[1].strip()# 4. 解析模型的输出,提取 Thought 和 Actionthought=extract_thought(out)# 假设我们有一个提取 Thought 的辅助函数action,action_input=parse_action(out)# 5. 安全校验:模型选的工具存在吗?ifnotactionoractionnotintools:obs=f"错误: 无效的工具。你必须从{list(tools.keys())}中选择。"else:# 6. 【最关键一步】执行真实的工具代码,获取观察结果!obs=tools[action](action_inputor"")# 7. 把这一轮的思考、行动和观察结果存入历史轨迹,供下一轮使用history.append((thought,f"{action}[{action_input}]",obs))# 如果把 max_steps 耗尽了还没得出答案,兜底退出return"执行失败:超过最大步数限制。"5. 🎯 高频面试题与完美回答
Q1:ReAct 框架和“普通的 CoT(思维链)提示词”有什么本质区别?
标准答案:
- 普通的 CoT只在大模型内部展开推理,不强制与外部环境交互,容易产生闭卷幻觉。
- ReAct把推理与可执行的“行动”绑定在一起,每一步行动后都会有真实的 Observation(外部反馈)打断模型的盲目生成,用外部真实数据约束推理过程。
Q2:ReAct 为什么要显式地写出 Thought(思考过程)?直接输出动作不行吗?
标准答案:
显式写出 Thought 具有三大价值:
- 可解释性:出了 bug,程序员看日志一眼就能看出它是哪一步想歪了。
- 引导模型:就像人类做数学题写草稿一样,先写推理过程能显著提高大模型选择正确工具和参数的稳定性。
- 学习信号:这些完整的 Thought 轨迹可以收集起来,未来用于做微调(SFT)训练更强的专属模型。
🔥 追问应对 1:如果大模型自己在输出里伪造了 Observation(观察结果)怎么办?
应对策略:在工程实现时,系统的解析器一旦检测到模型输出了
Action和Action Input,就必须**强制截断(Stop Sequence)**它的生成!Observation必须由我们的 Python 代码调用真实 API 后填入,绝不能让模型自己续写。
🔥 追问应对 2:ReAct 循环这么多次,API 成本是不是很高?
应对策略:是的,每步一次调用,延迟和成本随步数线性上升。缓解方法包括:设置强制的
max_steps上限、引入小模型作为专门的路由节点、以及对工具查询结果增加本地缓存(Cache)。
6. ⚖️ 优缺点与适用场景总结
| 维度 | 分析说明 |
|---|---|
| ✅优点 | 可解释强(轨迹清晰)、回答踏实(锚定真实数据)、通用性好(与具体工具解耦,容易加新工具)。 |
| ❌缺点 | 成本高/延迟大(反复调用大模型)、格式脆弱(一旦大模型没按 JSON/特定模板输出,解析器容易崩溃)、全局规划弱(它偏向于“走一步看一步”的贪心策略,不如 Plan-and-Execute 有大局观)。 |
| 🛠️适用场景 | 需要频繁与外部工具交互、依赖环境实时反馈才能推进的任务(如:搜索查资料、写代码并看报错修复、查数据库)。 |