5.3 Reflexion:失败后的自我修正
ReAct 会解释自己,Plan-and-Solve 会先谋后动,但它们有一个共同的死角:做完就结束了。如果第一次跑出来的答案是错的,循环不会知道,更不会尝试修正——它把错答案当成终答案交出去。人不是这么干活的:一件事做砸了,我们会复盘「哪一步错了」,下次带着这个教训重来。这一节就把「复盘—重试」这个环节加进 minagent。
本节把 minagent 推进到「会自我修正的循环」:新增
minagent/reflexion.py里的Evaluator与ReflexionAgent,把「执行 → 判定 → 反思 → 带着反思重跑」串成最多 N 轮的闭环。
5.3.1 失败的循环需要一个「复盘」环节
先看清问题:前两节的循环都假设「模型一次能跑对」。但真实模型会犯错——算错、用错工具、漏掉条件。朴素循环对此完全无感:它只判断「有没有工具调用」,不判断「答案对不对」。一个算错的答案和一个算对的答案,在循环眼里长得一模一样,都是「没有 tool_calls 的 final」。
Reflexion 补的就是这个判断环节。它把「跑一次」升级成「跑一次 → 有人验收 → 不行就带着教训重跑」,结构上是在既有循环外面再套一层:
尝试 1 → 执行 → 评估 → 失败 → 生成反思
↓
尝试 2 → 执行(带反思)→ 评估 → 成功 → 返回
关键有三件东西:一个评估者(判断这次对不对)、一段反思(把「错在哪」写成文本)、一次带反思的重跑(把反思作为额外上下文喂回模型)。三者缺一不可——没有评估者就不知道要重跑,没有反思就是盲目重试(换汤不换药),没有重跑就白反思。
5.3.2 Evaluator:谁来判断失败
Reflexion 的第一步是「判定成败」。这一步不能靠执行循环自己——它不知道正确答案是什么。所以要把判断逻辑抽成一个独立的 Evaluator:
| 判定方式 | 实现 | 适用 |
|---|---|---|
| 期望值比对 | 断言 answer == expected | 算数、格式化、有标准答案的任务 |
| 工具校验 | 跑一个验证工具(跑测试、跑 lint) | 代码、配置、可执行产物 |
| 模型自评 | 让另一个模型判断「是否完成」 | 开放任务、无标准答案 |
| 规则检查 | 正则、schema 校验、非空检查 | 结构化输出、字段齐全性 |
本节用最简单的「期望值比对」——把正确答案 expected 交给 Evaluator,它比一下就知道成败。为了教学,我们把评估也做成一次模型调用(用假模型脚本化),真实系统里它常常就是一行 Python 断言:
# minagent/reflexion.py(上半)
from __future__ import annotations
import json
from .types import Message, Model
class Evaluator:
"""判定一次尝试是否成功;失败时给出一段可写回上下文的反思。"""
def __init__(self, model: Model, expected: str) -> None:
self.model = model
self.expected = expected
def evaluate(self, task: str, answer: str) -> tuple[bool, str]:
resp = self.model.complete(
[Message(role="user",
content=f"判定任务是否完成。期望={self.expected},实际={answer}")], [])
data = json.loads(resp.message.content)
return bool(data["ok"]), data.get("reflection", "")
evaluate 返回一个二元组:ok 表示这次尝试过没过,reflection 是失败时给出的反思文本。反思必须是可读的自然语言,因为它下一步要作为提示词的一部分喂回模型——「第 1 步用了 3 而不是 2」这样的句子,模型才看得懂并据此改正;如果只返回一个布尔值,重试就等于盲猜。
这里用模型来做评估是一个刻意的简化:真实系统里,能用确定性断言(assert answer == expected)就别用模型,因为模型自评既慢又可能判错(它会把错的判成对的)。把评估器写成纯函数是 Reflexion 最稳的形态,本节用假模型只是为了展示「反思文本从哪来」这条完整链路。
那「反思」和普通的「重试」到底差在哪?两者都是「失败了再来一次」,但信息量完全不同:
| 维度 | 朴素重试 | Reflexion |
|---|---|---|
| 重试时的输入 | 和第一次完全一样 | 原任务 + 历史反思 |
| 模型能否知道上次错在哪 | 不能 | 能 |
| 两次结果的关系 | 大概率一样(除非采样随机) | 第二次带着修正方向 |
| 本质 | 碰运气 | 带信息的迭代 |
区别的关键在第 2 行:朴素重试把「为什么错」这条最有价值的信息扔掉了。模型第二次拿到的提示词和第一次一字不差,它只能指望「这次运气好点」。Reflexion 把评估者的判断转成文字喂回去,第二次尝试就变成了「有方向的修正」。这也解释了为什么 Reflexion 必须有一个能说清「错在哪」的评估器——如果评估器只会说「不对」,那反思就等于空话,退化成朴素重试。
5.3.3 实现 ReflexionAgent
把三层拼起来:外层管「最多重试 N 轮」,中层是「跑一次 + 评估」,内层是第 3 章的 run()。
# minagent/reflexion.py(下半)
from .loop import run
from .tools import ToolRegistry
class ReflexionAgent:
"""失败后把反思写回上下文,带着反思重跑,最多 max_rounds 轮。"""
def __init__(self, model: Model, registry: ToolRegistry,
evaluator: Evaluator, max_rounds: int = 3) -> None:
self.model = model
self.registry = registry
self.evaluator = evaluator
self.max_rounds = max_rounds
def run(self, task: str) -> tuple[str, list[dict]]:
reflections: list[str] = []
log: list[dict] = []
for rnd in range(1, self.max_rounds + 1):
prompt = task
if reflections:
prompt = task + "\n历史反思:\n" + "\n".join(
f"- {x}" for x in reflections)
result = run(self.model, self.registry, prompt, max_steps=4)
ok, reflection = self.evaluator.evaluate(task, result.final)
log.append({"round": rnd, "final": result.final,
"ok": ok, "reflection": reflection,
"steps": result.steps})
if ok:
return result.final, log
reflections.append(reflection)
return log[-1]["final"], log
三个要点:
- 反思是累积的。
reflections是一个列表,第二轮会带上第一轮的反思,第三轮会带上前两轮的全部反思。这样模型能记住「之前试过哪些错法」,避免在同一处反复栽跟头。 max_rounds是硬止损。Reflexion 天然有「越改越离谱」的风险——模型可能为了绕开一个错误引入更大的错误。没有轮数上限,它可能永远在「失败 → 反思 → 再失败」里打转,每轮都在烧 token。- 返回
log是为了可审计。每轮记下final、ok、reflection与完整steps,调用方(和第 16 章的测试)能回放「它一共试了几次、每次错在哪、最后靠哪条反思改对」。这是 Reflexion 相对朴素重试最大的价值——重试是盲目的,Reflexion 的重试带着理由。
5.3.4 跑一遍:第一次失败 → 反思 → 第二次成功
设计一个「第一次必然算错」的场景:目标是算 (8+4)*2,但脚本让模型第一次把乘数写成 3(得 36),评估器发现与期望的 24 不符,给出一段反思;第二轮模型照着反思改成乘以 2,得 24,通过。
# demo_reflexion.py
import json
from minagent import FakeModel, Tool, ToolCall, ToolRegistry
from minagent.reflexion import Evaluator, ReflexionAgent
reg = ToolRegistry()
for name, fn in [("add", lambda a, b: a + b),
("multiply", lambda a, b: a * b)]:
reg.add(Tool(name=name, description=f"{name} 两个整数",
parameters={"type": "object",
"properties": {"a": {"type": "integer"},
"b": {"type": "integer"}},
"required": ["a", "b"]},
func=fn))
model = FakeModel([
# 第 1 次尝试:乘数写错
[ToolCall("r1a", "add", {"a": 8, "b": 4})],
[ToolCall("r1b", "multiply", {"a": 12, "b": 3})],
"结果是 36。",
# 第 2 次尝试:纠正后
[ToolCall("r2a", "add", {"a": 8, "b": 4})],
[ToolCall("r2b", "multiply", {"a": 12, "b": 2})],
"结果是 24。",
])
evaluator_model = FakeModel([
json.dumps({"ok": False,
"reflection": "目标要求乘以 2,你用了 3;请用 (8+4) 的结果乘以 2。"},
ensure_ascii=False),
json.dumps({"ok": True, "reflection": ""}, ensure_ascii=False),
])
agent = ReflexionAgent(model, reg, Evaluator(evaluator_model, expected="24"))
final, log = agent.run("计算 (8+4)*2 的结果")
for entry in log:
print(f"=== 第 {entry['round']} 轮 ===")
for s in entry["steps"]:
print(f" [{s.index}] {s.kind:<9} {s.detail}")
print(f" 判定: {'成功' if entry['ok'] else '失败'}")
if entry["reflection"]:
print(f" 反思: {entry['reflection']}")
print(f"最终回答: {final}")
本机实测输出(Python 3.14.6):
=== 第 1 轮 ===
[0] tool_call add({'a': 8, 'b': 4}) -> 12
[1] tool_call multiply({'a': 12, 'b': 3}) -> 36
[2] final 结果是 36。
判定: 失败
反思: 目标要求乘以 2,你用了 3;请用 (8+4) 的结果乘以 2。
=== 第 2 轮 ===
[0] tool_call add({'a': 8, 'b': 4}) -> 12
[1] tool_call multiply({'a': 12, 'b': 2}) -> 24
[2] final 结果是 24。
判定: 成功
最终回答: 结果是 24。
两轮轨迹对照着看,差别只在 multiply 的第二个参数:第 1 轮是 3,第 2 轮变成 2。这个改动不是随机的——它是被第 1 轮的反思「请用 (8+4) 的结果乘以 2」直接驱动的。add 那一步两轮完全相同,说明 Reflexion 没有推倒重来,而是在保留正确步骤的基础上只修正错的那一步,这正是「带着反思重跑」相对「从头瞎试」的优势。
log 的结构也让这件事可断言:[e["ok"] for e in log] == [False, True],len(log) == 2。第 16 章写 Reflexion 的测试时,断言「第几轮转成功」比断言最终答案更能锁住行为。
还可以做一件有意思的对照:把 max_rounds 设成 1,同一份脚本会怎样?答案是第二轮根本不会跑,log 只有一条 ok=False 的记录,final 停在错误的「结果是 36。」。这说明 Reflexion 的「修正」能力完全来自那多出来的轮次——轮数就是它纠正错误的机会预算。设成 1 等于关掉 Reflexion,退化成朴素循环;设得越大越有可能改对,但成本也越高。这正是下一节要讨论的「何时值得」。
5.3.5 何时值得 Reflexion
Reflexion 不是免费的——每一轮重试都是一次完整的模型调用,成本随轮数线性上涨,而且它有「越改越糟」的风险。所以不能无脑用。下面这份清单帮你判断:
| 信号 | 值得 Reflexion | 不值得 |
|---|---|---|
| 能否自动判定成败 | 能(有期望值 / 可跑测试) | 不能,只能靠人眼看 |
| 失败是否可定位 | 能说出「错在哪」 | 只知「不对」不知「哪不对」 |
| 重试是否有信息量 | 反思能给出新方向 | 每次都在同一个坑里打转 |
| 单次成本 | 低(短任务、便宜模型) | 高(长上下文、贵模型) |
| 失败率 | 中低(偶尔错,重试有用) | 极高(模型根本不会)或极低(一次就对) |
把清单浓缩成一句话:有廉价、自动、可定位的评估信号时,Reflexion 才划算。典型的好场景是「代码生成 + 跑测试」「SQL 生成 + 执行校验」「结构化输出 + schema 校验」——这些都有确定性验证器,且失败原因能精确说清。反例是「写一篇有说服力的文案」这类既无法自动评分、又无法说清「错在哪」的开放任务,Reflexion 只会让模型反复改来改去,越改越平庸。
还有一条工程经验:先确保第一次尝试尽可能对,再考虑 Reflexion。Reflexion 是「补救」,不是「主力」。如果模型的首次成功率只有 20%,靠 Reflexion 硬撑到 80% 要烧四倍的钱;不如先把提示词、工具描述、上下文工程(第 6 章)做好,让首次成功率到 70%,再用 Reflexion 兜那 30% 的边角。把 Reflexion 当保险,而不是当拐杖。
5.3.6 真实模型下会怎样
本节的假模型把两轮轨迹都写死了,反思也是脚本里的一句固定文本。真实模型下,这套机制会暴露三个新问题:
- 反思质量参差。真实模型生成的反思可能是对的(「乘数应该用 2」),也可能抓错重点(把没错的
add步骤当成问题),甚至编出一条错误的教训。假模型的反思永远精准,真实世界的反思本身就可能需要被评估——这就滑向了「谁来反思反思」的递归难题,实务中靠「限轮数 + 确定性验证器」兜底。 - 可能「越改越错」。模型有时为了绕开第 1 轮的失败,在第 2 轮换了个更大的错误。所以
max_rounds之外,通常还要记录「每轮的评估分数」,一旦发现分数在下降就提前放弃,而不是傻等 N 轮跑完。 - 评估器本身可能不可靠。本节让评估也走一次模型调用,真实系统里这是大忌——模型自评会把错的判成对的(它容易「自我认可」)。能用
assert、能跑测试、能查 schema 的,一律别用模型判。Reflexion 的可靠性上限,取决于评估器的可靠性,而不是反思写得多漂亮。
一句话:假模型证明了「执行—评估—反思—重跑」这个闭环的接线是对的;真实模型暴露的是「反思与评估这两个环节本身都可能有噪声」。循环结构不用改,但每一个环节都要有独立的可靠性设计。
5.3.7 小结
- Reflexion = 在既有循环外再套一层:执行 → 评估 → 失败则生成反思 → 带着反思重跑,最多 N 轮。
- 三件必需品:评估者(判成败)、反思文本(说清错在哪)、带反思的重跑(不是盲目重试)。
- 反思是累积的,模型能记住之前试过的错法;
max_rounds是硬止损,防「越改越糟」。 - 评估器越确定,Reflexion 越可靠——能用
assert/ 跑测试就别用模型自评。 - 先提首次成功率,再用 Reflexion 兜边角;把它当保险,别当拐杖。
至此,第 5 章的三种推理模式全部落地:ReAct 边想边做、Plan-and-Solve 先谋后动、Reflexion 失败复盘。它们共享同一套消息与工具系统,差别只在编排——这也是卷一一以贯之的主线。下一章 6.1 系统提示与指令分层 会转向上下文工程,讲清楚「喂给模型的每一段文字该怎么组织」。想回看先谋后动的两段式,见 5.2 Plan-and-Solve:先出计划再执行 。
阅读导航:上一节:5.2 Plan-and-Solve:先出计划再执行 · 下一节:6.1 系统提示与指令分层 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。