ReAct Agent执行细节深度解析:从 Thought-Action-Observation 循环到安全护栏
ReAct Agent执行细节深度解析:从 Thought-Action-Observation 循环到安全护栏
2025 年 11 月,4 个 LangChain Agent 因未设置步数限制,连续运行了11 天,消耗$47,000才被发现。问题不在于模型不够聪明——而是 Agent 的 ReAct 循环缺少基本的安全护栏。Thought、Action、Observation 这三步看似简单,但每一步都可能让 Agent 进入死循环、Token 爆炸、工具误调用。真正可用的 Agent,不是"能推理的模型",而是有边界控制的推理系统。
一、ReAct 框架核心:一个三元循环如何让模型"边想边做"
1.1 什么是 ReAct?
ReAct(Reasoning + Acting)是 2022 年由 Google 和普林斯顿提出的框架,核心思想是让语言模型交替进行推理和行动,而不是一次性生成最终答案。
传统 LLM 模式(Chain-of-Thought):
- 用户提问 → 模型思考 → 生成最终答案
- 问题:模型在"真空"中思考,无法获取外部信息
ReAct 模式:
- 用户提问 → Thought(思考) → Action(行动) → Observation(观察) → Thought → Action → ... → 最终答案
- 核心:模型可以"边想边做、边做边看、看了再想"
1.2 三元循环详解
每一步的具体内容:
| 阶段 | 做什么 | 举例 |
|---|---|---|
| Thought(思考) | 分析当前信息,决定下一步做什么。输出的是"推理",不是"行动" | "用户想查北京天气,我需要调用天气 API。先确认一下城市参数是否正确。" |
| Action(行动) | 执行一个具体操作——调用工具、搜索文件、发送请求。输出的是"工具调用" | get_weather(city="Beijing") |
| Observation(观察) | 接收行动的结果,作为下一轮思考的输入。这是 Agent 与外部世界的"感官" | {"city": "Beijing", "temp": 28, "weather": "晴"} |
💡关键洞察:ReAct 的核心不是"模型更聪明了",而是模型能从外部世界获取反馈,并根据反馈调整行为。这跟人类的"试错学习"非常相似——你试了一个方法,看到结果,决定下一步怎么走。
1.3 一个完整的 ReAct 执行示例
用户:"帮我看看明天北京的天气,如果下雨就提醒我带伞。" 第 1 轮: Thought: 用户想知道明天北京天气。我需要先获取天气预报。 Action: get_weather(city="北京", date="2026-06-29") Observation: {"date": "2026-06-29", "weather": "中雨", "temp": "22-28℃"} 第 2 轮: Thought: 明天北京中雨,需要提醒用户带伞。 Final Answer: "明天(6月29日)北京有中雨,气温22-28℃,出门记得带伞!"总共 2 轮就完成了——这就是标准的多步推理执行。
二、Agent 如何"知道自己该干什么"——状态感知与工具选择
2.1 Agent 的"自我意识"从哪来?
Agent 不像传统程序有明确的状态变量。它的"状态感知"来自上下文中积累的信息:
Agent 的"状态" = 累积的上下文。每一轮循环中,Agent 看到的上下文包括:
- System Prompt— 角色定义、安全边界、工具列表
- 用户原始任务— 最初的目标
- 所有历史 Thought— 之前的推理链
- 所有历史 Action + Observation— 已执行的工具调用和结果
- 当前轮次的思考空间— 模型正在生成的 Thought
Agent 通过阅读这些累积信息,判断:"我已经做了什么?还缺什么?接下来该做什么?"
2.2 工具选择的决策逻辑
Agent 选择工具不是"随机试",而是基于语义匹配 + 上下文推理:
工具选择的三个输入:
1. 当前 Thought 的结论 — "我需要查天气"
2. 可用工具的 name + description —get_weather: 查询指定城市的天气
3. 工具的参数 schema —{city: string, date: string}
决策过程(模型内部推理,非显式代码):
1. 将 Thought 与所有工具描述做语义匹配
2. 筛选出相关工具(通常 1-3 个候选)
3. 检查是否具备调用条件(参数是否齐全)
4. 选择最佳工具,填充参数,生成 tool_call
💡关键洞察:Agent 选择工具的过程完全基于语言理解,没有硬编码的 if-else。这意味着:当用户说"帮我查查明天要不要带伞"时,Agent 能理解这等于"查天气 + 判断降水概率"——即使 tool description 里没有提到"伞"。
2.3 Observation 之后的"下一步判断"
Observation 返回后,Agent 面临三个选择:
| 判断 | 条件 | 行为 |
|---|---|---|
| 继续执行 | Observation 提供了新信息,但还不够完成任务 | 进入下一轮 Thought,继续规划 |
| 调整方向 | Observation 显示之前的路径不对(如工具返回空、报错) | 换一个工具或调整参数,重试 |
| 生成答案 | Observation 已经提供了足够信息 | 停止工具调用,生成 Final Answer |
示例:Observation 驱动方向调整
第 1 轮: Thought: 需要读取 config.yaml Action: read_file("config.yaml") Observation: Error: File not found 第 2 轮: Thought: config.yaml 不存在,可能文件名是 config.yml。先搜索一下目录里有哪些配置文件。 Action: search_file(pattern="config.*") Observation: ["config.yml", "config.prod.yml"] 第 3 轮: Thought: 找到了 config.yml,现在读取它。 Action: read_file("config.yml") Observation: ...(文件内容) → 继续处理...三、ReAct vs If-Else:为什么不能把控制流写死?
3.1 If-Else 的天花板
传统编程的控制流是预定义的:
# 传统 if-else 模式 def handle_user_request(task): if "天气" in task: city = extract_city(task) weather = get_weather(city) if "雨" in weather: return f"{weather},记得带伞!" else: return f"{weather}" elif "文件" in task: # 读文件、处理... pass else: return "我不知道怎么处理"这种方式的致命缺陷:
| 问题 | 说明 |
|---|---|
| 组合爆炸 | 每增加一种工具,可能的执行路径呈指数增长。10 个工具 × 10 步 = 100 种组合,if-else 根本写不完 |
| 无法应对未知 | "查天气+读文件+发邮件"这种组合需求,必须提前写死分支 |
| 缺乏纠错 | 工具调用失败只能按预设的错误处理走,无法"换个思路试试" |
| 不灵活 | 用户说"帮我看看明天要不要带伞",if-else 匹配不到关键词就傻了 |
3.2 ReAct 如何解决
ReAct 的本质差异:
- if-else 模式:人提前定义"所有可能的路径"
- ReAct 模式:模型在每一步"现场决定路径"
类比:if-else = 跟着导航开,走错了只能掉头;ReAct = 看着路牌开,走错了可以绕路。
关键能力来源:不是代码写得好,而是模型具备"语义理解 + 常识推理 + 试错调整"。
💡关键洞察:ReAct 的灵活性本质上来自将控制流从代码层提升到语义层。控制流不再由程序员预定义,而是由模型在上下文中实时推理生成。这意味着你不需要预见到所有可能的情况——你只需要给模型工具和判断标准,它会自己找出路径。
3.3 但灵活性也有代价
ReAct 的优势同时也是风险来源:
好处:
- 可以自动组合多个工具完成复杂任务
- 遇到错误能换策略重试
- 用户用自然语言表达意图即可,不需要精确命令
代价:
- 模型可能"想太多",进入无意义的工具调用循环
- 每一步都要消耗 Token,成本线性增长
- 没有硬边界,可能永远停不下来
- 模型的"现场决定"不一定是最优解
四、安全护栏一:最大步数限制——最简单也最有效
4.1 为什么必须限制步数?
回到开头的 $47,000 案例:4 个 Agent 连续运行 11 天,根本原因是没有步数上限。Agent 在某个任务上陷入循环,不断调用工具、获取空结果、再次调用——永不停机。
无步数限制的典型灾难场景:
轮次 1: Thought: 我需要读文件 X 轮次 2: Action: read_file("x") → Observation: 文件不存在 轮次 3: Thought: 文件不存在,换个路径试试 轮次 4: Action: read_file("/path/x") → Observation: 文件不存在 轮次 5: Thought: 再换个路径... ... 轮次 847: 还在试...每一步都消耗 Token + 工具调用成本,且没有进度。
4.2 如何设置合理的步数上限
步数限制的经验值(基于生产环境数据):
- 轻量任务(单次查询/简单操作):
max_steps = 5-10 - 例:查天气、翻译文本、格式化代码
- 中等任务(多步推理/文件操作):
max_steps = 15-25 - 例:代码审查、Bug 修复、文档生成
- 复杂任务(多工具编排/项目级操作):
max_steps = 30-50 - 例:项目初始化、全链路测试、数据库迁移
原则:宁可设低(快速失败)也不要设太高。快速失败 → 用户重新描述需求 → 2 秒后重新开始;设太高 → Agent 在错误的路径上消耗大量 Token → 浪费钱和时间。
| 实现方式 | 示例 |
|---|---|
| 框架配置 | agent.invoke(task, max_iterations=20) |
| Prompt 注入 | "你最多只能执行 15 步操作,之后必须给出当前结果" |
| 代码层硬限制 | 在 while 循环中if step_count > MAX_STEPS: break |
4.3 超限后的行为设计
步数用完后不只是"停止"——需要优雅降级:
- 停止所有工具调用
- 总结已完成的工作:"我已经完成了 A、B、C 三个步骤"
- 说明未完成的部分:"D 步骤因为步数用完未执行"
- 建议用户如何继续:"你可以直接要求我执行 D 步骤"
错误做法:直接丢弃上下文,返回"我无法完成任务"
正确做法:保留进展,让用户可以从断点继续
五、安全护栏二:重复动作检测——别让 Agent 原地打转
5.1 什么是重复动作循环?
Agent 最常见的失控模式不是"步子太多",而是同样的动作重复执行:
模式一:相同工具 + 相同参数
round 3: search_file("config.*") → 3 个结果 round 4: search_file("config.*") → 还是那 3 个结果 round 5: search_file("config.*") → 又来一遍...模式二:A→B→A→B 摆动
round 3: read_file("x") → 文件太大 round 4: grep("keyword", "x") → 找到了 round 5: read_file("x") → 又读一遍... round 6: grep("keyword", "x") → 又搜一遍...模式三:渐进式重复(最隐蔽)
round 3: search_file("config.*") → 无结果 round 4: search_file("*config*") → 无结果 round 5: search_file("*.yaml") → 无结果 round 6: search_file("*.yml") → 无结果 ...(同类型工具无限尝试不同参数)5.2 检测策略
策略一:精确匹配(检测完全相同的工具调用)
- 记录每轮的 (tool_name, params_hash)
- 如果连续 2 次完全相同 → 触发告警 → 强制 Agent 换方向
策略二:语义相似匹配(检测换汤不换药的调用)
- 计算连续工具调用的语义相似度
- 如果相似度 > 0.8 → 可能陷入变体循环
策略三:类别计数(检测"一直在搜但找不到"的模式)
- 统计同类型工具的连续调用次数
- 如果 read_file 连续失败 5 次 → 建议 Agent 换个思路
策略四:进度检测(最根本的方法)
- 每轮对比 Observation 是否包含"新信息"
- 如果连续 3 轮 Observation 无实质性进展 → 可能陷入停滞
| 策略 | 检测能力 | 误报风险 | 推荐度 |
|---|---|---|---|
| 精确匹配 | 相同调用 | 几乎为零 | 必备 |
| 语义相似 | 变体循环 | 中等 | 推荐 |
| 类别计数 | 渐进式重复 | 较低 | 建议有 |
| 进度检测 | 信息停滞 | 较高(需调阈值) | 作为兜底 |
5.3 触发检测后的行为
检测到重复 → Agent 必须切换策略,而不是继续重复:
- 第 1 次检测:在下一轮的 Thought 前注入提醒 → "注意:你刚才的 search_file 已经执行过相同参数,没有新结果。请换一种方法。"
- 第 2 次检测:强制切换工具类别 → "你已经连续 5 次执行搜索类操作。请停止搜索,尝试其他方法获取信息。"
- 第 3 次检测:强制终止并降级 → "检测到重复动作循环。停止执行,向用户汇报当前进展。"
💡关键洞察:重复检测不是为了"打断"Agent,而是为了让 Agent在错误的方向上少浪费 Token。3 轮重复被终止,远比 50 轮重复后自然停止要经济。
六、安全护栏三:降级机制——当 Agent 走不通时有路可退
6.1 为什么需要降级?
Agent 设计中最容易被忽视的是失败路径。大多数 Agent 只有"成功路径"的设计——假设模型总能推理出正确结果。但现实是:
- 工具返回格式不符合预期 → Agent 反复尝试解析
- 缺少关键工具 → Agent 用现有工具"绕路",越绕越远
- 歧义任务描述 → Agent 在两个解释之间反复横跳
- 上下文太长 → 模型开始"遗忘"早期信息,决策质量持续下降
6.2 三级降级策略
Level 1: 自动重试(透明降级)
- 触发条件:工具调用失败(网络超时、参数格式错误)
- 行为:自动修正参数后重试(最多 2 次)、换成同类替代工具、对用户完全透明
- 示例:read_file("config.yaml")失败 → 自动 tryread_file("./config.yaml")→ 自动 trysearch_file("config.*")先确认存在
Level 2: 简化目标(半透明降级)
- 触发条件:步数接近上限 或 重复检测触发
- 行为:放弃"完美方案",选择"能用的方案";通知用户:"最优方案无法完成,以下是当前最佳结果"
- 示例:原目标"分析并修复所有代码问题" → 降级后"我发现 3 个问题,已修复 2 个,第 3 个需要你确认"
Level 3: 安全退出(显式降级)
- 触发条件:步数用尽 或 重复检测 2 次以上
- 行为:停止所有操作、输出已完成的工作 + 未完成的工作 + 建议的下一步、将完整上下文保留
- 示例:"我在以下方面取得了进展: 已读取项目结构(15 个文件) 已识别 3 个潜在问题 未完成修复(步数用尽)💡 建议:请让我 focus 在某个具体文件上继续"
| 降级层级 | 何时触发 | 用户体验 | 核心思想 |
|---|---|---|---|
| Level 1 | 单次操作失败 | 无感知 | 内部消化小问题 |
| Level 2 | 接近限制 | 轻感知 | 交付部分价值 |
| Level 3 | 已达上限 | 明确告知 | 保留进展,允许继续 |
七、好处与坑:ReAct Agent 的实战体验
7.1 三大核心好处
好处一:模型可以自己规划路径
- 传统方式:人必须把每一步都写清楚
- ReAct 方式:描述目标,Agent 自己找路
- 案例:"帮我给这个项目加单元测试" → Agent 自动扫描项目结构 → 识别源文件 → 检查现有测试 → 补充缺失测试
好处二:遇到意外能自行调整
- 传统 if-else:预定义的错误处理覆盖不到的情况 → 崩溃
- ReAct:工具返回 unexpected → Thought 分析 → 换方案继续
- 案例:read_file("config.json")返回 YAML 格式内容 → 传统代码 JSON.parse 报错崩溃 → ReAct Agent 理解是 YAML → 用 YAML 解析器
好处三:多工具自然编排
- ReAct 天然支持"不固定顺序"的工具编排
- 不像工作流引擎需要预先定义 DAG
- 模型根据当前状态动态决定"下一个工具是什么"
7.2 五个常见坑
坑一:Token 消耗不可控
- 每轮 Thought + Action + Observation 都占 Token
- 复杂任务 30 轮 → 可能消耗 50K-100K tokens
- 成本可能是单次调用的 10-30 倍
- 缓解方案:设置合理的 max_steps、使用 Tool Search 减少工具定义 Token、上下文压缩
坑二:模型"想太多"
- 简单任务也可能被 Agent 复杂化
- "读一个文件" → Agent 可能先 ls、再思考、再读文件
- 缓解方案:System Prompt 中加入"简洁原则"、检测"过度思考"模式
坑三:Observation 依赖性
- 模型决策完全依赖 Observation 的准确性和完整性
- 工具返回格式变化 → Agent 可能误解
- 缓解方案:工具返回格式标准化、关键工具返回加入"置信度"字段
坑四:上下文窗口污染
- 每轮都追加新内容到上下文
- 早期关键信息(用户任务、约束)可能被"挤出"注意力焦点
- 缓解方案:关键约束在每轮 System Prompt 中重复注入、定期自动总结
坑五:模型幻觉在中间步骤放大
- Chain-of-Thought 的幻觉只在最终答案体现
- ReAct 的幻觉会在中间 Thought 中放大:错误 Thought → 错误 Action → 错误 Observation → 更错误 Thought
- 缓解方案:重要 Action 前加"确认"步骤、工具返回中加入"校验信息"
7.3 一张表总结
| 维度 | 传统 if-else | ReAct Agent |
|---|---|---|
| 控制流定义 | 程序员预定义 | 模型现场推理 |
| 灵活性 | 低(只能处理预见的情况) | 高(可应对未预见的需求) |
| 工具调用错误处理 | 预定义 catch 块 | 动态调整策略 |
| 多工具编排 | 需手动写编排逻辑 | 模型自动编排 |
| Token 成本 | 固定、可控 | 动态、可能失控 |
| 可调试性 | 高(断点、日志) | 低(黑盒推理链) |
| 适用场景 | 确定性流程 | 探索性、创造性任务 |
| 安全边界 | 代码级硬边界 | 需要 Prompt + 代码双重护栏 |
八、总结:生产级 ReAct Agent 的安全设计清单
💡核心理念:ReAct Agent 的强大之处在于灵活性——模型可以根据实际情况动态规划执行路径。但灵活性必须配边界控制——没有护栏的灵活性就是失控。好的 Agent 设计不是"让模型更聪明",而是"让聪明的模型在安全边界内运行"。