Agent_02_ReAct模式
ReAct 是什么
ReAct(Reasoning + Acting)是 Agent 领域被引用最多的模式之一。本质上就是在每一步行动之前,让 LLM 先输出一段思考过程。
没有这个思考步骤时,LLM 选工具的决策是黑盒的:
# 反例:没有 Thought,直接调工具
for step in steps:
tool_name = llm.choose_tool(step) # LLM 直接选工具
result = tools.execute(tool_name) # 执行它为什么选这个工具而不是那个?不知道。选错了也不知道它当时怎么想的。
加上 Thought 之后:
Thought: 用户想分析销售数据,但首先需要读取文件。我应该用 read_csv 工具。
Action: read_csv(path="sales.csv")
Observation: 读取成功,共 1000 行,5 列...
Thought: 数据已经读取完毕,接下来需要分析趋势。用 analyze 工具。
Action: analyze(data=...)
Observation: Q4 增长 15%...
Thought: 分析完成了,可以生成最终报告了。
Final Answer: 根据分析,Q4 销售增长 15%...每一行 Thought 都是一个可检查的决策点。回头看日志,能清楚知道 Agent 为什么在这一步选择了这个工具、基于什么信息做了判断。
ReAct 的循环就是三步:Thought → Action → Observation,不断循环直到输出 Final Answer。
Python 代码示例
示例一:ReAct Agent 完整实现
完整的 ReAct 循环,核心逻辑不到 50 行:
"""
ReAct Agent —— Thought → Action → Observation 循环
"""
from dataclasses import dataclass
from typing import Any
@dataclass
class ReActStep:
"""ReAct 单步记录"""
thought: str = ""
action_name: str = ""
action_args: dict = None
observation: str = ""
is_final: bool = False
class ReActAgent:
"""ReAct Agent 主循环"""
def __init__(self, llm, tool_registry, max_steps: int = 10, verbose: bool = True):
self.llm = llm
self.tools = tool_registry
self.max_steps = max_steps
self.verbose = verbose
def run(self, goal: str) -> str:
system_prompt = self._build_system_prompt()
context = self._build_initial_context(goal)
steps: list[ReActStep] = []
for step_num in range(1, self.max_steps + 1):
if self.verbose:
print(f"\n{'='*50}")
print(f" 第 {step_num} 步")
print(f"{'='*50}")
# 1. LLM 输出 Thought + Action
response = self.llm.generate_with_react_format(
context=context,
system=system_prompt,
steps=steps,
)
step = ReActStep(
thought=response.get("thought", ""),
action_name=response.get("action", ""),
action_args=response.get("action_args", {}),
is_final=response.get("is_final", False),
)
if self.verbose:
print(f" Thought: {step.thought}")
# 2. 判断是否是最终答案
if step.is_final:
step.observation = step.action_name
steps.append(step)
if self.verbose:
print(f" Final Answer: {step.observation}")
return step.observation
# 3. 执行工具
if self.verbose:
print(f" Action: {step.action_name}({step.action_args})")
result = self.tools.execute(step.action_name, **step.action_args)
step.observation = result.output if result.success else f"错误: {result.error}"
steps.append(step)
if self.verbose:
status = "成功" if result.success else f"失败: {result.error}"
print(f" Observation: {status}")
# 4. 追加到上下文
context = self._append_step(context, step)
return "达到最大步数限制,未能完成。最后的结果:\n" + self._format_steps(steps)
def _build_system_prompt(self) -> str:
tool_list = "\n".join(
f"- {t['name']}: {t['description']}"
for t in self.tools.get_schemas()
)
return f"""你是一个 ReAct Agent。你必须严格遵守以下格式输出:
Thought: <你的思考过程>
Action: <工具名称>
Action Input: <工具的 JSON 参数>
或者当你有了最终答案时:
Thought: 我已经有了答案
Final Answer: <你的最终回复>
可用工具:
{tool_list}
注意:
1. 每一步必须先 Thought,再 Action
2. 不要编造 Observation,Observation 由我提供
3. 一旦有了最终答案,输出 Final Answer 并结束"""
def _build_initial_context(self, goal: str) -> str:
return f"目标: {goal}\n\n开始执行。\n"
def _append_step(self, context: str, step: ReActStep) -> str:
context += f"Thought: {step.thought}\n"
if step.is_final:
context += f"Final Answer: {step.observation}\n"
else:
context += f"Action: {step.action_name}\n"
context += f"Action Input: {step.action_args}\n"
context += f"Observation: {step.observation}\n"
return context
def _format_steps(self, steps: list[ReActStep]) -> str:
lines = []
for i, s in enumerate(steps, 1):
lines.append(f"Step {i}:")
lines.append(f" Thought: {s.thought}")
if not s.is_final:
lines.append(f" Action: {s.action_name}({s.action_args})")
lines.append(f" Observation: {s.observation}")
return "\n".join(lines)几个要点:
- 格式由系统 prompt 约束,不是代码强制的。LLM 必须按
Thought → Action → Action Input输出 - Final Answer 是退出条件:LLM 判断信息够了就输出 Final Answer,循环终止
- 每步结果追加到上下文:LLM 下一轮能看到所有历史信息,这是 ReAct 的记忆机制
- max_steps 是安全阀:防止 LLM 陷入无限循环
示例二:LLM 的 ReAct 输出解析
这是最容易搞砸的部分。LLM 不一定乖乖遵守格式,解析逻辑必须有容错:
import re
import json
class ReActOutputParser:
"""ReAct 输出解析器 —— 把 LLM 的文本输出解析为结构化数据"""
def parse(self, text: str) -> dict:
"""
解析 LLM 输出
返回格式:
{
"thought": "思考内容",
"action": "工具名" 或 "",
"action_args": {"key": "value"},
"is_final": False,
"final_answer": "",
}
"""
# 策略一:精确匹配 Thought/Action/Observation 格式
thought_match = re.search(r"Thought:\s*(.+?)(?=\nAction:|\nFinal Answer:|$)", text, re.DOTALL)
final_match = re.search(r"Final Answer:\s*(.+)$", text, re.DOTALL)
action_match = re.search(r"Action:\s*(.+?)(?=\n|$)", text)
args_match = re.search(r"Action Input:\s*(.+?)(?=\n|$)", text, re.DOTALL)
thought = thought_match.group(1).strip() if thought_match else ""
if final_match:
return {
"thought": thought,
"action": "",
"action_args": {},
"is_final": True,
"final_answer": final_match.group(1).strip(),
}
if action_match:
action_name = action_match.group(1).strip()
action_args = {}
if args_match:
args_text = args_match.group(1).strip()
action_args = self._safe_parse_json(args_text)
return {
"thought": thought,
"action": action_name,
"action_args": action_args,
"is_final": False,
"final_answer": "",
}
# 策略二:模糊匹配
return self._fuzzy_parse(text)
def _safe_parse_json(self, text: str) -> dict:
"""安全解析 JSON,失败返回空字典"""
try:
return json.loads(text)
except json.JSONDecodeError:
try:
fixed = re.sub(r'(\w+):', r'"\1":', text)
return json.loads(fixed)
except json.JSONDecodeError:
brace_match = re.search(r'\{(.+?)\}', text, re.DOTALL)
if brace_match:
try:
fixed = re.sub(r'(\w+):', r'"\1":', brace_match.group(1))
return json.loads("{" + fixed + "}")
except json.JSONDecodeError:
pass
return {}
def _fuzzy_parse(self, text: str) -> dict:
"""模糊解析——LLM 不按格式输出时的兜底"""
conclusion_keywords = ["结论", "总结", "最终", "因此", "所以", "综上所述"]
if any(kw in text for kw in conclusion_keywords):
return {
"thought": "",
"action": "",
"action_args": {},
"is_final": True,
"final_answer": text,
}
return {
"thought": "",
"action": "",
"action_args": {},
"is_final": True,
"final_answer": text,
}LLM 经常:漏写 Action: 前缀、JSON key 不带引号、Thought 和 Action 写同一行、直接给答案不写格式。好的解析器必须做精确匹配到模糊匹配到兜底解析的三级容错。
示例三:ReAct 循环的三种变体
根据任务不同,有不同的循环策略:
from dataclasses import dataclass
@dataclass
class ReactResponse:
"""ReAct 响应"""
thought: str = ""
action: str = ""
args: dict = None
is_final: bool = False
answer: str = ""
needs_replan: bool = False
class ReActStrategy:
"""ReAct 循环的三种策略"""
@staticmethod
def standard(llm, tools, goal, max_steps=10):
"""
标准 ReAct:每步 Thought → Action → Observation
适用场景:工具不多、步骤不长的任务。
优点:简单直接。
缺点:容易陷入长循环,每步都要 LLM 输出。
"""
context = f"目标: {goal}\n"
for _ in range(max_steps):
response = llm.generate_react(context)
if response.is_final:
return response.answer
result = tools.execute(response.action, **response.args)
context += f"\nThought: {response.thought}\nAction: {response.action}\nObservation: {result}"
return "达到最大步数"
@staticmethod
def plan_then_act(llm, tools, goal, max_steps=10):
"""
先规划再执行:先让 LLM 生成完整计划,然后按 ReAct 循环执行
适用场景:步骤较多、逻辑复杂的任务。
优点:LLM 先想好全局计划,不容易跑偏。
缺点:规划可能不准确,执行中需要动态调整。
"""
# Phase 1:规划
plan = llm.generate_plan(goal)
context = f"目标: {goal}\n计划: {plan}\n"
# Phase 2:按 ReAct 执行,但可以偏离计划
for step in range(max_steps):
response = llm.generate_react_with_plan(context, plan)
if response.is_final:
return response.answer
result = tools.execute(response.action, **response.args)
context += f"\nObservation: {result}"
if response.needs_replan:
plan = llm.update_plan(context, plan)
return "达到最大步数"
@staticmethod
def reflexion(llm, tools, goal, max_steps=10, max_retries=2):
"""
Reflexion ReAct:执行后自我反思,结果不好就重试
适用场景:对输出质量要求高、允许重试的任务。
优点:自我纠错,结果质量高。
缺点:成本高,每轮重试都要重新调 LLM。
"""
context = f"目标: {goal}\n"
for attempt in range(max_retries):
result, steps = ReActStrategy._run_react_loop(
llm, tools, context, max_steps
)
critique = llm.critique(result, goal)
if critique.is_good:
return result
context += f"\n\n上次尝试结果: {result}\n反思: {critique.feedback}\n请重新执行,注意避免以上问题。"
return result
@staticmethod
def _run_react_loop(llm, tools, context, max_steps):
"""ReAct 循环的内部实现"""
steps = []
for _ in range(max_steps):
response = llm.generate_react(context)
if response.is_final:
return ReactResponse(is_final=True, answer=response.answer), steps
result = tools.execute(response.action, **response.args)
context += f"\nObservation: {result}"
steps.append(response)
return ReactResponse(is_final=True, answer="达到最大步数"), steps选择标准:工具少步骤短用 Standard;步骤多逻辑复杂用 Plan-Then-Act;对质量要求高且允许重试用 Reflexion。
示例四:Few-shot ReAct Prompt——好示例 vs 坏示例
ReAct 的成败,一半取决于 system prompt 写得好不好。下面对比三种写法:
"""
Few-shot ReAct Prompt 设计
"""
class ReActPromptBuilder:
"""ReAct 系统 prompt 构建器"""
@staticmethod
def bad(tools: list[dict]) -> str:
"""坏 prompt —— 只有规则描述,没有示例"""
tool_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
return f"""你是一个智能助手。请使用以下格式回答:
Thought: 你的思考
Action: 工具名
Action Input: 参数
Observation: 结果
可用工具:
{tool_desc}
请先思考再行动。"""
@staticmethod
def good(tools: list[dict]) -> str:
"""好 prompt —— 一个完整的 few-shot 示例"""
tool_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
return f"""你是一个数据分析助手。请严格遵守以下格式:
Thought: <思考为什么需要这个步骤>
Action: <工具名称>
Action Input: <JSON 格式的参数>
Observation: <工具执行结果(由我提供)>
或者当你有最终答案时:
Thought: <思考为什么可以结束了>
Final Answer: <你的最终回复>
可用工具:
{tool_desc}
---
示例:
用户:北京今天天气怎么样?
Thought: 用户想知道北京的天气,我需要调用天气查询工具。
Action: get_weather
Action Input: {{"city": "北京", "unit": "celsius"}}
Observation: 北京今天晴,气温 15-22°C,微风。
Thought: 已经获取了天气信息,可以回答用户了。
Final Answer: 北京今天晴天,气温 15-22°C,风力较小。
---
现在开始执行。遵守上面的格式,不要跳过 Thought。"""
@staticmethod
def production(tools: list[dict], preferences: dict = None) -> str:
"""生产级 prompt —— 多个示例 + 边界情况处理 + 偏好设置"""
tool_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
tool_names = ", ".join(t["name"] for t in tools)
prefs = ""
if preferences:
prefs = "\n用户偏好:\n" + "\n".join(f"- {k}: {v}" for k, v in preferences.items())
return f"""你是一个 ReAct Agent。你必须严格遵守以下格式输出。
## 输出格式
每一步必须按以下顺序输出(不要跳过任何一步):
Thought: <解释你为什么要做这个动作,1-2 句话>
Action: <工具名称,必须是以下之一:{tool_names}>
Action Input: <严格的 JSON 格式参数,key 用双引号>
Observation: <我会在下轮提供执行结果>
当你有了足够的信息来回答用户时:
Thought: <解释为什么可以结束了>
Final Answer: <你的最终回复>
## 禁止行为
1. 不要编造 Observation——Observation 由我提供
2. 不要使用不在工具列表中的工具
3. 不要在 Action Input 中使用单引号(必须用双引号)
4. 不要连续两次调用同一个工具(除非参数不同)
5. 如果已经尝试了 5 步还没有进展,请总结当前信息并输出 Final Answer
## 示例
示例一:工具调用
用户:比较北京和上海的人口
Thought: 用户需要比较两个城市的人口,我需要分别查询北京和上海的人口数据。先查北京。
Action: get_city_population
Action Input: {{"city": "北京"}}
Observation: 北京人口约 2189 万。
Thought: 已获取北京的数据,接下来查询上海的人口。
Action: get_city_population
Action Input: {{"city": "上海"}}
Observation: 上海人口约 2487 万。
Thought: 两个城市的数据都获取到了,可以比较并回答用户。
Final Answer: 北京人口约 2189 万,上海人口约 2487 万,上海比北京多约 298 万人。
示例二:信息不足时直接说明
用户:火星上有水吗?
Thought: 这个问题涉及天文学知识,我不需要调用任何工具,可以直接回答。但需要注意我掌握的信息可能不是最新的。
Final Answer: 根据目前的研究,火星上存在水冰形式的,主要分布在两极地区和地下。
{prefs}
现在开始执行。"""Few-shot 比纯指令有效,因为 LLM 本质是模式匹配。给它看一个 JSON 格式示例,它大概率模仿。示例是最好的指令。
示例五:上下文窗口耗尽的应对策略
ReAct 每步追加 Thought + Action + Observation,上下文增长很快。下面是三种应对策略:
"""
ReAct 上下文管理
"""
class ContextStrategy:
"""上下文管理策略"""
@staticmethod
def sliding_window(messages: list[dict], keep: int = 8, system: str = "") -> list[dict]:
"""
策略一:滑动窗口
只保留最近的 N 条消息。
"""
system_msgs = [m for m in messages if m.get("role") == "system"]
non_system = [m for m in messages if m.get("role") != "system"]
return system_msgs + non_system[-keep:]
@staticmethod
def summary_compression(llm, messages: list[dict], keep: int = 5, system: str = "") -> list[dict]:
"""
策略二:摘要压缩
用 LLM 把早期步骤压缩成一条摘要消息。
"""
system_msgs = [m for m in messages if m.get("role") == "system"]
non_system = [m for m in messages if m.get("role") != "system"]
if len(non_system) <= keep:
return messages
early = non_system[:-keep]
early_text = "\n".join(f"{m['role']}: {m['content']}" for m in early)
prompt = f"""请将以下对话历史压缩为一段摘要,只保留:
1. 用户的原始目标
2. 已完成的工具调用和关键结果
3. 当前的进度和发现
不要保留 Thought 和 Action 的细节。
{early_text}
"""
summary = llm.generate(prompt, system="你是对话摘要专家。输出要简洁。")
compressed = system_msgs + [
{"role": "system", "content": f"[早期对话摘要]\n{summary}"},
] + non_system[-keep:]
return compressed
@staticmethod
def tool_output_truncation(messages: list[dict], max_observation_len: int = 300) -> list[dict]:
"""
策略三:工具输出截断
从源头控制 Observation 的长度。
"""
result = []
for msg in messages:
if msg.get("role") == "tool" and len(msg["content"]) > max_observation_len:
truncated = msg["content"][:max_observation_len] + "...(已截断)"
result.append({**msg, "content": truncated})
else:
result.append(msg)
return result
@staticmethod
def hybrid(llm, messages: list[dict], token_threshold: float = 0.7) -> list[dict]:
"""
混合策略:截断 + 滑动窗口 + 摘要压缩
"""
messages = ContextStrategy.tool_output_truncation(messages, max_observation_len=500)
messages = ContextStrategy.sliding_window(messages, keep=12)
estimated_tokens = sum(len(m.get("content", "")) // 3 for m in messages)
if estimated_tokens > token_threshold * 4096:
messages = ContextStrategy.summary_compression(llm, messages, keep=5)
return messages以 20 步 ReAct 为例,不做处理直接超限制报错。滑动窗口省到约 1600 token 但丢失前 12 步。摘要压缩约 1200 token 但多一次 LLM 调用。混合策略最实用——从源头截断加事后压缩。
示例六:幻觉传播链——ReAct 中最隐蔽的 bug
某一步的幻觉会传播到后续所有步骤,因为每轮的 Observation 会追加到上下文,LLM 下一轮把它当事实:
"""
幻觉传播链分析
"""
class HallucinationTracker:
"""
幻觉传播追踪器
标记哪些信息是工具返回的,哪些是 LLM 推断的。
如果最终答案引用了未被工具验证的信息,发出警告。
"""
def __init__(self):
self.steps: list[dict] = []
self.facts: dict[str, str] = {}
self.unverified_claims: list[str] = []
def record_step(self, thought: str, action: str, observation: str) -> None:
"""记录一步,区分事实和推断"""
self.steps.append({
"thought": thought,
"action": action,
"observation": observation,
})
if observation:
self._extract_facts(observation, source="observation")
if thought:
self._extract_claims(thought)
def _extract_facts(self, text: str, source: str) -> None:
"""从 Observation 中提取事实"""
import re
patterns = [
r"(\S+)\s+(?:是|有|为|约)\s+(.+?)[\.\,,。]",
r"(\S+):\s*(.+?)[\.\,,。]",
]
for pattern in patterns:
for match in re.finditer(pattern, text):
key = match.group(1).strip()
value = match.group(2).strip()
self.facts[f"{key}: {value}"] = source
def _extract_claims(self, thought: str) -> None:
"""从 Thought 中提取声明"""
import re
patterns = [
r"(?:因此|所以|说明|意味着|看来).+?[\.\,,。]",
r"(?:应该|可能|大概|似乎).+?[\.\,,。]",
]
for pattern in patterns:
for match in re.finditer(pattern, thought):
self.unverified_claims.append(match.group().strip())
def verify_final_answer(self, answer: str) -> list[str]:
"""验证最终答案——检查是否引用了未被验证的信息"""
import re
unverified = []
for claim in self.unverified_claims:
if any(word in answer for word in re.findall(r'\w{2,}', claim)):
supported = any(
fact_key in claim or fact_val in claim
for fact_key, fact_val in (f.split(": ") for f in self.facts)
)
if not supported:
unverified.append(claim)
return unverified
def report(self) -> str:
"""生成追踪报告"""
lines = [
f"共记录 {len(self.steps)} 步",
f"已验证事实: {len(self.facts)} 条",
f"未验证声明: {len(self.unverified_claims)} 条",
]
if self.unverified_claims:
lines.append("\n未验证声明:")
for claim in self.unverified_claims:
lines.append(f" - {claim}")
return "\n".join(lines)
# 使用示例
tracker = HallucinationTracker()
tracker.record_step(
thought="数据已读取,接下来分析趋势",
action="analyze(data)",
observation="Q4 增长 15%,总销售额 500 万",
)
tracker.record_step(
thought="看来 Q3 表现不佳,可能是季节性因素",
action="search('Q3 销售下降原因')",
observation="未找到相关信息",
)
tracker.record_step(
thought="因此 Q3 下降主要是因为市场竞争加剧",
action="",
observation="",
)
report = tracker.report()
print(report)ReAct 中每一步的 Thought 可以包含推断,但这些推断不一定是事实。如果后续步骤没有通过工具调用验证,它们就作为伪事实传播到最终答案。我个人的做法是定期让 LLM 自我审查——哪些结论有工具数据支持,哪些只是推断。
讨论
ReAct 真的有必要吗?
不需要用的场景:
- 工具调用是固定的、线性的。比如"先读文件再分析再画图",不需要 LLM 每步思考接下来该做什么。直接写代码按顺序调就行
- 只有 1-2 个工具,LLM 每次都会选对(因为没别的选择)。Thought 成了废话
- 对延迟要求高,多一轮 LLM 调用多 2-5 秒
- 5 个以上工具,LLM 需要根据当前信息动态选择
- 任务步骤不固定,需要根据中间结果决定下一步
- 需要调试和审查——Thought 日志能帮你理解 Agent 的决策过程
- 任务涉及搜索或探索,Agent 不知道答案在哪
Thought 是不是废话?能不能省?
很多人觉得 Thought 就是一堆废话,想省掉来节省 token。至少我目前的情况,不能完全省掉:
- Thought 是 LLM 的 Chain of Thought。模型先生成推理过程再生成答案,准确率会显著提升
- Thought 是调试的生命线。没有它,只看得到
Action: readcsv到Observation: 成功,但不知道 Agent 为什么选 readcsv - Thought 约束了 LLM 的行为。没有 Thought 的 prompt,LLM 更可能输出不规范格式
# 低质量 Thought(纯复述):
Thought: 我需要读取文件。
Action: read_csv
# 高质量 Thought(有推理):
Thought: 用户提到"销售数据",但没有指定格式。根据文件扩展名 .csv,
应该用 read_csv 工具而不是 read_excel。如果文件不存在,
需要提示用户确认路径。
Action: read_csv(path="sales.csv")低质量 Thought 没有提供 Action 之外的任何信息。高质量 Thought 包含:为什么选这个工具、排除了哪些其他选项、有什么前置条件需要检查。
提升方向:在 few-shot 示例中展示高质量 Thought;在 system prompt 中要求解释为什么选这个工具而不是其他;用结构化 Thought(JSON 格式的决策理由)替代自由文本。
ReAct 的代价
| 代价 | 说明 | 缓解方案 |
|---|---|---|
| LLM 调用次数暴增 | 每步一次调用。10 步 = 10 次 | Plan-Then-Act 策略减少步数 |
| 延迟高 | 每次 2-5 秒。10 步就是 20-50 秒 | 流式输出 Thought |
| 格式不稳定 | LLM 可能不按格式输出 | Few-shot + 三级解析器 |
| 循环失控 | 反复调同一个工具找不到答案 | max_steps + 进度检查 prompt |
| 上下文膨胀 | 每步都追加,很快触 token 限制 | 滑动窗口 / 摘要压缩 / 截断 |
| 幻觉传播 | 未验证的推断传播到后续步骤 | HallucinationTracker + 定期自查 |
| 工具选择偏差 | LLM 倾向关键词匹配而非功能匹配 | 工具描述包含"不适合"场景 |
最容易被滥用的场景
每步 Thought 都是废话:
Thought: 我需要读取文件。
Action: read_csv
Observation: 成功
Thought: 文件读完了,我需要分析。
Action: analyze
Observation: 分析完成
Thought: 分析完了,我输出结果。
Action: output
Observation: 输出完成这种没有任何推理价值,说明任务不需要 ReAct,直接写代码按顺序调用就行。
循环 20 步还没结果: max_steps 设为 10 跑不完,改成 20 还是没完。Agent 迷失了,在随机试工具。
解决:系统 prompt 加进度检查——"尝试了 5 步没有进展就总结当前信息并输出 Final Answer"。
Thought 和 Action 脱节:
Thought: 我需要分析销售数据的季度趋势
Action: read_image("photo.jpg")Thought 说分析数据,Action 读图片。解决:系统 prompt 里加工具使用规则,要求 Thought 和 Action 逻辑一致。
更简单的替代方案
| 场景 | 替代方案 | 说明 |
|---|---|---|
| 固定步骤 | 写死代码 | data = readcsv(); result = analyze(data) |
| OpenAI 生态 | Function Calling | SDK 自动处理 Action/Observation 循环 |
| 需要高质量输出 | Few-shot 示例 | prompt 里给 2-3 个正确示例,LLM 模仿格式 |
| 需要规划 | 先输计划再执行 | 比 ReAct 少一半 LLM 调用 |
和其他模式的关系
ReAct 是边想边做,Plan-Execute 是先想好再做。ReAct 更灵活但不稳定,Plan-Execute 更稳定但不够灵活。
Reflexion = ReAct + 自我评估。ReAct 只管执行,Reflexion 执行完还要判断好不好。
ReAct 的循环也可以用状态模式实现:ThoughtState 到 ActionState 到 ObservationState 循环。多工具串联时,每个工具是责任链节点,ReAct 决定走哪条链。
框架中的实际应用
| 框架 | ReAct 实现 | 说明 |
|---|---|---|
| LangChain | createreactagent() | 最经典的实现,内置 output parser 和 system prompt |
| LlamaIndex | ReActAgent | 与检索增强结合,工具中包含向量检索 |
| OpenAI | Function Calling + 循环 | 本质一样:模型输出函数调用到执行到返回结果 |
| Anthropic | Tool Use + 循环 | Claude 的原生工具调用,同样遵循 Thought 到 Action 到 Observation |
演进路线
ReAct 不是起点,是逐步演进出来的。
最初可能只是直接调工具,步骤固定不需要 LLM 决策:
data = read_csv("sales.csv")
result = analyze(data)后来发现需要 LLM 决定用什么工具,但每步独立调用:
tool_name = llm.choose_tool(user_input)
result = tools.execute(tool_name)再后来发现需要多步串联,让 LLM 一次性输出所有步骤(Plan-Execute):
plan = llm.generate_plan(user_input)
for tool in plan:
result = tools.execute(tool, result)最后发现中间结果会影响后续决策,这才上了 ReAct:
for _ in range(max_steps):
thought, action = llm.think_and_act(context)
observation = tools.execute(action)
context += f"\nThought: {thought}\nObservation: {observation}"先写最简单的代码,发现需要多步决策、中间结果影响后续,再引入 ReAct。
使用判断
该用(至少命中两条):
- 3 个以上工具,LLM 需要动态选择
- 任务步骤不固定,依赖中间结果
- 需要调试日志追踪每步决策理由
- 任务涉及探索或搜索,无法预先规划全部步骤
- 工具调用固定、线性
- 只有 1-2 个工具
- 对延迟要求高(小于 5 秒)
- 任务可以预先规划全部步骤
写在最后
ReAct 是 Agent 架构的核心执行模式,但不是万能的。很多场景下固定工具调用或预先规划反而更高效稳定。ReAct 的真正价值在于处理不确定性——当 Agent 不知道该做什么、需要根据中间结果动态决策时,Thought 到 Action 到 Observation 循环才是最合适的选择。