ReAct 、Plan-and-Solve、Reflection:三种 Agent 范式的原理与代码
ReAct 、Plan-and-Solve、Reflection:三种 Agent 范式的原理与代码
一句话让Agent变成昇腾专家,昇腾任务轻松搞定。评测入口:请按这个开源仓接入昇腾图谱 https://gitcode.com/agent0/kg-tools
适用:刚开始接触 Agent,想把 ReAct、Plan-and-Solve、Reflection 这三套循环搞清楚
前置:能读懂 Python 的函数和字典,知道 LLM 的输入输出是什么;不需要 GPU,也不需要 API key
口径:示例在 Python 3.13 上运行;模型调用用占位函数替代,接真模型只需换掉这一个函数
一个 Agent 和一次普通问答的区别,只有一件事:它能不能自己去看一眼,再决定下一步。这件事落到代码里就是一个循环:模型说「我要查一下」,程序去查,把结果塞回去,模型接着说。循环转几圈,答案就出来了。所有 Agent 框架,无论包装得多复杂,中间都是这个循环。
ReAct、Plan-and-Solve、Reflection 是给这个循环加东西的三种位置。ReAct 加在每一步的动作上,Plan-and-Solve 加在执行之前的计划上,Reflection 加在一次任务结束之后的复盘上。三者解决的不是同一类问题,也不存在谁替代谁。
一、先看清共同点:Agent 就是一个循环
一次普通的模型调用是「输入进、输出出」,中间没有回头的机会。Agent 在中间插了一步:模型可以要求调用某个工具,拿到结果再继续。这个「再继续」就是循环。
用户问题
↓
[模型] ── 要调工具 ──▶ [工具 / 环境]
↑ │
└────── 结果写回上下文 ◀────┘
│
└── 不再调工具 ──▶ 输出答案
拆开看只有四件东西:一个模型、一组工具、一份不断变长的上下文、一个停下来的条件。第一版 Agent 出问题,基本都出在这四件里:工具的参数没校验、上下文被无关内容撑爆、终止条件写得太松导致死循环。
三种范式的差别,全在「往这四件上加什么」。
| ReAct | Plan-and-Solve | Reflexion | |
|---|---|---|---|
| 循环体是什么 | 一步动作 | 整个任务 | 一次尝试(trial) |
| 多出来的是什么 | 工具与环境 | 一份先定好的计划 | 一个判卷器和一份复盘 |
| 谁决定下一步 | 模型看 observation 现定 | 按计划表走 | 上一轮的复盘 |
结论:三个范式的区别不在「高级程度」,而在循环套在哪一层。
二、ReAct:想一步、做一步、看一步
ReAct 要解决的问题很具体:模型不知道的事,让它自己去查,而不是硬猜。
2.1 设计原理
ReAct 把模型的输出分成两类,这两类对世界的影响完全不同。
一类是 thought,也就是推理过程。它只写进上下文,下一轮模型看得见,但外部世界什么都没发生,数据库没被查、文件没被改。另一类是 action,它会被程序真的执行,回来的 observation 由外部给出,不是模型编的。
这个区分是整个范式的支点。纯 CoT 的推理链再长,也只在参数里打转:某一步记错了,后面顺着错的往下推,推得还很流畅,从文本上看不出问题。加上 action 之后,每一步都能拿外部返回对一次账,错了当场就露出来。
2.2 工作流程
① 问题 + 历史 → 拼成 prompt
② 模型输出:Thought + Action
③ 执行 Action,得到 Observation
④ Observation 原样写回历史 ──┐
⑤ 回到 ② ←┘
直到模型输出 Final Answer,或撞到步数上限

图里只有 Action 那一格会改变外部状态,Thought 是留给下一轮自己的字条。第 ③ 步和第 ④ 步之间有两个容易写错的地方。一是终止条件只认 Final Answer,不能认「看起来像答案」,否则模型在 observation 里复述一遍也会被当成结束。二是 observation 要原样回灌,一旦顺手总结,模型拿到的是我们的总结,不是证据。
2.3 代码示例
这段代码只保留循环本身。工具是两个 lambda,模型是一个按 observation 条数返回脚本的函数。
"""最小 ReAct 循环:Thought / Action / Observation,不依赖框架。"""
import re
KB = {"SKU-42": 19.9}
TOOLS = {"lookup": lambda s: f"{s} 单价 {KB[s]} 元",
"calc": lambda e: str(round(eval(e, {"__builtins__": {}}, {}), 2))} # eval 仅演示
def llm(prompt):
"""占位模型:按已有 Observation 条数取下一条,接真模型时只换这个函数。"""
return ["Thought: 不知道单价,先查表。\nAction: lookup[SKU-42]",
"Thought: 单价 19.9,再算 37 件九折。\nAction: calc[19.9 * 37 * 0.9]",
"Thought: 数字齐了。\nFinal Answer: 662.67 元"][prompt.count("Observation:")]
PAT = re.compile(r"Thought:\s*(.+?)\s*Action:\s*(\w+)\[(.*?)\]", re.S)
def react(q, max_steps=6):
pad = ""
for _ in range(max_steps):
out = llm(f"Question: {q}\n{pad}")
if "Final Answer:" in out:
return out.split("Final Answer:")[1].strip()
m = PAT.search(out)
if not m:
return f"格式不合规:{out!r}"
obs = TOOLS[m[2]](m[3]) # 循环里只有这一行改变外部状态
step = f"Thought: {m[1]}\nAction: {m[2]}[{m[3]}]\nObservation: {obs}\n"
print(step)
pad += step
return "达到步数上限,没有给出答案"
print("答案:", react("SKU-42 买 37 件打九折,一共多少钱?"))
跑出来是这样:
Thought: 不知道单价,先查表。
Action: lookup[SKU-42]
Observation: SKU-42 单价 19.9 元
Thought: 单价 19.9,再算 37 件九折。
Action: calc[19.9 * 37 * 0.9]
Observation: 662.67
答案: 662.67 元
这段代码有三处要留意。工具放在字典里,Action 的名字只能命中 TOOLS 的键,模型写错一个名字就落到解析失败分支,不会执行任意函数。max_steps 是兜底,真模型偶尔会一直查下去,没有这个上限就是死循环。解析那段最脆弱。真模型常在 Action 后面加解释、用中文括号,或者把两个动作挤在一行。工程上有两种处理:在 prompt 里把格式钉死,或者解析失败时把错误原文回灌让它重写。直接抛异常,整个循环会停在一步格式错误上。
2.4 适用场景
适合:信息在外部且工具够得着,比如检索、数据库、内部 API;任务状态会被动作改变,必须边做边看;出了问题要能追到是哪一步、哪个动作。
不适合:答案基本在参数里,查一次反而更慢;工具有副作用,比如下单、发消息、删数据,一步错就回不去;步数预算很紧,每一步都贵。
三、Plan-and-Solve:先把清单列出来
这个范式没有引入新的执行结构,它改的是提示词里的一句话。
3.1 设计原理
「Let’s think step by step」有个隐含假设:模型知道该走几步。这句话只要求「有过程」,没规定过程的形状,所以漏步来自这个提示词自身的缺陷,与模型聪不聪明无关。模型可以从第一步直接跳到结论,中间那步它觉得显然,但对题目来说那步才是关键。
Plan-and-Solve 的做法是把步骤集合从执行里提出来,先定型再走。第一步只让模型输出子任务清单,第二步按清单逐条执行。执行时每一步都能看到完整计划和前面步骤的结果,所以它知道自己在哪儿、还差什么。
Plan-and-Solve 还有一个加强版 PS+,追加了三条要求:把题目里的变量和数值抽出来、显式计算中间量、最后回头检查一遍计算。前两条治的是「算错」,第三条治的是「算完不验」。
3.2 工作流程
① 问题 → 让模型只做计划,输出 k 个子任务
② 拆出计划表(这一步是程序做的,不是模型)
③ 取第 i 步,连同完整计划 + 已完成结果拼成 prompt
④ 模型给出这一步的结果,记进「已完成」
⑤ i + 1,回到 ③,直到走完
⑥ 汇总最后一步的结果作为答案

图里的箭头是单向的,执行阶段不回头改计划。计划基于开工前的信息做出来,执行中拿到新信息也不改,代价就在这里。
3.3 代码示例
计划和执行拆成两次调用,执行阶段把完整计划和已完成步骤一起塞进 prompt。
"""Plan-and-Solve:计划和执行分两次调用,执行阶段不回头改计划。"""
PLAN = """1. 查出 SKU-42 的单价
2. 算 37 件的原价
3. 按九折算应付金额"""
STEPS = [("查出 SKU-42 的单价", "单价 19.9 元"),
("算 37 件的原价", "19.9 × 37 = 736.3"),
("按九折算应付金额", "736.3 × 0.9 = 662.67")]
def llm(prompt):
"""占位模型:按提示词里的步骤返回预设结果,真模型下换成一次 completion。"""
if "先做计划" in prompt:
return PLAN
return next((r for k, r in STEPS if f"当前步骤:{k}" in prompt), "")
def plan_and_solve(q):
plan = [l.split(".", 1)[1].strip() for l in llm(f"{q}\n先做计划").splitlines() if l.strip()]
print("计划:", plan)
done = []
for step in plan:
ctx = "\n".join(f"已完成:{d}" for d in done) # 执行时要能看到完整计划和已完成的步骤
result = llm(f"{q}\n计划:{plan}\n{ctx}\n当前步骤:{step}")
done.append(f"{step} → {result}")
print(f"[{step}] {result}")
return done[-1].split("=")[-1].strip()
print("答案:", plan_and_solve("SKU-42 买 37 件打九折,一共多少钱?"))
计划: ['查出 SKU-42 的单价', '算 37 件的原价', '按九折算应付金额']
[查出 SKU-42 的单价] 单价 19.9 元
[算 37 件的原价] 19.9 × 37 = 736.3
[按九折算应付金额] 736.3 × 0.9 = 662.67
答案: 662.67
这里有个隐蔽的坑:执行阶段的 prompt 必须带上完整计划。只传当前步骤,模型不知道自己在第几步,第三步会把前两步重算一遍,等于做了三遍功。代码里 ctx 那一行就是干这个的。
3.4 适用场景
适合:步骤能事先列全,执行中基本不变;一次性任务,不值得为它攒 few-shot 样本;中间结果要给人看,计划本身就是一份可审的产物。
不适合:环境会中途变卦,计划基于旧信息,工具返回意外结果时它没有改计划的入口;一步能做完的任务硬拆成五步,只多花四次调用。
3.5 和 ReAct 的关系
两者是正交的,不冲突。ReAct 决定每一步怎么拿信息,Plan-and-Solve 决定步骤从哪来。先列计划、执行每一步时允许调工具,就是常见的 plan-and-execute 结构,LangGraph 的教程里有同名示例。
只上 Plan-and-Solve 不上工具,需要查证的信息只能靠模型编;只上 ReAct 不上计划,跑长了容易在局部打转。这两个短板正好是对方补的那一层。
四、Reflexion:把失败写成一句话
前两个范式比的是这一轮做得好不好,Reflexion 换了个问法:下一轮能不能比这一轮强。
4.1 设计原理
ReAct agent 有个常见的死法:每次重试都从同一个静态 prompt 出发,上次怎么错,这次还怎么错。它没有记忆,只有上下文,而上下文每轮都重置。
Reflexion 不动权重,动记忆。它由三部分组成:Actor 干活,Evaluator 判卷,Self-Reflection 写复盘。判卷器给一个分数或一个通过/不通过,Self-Reflection 把这个信号连同轨迹翻成一段自然语言批评,存进记忆,下一轮开头念一遍。
参数没动,动的是上下文。
4.2 工作流程
① Actor 按当前记忆执行任务,交出结果
② Evaluator 判卷:跑测试 / 编译 / 规则检查,给出通过与否
③ 通过 → 结束;不通过 → Self-Reflection 写一段批评
④ 批评写入记忆(跨轮累积)
⑤ 下一轮 trial,记忆前置进 prompt,回到 ①

三个角色里,判卷器的质量决定上限。它判不准,后面的复盘就是在放大噪声。
4.3 代码示例
判卷靠执行:exec 跑 Actor 交上来的函数,再逐条比对期望值。
"""Reflexion:Actor 执行、Evaluator 判卷、Self-Reflection 写复盘,复盘跨轮传递。"""
TESTS = [("add(2, 3)", 5), ("add(-1, 1)", 0), ("add(0, 0)", 0)]
def evaluate(code):
ns = {}
exec(code, ns) # 演示用,生产环境必须放子进程或容器
return [f"{e} 得到 {eval(e, ns)},期望 {v}" for e, v in TESTS if eval(e, ns) != v]
def actor(memory):
"""占位 Actor:没有记忆时交带 bug 的版本,读到复盘后交修复版。"""
return "def add(a, b):\n return a - b" if not memory else "def add(a, b):\n return a + b"
def reflect(memory, fails):
"""占位 Self-Reflection:真模型下把代码、报错、旧记忆一起丢给模型。"""
return f"第 {len(memory) + 1} 次失败:{fails[0]}。减法方向写反了,改成 a + b。"
def run(task, max_trials=3):
memory = []
for i in range(1, max_trials + 1):
code = actor(memory)
fails = evaluate(code)
print(f"trial {i}:", "通过" if not fails else f"失败 {fails[0]}")
if not fails:
return code
memory.append(reflect(memory, fails))
print(" 复盘写入记忆:", memory[-1])
print(run("实现 add(a, b)"))
trial 1: 失败 add(2, 3) 得到 -1,期望 5
复盘写入记忆: 第 1 次失败:add(2, 3) 得到 -1,期望 5。减法方向写反了,改成 a + b。
trial 2: 通过
def add(a, b):
return a + b
Actor 和 Reflection 是占位实现。evaluate 执行三条用例、逐条比对结果。生产环境里 exec 必须隔离,放子进程或容器,别在主进程执行模型生成的代码。
4.4 和 Self-Refine 的区别
这两个名字很像,机制差得远:
| Self-Refine | Reflexion | |
|---|---|---|
| 谁给反馈 | 同一个模型自评 | 外部判卷器 |
| 改什么 | 这一稿的答案 | 下一轮的策略 |
| 记忆 | 单轮内有效,结束即丢 | 跨 trial 累积 |
| 能治什么 | 文风、结构、遗漏点 | 对错 |
Self-Refine 适合质量多维、容易用话说清的任务,比如把一封邮件改得更客气。它治不了对错,因为答题的和判卷的是同一个盲区。
4.5 适用场景与一条前提
适合:有确定性判卷器,比如单测、编译器、schema 校验、规则引擎;任务可重复执行,多跑几轮不出副作用;单轮失败的代价高于多跑几轮的成本。
这里有个硬前提:判卷器必须比答题的多知道一点。测试用例知道答案,编译器知道语法,规则引擎知道约束,第二个模型至少换了组参数。模型自己什么都没有。Huang 等人专门验证过这件事:没有外部反馈时让模型自查推理题,准确率不会涨,有时还会掉,因为被要求「挑错」的模型会幻觉出不存在的错误,把对的改成错的。
所以判卷器如果是同一个模型,这套结构不是在纠错,是在给错误找理由。这时候不如不加。
五、三个问题定选型
选型不需要背结论,按顺序问三个问题,前一个答不上来就别问下一个。
| 问题 | 是 | 否 |
|---|---|---|
| 答案是不是在模型外面? | 上 ReAct | 别上,纯成本 |
| 步骤能不能事先列全、执行中不变? | 上 Plan-and-Solve | 别上,计划会变负债 |
| 有没有一个不依赖模型的判卷器? | 上 Reflexion | 别上,会退化 |
能叠,但顺序不能反:先定信息通道,再定计划粒度,最后加反思。反过来先加反思,等于给一个信息不足的循环加一层自我安慰,成本上去了,准确率没动。
三个范式叠在一起看,包含关系就出来了:
一次 trial
├─ 规划 Plan:先列 k 步 ← 没有反馈入口
├─ 执行 Step i:Thought → Action → Observation ← 工具 / 环境
│ (ReAct 在这一层循环,一直反复到给出答案)
└─ 校验 Evaluator 判卷 → Reflection 写复盘 → 写入记忆
↓
下一轮 trial 的 prompt 前置这段复盘
常见的组合是 plan-and-execute:Plan 定骨架,执行每一步时允许调工具,把 ReAct 塞进 Plan 的每一步里。两个范式在这里套在一起用。
六、小结
- 三个范式是同一个循环的三处加料:ReAct 加在一步的动作上,Plan-and-Solve 加在执行前的计划上,Reflexion 加在一次尝试之后的复盘上。
- 每个都有一条前提。ReAct 的前提是信息在外部且拿得到,Plan-and-Solve 的前提是步骤能事先定,Reflexion 的前提是有一个不依赖模型的判卷器。前提不成立时加上去只是成本。
- 代码里最容易错的三处:ReAct 的解析与终止条件,Plan-and-Solve 的执行上下文,Reflexion 的
exec隔离。
练习
三道题由浅到深:
- 把 ReAct 示例里的
KB换成一个真实的 HTTP 查询(比如查天气),跑一遍,记录循环走了几步、哪一步的 observation 最长。 - 给 Plan-and-Solve 示例加一个分支:某一步返回空结果时重新做一次计划。对比加上前后,调用次数和最终答案分别有什么变化。
- 给 Reflexion 示例换一个判卷器,把单测换成「检查函数名和参数个数是否正确」,观察复盘内容会变成什么样,还能不能修好这个 bug。
参考
- ReAct: Synergizing Reasoning and Acting in Language Models — https://arxiv.org/abs/2210.03629
- ReAct 项目页(含提示词与示例轨迹) — https://react-lm.github.io/
- Plan-and-Solve Prompting(ACL 2023) — https://aclanthology.org/2023.acl-long.147/
- Reflexion: Language Agents with Verbal Reinforcement Learning — https://arxiv.org/abs/2303.11366
- Self-Refine: Iterative Refinement with Self-Feedback — https://arxiv.org/abs/2303.17651
- Large Language Models Cannot Self-Correct Reasoning Yet — https://arxiv.org/abs/2310.01798
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)