L2 ReAct 循环:Agent 是怎么"思考—行动"的
课程目标:理解 ReAct 范式的核心机制,能用伪代码描述一个 ReAct 循环,并在脑中运行一遍完整的思考—行动—观察过程。
一、一句话回答#
如果 L1 你记住了一句话——"Agent = LLM + 工具 + 自主决策循环"——那么 L2 要记住的一句话是:
ReAct(Reasoning + Acting)是当前 Agent 实现"自主决策循环"的事实标准范式。
它的核心只有三步,反复循环:
Thought(思考):现在情况怎样,下一步该做什么
Action(行动):调用工具或决定给出最终答案
Observation(观察):工具返回的结果是什么,把它纳入下一轮思考
二、从一个真实问题看 ReAct#
假设你问 Agent:
"《辛德勒名单》的导演还导演过哪些奥斯卡最佳影片?"
注意:这个问题不能靠 LLM 的记忆直接回答——LLM 训练数据截止后的事它不知道,而且"奥斯卡最佳影片"这种精确信息容易记错。Agent 必须主动查证。
我们跟着 Agent 跑一遍 ReAct 循环:
🔄 第 1 轮循环#
Thought 1:
用户问的是辛德勒名单的导演还导演过哪些奥斯卡最佳影片。我需要先确认导演是谁,再查他的其他作品。辛德勒名单的导演是斯皮尔伯格,但我不完全确定,需要先查一下。
Action 1: Search["辛德勒名单 导演"]
Observation 1:
搜索结果:辛德勒名单(1993)由史蒂文·斯皮尔伯格(Steven Spielberg)导演。
🔄 第 2 轮循环#
Thought 2:
好的,确认是斯皮尔伯格。现在我需要列出他导演过的所有奥斯卡最佳影片。但我不知道"奥斯卡最佳影片"具体指什么——是最佳影片(Best Picture)还是他获得的最佳导演奖?需要查清楚。
Action 2: Search["斯皮尔伯格 奥斯卡 获奖"]
Observation 2:
搜索结果:斯皮尔伯格 1994 年凭《辛德勒名单》获最佳导演奖;2013 年凭《林肯》提名;1994 年《辛德勒名单》获最佳影片;其他作品多数是提名而非获奖。
🔄 第 3 轮循环#
Thought 3:
嗯,根据我的知识,斯皮尔伯格作为导演拿过最佳导演奖和最佳影片的,主要是《辛德勒名单》。但我不确定《拯救大兵瑞恩》有没有拿最佳影片。让我再查一下。
Action 3: Search["拯救大兵瑞恩 奥斯卡 最佳影片"]
Observation 3:
搜索结果:《拯救大兵瑞恩》(1998)获 1999 年奥斯卡 5 项大奖,但最佳影片颁给了《莎翁情史》。斯皮尔伯格获最佳导演提名但未获奖。
🔄 第 4 轮循环(终止)#
Thought 4:
我现在有足够信息了。斯皮尔伯格作为导演,唯一一次让作品获得奥斯卡最佳影片是《辛德勒名单》(1993)。他的其他作品多数是提名。我应该诚实地告诉用户这一点,而不是编造。
Action 4: Finish[根据可查证的资料,史蒂文·斯皮尔伯格作为导演,唯一让作品获得奥斯卡最佳影片奖的是《辛德勒名单》(1993 年)。他的其他作品如《拯救大兵瑞恩》《林肯》等虽获多项提名,但未获最佳影片。]
三、为什么 ReAct 这么重要?#
对比两种范式:
❌ 范式 A:纯 Chain-of-Thought(CoT)#
问题 → LLM 直接思考 → LLM 直接回答
问题:LLM 可能"自信地编造"。比如它可能直接回答"斯皮尔伯格还导演过《E.T.》并获最佳影片"——但 E.T. 没有获最佳影片(只提名)。这就是幻觉(hallucination)。
✅ 范式 B:ReAct#
问题 → 思考 → 行动 → 观察 → 思考 → 行动 → 观察 → ... → 回答
优势:每一步都有外部证据支撑(Observation)。LLM 不能凭空捏造,因为下一步思考必须基于上一步观察到的真实结果。
这就是 ReAct 论文(Yao et al., 2022,arXiv:2210.03629)证明的核心结论:ReAct 比纯 CoT 更能减少幻觉,因为它把推理"锚定"在外部事实上。
四、用伪代码写出 ReAct 循环#
抛开框架,我们用 30 行 Python 伪代码把 ReAct 循环的骨架写清楚:
def react_agent(question: str, tools: dict, llm_call, max_steps: int = 10):
"""
一个最简 ReAct Agent 的实现骨架
参数:
question: 用户的原始问题
tools: 工具字典,如 {"Search": search_function, "Lookup": lookup_function}
llm_call: 调用 LLM 的函数,接收 prompt 字符串,返回文本
max_steps: 最多循环多少步,防止死循环
"""
# 1. 准备初始 prompt:Few-shot 示例 + 用户问题
prompt = build_react_prompt(question) # 包含 Thought/Action/Observation 的格式说明和示例
# 2. 进入循环
for step in range(max_steps):
# 2a. LLM 思考 + 决定行动
llm_output = llm_call(prompt)
# 输出形如:
# Thought: 我需要先查导演是谁
# Action: Search[辛德勒名单]
# 2b. 解析 LLM 的输出,提取 Action
thought, action_name, action_input = parse_react_output(llm_output)
print(f"Step {step+1} | Thought: {thought}")
# 2c. 判断是否要终止
if action_name == "Finish":
return action_input # 这就是最终答案
# 2d. 执行工具(行动)
if action_name not in tools:
observation = f"Error: 工具 {action_name} 不存在"
else:
try:
observation = tools[action_name](action_input)
except Exception as e:
observation = f"Error: 工具执行失败: {e}"
# 2e. 把观察结果追加到 prompt(这样下一轮 LLM 能看到)
prompt += f"\nThought: {thought}\n"
prompt += f"Action: {action_name}[{action_input}]\n"
prompt += f"Observation: {observation}\n"
# 3. 超过最大步数仍未 Finish,强制返回
return "Agent 达到最大步数,未能完成回答。"
这段伪代码里最重要的三行#
prompt += f"\nObservation: {observation}\n"
这是 ReAct 的灵魂:每一步的观察都被追加回 prompt,作为下一步推理的事实基础。这就是为什么 ReAct 能减少幻觉——LLM 不是凭空思考,而是基于证据思考。
五、ReAct 在生产框架中的真实样貌#
1. smolagents 中的 ReAct(HuggingFace)#
src-002 §Unit 2 中,CodeAgent 内部就是 ReAct 循环,只是输出的是 Python 代码而不是 Thought/Action 文本:
from smolagents import CodeAgent, HfApiModel, tool
@tool
def search_wikipedia(query: str) -> str:
"""在 Wikipedia 上搜索关键词,返回第一段摘要。
Args:
query: 搜索关键词
"""
# 真实实现会调用 Wikipedia API
return f"这是关于 '{query}' 的搜索结果..."
agent = CodeAgent(tools=[search_wikipedia], model=HfApiModel())
result = agent.run("《辛德勒名单》的导演还导演过哪些奥斯卡最佳影片?")
print(result)
# 内部循环大致是:
# Thought → 生成 Python 代码调用 search_wikipedia → 执行 → 观察 → 再 Thought → ...
2. LangGraph 中的 ReAct(LangChain)#
src-002 §Unit 3 中,StateGraph 把 ReAct 显式建模为状态图:
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
graph = StateGraph(MessagesState)
graph.add_node("assistant", call_llm) # 思考节点
graph.add_node("tools", ToolNode([search_wikipedia])) # 行动节点
graph.add_edge(START, "assistant") # 开始 → 思考
graph.add_conditional_edges("assistant", tools_condition) # 思考后判断
graph.add_edge("tools", "assistant") # 行动 → 再思考(循环)
agent = graph.compile()
注意 LangGraph 把 ReAct 拆成了显式的图结构——这让你能可视化地看到 Agent 在"思考"和"行动"之间循环多少次,这对调试非常有用。
六、ReAct 的三个常见陷阱#
陷阱 1:无限循环#
LLM 可能陷入"查—查—查—查"永远不 Finish。对策:设置 max_steps,并设计一个"兜底 Action"。
陷阱 2:Thought 编造 Observation#
LLM 可能在 Thought 里写"我观察到 X 是 Y",但实际上 Observation 里没有这条信息。对策:在 prompt 里强调"Thought 必须只引用前面 Observation 中的内容"。
陷阱 3:Action 格式错误#
LLM 可能输出格式不对的 Action(比如 Search(辛德勒名单) 而不是 Search[辛德勒名单])。对策:使用结构化输出(Function Calling / Tool Use),让 LLM 只能按预定义 schema 返回 Action。这正是 L4 课要深入讲的。
七、和 L1 的连接#
回到 L1 我们说的三件套:
| 要素 | 在 ReAct 中对应什么 |
|---|---|
| LLM | 产生 Thought 的引擎 |
| Tools | Action 执行的实际工具 |
| Planning Loop | Thought → Action → Observation 的循环本身 |
ReAct 就是把这三件套串联起来的标准范式。所有现代 Agent 框架(smolagents、LangGraph、AutoGen、Semantic Kernel)底层都在跑 ReAct 循环,区别只是循环的载体(代码生成 vs 状态图 vs 群聊)。
八、动手时间:模拟 ReAct#
假设 LLM 是你本人。请用 Thought/Action/Observation 格式回答:
"帮我查一下北京到上海明天的高铁票,选最便宜的一班,把车次告诉我。"
📋 参考回答(点击展开)
Thought 1: 我需要先查明天北京到上海的高铁有哪些班次和价格。
Action 1: Search[北京到上海高铁 明天]
Observation 1: 明天共有 28 班高铁,价格在 309-553 元之间。
Thought 2: 最便宜的是 309 元。我需要确认这班的具体车次和出发时间。
Action 2: Search[G1 车次 北京南 上海 309]
Observation 2: G1 次列车,08:00 北京南发车,12:28 到达上海虹桥,二等座 309 元。
Thought 3: 信息齐全了,可以回答。
Action 3: Finish[最便宜的车次是 G1,明天 08:00 从北京南出发,12:28 到达上海虹桥,二等座 309 元。]
注意几个要点:
每一步 Thought 都基于前一步 Observation,不是凭空想象
真实场景中"最便宜"可能需要查多个数据源,这里为了简洁只查两次
真实 Agent 还要考虑用户偏好(如不要凌晨的班次),这会改变搜索策略
九、小结#
| 知识点 | 一句话回顾 |
|---|---|
| ReAct 全称 | Reasoning + Acting(推理 + 行动) |
| 三步循环 | Thought → Action → Observation → 回到 Thought |
| 核心价值 | 减少幻觉(推理锚定在外部证据上) |
| 来源 | Yao et al., 2022, arXiv:2210.03629 |
| 现代实现 | 所有主流框架底层都是 ReAct 循环 |
十、Read next#
下一节课 L3 用 smolagents 30 行代码构建第一个 Agent,我们会用真实的代码跑通一个 ReAct 循环,并亲眼看到 LLM 是怎么决定"该不该调工具"的。
主要参考:
src-002 §Unit 1 ReAct framework — HuggingFace 课程对 ReAct 的官方阐述
src-002 §Unit 2 smolagents guided tour — smolagents 的 @tool 和 CodeAgent 实战
ReAct 原论文:Yao et al., 2022, arXiv:2210.03629
十一、Sources#
src-002 — Hugging Face Agents Course · T1 · Course
ReAct 原论文 — Yao et al., 2022 · T1 · arXiv:2210.03629
💬 有疑问? 试试问我:"Thought 和 Action 必须分开吗?"、"为什么不用 ReAct 也能调工具?"、"smolagents 和 LangGraph 实现 ReAct 的区别是什么?"——我会用代码或比喻为你解答。
1. ReAct 循环中三个步骤的正确顺序是什么?
2. ReAct 相比纯 Chain-of-Thought 更能减少幻觉,根本原因是什么?
3. 在 ReAct 伪代码中,把 Observation 追加回 prompt 这一步为什么最关键?