一、为什么需要 Reflexion?
传统强化学习的修复路径是:
text
失败 → 算梯度 → 更新权重 → 上千次试验 → 收敛
但生产 Agent 不可能为每次失败都备一份训练预算 。Shinn 等人在论文 arXiv:2303.11366 里换了个问法:
如果 Agent 只是想一想自己为什么失败 ,然后带着这段思考再试一次,会怎么样?
- 不动权重
- 不算梯度
- 只在试验之间存一段自然语言
结果是:
- ALFWorld:打过 ReAct 等所有未微调基线(论文里有 ~130% 提升的说法);
- HotpotQA:稳定优于 ReAct;
- HumanEval / MBPP(代码生成):当时的 SOTA。
全程零梯度。
二、Reflexion 的三件套 + 一个数据结构
text
Actor : 跑一次 trajectory(典型 ReAct 循环)
Evaluator : 给 trajectory 打分(二值 / 启发式 / 自评)
Self-Reflector : 写一段自然语言反思 ------「我为什么失败」
─────────────────────────────────────────────
Episodic Memory: 反思的有界列表,下一次试验时前置到 prompt
一轮 Reflexion 长这样
text
trial 1
Actor 跑一次 → Evaluator 判定失败
Self-Reflector:"我挑错了工具,因为把问题误读成 X,其实是 Y"
记忆 += 这段反思
trial 2
Actor 看到记忆里的反思 → 选了正确的工具 → Evaluator 判定成功
关键点 :每次试验都是从零开始的全新轨迹,但 prompt 里多了"上次为什么错"。这是它和 ReAct(同一条轨迹里递归思考)的本质差别。
三、三种 Evaluator,按信号强度排序
| 类型 | 信号来源 | 优势 | 风险 |
|---|---|---|---|
| 标量(Scalar) | 外部 ground truth(测试通过 / 任务成功) | 信号最强、最干净 | 不是所有任务都有 GT |
| 启发式(Heuristic) | 预定义失败特征(连续相同动作、超过 50 步 ...) | 便宜、不依赖 LLM | 漏报 / 误报 |
| 自评(Self-Eval) | LLM 给自己的轨迹当裁判 | 任何场景都能用 | 信号最弱,容易自吹自擂 |
2026 年的默认混搭
有 ground truth 就用 scalar;没有就用 self-eval;启发式做安全护栏。
特别地:self-eval 最好和工具锚定验证一起用,单独用很容易飘。
四、为什么 Reflexion 几乎被所有"自愈"Agent 抄袭?
它不是新算法,而是一个被命名的模式。生产里到处是它的变体:
| 产品 / 框架 | 它的"Reflexion" |
|---|---|
| Letta | sleep-time compute ------ 独立 agent 反思过往对话,写进 memory block |
| Claude Code | CLAUDE.md / "保存记忆" ------ 反思被捕获为 learnings,前置到未来会话 |
| pro-workflow | /learn-rule 命令 ------ 把纠正显式化成规则 |
| LangGraph | "反思节点" ------ 给输出打分,必要时路由去 refine |
它们共享同一个洞见:
自然语言是个足够丰富的媒介,能在多次运行之间承载"我从失败里学到了什么"。
五、Reflexion 什么时候有用、什么时候帮倒忙
✅ 有效场景
- 失败信号清晰:测试失败、工具报错、答案错误;
- 任务可复现:同一类问题能被再问一遍;
- 预算够:还允许多试几次。
❌ 帮倒忙场景
- 第一次就成功 → 反思纯属浪费 token;
- 失败是外部的(网络挂了、服务 500)→ 反思"网络挂了"毫无价值;
- 反思变成迷信:一次偶发抖动,被存成"以后永远别走这条路"。
⚠️ 2026 年的最大坑:记忆腐烂(Memory Rot)
反思会越攒越多,里面总有过时或错误的条目。情景缓冲区一大,重试反而变慢。缓解办法:
- 周期性压实(compaction):把多条反思总结成一条;
- TTL(Time To Live):每条反思加过期时间;
- sleep-time 清理 agent:把"整理记忆"挪出热路径,主 agent 保持低延迟。
六、关键术语速查表
| 术语 | 通俗叫法 | 它实际是什么 |
|---|---|---|
| Reflexion | "自我纠正" | Actor + Evaluator + Self-Reflector + 情景记忆 |
| Verbal Reinforcement | "无梯度学习" | 反思前置进下次 prompt,等价于一种 in-context 学习 |
| Episodic Memory | "按任务反思" | 某任务类别下既往反思的有界缓冲区 |
| Scalar Evaluator | "二值成功信号" | GT 给出的 pass/fail 或数值分数 |
| Heuristic Evaluator | "模式探测器" | 预定义的失败特征(卡住、步数过多) |
| Self-Evaluator | "自己当裁判" | LLM 给自己打分 ------ 弱信号兜底 |
| Memory Rot | "记忆腐烂" | 缓冲区里过时条目越来越多 |
| Sleep-time Reflection | "异步反思" | 反思挪出热路径,避免拖主 agent 延迟 |
代码精讲:约 120 行的玩具 Reflexion
任务定义:从 1...9 里挑 3 个整数,让它们的和等于 20。
Actor 被故意写"死板":
- 没看到反思 → 永远挑
[1, 2, 3]; - 看到第 1 条反思 → 改成
[5, 6, 7]; - 看到第 2 条反思 → 改成
[6, 7, 7]→ 命中。
这样我们能同一份代码跑两遍(开 / 关 memory),直接看出反思的价值。
1. 用 @dataclass 定义反思和情景记忆
python
@dataclass
class Reflection:
trial: int
text: str
@dataclass
class EpisodicMemory:
items: list[Reflection] = field(default_factory=list)
max_len: int = 6
def add(self, r: Reflection) -> None:
self.items.append(r)
if len(self.items) > self.max_len:
self.items.pop(0)
def as_prompt(self) -> str:
if not self.items:
return "(no prior reflections)"
lines = [f"- trial {r.trial}: {r.text}" for r in self.items]
return "\n".join(lines)
语法点
field(default_factory=list):上一节讲过 ------ 可变默认值的正确写法,每个实例各自一个 list,不共享。max_len: int = 6:dataclass 字段可以带默认值;带默认的必须放在没默认的字段后面(和函数参数一样)。self.items.pop(0):删除并返回列表第一个元素 ,相当于一个"先进先出"队列。list.pop(0)是 O(n) 的,如果性能敏感应换collections.deque(maxlen=6),效果一样且 O(1)。- 列表推导式
[f"- trial {r.trial}: {r.text}" for r in self.items]:把每条反思格式化成一行,返回新 list。 "\n".join(lines):用换行把字符串列表拼起来。比"+".join(...)或循环+=都更 Pythonic、更快。
设计意图
这就是最简的"有界 FIFO 反思缓冲区":
- 超过
max_len自动淘汰最早的(粗暴版的"记忆压实"); as_prompt()把所有反思格式化进 prompt,供下次 Actor 看到。
这两点合起来就是 LangGraph / Claude Code memory / Letta sleep-time 的最小内核 ------ 真实系统只是把"淘汰策略"和"压实策略"做得更聪明。
2. Actor:脚本化的"死板策略"
python
class Actor:
"""Scripted policy. Without reflections it stays on bad choices; with
at least one reflection it moves toward the target sum."""
def act(self, memory: EpisodicMemory) -> list[int]:
n = len(memory.items)
if n == 0:
return [1, 2, 3]
if n == 1:
return [5, 6, 7]
if n == 2:
return [6, 7, 7]
return [6, 7, 7]
设计意图
这里不调真 LLM ,而是用反思条数模拟"模型看到反思后改进行为":
| 反思条数 | 选择 | sum | 行为 |
|---|---|---|---|
| 0 | 1, 2, 3 | 6 | 远离目标 |
| 1 | 5, 6, 7 | 18 | 已接近 |
| 2 | 6, 7, 7 | 20 | 命中 |
把这个
act替换成真 LLM 调用,循环本身一行不用改 ------ 和前两节的ToyLLM/ScriptedPlanner是一个套路。
语法点
len(memory.items):取列表长度。- 多个
if而不用elif:因为每个分支都return,等价但更清晰。Python 里这是常见风格。 - 类型注解
-> list[int]:明确告诉调用方"返回 int 的 list"。
3. Evaluator:返回二元组(成功 + 偏差)
python
def binary_evaluator(attempt: list[int], target: int) -> tuple[bool, int]:
total = sum(attempt)
return total == target, total - target
语法点
-
tuple[bool, int]:返回一个二元组,类型注解明确告诉外面"第一个是 bool,第二个是 int"。 -
return total == target, total - target:Python 里多返回值的本质就是一个 tuple ,逗号自动打包成 tuple,调用方可以解包:pythonsuccess, delta = binary_evaluator([1,2,3], 20)
设计意图
虽然叫 binary_evaluator,但附带了一个连续的 delta(差多少)------ 给 Self-Reflector 提供更丰富的信号,能写出"少了 14"而不仅仅是"失败"。
这正是真实 Reflexion 的关键工程经验:
二值评估器虽然干净,但额外暴露一点点连续信号能让反思质量大幅提升。
4. Self-Reflector:把数字翻译成"建议"
python
class SelfReflector:
def reflect(self, attempt: list[int], delta: int) -> str:
if delta < 0:
return f"sum {sum(attempt)} is {-delta} short; pick larger values"
if delta > 0:
return f"sum {sum(attempt)} overshoots by {delta}; pick smaller values"
return "succeeded"
语法点
-delta:取负数 ------ delta 是负的(不够),描述成"还差多少"更自然。f"{sum(attempt)}":f-string 里可以直接嵌入函数调用。- 三个分支覆盖 少了 / 多了 / 刚好 三种情形。
设计意图
真实 Reflexion 里这里是一次 LLM 调用:
text
"你刚才做了 X,得到 Y,目标是 Z,写一段一句话反思,告诉下一次该怎么调整。"
但反思的有效性高度依赖:
- 它必须给出可执行建议("pick larger values"),不能是空话("再小心点");
- 它必须只关于这次失败,不能夹带不相关的全局教训。
工程里最常见的反模式:让 LLM 自由发挥写反思,结果第 10 条反思变成一段"agent 哲学独白" ------ 既没营养又占 token。让 reflector 输出结构化字段(如 root_cause + suggestion)比让它自由写要稳定得多。
5. 一次 Reflexion 跑通(核心循环)
python
@dataclass
class TrialResult:
trial: int
attempt: list[int]
success: bool
delta: int
reflection: str
def run_reflexion(max_trials: int, use_memory: bool) -> list[TrialResult]:
actor = Actor()
reflector = SelfReflector()
memory = EpisodicMemory()
trials: list[TrialResult] = []
for t in range(1, max_trials + 1):
attempt = actor.act(memory if use_memory else EpisodicMemory())
success, delta = binary_evaluator(attempt, TARGET)
text = reflector.reflect(attempt, delta)
trials.append(TrialResult(t, attempt, success, delta, text))
if success:
break
memory.add(Reflection(trial=t, text=text))
return trials
设计意图(逐行精读)
| 行 | 在做什么 | 对应 Reflexion 哪一块 |
|---|---|---|
actor.act(memory if use_memory else EpisodicMemory()) |
把"反思记忆"传进去;关掉时传空记忆 | 控制变量对比 |
binary_evaluator(attempt, TARGET) |
打分 | Evaluator |
reflector.reflect(attempt, delta) |
写反思 | Self-Reflector |
trials.append(...) |
记录这次试验完整结果 | 调试 / 可观测 |
if success: break |
命中就退出 | 早停 |
memory.add(Reflection(...)) |
失败才写记忆 | 情景记忆 |
语法点
memory if use_memory else EpisodicMemory():三元表达式 (条件表达式),等价于use_memory ? memory : EpisodicMemory()。这是 Python 控制变量对照的常用写法。range(1, max_trials + 1):从 1 开始计数,而不是默认的 0;这是因为下游trial字段对人更友好。for t in range(...):经典的"轮数预算"守卫,思路和第 1 节AgentLoop.max_turns完全一样。if success: break:典型早停模式。如果省掉这行,Reflexion 会一直跑到 max_trials,浪费 token ------ 这是新手常见错。
关键设计取舍
只有失败才写反思 (
if success: break在memory.add之前)。如果你成功也写反思("我成功了,因为 ......"),会变成自我表扬式 prompt 污染。Reflexion 的核心假设是"反思的价值在于纠错",不要破坏它。
6. 打印对比 + 主函数
python
def summarize(trials: list[TrialResult], name: str) -> None:
print(f"\n{name}")
print("-" * 60)
for r in trials:
mark = "OK " if r.success else "..."
print(f" trial {r.trial}: {r.attempt} sum={sum(r.attempt)} "
f"delta={r.delta:+d} {mark} -> {r.reflection}")
last = trials[-1]
print(f" final: {'success' if last.success else 'failed'} "
f"at trial {last.trial}")
def main() -> None:
print("=" * 70)
print(f"REFLEXION --- pick three ints in [1..9] summing to {TARGET}")
print("=" * 70)
trials_no_mem = run_reflexion(max_trials=4, use_memory=False)
summarize(trials_no_mem, "BASELINE (no episodic memory)")
trials_mem = run_reflexion(max_trials=4, use_memory=True)
summarize(trials_mem, "REFLEXION (episodic memory on)")
语法点
{r.delta:+d}:f-string 格式化指令,+d表示正数也加 + 号 ,便于阅读(例如-14、+0、+3)。'success' if last.success else 'failed':再次出现的三元表达式,把 bool 翻译成可读字符串。trials[-1]:负下标取最后一个元素 ,Python 特色,避免写trials[len(trials)-1]。
期望输出
text
BASELINE (no episodic memory)
trial 1: [1, 2, 3] sum=6 delta=-14 ... -> sum 6 is 14 short; pick larger values
trial 2: [1, 2, 3] sum=6 delta=-14 ... -> sum 6 is 14 short; pick larger values
trial 3: [1, 2, 3] sum=6 delta=-14 ... -> sum 6 is 14 short; pick larger values
trial 4: [1, 2, 3] sum=6 delta=-14 ... -> ...
final: failed at trial 4
REFLEXION (episodic memory on)
trial 1: [1, 2, 3] sum=6 delta=-14 ... -> sum 6 is 14 short; pick larger values
trial 2: [5, 6, 7] sum=18 delta=-2 ... -> sum 18 is 2 short; pick larger values
trial 3: [6, 7, 7] sum=20 delta=+0 OK -> succeeded
final: success at trial 3
对比一目了然:没有反思的 Actor 卡死在初始策略;带反思 3 步收敛 。
这就是 Reflexion 的精髓,无关 RL,无关梯度,全凭一段自然语言。
七、本节小结 & 易错点回顾
核心知识点
- Reflexion = Actor + Evaluator + Self-Reflector + 情景记忆。
- 核心创新:用自然语言代替梯度做强化。
- 三种评估器:标量(首选)→ 自评(兜底)→ 启发式(护栏)。
- 失败时才写反思,避免自我表扬式污染。
- 生产里最大风险是记忆腐烂:必须配合压实 / TTL / sleep-time 清理。
Python 入门易错点
| 坑 | 正确做法 |
|---|---|
dataclass 默认值用 [] / {} |
field(default_factory=list / dict) |
长 FIFO 用 list.pop(0) 性能差 |
用 collections.deque(maxlen=N) |
| 返回多个值忘了类型注解 | -> tuple[bool, int] |
| 解包时左右数量不匹配 | success, delta = func() 必须严格对齐 |
| 三元表达式顺序写反 | Python 是 X if cond else Y,不是 C 风格 cond ? X : Y |
for x in [-1] 想取最后一个 |
直接 lst[-1] |
循环里忘了 break 提前退出 |
成功后必须 break,否则浪费 trial |
设计层易错点
| 坑 | 解释 |
|---|---|
| 反思写得太泛("再小心点") | 应输出结构化字段 {root_cause, suggestion} |
| 成功后也写反思 | 自表扬污染 prompt,下次反而走偏 |
不限定 max_len |
记忆腐烂 + token 爆炸 |
| 自评 Evaluator 单独用 | 容易自吹自擂;要和工具锚定验证(CRITIC)搭配 |
| 把外部错误(网络挂了)也存反思 | 该类反思对未来运行无价值,应过滤 |
参考项目 https://github.com/fancyboi999/ai-engineering-from-scratch-zh