欢迎光临
我们一直在努力

Reflexion:让 Agent 用「言语」做强化学习

一、为什么需要 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 抄袭?

它不是新算法,而是一个被命名的模式。生产里到处是它的变体:

产品 / 框架它的"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 定义反思和情景记忆

    @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,而是用反思条数模拟"模型看到反思后改进行为":

    反思条数选择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:返回二元组(成功 + 偏差)

    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

    设计意图(逐行精读)

    行在做什么对应 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. 打印对比 + 主函数

    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,无关梯度,全凭一段自然语言。


    七、本节小结 & 易错点回顾

    核心知识点

  • 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

    赞(0)
    未经允许不得转载:171主机测评 » Reflexion:让 Agent 用「言语」做强化学习
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址