6.1 引言:推理模式决定Agent的思维方式
在前几章里,我们已经搭好了智能体的"骨架"——大模型、工具调用、提示工程。但骨架本身不会思考,真正决定一个智能体"怎么想"的,是它的推理模式(Reasoning Pattern)。
打个比方:同样是去机场接人,有的人会一路走一路看路标、随时调整路线;有的人会先在手机上规划好完整路线,然后一步步执行,遇到堵车再重新规划。两种方式都能到达,但代价、速度、适应性完全不同。
推理模式就是智能体的"思维方式"。它定义了四个关键问题:
- 何时思考:每一步都思考,还是先规划再执行?
- 何时行动:思考完立即行动,还是按计划顺序行动?
- 如何纠错:基于每一步的观察纠错,还是基于整体进度重规划?
- 如何终止:达到某个中间状态就停,还是完成全部计划才停?
本章我们将深入两种最经典的推理模式——ReAct 和 Plan-and-Execute,用纯 Python 从零实现它们的推理引擎,并通过实战对比它们的优劣。最后我们会介绍混合模式 Plan-ReAct,并给出工程上的选型建议。
💡 小贴士:推理模式是智能体区别于普通"聊天机器人"的关键。聊天机器人只会一问一答,而智能体能"想一步做一步"或"先规划再执行",这种自主决策能力才是"智能"的体现。
6.2 ReAct模式深入解析
6.2.1 论文核心思想
ReAct 由 Yao 等人在 2022 年的论文《ReAct: Synergize Reasoning and Acting in Language Models》中提出。名字是 Reasoning + Acting 的缩写,核心思想一句话概括:让大模型在"推理"和"行动"之间交替进行,互相反馈。
🧠 生活比喻——遇到难题自言自语:回想一下你独自解题时的情景。你不会闷头乱试,而是会"自言自语":“这道题先求什么?先查一下北京的人口……嗯,2189万。那再查上海的人口……2487万。好,现在把它们加起来,2189+2487=4676。搞定!“这个"想一步→做一步→看结果→再想"的自言自语过程,就是 ReAct 的精髓。Thought 是你的自言自语,Action 是你动手查资料或算数,Observation 是你看到的结果。
在此之前,大模型做推理(如 Chain-of-Thought)只是"空想”,不接触外部信息;做行动(如 Tool Use)只是"反射”,不解释为什么。ReAct 把两者缝合起来,形成一个闭环:
1
|
Thought(思考) → Action(行动) → Observation(观察) → Thought → ...
|
这个循环会一直转,直到模型认为任务完成,输出 Finish 动作。
6.2.2 Thought-Action-Observation循环详解
让我们拆解这个循环的每一步:
- Thought(思考):模型基于当前任务和历史轨迹,推理"下一步该做什么、为什么"。这是显式的推理过程,写在提示里,让模型"想出声"——不是闷头做,而是把推理过程说出来。
- Action(行动):模型输出一个结构化的动作,通常是
工具名(参数) 格式,比如 search_wiki(北京)。系统解析后调用对应工具。
- Observation(观察):工具返回的结果被包装成观察值,喂回给模型,作为下一轮 Thought 的输入。
- 循环:回到第 1 步,直到模型输出
Finish(最终答案)。
这个循环的关键在于每一步都基于最新的观察——就像你查完一个资料后,会根据查到的内容决定下一步查什么,而不是死板地按预设路线走。这正是 ReAct 最大的优势:实时适应。
6.2.3 ReAct的优势与劣势
优势:
- 实时决策:每一步都根据最新观察思考,能处理动态环境。
- 灵活适应:工具返回意外结果时,模型可以立即调整策略。
- 可解释性强:Thought 链路清晰记录了每一步的推理过程,便于调试——出了问题你能看到模型在哪一步"想歪了"。
- 实现简单:一个循环 + 一个提示模板就能跑起来。
劣势:
- 可能陷入循环:模型在 Thought 和 Action 之间反复横跳——“我再查一下"“再确认一下”——迟迟不 Finish。
- 缺乏全局规划:每一步只看眼前,容易走弯路,对需要多步骤、有依赖关系的任务效率低。
- Token 消耗随步数线性增长:整个轨迹都要塞进上下文,步数多了成本和延迟都上来。
- 错误传播:某一步的 Thought 走偏,后续步骤会基于错误前提继续,没有"全局重规划"机制。
6.2.4 完整Python实现
下面我们用纯 Python(不用任何框架)实现一个完整的 ReAct 推理引擎。代码分三部分:工具定义(智能体能用什么)、提示模板(告诉模型怎么输出)、主循环(驱动 Thought-Action-Observation 循环)。模型用 gpt-5.4,通过 OpenAI 兼容接口调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
# react_agent.py
# ReAct 推理引擎的纯 Python 实现,不依赖任何框架
import re
from typing import Callable
from openai import OpenAI
client = OpenAI() # 默认从环境变量 OPENAI_API_KEY 读取密钥
MODEL = "gpt-5.4"
MAX_STEPS = 10 # 安全阀:防止智能体陷入无限循环
# ---------- 工具定义 ----------
def search_wiki(query: str) -> str:
"""模拟维基百科检索工具,实际项目可替换为真实API"""
fake_db = {
"北京": "北京是中华人民共和国首都,常住人口约2189万(2020年)。",
"上海": "上海是中国直辖市,GDP居中国城市首位(2023年约4.72万亿元)。",
"深圳": "深圳是中国经济特区之一,位于广东省,常住人口约1768万。",
}
# 简单的关键词匹配,实际项目用向量检索或API
for key, val in fake_db.items():
if key in query:
return val
return f"未找到与'{query}'相关的信息。"
def calculate(expression: str) -> str:
"""安全计算器工具,支持基本四则运算"""
try:
# 只允许数字和运算符,防止代码注入
if not re.match(r'^[\d\s+\-*/().]+$', expression):
return "错误:表达式包含非法字符"
# ⚠️ eval 有安全风险,这里靠正则白名单防护;生产环境建议用 ast.literal_eval
result = eval(expression, {"__builtins__": {}}, {})
return f"{expression} = {result}"
except Exception as e:
return f"计算错误:{e}"
# 工具注册表:名字 -> (函数, 描述, 参数说明)
# 智能体通过这张表知道"自己有哪些工具、每个工具怎么用"
TOOLS: dict[str, tuple[Callable, str, str]] = {
"search_wiki": (search_wiki, "检索维基百科获取城市信息", "query: 检索关键词"),
"calculate": (calculate, "执行四则运算", "expression: 数学表达式字符串"),
}
# ---------- 提示模板 ----------
# 这是 ReAct 的"灵魂"——用提示词教模型按固定格式输出 Thought 和 Action
REACT_PROMPT = """你是一个使用 ReAct 模式推理的智能体。
可用工具:
{tool_desc}
请严格按以下格式输出,每一步包含两行:
Thought: 你的推理过程,解释为什么这么做
Action: 工具名(参数)
(系统会返回 Observation,然后你继续下一轮 Thought)
当任务完成时,输出:
Thought: 任务已完成的推理
Action: Finish(最终答案)
任务:{task}
{history}
Thought:"""
def build_tool_desc() -> str:
"""构造工具描述文本,注入提示模板"""
lines = []
for name, (_, desc, params) in TOOLS.items():
lines.append(f"- {name}: {desc},参数:{params}")
return "\n".join(lines)
def parse_action(text: str) -> tuple[str, str]:
"""从模型输出中解析出 Action 名和参数,例如 search_wiki(北京)"""
# 用正则匹配 "Action: 工具名(参数)" 的格式
match = re.search(r'Action:\s*(\w+)\((.*)\)', text)
if not match:
# 解析失败:可能是模型没按格式输出,直接当作最终答案返回
return "Finish", text
return match.group(1), match.group(2)
def run_tool(action_name: str, args: str) -> str:
"""执行工具调用,返回结果字符串"""
if action_name == "Finish":
return args # Finish 不调用工具,直接返回最终答案
if action_name not in TOOLS:
return f"错误:未知工具 {action_name}"
func, _, _ = TOOLS[action_name]
try:
# 简单参数解析:去掉首尾引号后直接传入
args = args.strip().strip('"').strip("'")
return func(args)
except Exception as e:
return f"工具执行错误:{e}"
def call_llm(prompt: str) -> str:
"""调用大模型,返回文本"""
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0, # 推理任务用低温度,保证输出稳定可复现
)
return resp.choices[0].message.content
def react_agent(task: str) -> str:
"""ReAct 主循环:Thought → Action → Observation → 重复"""
history = "" # 累积的推理轨迹,每轮都塞进提示让模型"看到"完整历史
for step in range(MAX_STEPS):
# 构造提示:工具描述 + 任务 + 历史轨迹
prompt = REACT_PROMPT.format(
tool_desc=build_tool_desc(),
task=task,
history=history,
)
# 提示模板末尾预留了 "Thought:",模型会从这里自然续写推理过程
output = call_llm(prompt).strip()
# 把模型输出拼回历史轨迹(包含它写的 Thought 和 Action)
history += output + "\n"
# 解析出动作名和参数
action_name, args = parse_action(output)
print(f"[Step {step+1}] Action: {action_name}({args})")
# 如果模型说 Finish,任务完成,返回最终答案
if action_name == "Finish":
return args
# 否则执行工具,把结果作为 Observation 拼回历史
observation = run_tool(action_name, args)
history += f"Observation: {observation}\n"
print(f"[Step {step+1}] Observation: {observation}\n")
return "达到最大步数,任务未完成。"
|
💡 小贴士:
MAX_STEPS 是安全阀——没有它,模型可能无限循环"再查一下”。生产环境建议配合超时机制。
temperature=0 让推理过程稳定可复现,调试阶段尤其重要:同样的输入应该产出同样的轨迹。
eval 有安全风险,这里靠正则白名单防护。生产环境务必换成 ast.literal_eval 或专门的数学表达式解析库。
6.2.5 实战示例:用ReAct完成信息检索任务
我们用一个简单任务测试:“查询北京和上海的人口,然后计算两地人口之和。”
1
2
3
4
5
|
if __name__ == "__main__":
task = "查询北京和上海的人口(万人),然后计算两地人口之和。"
result = react_agent(task)
print("\n=== 最终答案 ===")
print(result)
|
运行后你会看到类似这样的轨迹(具体文字取决于模型):
1
2
3
4
5
6
7
8
9
10
|
[Step 1] Action: search_wiki(北京)
[Step 1] Observation: 北京是中华人民共和国首都,常住人口约2189万(2020年)。
[Step 2] Action: search_wiki(上海)
[Step 2] Observation: 上海是中国直辖市,GDP居中国城市首位(2023年约4.72万亿元)。
[Step 3] Action: calculate(2189+2487) # 模型可能记得上海人口约2487万
[Step 3] Observation: 2189+2487 = 4676
[Step 4] Action: Finish(北京约2189万,上海约2487万,两地人口之和约4676万人)
|
注意看这个轨迹:ReAct 在每一步都根据上一步的观察决定下一步——它一开始并不知道要查两次再算一次,是"走一步看一步"地探索出来的。查完北京才知道北京2189万,再查上海,查完上海才有两个数字可以加。这正是 ReAct 的精髓,也是它的局限:对这种有明显依赖关系的多步任务,它的探索过程其实有点低效——每一步都要把完整历史塞进提示,Token 消耗随步数线性增长。
6.3 Plan-and-Execute模式深入解析
6.3.1 先规划再执行的分层架构
Plan-and-Execute 模式(在 LangChain 中也被称为 Plan-and-Solve)针对 ReAct 的"缺乏全局规划"问题,提出了分层架构:
🧠 生活比喻——出差列行程单:想象你要去上海出差三天。你不会到了机场才开始想"接下来干嘛",而是出发前一晚坐在桌前,列一张行程单:“Day 1:飞上海、入住酒店;Day 2:上午见客户A、下午参观工厂B;Day 3:收尾、返程。“然后按行程单一步步执行。如果 Day 1 航班延误,你就调整行程——把工厂参观推到 Day 3。这就是 Plan-and-Execute:先列行程单(Planner),再按单执行(Executor),遇到变故就改单(Replanner)。
1
2
3
4
5
|
Planner(规划器) → 生成完整步骤列表
↓
Executor(执行器) → 逐步执行每个步骤
↓
Replanner(重规划器)→ 根据执行结果决定下一步
|
它的核心思想是:先让模型把整个任务拆解成有序的步骤列表,再逐步执行,执行过程中根据结果动态调整剩余计划。这种"先想清楚再做"的方式,更接近人类处理复杂任务的思维方式。
6.3.2 三个核心组件
Planner(规划器):接收用户原始任务,输出一个有序的步骤列表。规划时它只看任务本身,不看执行结果——这叫"开环规划”,好比出发前凭经验列行程,还不知道路上会不会堵车。
Executor(执行器):接收单个步骤,调用工具或子智能体完成它,返回执行结果。执行器本身可以是 ReAct 智能体,也可以是简单的工具调用——就像行程单上每一项你可以自己完成,也可以委托给同事。
Replanner(重规划器):接收"原始任务 + 已完成步骤及结果 + 剩余步骤”,决定:
- 如果任务已完成 → 输出最终答案;
- 如果需要调整 → 输出新的剩余步骤列表(相当于改行程单)。
重规划器是 Plan-and-Execute 的灵魂——它让计划成为"活"的,而不是死板的脚本。没有重规划器,Planner 的一次性规划出了错就会一路错到底。
6.3.3 完整Python实现
下面是 Plan-and-Execute 的纯 Python 实现。它由三个函数组成——planner(列行程单)、executor(执行单步)、replanner(改行程单)——外加一个主循环把它们串起来。代码复用了上一节 ReAct 实现中的工具表和 call_llm 函数,避免重复定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
# plan_execute_agent.py
# Plan-and-Execute 推理引擎的纯 Python 实现
import re
import json
# 复用 react_agent.py 中的工具表、工具执行器和 LLM 调用函数
# call_llm 内部已创建好 OpenAI client 并使用 gpt-5.4,这里无需重复创建
from react_agent import TOOLS, run_tool, call_llm, build_tool_desc
MAX_REPLANS = 8 # 重规划次数上限,防止无限重规划
# ---------- Planner:列出"行程单" ----------
PLANNER_PROMPT = """你是一个任务规划器。给定一个任务,把它拆解成有序的执行步骤。
可用工具:
{tool_desc}
要求:
1. 每个步骤应该是一个具体的、可执行的动作
2. 步骤之间可以有依赖,但顺序要合理
3. 最后一步应该是"汇总并给出最终答案"
任务:{task}
请用JSON数组输出步骤列表,每个元素是一个字符串,例如:
["步骤1: ...", "步骤2: ...", "步骤3: 汇总答案"]
只输出JSON,不要其他文字。"""
def planner(task: str) -> list[str]:
"""生成初始计划——相当于出发前列行程单"""
prompt = PLANNER_PROMPT.format(tool_desc=build_tool_desc(), task=task)
output = call_llm(prompt).strip()
# 容错处理:模型可能用 ```json ... ``` 包裹输出,需要剥掉
output = output.strip("`").replace("json\n", "", 1)
try:
steps = json.loads(output)
if isinstance(steps, list):
return [str(s) for s in steps]
except json.JSONDecodeError:
pass # JSON 解析失败,走兜底逻辑
return [f"执行任务:{task}", "汇总答案"] # 兜底:至少有个最小计划
# ---------- Executor:执行单步 ----------
EXECUTOR_PROMPT = """你是一个步骤执行器。执行给定的单个步骤,可以调用工具。
可用工具:
{tool_desc}
步骤:{step}
已完成的步骤及结果:
{past}
请直接输出该步骤的执行结果(自然语言即可),如果需要调用工具请说明用了哪个工具。"""
def executor(step: str, past: list[tuple[str, str]]) -> str:
"""执行单个步骤——相当于按行程单做一件事"""
past_text = "\n".join(f"- {s} → {r}" for s, r in past) or "(无)"
prompt = EXECUTOR_PROMPT.format(
tool_desc=build_tool_desc(),
step=step,
past=past_text,
)
output = call_llm(prompt)
# 简化处理:如果输出里提到调用某工具,尝试解析并执行
# 实际项目里这里可以嵌套一个完整的 ReAct 循环来执行复杂步骤
tool_match = re.search(r'(\w+)\(([^)]*)\)', output)
if tool_match and tool_match.group(1) in TOOLS:
result = run_tool(tool_match.group(1), tool_match.group(2))
return f"{output}\n[工具结果] {result}"
return output
# ---------- Replanner:检查进度,决定是否改"行程单" ----------
REPLANNER_PROMPT = """你是一个重规划器。根据当前进度决定下一步。
原始任务:{task}
已完成的步骤及结果:
{past}
剩余的原始计划:
{remaining}
请判断:
- 如果任务已完成,输出:FINISH: <最终答案>
- 如果需要调整计划,输出新的剩余步骤JSON数组(替换上面的remaining)
只输出 FINISH: ... 或 JSON数组,不要其他文字。"""
def replanner(task: str, past: list[tuple[str, str]], remaining: list[str]) -> tuple[str, list[str]]:
"""重规划:检查进度,返回 (状态, 新剩余步骤)"""
past_text = "\n".join(f"- {s} → {r}" for s, r in past) or "(无)"
remaining_text = "\n".join(remaining) or "(无)"
prompt = REPLANNER_PROMPT.format(
task=task,
past=past_text,
remaining=remaining_text,
)
output = call_llm(prompt).strip().strip("`")
# 优先检查是否输出 FINISH 信号
if output.startswith("FINISH:"):
return "finish", [output[len("FINISH:"):].strip()]
# 否则尝试解析为新的步骤列表
try:
steps = json.loads(output.replace("json\n", "", 1))
if isinstance(steps, list):
return "continue", [str(s) for s in steps]
except json.JSONDecodeError:
pass # 解析失败则保持原计划不变
return "continue", remaining
# ---------- 主循环:规划 → 执行 → 重规划 → 重复 ----------
def plan_execute_agent(task: str) -> str:
"""Plan-and-Execute 主循环"""
# 第一步:Planner 生成初始计划
plan = planner(task)
print(f"[初始计划] 共{len(plan)}步:")
for i, s in enumerate(plan, 1):
print(f" {i}. {s}")
print()
past: list[tuple[str, str]] = [] # 已完成的步骤及结果
remaining = plan # 剩余未执行的步骤
for _ in range(MAX_REPLANS):
if not remaining:
break # 所有步骤执行完毕
# 取下一步执行
current = remaining[0]
print(f"[执行] {current}")
result = executor(current, past)
past.append((current, result)) # 记录已完成步骤
print(f"[结果] {result}\n")
# 重规划:检查是否完成、是否需要调整剩余计划
status, new_remaining = replanner(task, past, remaining[1:])
if status == "finish":
return new_remaining[0] # 任务完成,返回最终答案
remaining = new_remaining # 更新剩余计划(可能不变,也可能调整)
# 兜底:超过重规划上限,让模型基于历史给出答案
return executor("基于以上所有结果,汇总最终答案", past)
if __name__ == "__main__":
task = "查询北京和上海的人口(万人),然后计算两地人口之和。"
result = plan_execute_agent(task)
print("\n=== 最终答案 ===")
print(result)
|
运行后你会看到 Planner 先生成一个清晰的三步计划(查北京 → 查上海 → 算总和),然后逐步执行,每步之后 Replanner 确认进度。整个流程比 ReAct 更"有章法"——先有计划再动手,对这种结构化任务效率明显更高。
💡 小贴士:
- Planner 的规划是"开环"的——它不预知执行结果,所以计划可能不准。这正是 Replanner 存在的意义:执行过程中动态修正。
executor 里对工具调用的解析比较简化(用正则匹配 工具名(参数))。生产环境建议换成结构化输出(如 function calling),更可靠。
- 如果某一步本身很复杂,可以把
executor 换成完整的 react_agent——这就是下一节混合模式的思路。
6.4 两种模式对比
为了让你直观感受两种模式的差异,我们用一个详细表格对比:
| 维度 |
ReAct |
Plan-and-Execute |
| 决策方式 |
每步实时决策,走一步看一步 |
先全局规划,再按计划执行 |
| 思考粒度 |
局部(只看当前观察) |
全局(看整体任务和进度) |
| 适用任务 |
探索性、动态、不可预测的任务 |
结构化、多步骤、有依赖的任务 |
| Token消耗 |
较高(每步都要塞完整轨迹) |
较低(Planner一次性输出,Executor上下文短) |
| 延迟分布 |
每步延迟相近,单步快 |
首次规划延迟高,后续每步快 |
| 错误恢复 |
基于单步观察纠错,反应快但可能反复 |
基于整体进度重规划,纠错更系统 |
| 循环风险 |
高(容易陷入"再查一下"循环) |
低(计划有明确终点) |
| 全局一致性 |
弱(可能走偏) |
强(有计划约束) |
| 实现复杂度 |
简单(一个循环) |
中等(三个组件) |
| 可解释性 |
Thought链路清晰 |
计划+执行轨迹清晰 |
| 典型场景 |
客服问答、实时检索、交互式调试 |
项目管理、数据分析流水线、复杂研究 |
一个直观的经验:任务越能预先拆解,越适合 Plan-and-Execute;任务越需要边走边看,越适合 ReAct。 用我们前面的比喻:如果任务像"出差"有明确目的地和日程,就列行程单(Plan-and-Execute);如果任务像"在陌生城市找一家还开着的老字号面馆",就边走边问边调整(ReAct)。
6.5 混合模式:Plan-ReAct
两种模式并非水火不容。实际工程中最常用的往往是混合模式:先用 Plan-and-Execute 规划大纲,每个步骤内部用 ReAct 执行。这样既保证全局方向,又保留单步的灵活性。
用出差的比喻来说:行程单上每一项(比如"参观工厂B")到了现场可能遇到各种情况——门卫不让进、负责人临时外出——这时你不会死等,而是像 ReAct 一样"想一步做一步"地灵活处理。大纲用 Plan,细节用 ReAct,这就是混合模式。
1
2
3
4
5
6
|
Planner 生成大纲
↓
对每个大纲步骤:
用 ReAct 智能体执行(可调用工具、可循环)
↓
Replanner 检查进度,必要时调整剩余大纲
|
6.5.1 代码示例
下面这个实现复用了前面的 planner、replanner 和工具函数。核心思路是:外层用 Plan-and-Execute 的"规划→执行→重规划"循环,但把"执行"这一步换成 ReAct 风格的灵活执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
# plan_react_agent.py
# 混合模式:Plan-ReAct(外层规划,内层ReAct)
import re
# 复用前两个模块的组件,避免重复造轮子
from react_agent import call_llm, TOOLS, run_tool
from plan_execute_agent import planner, replanner
MAX_OUTER = 6 # 大纲步数上限
def react_on_step(step: str, context: str) -> str:
"""用 ReAct 风格执行单个大纲步骤。
context 是前序步骤的执行结果,作为这一步的背景信息。"""
# 把大纲步骤包装成一个"迷你任务"交给智能体
prompt = f"""你是一个执行单个步骤的智能体,可用工具:{list(TOOLS.keys())}。
步骤:{step}
上下文:{context}
请执行步骤,需要时调用工具,最后给出该步骤的结果。"""
output = call_llm(prompt)
# 简化处理:如果输出里提到工具调用,解析并执行
# 完整实现可嵌套 react_agent 函数,这里为了演示做了简化
m = re.search(r'(\w+)\(([^)]*)\)', output)
if m and m.group(1) in TOOLS:
tool_result = run_tool(m.group(1), m.group(2))
return f"{output}\n[工具结果] {tool_result}"
return output
def plan_react_agent(task: str) -> str:
"""混合模式主循环:外层 Plan-and-Execute,内层 ReAct"""
# 第一步:Planner 生成大纲
plan = planner(task)
print(f"[Plan] 大纲共{len(plan)}步:")
for i, s in enumerate(plan, 1):
print(f" {i}. {s}")
print()
past: list[tuple[str, str]] = [] # 已完成步骤及结果
remaining = plan
context = "(开始)" # 上下文:把前一步结果传给后一步
for _ in range(MAX_OUTER):
if not remaining:
break
current = remaining[0]
# 关键区别:用 ReAct 风格执行这一步(而非简单工具调用)
print(f"[ReAct执行] {current}")
result = react_on_step(current, context)
past.append((current, result))
context = result # 把上一步结果作为下一步的上下文
print(f"[结果] {result}\n")
# 重规划:检查进度,必要时调整大纲
status, new_remaining = replanner(task, past, remaining[1:])
if status == "finish":
return new_remaining[0]
remaining = new_remaining
# 兜底:让智能体基于所有结果汇总答案
return react_on_step("基于以上结果汇总最终答案", context)
if __name__ == "__main__":
task = "查询北京和上海的人口(万人),然后计算两地人口之和。"
print(plan_react_agent(task))
|
混合模式继承了两种模式的优点:Planner 保证方向不跑偏,ReAct 保证单步能灵活应对意外。代价是实现复杂度更高,需要小心处理两层循环之间的状态传递——context 变量就是连接外层规划和内层执行的桥梁。
💡 小贴士:本例中 react_on_step 做了简化,只做一轮工具调用。真正的混合模式里,这一步应该嵌套完整的 react_agent 函数,让每个大纲步骤内部都能"想一步做一步"地多轮探索。感兴趣的读者可以试着把 react_on_step 的实现替换成调用 react_agent(mini_task)。
6.6 推理模式的工程选择
理论讲完了,落到工程上到底该选哪个?下面是实战中的选型建议。
6.6.1 什么时候用ReAct
- 任务不可预测:用户问题五花八门,无法预先规划,如开放域问答、客服对话。
- 环境动态变化:工具返回结果高度不确定,需要实时适应,如实时数据查询、交互式调试。
- 任务步骤少:通常 2-4 步就能完成,规划的开销不划算。
- 延迟敏感:希望尽快给出第一个有意义的动作,不能等规划完成。
6.6.2 什么时候用Plan-and-Execute
- 任务结构化:步骤明确、有依赖关系,如"先取数、再清洗、再分析、再出报告"。
- 步骤多:5 步以上,ReAct 容易跑偏或陷入循环。
- 需要全局一致性:最终结果依赖各步骤的协同,不能走一步看一步。
- 成本敏感:长任务下 Plan-and-Execute 的总 Token 消耗通常更低。
6.6.3 什么时候用混合模式
- 任务既有结构又有不确定性:大纲清晰但每步内部需要探索。
- 复杂研究型任务:如"调研某技术的现状,输出报告"——先规划调研维度,每个维度用 ReAct 深挖。
- 生产环境:混合模式的鲁棒性最好,是多数真实系统的选择。
6.6.4 决策表格
| 任务特征 |
推荐模式 |
理由 |
| 步骤≤3、不可预测 |
ReAct |
规划开销不划算,实时适应更重要 |
| 步骤≤3、可预测 |
ReAct 或 Plan-and-Execute |
都可以,ReAct 更简单 |
| 步骤4-6、有依赖 |
Plan-and-Execute |
全局规划避免走弯路 |
| 步骤>6、结构化 |
Plan-and-Execute |
ReAct 易陷入循环 |
| 步骤>6、部分不可预测 |
混合模式 Plan-ReAct |
兼顾方向与灵活 |
| 实时对话/低延迟 |
ReAct |
首次响应快 |
| 批处理/后台任务 |
Plan-and-Execute |
总成本和一致性更优 |
| 研究型、开放性 |
混合模式 Plan-ReAct |
大纲+深挖 |
一个实用的工程经验:从 ReAct 开始,它能用最少代码跑通 80% 的任务;当发现模型频繁陷入循环、或长任务成本失控时,再升级到 Plan-and-Execute 或混合模式。不要一上来就上复杂架构,过早优化是智能体项目的常见陷阱。
💡 小贴士:选型不是一锤子买卖。项目初期用 ReAct 快速验证可行性,跑起来后根据实际瓶颈(循环?成本?一致性?)决定是否升级。这种"先跑通再优化"的思路,比一开始就纠结架构选型高效得多。
6.7 小结
本章我们深入了智能体的"思维方式"——推理模式:
- ReAct 通过 Thought-Action-Observation 循环实现实时决策,就像遇到难题自言自语——灵活但易陷入循环,适合探索性任务。
- Plan-and-Execute 通过 Planner-Executor-Replanner 分层架构实现全局规划,就像出差前列行程单——结构化但反应慢,适合多步骤任务。
- 两者各有优劣,混合模式 Plan-ReAct 取长补短——大纲用 Plan 保证方向,细节用 ReAct 保证灵活,是生产环境的常见选择。
- 工程选型的核心判断:任务能否预先拆解决定用不用 Plan,环境是否动态决定用不用 ReAct。
需要强调的是,推理模式只是智能体"怎么想"的部分。智能体还要解决"怎么记“的问题——跨步骤、跨会话地保存和调用信息。这正是下一章的主题。
6.8 预告:第7章——记忆系统
到目前为止,我们的智能体每次启动都是"失忆"的——所有上下文都靠提示塞进去,会话结束就丢失。但真正的智能体需要记住:
- 短期记忆:当前会话内的关键事实和中间结果;
- 长期记忆:跨会话的用户偏好、历史决策、学到的经验;
- 工作记忆:当前任务的"草稿本”,类似人类的工作记忆。
第7章我们将实现一个完整的智能体记忆系统,包括记忆的写入、检索、遗忘、压缩机制,并讨论如何用向量数据库构建可扩展的长期记忆。届时,我们的智能体将真正"长记性"。
💡 本章代码:所有代码均为纯 Python 实现,不依赖 LangChain 等框架,便于你理解底层机制。建议动手跑一遍,观察两种模式在同一个任务上的轨迹差异——这种直观感受比读十遍论文都管用。