【AI应用开发】ReAct 原理是什么?和普通直接提问 LLM 差别在哪?
1. 一句话理解 ReAct
ReAct = Reasoning(推理)+ Acting(行动),是一种让 LLM 在解决问题时"边想边做"的范式。
普通 LLM 面对问题 → 直接输出答案(一步完成)
ReAct Agent 面对问题 → 思考→行动→观察→思考→行动→观察→...→输出答案(循环完成)
2. ReAct 名字拆解:三个关键词
ReAct 这个缩写由三个词组成,分别对应 Agent 循环中的三种输出模式:
ReAct = Reasoning + Acting
│ │
▼ ▼
Thought Action
(思考/推理) (行动/调用工具)
│
▼
Observation
(观察/获取结果)
| 组件 | 英文 | 含义 | 形式 |
|---|---|---|---|
| Thought | 思考/推理 | LLM 分析当前状态,决定下一步做什么 | 自然语言推理描述 |
| Action | 行动 | LLM 选择调用哪个工具及其参数 | 结构化工具调用 |
| Observation | 观察 | 接收工具执行结果,作为下一轮思考的输入 | 工具返回的原始数据 |
3. 普通 LLM 的"一次推理"模式
先理解普通 LLM 是怎么工作的,才能理解 ReAct 改进了什么。
3.1 普通 LLM 的工作流程
用户提问 → [Prompt 构造] → LLM 一次推理 → 输出答案
完整走一遍:
用户:"北京今天天气怎么样?"
普通 LLM 处理:
┌─────────────────────────────────────────────┐
│ 输入: "User: 北京今天天气怎么样?" │
│ │
│ LLM 内部一次推理: │
│ "我不知道今天天气,但我可以根据常识推测..." │
│ │
│ 输出: "北京今天可能是晴天,大约25度左右" │ → 幻觉!没有真实数据
└─────────────────────────────────────────────┘
核心特征:只有一次输入→一次输出的过程。
3.2 普通 LLM 的根本困境
LLM 的知识 = 训练数据(截止到某个时间点)
问题类型 │ 普通 LLM 的表现
──────────────────────┼─────────────────────
"写一首诗" │ ✅ 完美,需要创意
"1+1等于几" │ ✅ 完美,基础常识
"今天深圳天气" │ ❌ 瞎编,知识已过期
"我公司上月销量" │ ❌ 瞎编,私有数据
"帮我订机票" │ ❌ 无法行动,只能说话
普通的 LLM 就像一个被锁在房间里的天才会话者------他脑子很好,但看不到外面,也动不了手。
4. ReAct 的"循环推理"模式
4.1 ReAct 的工作流程
ReAct 给 LLM 装上了"眼睛"和"手":
用户提问 → Thought → Action → Observation → Thought → Action → Observation → ... → Final Answer
↑ │
└──────────────── 循环往复 ──────────────────────────┘
走一遍同一个问题:
用户:"北京今天天气怎么样?"
ReAct Agent 处理:
┌─────────────────────────────────────────────────────────┐
│ Thought 1: 用户想知道北京今天的天气,我需要调用天气API │
│ │
│ Action 1: get_weather(city="北京") │
│ │
│ Observation 1: "北京今天多云,23°C,湿度65%" │
│ │
│ Thought 2: 已经获得了真实的天气数据,可以直接回答用户 │
│ │
│ Final Answer: "北京今天多云,气温 23°C,湿度 65%" │
└─────────────────────────────────────────────────────────┘
这一次,LLM 没有瞎猜,因为它拿到了真实的、实时的数据。
4.2 为什么是"循环"?
关键是 ReAct 允许 LLM 在每一次 Action 后重新"观察"结果,然后再次"思考"。这个循环让 Agent 能够:
- ✅ 发现第一次工具结果不够 → 再调用一次
- ✅ 发现工具调用失败 → 换一个工具
- ✅ 发现需要多步操作 → 继续下一步
- ✅ 判断任务已完成 → 输出最终答案
5. 两者核心差异对比
5.1 一张表说清
| 维度 | 普通 LLM 直接提问 | ReAct Agent |
|---|---|---|
| 推理次数 | 1 次 | 多次循环 |
| 外部工具 | 无 | 可调用任意工具 |
| 数据来源 | 仅训练记忆 | 工具实时返回 + 训练记忆 |
| 回答准确性 | 依赖训练知识,易幻觉 | 基于真实工具数据,更准确 |
| 可处理任务 | 简单的、知识类 | 复杂的、需要实时信息/操作的 |
| 错误处理 | 无法自查 | 可观察错误并重试 |
| Token 消耗 | 低 | 高(多轮对话) |
| 响应速度 | 快(一次推理) | 慢(多次推理 + 工具等待) |
| 实现复杂度 | 极低 | 中等 |
5.2 直观对比:同一个任务的不同表现
任务:"查一下茅台今天的股价,如果低于 1800 就建议买入,否则建议观望"
普通 LLM 回答:
"茅台今天的股价大概是 1750 左右,建议您关注..."
→ 问题:股价是编的,决策没有依据
ReAct Agent 回答:
Thought 1: 需要获取茅台实时股价
Action 1: stock_price("600519")
Observation 1: 当前价 1782.50
Thought 2: 1782.50 < 1800,触发买入条件
Thought 3: 已有了所有需要的信息
Final Answer: "茅台(600519)当前股价 1782.50 元,低于 1800 元阈值,建议考虑买入。"
→ 每个数字都是真实数据,决策有据可查
6. ReAct 的运作机制深度拆解
6.1 完整循环详解
┌──────────────────────────┐
│ 接收用户输入 │
└────────┬─────────────────┘
│
▼
┌──────────────────────────┐
│ LLM 产生 Thought │ ← "我应该做什么?需要什么工具?"
│ (分析现状,规划下一步) │
└────────┬─────────────────┘
│
▼
┌──────────────────────────┐
│ LLM 产生 Action │ ← "调用哪个工具?参数是什么?"
│ (选择工具及参数) │
└────────┬─────────────────┘
│
▼
┌──────────────────────────┐
│ 执行工具调用 │ ← 你的代码实际运行工具函数
│ 获取 Observation │
└────────┬─────────────────┘
│
▼
┌──────────────────────────┐
│ Observation 喂回 LLM │ ← 工具结果拼入上下文
└────────┬─────────────────┘
│
▼
┌──────────────────────────────────┐
│ LLM 判断:任务完成了吗? │
│ │
│ ├── 没有 → 产生新的 Thought │ → 回到循环开头
│ │ │
│ └── 完成了 → 输出 Final Answer │ → 退出循环
└──────────────────────────────────┘
6.2 每一步的 Prompt 怎么变
以一个真实的调用序列为例,看看每一轮 Prompt 的变化:
第 1 轮 ------ LLM 决定调用工具
System: 你是一个能使用工具的助手。可用工具: get_weather
User: 北京今天天气怎么样?
LLM 输出:
Thought: 需要查询北京天气
Action: get_weather(city="北京")
第 2 轮 ------ 工具结果注入后 LLM 再推理
System: 你是一个能使用工具的助手。可用工具: get_weather
User: 北京今天天气怎么样?
Assistant: Thought: 需要查询北京天气
Action: get_weather(city="北京")
Tool Result: "北京今天多云,23°C"
LLM 输出:
Thought: 已拿到真实天气数据,可以直接回答
Final Answer: "北京今天多云,气温 23°C"
关键观察:每一轮工具结果都被追加到对话历史中,LLM 能"看到"之前发生了什么,基于新信息做下一步决策。
6.3 多步 ReAct 的完整示例
用户: "帮我查一下苹果(AAPL)和谷歌(GOOGL)今天股价,告诉我哪个涨得多"
Round 1:
Thought: 需要分别查询两只股票的股价
Action: stock_price(symbol="AAPL")
Observation: AAPL 当前 $175.30, 涨跌幅 +1.2%
Round 2:
Thought: AAPL 数据拿到了,还需要查 GOOGL
Action: stock_price(symbol="GOOGL")
Observation: GOOGL 当前 $141.80, 涨跌幅 +0.8%
Round 3:
Thought: 两只股票的数据都有了。
AAPL +1.2% vs GOOGL +0.8%,AAPL 涨得更多
Final Answer: "今天 AAPL 涨 1.2%,GOOGL 涨 0.8%,苹果涨得更多。"
7. 为什么 ReAct 能降幻觉?
这是 ReAct 最核心的价值。降低幻觉的机制有三层:
7.1 真实数据替代记忆猜测
普通 LLM:
"茅台股价多少?" → 翻内部记忆 → 记忆中可能有错误训练数据 → 瞎编
ReAct:
"茅台股价多少?" → 调用股票 API → 拿到实时精确数据 → 基于数据回答
幻觉的第一来源是模型记忆不准确。ReAct 通过工具获取外部真实数据,直接绕过这一问题。
7.2 观察发现矛盾时可纠正
Round 1:
Thought: 用户要查"A公司Q2营收"
Action: search(query="A公司 Q2 营收")
Observation: "未找到 A公司 Q2 营收数据" ← 搜索无结果
Round 2:
Thought: 搜索结果没有 Q2 数据,但可能有其他相关财报
Action: search(query="A公司 最新财报 营收")
Observation: "A公司 2024 Q3 财报:营收 5.2 亿" ← 找到了更近期的
Round 3:
Thought: 只找到了 Q3 数据,没有 Q2 的。诚实告知用户。
Final Answer: "未找到 A公司 Q2 营收数据,最新的 Q3 财报显示营收 5.2 亿。"
普通 LLM 在搜索无果时,很可能直接开始编造。ReAct 的"观察→再思考"机制让 LLM 有机会承认"没找到"而不是"编一个"。
7.3 多步交叉验证
搜索工具 A 返回: "iPhone 16 起售价 6999 元"
搜索工具 B 返回: "iPhone 16 起售价 5999 元"
ReAct 可以:
Thought: 两个来源数据不一致,需要更多交叉验证
Action: search(query="iPhone 16 官方价格")
Observation: "苹果官网显示 iPhone 16 起售价 5999 元"
Thought: 官网价格确认为 5999 元,以官网为准
8. 手写 ReAct Agent 完整实现
下面是一个完整可运行的 ReAct Agent 实现(约 150 行),使用 OpenAI API:
python
"""
ReAct Agent 手写实现
零框架依赖,只用了 openai 库做 LLM 推理
"""
import json
import re
from openai import OpenAI
client = OpenAI() # 需要设置 OPENAI_API_KEY 环境变量
# ============================================================
# 第一部分:定义工具
# ============================================================
def get_weather(city: str) -> str:
"""获取城市天气(模拟)"""
weather_data = {
"北京": "多云,23°C,湿度 45%",
"上海": "晴,28°C,湿度 60%",
"深圳": "阵雨,30°C,湿度 80%",
}
return weather_data.get(city, f"未找到 {city} 的天气数据")
def calculator(expression: str) -> str:
"""安全执行数学计算"""
try:
allowed = set("0123456789+-*/().% ")
if not all(c in allowed for c in expression):
return "错误:表达式包含不允许的字符"
result = eval(expression)
return str(result)
except Exception as e:
return f"计算错误:{e}"
def web_search(query: str) -> str:
"""模拟网络搜索"""
search_results = {
"茅台股价": "贵州茅台(600519)当前股价 1782.50 元",
"iPhone": "iPhone 16 起售价 5999 元",
}
for key, value in search_results.items():
if key in query:
return value
return f"关于「{query}」未找到相关结果"
# 工具注册表
TOOLS = {
"get_weather": {
"function": get_weather,
"description": "获取指定城市的天气信息",
"parameters": {"city": "城市名称,例如'北京'"},
},
"calculator": {
"function": calculator,
"description": "执行数学计算,支持加减乘除",
"parameters": {"expression": "数学表达式,例如'3*15'"},
},
"web_search": {
"function": web_search,
"description": "搜索网络信息",
"parameters": {"query": "搜索关键词"},
},
}
# ============================================================
# 第二部分:ReAct Prompt 模板
# ============================================================
SYSTEM_PROMPT = """你是一个能使用工具的智能助手。请严格按照以下格式回复:
当需要调用工具时:
Thought: [你的推理过程,分析当前状态和下一步计划]
Action: tool_name(param1="value1", param2="value2")
当获得工具结果后需要继续思考:
Thought: [基于观察结果的下一步推理]
Action: tool_name(...) 或 Final Answer
当你有了足够信息回答用户时:
Thought: [最终推理总结]
Final Answer: [你的完整回答]
重要规则:
1. 每次只能调用一个工具
2. 必须先有 Thought 再有 Action
3. 不要在 Final Answer 之前输出任何其他内容
4. 如果工具调用失败或返回空结果,尝试其他方法而不是编造
当前可用的工具:
{}
请开始。""".format(
"\n".join(
f"- {name}: {info['description']},参数:{info['parameters']}"
for name, info in TOOLS.items()
)
)
# ============================================================
# 第三部分:ReAct 循环引擎
# ============================================================
class ReactAgent:
def __init__(self, max_steps=10):
self.max_steps = max_steps
self.tools = TOOLS
def run(self, user_input: str) -> str:
"""执行 ReAct 循环"""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input},
]
step_count = 0
while step_count < self.max_steps:
step_count += 1
print(f"\n{'='*50}")
print(f"📍 Round {step_count}")
# 调用 LLM
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.0, # 确定性输出,适合工具调用
)
reply = response.choices[0].message.content
print(f"🤖 LLM 输出:\n{reply}")
# 将 LLM 回复加入对话历史
messages.append({"role": "assistant", "content": reply})
# 解析输出:判断是 Final Answer 还是 Action
if "Final Answer:" in reply:
final = reply.split("Final Answer:")[-1].strip()
print(f"✅ 最终回答: {final}")
return final
# 解析 Action
action_match = re.search(r'Action:\s*(\w+)\((.*?)\)', reply)
if not action_match:
# 没有解析到 Action,可能是格式错误
messages.append({
"role": "user",
"content": "请严格按照格式回复:先 Thought 再 Action 或 Final Answer"
})
continue
tool_name = action_match.group(1)
args_str = action_match.group(2)
# 解析参数
args = self._parse_args(args_str)
# 执行工具
if tool_name in self.tools:
result = self.tools[tool_name]["function"](**args)
else:
result = f"错误:未知工具 '{tool_name}'。可用工具:{list(self.tools.keys())}"
print(f"🔧 工具 {tool_name}({args}) → {result}")
# 将工具结果作为 Observation 加入对话
observation_msg = f"Observation: {result}"
messages.append({"role": "user", "content": observation_msg})
return "抱歉,达到最大执行步数,未能完成任务。"
def _parse_args(self, args_str: str) -> dict:
"""解析工具调用参数字符串为字典"""
args = {}
# 匹配 key="value" 模式
for match in re.finditer(r'(\w+)\s*=\s*"([^"]*)"', args_str):
args[match.group(1)] = match.group(2)
return args
# ============================================================
# 第四部分:运行
# ============================================================
if __name__ == "__main__":
agent = ReactAgent(max_steps=10)
# 测试一:单步工具
print("\n" + "="*60)
print(" 测试 1:单步工具调用")
print("="*60)
agent.run("北京今天天气怎么样?")
# 测试二:多步工具
print("\n" + "="*60)
print(" 测试 2:多步工具调用")
print("="*60)
agent.run("查一下茅台股价,再帮我算 1782.50 * 100 等于多少")
8.1 运行结果示例
==================================================
测试 1:单步工具调用
==================================================
==================================================
📍 Round 1
🤖 LLM 输出:
Thought: 用户想知道北京天气,需要调用天气工具
Action: get_weather(city="北京")
🔧 工具 get_weather({'city': '北京'}) → 多云,23°C,湿度 45%
==================================================
📍 Round 2
🤖 LLM 输出:
Thought: 已经拿到了北京的天气数据,可以回答了
Final Answer: 北京今天多云,气温 23°C,湿度 45%
✅ 最终回答: 北京今天多云,气温 23°C,湿度 45%
9. ReAct Prompt 模板的精妙之处
ReAct 的 Prompt 设计不是随意的,每一部分都有工程上的考量:
9.1 格式约束为什么这么严格
python
# ❌ 宽松格式 ------ LLM 容易不听话
"你可以调用工具,也可以直接回答"
# ✅ 严格格式 ------ LLM 知道每一步该输出什么
"""
当需要调用工具时:
Thought: ...
Action: tool_name(param="value")
当可以回答时:
Thought: ...
Final Answer: ...
"""
原因:不给 LLM 格式约束,它会在 Action 执行后直接又输出一个新的 Action(跳过 Observation 的消化),或者在 Action 和 Final Answer 混在一起输出,导致解析崩溃。
9.2 Thought 不是多余的
初学者常问:"既然有 Action,为什么还要 Thought?直接 Action 不行吗?"
答案是:Thought 强迫 LLM 做显式推理,极大提高行动质量。
有 Thought:
Thought: 用户要查天气和股价,这两个任务互相独立
我可以先查天气再查股价,但更高效的方法是...
Action: get_weather("北京")
无 Thought(直接 Action):
Action: get_weather("北京")
→ LLM 没有显式思考过程,容易选错工具或参数
Thought 的另一个作用是可解释性------用户能看到 Agent 的思考过程,知道它为什么做出了某个决定。
9.3 Observation 的角色
Observation 不是 LLM 生成的,而是系统注入的。它作为下一轮的上下文回填:
messages 数组随时间变化:
Round 1 输入: [system, user:"查天气"]
Round 1 LLM输出: "Thought: 需要天气工具\nAction: get_weather(city='北京')"
系统注入: "Observation: 多云,23°C"
Round 2 输入: [system, user, assistant(含Action), user(Observation)]
Round 2 LLM输出: "Thought: 拿到数据了\nFinal Answer: 今天多云..."
10. ReAct 的局限与改进方向
10.1 ReAct 的三大局限
| 局限 | 表现 | 影响 |
|---|---|---|
| Token 消耗大 | 每轮都要把完整历史发给 LLM | 长任务成本高 |
| 串行执行慢 | 一次只能调用一个工具 | 独立子任务不能并行 |
| 长链迷失 | 步骤多了 LLM 会忘记初衷 | 复杂任务容易偏航 |
10.2 改进方案
Plan-Execute:先规划全步骤,执行时不再每步推理,减少 Token 开销。
ReAct(每步都推理):
Thought(步1) → Action → Observation → Thought(步2) → Action → Observation → ...
Plan-Execute(规划一次,执行多步):
全局 Plan → 按步执行 → 仅在失败时重规划 → 汇总
ReWOO:规划与执行完全分离,可并行任务同时执行。
Tool-use with Parallel Calls:支持一次 Action 调用多个互不依赖的工具。
这些改进方案将在本系列《详解(三):Function Calling 的执行流程》中进一步展开。
11. 本篇总结
| 问题 | 答案 |
|---|---|
| ReAct 是什么? | Reasoning + Acting,让 LLM 边想边做的 Agent 范式 |
| 和普通 LLM 提问的核心区别? | ReAct 多了"思考→行动→观察"的循环,能获取外部真实数据 |
| 为什么能降幻觉? | 用工具获取真实数据替代记忆猜测,观察结果可验证 |
| Thought 有什么用? | 显式推理过程,提高决策质量 + 增加可解释性 |
| ReAct 有什么局限? | Token 消耗大、串行慢、长链易迷失 |
一句话记住:普通 LLM 是"一次推理,一次输出",ReAct Agent 是"多次思行循环,基于真实数据决策"。