一、为什么需要 Reflexion?
传统强化学习的修复路径是:
失败 → 算梯度 → 更新权重 → 上千次试验 → 收敛
但生产 Agent 不可能为每次失败都备一份训练预算。Shinn 等人在论文 arXiv:2303.11366 里换了个问法:
如果 Agent 只是想一想自己为什么失败,然后带着这段思考再试一次,会怎么样?
- 不动权重
- 不算梯度
- 只在试验之间存一段自然语言
结果是:
- ALFWorld:打过 ReAct 等所有未微调基线(论文里有 ~130% 提升的说法);
- HotpotQA:稳定优于 ReAct;
- HumanEval / MBPP(代码生成):当时的 SOTA。
全程零梯度。
二、Reflexion 的三件套 + 一个数据结构
Actor : 跑一次 trajectory(典型 ReAct 循环)
Evaluator : 给 trajectory 打分(二值 / 启发式 / 自评)
Self-Reflector : 写一段自然语言反思 ——「我为什么失败」
─────────────────────────────────────────────
Episodic Memory: 反思的有界列表,下一次试验时前置到 prompt
一轮 Reflexion 长这样
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 抄袭?
它不是新算法,而是一个被命名的模式。生产里到处是它的变体:
| Letta | sleep-time compute —— 独立 agent 反思过往对话,写进 memory block |
| Claude Code | CLAUDE.md / “保存记忆” —— 反思被捕获为 learnings,前置到未来会话 |
| pro-workflow | /learn-rule 命令 —— 把纠正显式化成规则 |
| LangGraph | “反思节点” —— 给输出打分,必要时路由去 refine |
它们共享同一个洞见:
自然语言是个足够丰富的媒介,能在多次运行之间承载"我从失败里学到了什么"。
五、Reflexion 什么时候有用、什么时候帮倒忙
✅ 有效场景
- 失败信号清晰:测试失败、工具报错、答案错误;
- 任务可复现:同一类问题能被再问一遍;
- 预算够:还允许多试几次。
❌ 帮倒忙场景
- 第一次就成功 → 反思纯属浪费 token;
- 失败是外部的(网络挂了、服务 500)→ 反思"网络挂了"毫无价值;
- 反思变成迷信:一次偶发抖动,被存成"以后永远别走这条路"。
⚠️ 2026 年的最大坑:记忆腐烂(Memory Rot)
反思会越攒越多,里面总有过时或错误的条目。情景缓冲区一大,重试反而变慢。缓解办法:
六、关键术语速查表
| 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 定义反思和情景记忆
@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:脚本化的"死板策略"
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,而是用反思条数模拟"模型看到反思后改进行为":
| 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:返回二元组(成功 + 偏差)
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,调用方可以解包:success, delta = binary_evaluator([1,2,3], 20)
设计意图
虽然叫 binary_evaluator,但附带了一个连续的 delta(差多少)—— 给 Self-Reflector 提供更丰富的信号,能写出"少了 14"而不仅仅是"失败"。
这正是真实 Reflexion 的关键工程经验:
二值评估器虽然干净,但额外暴露一点点连续信号能让反思质量大幅提升。
4. Self-Reflector:把数字翻译成"建议"
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 调用:
"你刚才做了 X,得到 Y,目标是 Z,写一段一句话反思,告诉下一次该怎么调整。"
但反思的有效性高度依赖:
- 它必须给出可执行建议(“pick larger values”),不能是空话(“再小心点”);
- 它必须只关于这次失败,不能夹带不相关的全局教训。
工程里最常见的反模式:让 LLM 自由发挥写反思,结果第 10 条反思变成一段"agent 哲学独白" —— 既没营养又占 token。让 reflector 输出结构化字段(如 root_cause + suggestion)比让它自由写要稳定得多。
5. 一次 Reflexion 跑通(核心循环)
@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
设计意图(逐行精读)
| 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. 打印对比 + 主函数
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]。
期望输出
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,无关梯度,全凭一段自然语言。
七、本节小结 & 易错点回顾
核心知识点
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



