Chapter 01 · Concepts

词汇表:七个零件如何拼出一个会反思的 agent

起点章给出了一句话本质——Reflexion 的"学习"不在权重里、而在上下文里。本章把这句话拆成七个可命名的零件,并用同一个"写函数失败 → 反思 → 重试"的场景把每个零件落到地上:看清它在循环里负责哪一步、为什么非它不可、以及它和下一个零件如何衔接。读完这七个名字,后续每一章都建立在它们之上。

本章你将建立的 schema

  • verbal reinforcement:权重冻结,策略是记忆里的一段文本,而非模型参数。
  • Actor / trajectory / Evaluator / Self-Reflection:生成、产物、打分、把标量翻成语言教训。
  • episodic memory:跨 trial 的反思 buffer,Ω=1–3 滑动窗口——是窗口,不是向量库。
  • 三个反直觉断言:Actor 不是新模型、reward 只是标量、信息量在反思那一步才产生。

贯穿本章的场景只有一个,小到可以在脑子里跑完:让一个 LLM 写出 Python 函数 median(nums)(返回数字列表的中位数,偶数长度取中间两数平均),并通过两条断言——其中一条专测偶数长度。第一次尝试只取了正中间那一个元素,偶数用例失败。这一次失败、以及它如何变成下一次的成功,正好把七个零件依次点亮。

流畅感警告

这一章的名词不多,读起来会很顺。但"我读得很顺""这些词我都见过"并不等于"我能在循环里指出每个零件负责哪一步"。真正的检验是:读完能不能脱稿画出三组件循环图、并说清为什么把上次轨迹原样塞回去(blind retry)不如反思一次。每节末尾的预测题就是用来戳破顺滑感的——先写下你的答案,再展开。

1.1verbal reinforcement:为什么不更新权重

用语言反馈而非梯度更新来"强化"一个 agent,策略就是记忆里那段文本。

为什么需要它

常规强化学习靠梯度把"做得好/不好"回灌进权重。但对一个跑在 API 后面的大模型,微调一轮昂贵、缓慢,且会改动全局行为。Reflexion 换了个赌注:权重一动不动,只在每次失败后往上下文里写一段自然语言教训;下一次尝试把这段文本读进 prompt。强化的载体从"梯度"换成了"文字"。

在 median 场景里,这意味着:第一次失败后,系统不会去调模型参数,而是生成一句话——"只取了中间单个元素,没处理偶数长度"——并把它存起来。第二次尝试时,这句话被拼进 prompt,模型在它的条件下重新生成代码。整个过程没有一次反向传播。

比文档深一层的机制:论文把策略 π 显式参数化为"agent 的记忆编码 + 选定的 LLM 参数"两部分。两次 trial 之间,LLM 参数那一半完全冻结,唯一变化的是记忆那一半——也就是上下文里的文本。所以"verbal reinforcement learning"是字面意义上的:学习确实发生了(行为在改善),但它发生在 context window 里,通过 in-context conditioning 生效,而不是通过 backprop 改动任何一个权重。这条线索后面会反复回来——它是整个范式与微调的根本分界。

衔接下一节:既然权重不动,那"生成代码"这一步由谁来做?答案是 Actor——而它正是读者已经熟悉的东西,只是换了个角色名。

1.2Actor:它不是新模型,就是 CoT / ReAct

生成 text + action 的 LLM 策略,内部就是 CoT 或 ReAct,不是另起炉灶的新模型。

为什么需要它

循环总得有人真正去"做事"——读任务、想一步、产出动作或代码。这个角色就是 Actor。给它单独起名,是为了在循环里把"生成"这一步和"打分""反思"这两步分开,而不是因为它需要一个特殊架构。

这是本章第一个反直觉点,值得钉死:Actor 不是一个为 Reflexion 新训练或新设计的模型。它就是一个普通的 LLM 推理策略——单遍生成中间推理就是 CoT;边推理边调工具、交错 reason+act+observe 就是 ReAct。Reflexion 没有改 Actor 的内部,它只是把 Actor 当成一个可替换的零件塞进更大的循环里。在 median 场景里,Actor 就是"接到任务描述、吐出一段函数定义"的那次模型调用,仅此而已。

比文档深一层的机制:既然 Actor 内部可以是 ReAct,那它本身就能在一次尝试内做多步推理与工具调用。这里要分清两个不同维度的"循环"——Actor 内部的 reason→act→observe 是单条轨迹之内的步进;而 Reflexion 外层套的 生成→评估→反思 是跨尝试的循环。前者不跨尝试积累教训,后者才是 Reflexion 的新增物。把两者叠在一起看:Reflexion = 一个(可能很会推理的)Actor + 外面一圈让它跨尝试改进的脚手架。

预测一下

如果把 Actor 从"单步生成代码"换成一个完整的 ReAct agent(它自己会查文档、跑中间命令),Reflexion 这一层还有存在意义吗?换句话说,ReAct 自带的 observe 步骤,能不能替代外层的反思?

展开看答案

仍然有意义,两者不重叠。ReAct 的 observe 让 Actor 在当前这条轨迹内看到环境反馈并即时调整,但当这条轨迹整体失败、被判不通过后,ReAct 自己不会把"这一整次为什么没成"沉淀成一条跨尝试的教训——下一条轨迹默认从头开始。Reflexion 外层正是补这一段:它在一整条轨迹失败后做诊断,把教训写进记忆,供下一条轨迹的 Actor 读取。一句话:ReAct 管"这一步看到什么就改什么",Reflexion 管"上一整次失败教会了什么"。维度不同,叠加使用。

衔接下一节:Actor 跑完一次,留下的产物——那一串"想了什么、做了什么、得到什么"——需要一个名字。这就是 trajectory。

1.3trajectory(轨迹):一次尝试的动作序列 = 短期记忆

Actor 一次尝试产生的完整 action / observation 序列,即这一回合的短期记忆。

为什么需要它

要给一次尝试"打分"或"诊断",得先有一个可被检查的对象。trajectory 就是这次尝试留下的完整痕迹——推理、动作、环境反馈,直到产出最终答案。它是 Evaluator 和 Self-Reflection 的共同输入。

在 median 场景里,trial 0 的 trajectory 很短:Actor 输出的那段代码 return s[len(s)//2],加上把它跑在两条测试上得到的结果(第二条偶数用例抛出断言失败)。在多步 ReAct 任务里,trajectory 会长得多——一连串 思考→动作→观察。无论长短,它都是"这一回合到底发生了什么"的唯一完整记录。

比文档深一层的机制:trajectory 是短期记忆——关键在"短期"二字。它的生命周期被绑死在单次 trial 之内:这一回合结束、教训被提炼出来后,下一回合的 Actor 并不会原样继承上一条 trajectory(否则上下文会迅速膨胀)。它服务于"评估"和"反思"这两个紧邻的下游步骤,用完即可丢弃。能跨回合活下来的,不是 trajectory 本身,而是从它身上提炼出的那一句教训。这个"短期 vs 长期"的切分是本章最容易被读顺、却最该记牢的一刀——图 2 会专门画它。

衔接下一节:有了可检查的 trajectory,下一步是判它成败。这件事交给 Evaluator。

1.4Evaluator:给轨迹打分 = reward,而 reward 只是标量

给一条 trajectory 打分、输出 reward 的角色;reward 通常只是一个标量或 pass/fail。

为什么需要它

循环必须知道"这次到底对没对",才能决定停下还是继续。Evaluator 提供这个判断信号。没有它,系统无从知道何时该停、也无从触发反思——这也是为什么后面会反复强调:没有可靠 Evaluator 的任务,Reflexion 会退化甚至变差。

Evaluator 不一定是一个 LLM。论文按任务类型用不同实现:推理任务对答案做 exact-match;决策任务用手写启发式(比如同一动作重复超过 3 次、或动作数超过 30 就判失败)或一个 LLM 二分类器;编程任务则跑自生成的单元测试。在 median 场景里,Evaluator 就是那段"逐条执行 assert、捕获异常"的代码——通过返回 (True, ""),失败返回 (False, "AssertionError: ...")。

这是本章第二个反直觉点:reward 只是一个标量(甚至只是 pass/fail),它的信息量极低。它能告诉系统"你失败了",却说不出哪一步错了、该怎么改。在 median 里,Evaluator 给出的只是"第二条断言挂了 + 一行报错";它不会、也无法说"你应该对偶数长度取中间两数平均"。这个"诊断"动作不在 Evaluator 的职责里。

比文档深一层的机制:这里藏着 credit assignment 问题。一个标量 reward 无法在一串动作里定位"是哪一步 ai 导致了后续连锁出错、它该被改成什么"。即便把 reward 升级成向量也不行——它仍然不是一个带定位、可操作的纠错信号。这正是为什么光有 Evaluator 还不够:它制造了一个"知道错了、但不知道错在哪"的缺口。填这个缺口的,是下一个零件。

预测一下

假设把 Evaluator 的输出从 pass/fail 升级成一个更细的分数(比如 0–1 连续值、或一个多维向量),Actor 下一次就能写对 median 了吗?为什么?

展开看答案

大概率不能,而且这恰恰是 Reflexion 的核心论点所在。把标量换成连续值或向量,提升的只是"失败程度"的分辨率——它告诉你"错得有多严重",却依旧不告诉你错在哪一步、该怎么改。median 的修复需要一个带定位的命题:"偶数长度时取了单个元素,应改成取中间两数平均"。这是自然语言才能承载的、高维且可操作的纠错信号,任何维度的数值 reward 都表达不出来。所以信息量不在打分这一步变多——它在下一步、把标量翻译成语言时才被创造出来。

衔接下一节:Evaluator 制造了"知道错、不知错在哪"的缺口。把这个标量缺口填成可操作教训的,是 Self-Reflection。

1.5Self-Reflection:把标量失败翻成第一人称语言教训

把 reward + trajectory 转成一条第一人称的自然语言教训(reflective text)。

为什么需要它

Evaluator 给的是"失败"这个标量,Actor 要的是"下次具体怎么改"。中间隔着一道翻译。Self-Reflection 就是这台翻译机:它读 trajectory(发生了什么)+ reward(结果如何),输出一句能直接指导下一次行动的话。整个循环里,信息量正是在这一步被创造出来的。

这是本章第三个、也是最关键的反直觉点的落点:前面 Evaluator 给的标量信息量近乎为零,而 Self-Reflection 通过"语言化的诊断"凭空造出了一个高维、带定位的纠错信号。在 median 场景里,它读到"代码取了 s[len(s)//2] + 偶数用例失败",输出的反思文本是:

trial 0 的 reflective text(被展示的程序产物)text
我只取了中间单个元素,没处理偶数长度;下次对偶数长度取中间两数的平均。
关于这里的"我"

上面这句反思是第一人称写的——这是论文明确的设计选择(reflective text written in the first person)。注意区分:第一人称只出现在被展示的 reflection 样本里(它是程序产物,像一段日志),而本章正文从头到尾是第三人称。看到引用块里的"我",那是 agent 在对自己说话,不是作者在说话。

比文档深一层的机制:Self-Reflection 之所以能替代梯度,是因为它在做 credit assignment ——而它用的工具是语言,不是数值。它能推断"动作 ai 导致了后续 ai+1、ai+2 出错,应该改成 a'i"。在 median 里,这个定位就是"问题出在'取单个元素'这一步,改成'偶数取两数平均'"。这一句话同时完成了三件标量做不到的事:定位错误环节、给出修正方向、且以 Actor 能直接读懂的形式表达。所以论文的消融结果才合理——只把上次 trajectory 原样塞回去(blind retry / episodic-memory-only)效果反而更弱,加上这一步反思能再贡献约 +8% 的绝对提升;盲目重试(52%)甚至低于不重试(60%)。起作用的从来不是"再试一次",而是"反思那一步做的诊断"。

衔接下一节:Self-Reflection 产出了一条教训。但单条教训若用完即丢,下一次就又从零开始。要让教训跨回合积累,需要一个存放它们的地方——episodic memory。

1.6episodic memory(情景记忆):跨 trial 的反思 buffer = 长期记忆

跨 trial 累积反思文本的 buffer,即长期记忆;Ω=1–3 的滑动窗口,不是向量库。

为什么需要它

trajectory 是短期的、用完即弃;但教训必须活过这一回合,才能在下一次被读到。episodic memory 就是那个"活下来的容器"——它只装反思文本,不装原始轨迹,并把它们一路带进后续每一次尝试的 prompt。

这是本章第四个反直觉点,也是最常被想偏的一个:episodic memory 不是向量数据库,而是一个长度上限为 Ω 的滑动窗口(论文 AlfWorld 用 3、programming 用 1)。它的实现朴素到近乎简陋:一个列表,每反思一次就 append 一条,只保留最近的 Ω 条。论文把"用向量库 / SQL 做检索式长期记忆"明确列为 future work——也就是说,标准 Reflexion 里没有嵌入、没有相似度检索,只有"最近几条"。

比文档深一层的机制:为什么是滑动窗口而不是全量保留?因为反思文本要被拼进下一轮的 prompt,而上下文长度有限。到 trial 3 时,prompt 已经 = 任务描述 + 多次尝试的痕迹 + 多条反思;若无上限,长任务会直接溢出窗口。Ω 因此是一个为适配上下文长度而设的容量上限,不是检索策略。把它理解成"队列尾部最近 Ω 条",而不是"按相关性召回的知识库"——这一刀切错,后面 02 章的 memory injection、04 章的 memory 膨胀全会跟着想偏。

短期记忆 · trajectory trial 内存在,跨 trial 重置 长期记忆 · reflections 跨 trial 保留,Ω 滑动窗 trial 0 τ₀:代码+报错 用完即弃 重置 trial 1 τ₁:新代码 从空轨迹起 每回合的 τ 不继承上一回合 episodic memory(列表 · 只追加反思) sr₀「我没处理偶数长度…」 sr₁「边界 index 又差一位…」 sr₂「这次该判 n 是否为偶…」 Ω=3 窗口 整段拼进下一轮 Actor 的 prompt
图 2短期记忆(trajectory)绑死在单次 trial 内、跨回合重置;长期记忆(reflections)是一个只追加的列表,被 Ω 滑动窗截到最近几条,整段送进下一轮 prompt。注意:右侧是"最近 Ω 条",不是按相关性检索的向量库——这是最常被想偏的一刀。

衔接下一节:四个角色、两种记忆都齐了。剩下的问题是——它们如何接成一个能自我改进的闭环?这就是 trio。

1.7trio:三者接成"生成 → 评估 → 反思 → 重试"循环

Actor、Evaluator、Self-Reflection 三个模型,经 trajectory、reward、memory 串成一个跨 trial 的闭环。

为什么需要它

单看任何一个零件都不构成"反思"。Actor 只会生成、Evaluator 只会打分、Self-Reflection 只会诊断;只有把三者按固定顺序连起来、并让记忆把这一轮的教训带到下一轮,"自我改进"才作为一个系统性质涌现出来。trio 就是这个连接方式本身。

三个模型(论文记作 Ma / Me / Msr)按这样一条数据流首尾相接:Actor 读"任务 + memory 里的反思"生成 trajectory → Evaluator 给 trajectory 打出 reward → 若未通过,Self-Reflection 把 (trajectory, reward) 翻成一条反思文本 → 这条文本 append 进 episodic memory → 回到 Actor,开始下一个 trial。循环在两种情况下停止:Evaluator 判定通过,或者达到 max_trials。

LLM 权重全程冻结 · 唯一变化的是 memory 里的文本 Actor (Mₐ) CoT / ReAct · 生成 Evaluator (Mₑ) 打分 · reward Self-Reflection (M_sr) 诊断 trajectory τ 短期记忆 reward(标量) 信息量低 episodic memory · 反思 buffer(Ω 滑动窗)= 长期记忆 append 反思文本 下一 trial:Actor 读回反思 通过 / 达到 max_trials → 停 信息量在 Self-Reflection 这一步从"标量"被翻成"带定位的语言教训"
图 1三组件循环:Actor 生成 trajectory,Evaluator 打出 reward,Self-Reflection 把标量翻成反思文本并 append 进 memory,下一 trial 由 Actor 读回。注意:外框标注权重全程冻结——唯一在 trial 间变化的是 memory 里的文本;信息量是在反思那一步、而非打分那一步产生的。

比文档深一层的机制:把图 1 横着读,会看到一条信息量先降后升的曲线。Actor 产出富信息的 trajectory → Evaluator 把它压成一个近乎零信息的标量 reward → Self-Reflection 又把这个标量重新展开成一条带定位的语言教训。中间这一压一展不是冗余:压缩是为了得到一个明确的"对/错"判定(停止条件需要它),展开是为了得到一个可操作的修正方向(下一次 Actor 需要它)。这条循环之所以能改进,靠的正是 Self-Reflection 在展开那一步注入的、标量里本不存在的信息——而这一切发生时,三个模型的权重一个都没动。

把 median 场景跑完整

trial 0:Actor 写 return s[len(s)//2] → Evaluator 跑测试,偶数用例抛 AssertionError(reward = fail)→ Self-Reflection 产出 sr₀「我没处理偶数长度,下次取中间两数平均」→ 写进 memory。trial 1:Actor 读到 sr₀,改写成"奇数取中间、偶数取两数平均"的版本 → Evaluator 两条测试全过(reward = pass)→ 停止。全程零梯度,改进只来自 memory 里多出来的那一句话。

自测:合上教程再看自己能不能答

  1. (概念)把这七个零件按数据流排成一条链:从 Actor 出发,经过哪些中间产物(trajectory / reward / reflective text),最后又怎么回到 Actor?哪些产物属于短期记忆、哪些属于长期记忆?
  2. (辨析)有人说"Evaluator 打分越精细,Actor 改得越好,所以应该把 pass/fail 升级成连续分数"。这句话错在哪?用"信息量在哪一步产生"来反驳。
  3. (设计)你要给一个真实任务接 Reflexion,但发现 episodic memory 到第 4 个 trial 就把上下文撑爆了。在不引入向量库的前提下(保持标准 Reflexion 的滑动窗设定),你有哪两三个调节旋钮可用?各自的代价是什么?
展开参考答案

1. Actor →(生成)trajectory τ →(Evaluator 打分)reward →(Self-Reflection 翻译)reflective text →(append)episodic memory →(下一 trial 读回)Actor。其中 trajectory 是短期记忆(单 trial 内,跨回合重置);episodic memory 里的反思文本是长期记忆(跨 trial 保留,受 Ω 截断)。

2. 错在把"分辨率"当成"信息量"。连续分数只让"失败程度"更精细,仍然回答不了"哪一步错、怎么改"——它不做 credit assignment。真正凭空创造可操作信息的是 Self-Reflection 那一步的语言化诊断;打分这一步再细,信息量也不增加(预测题二同理)。

3. 旋钮:① 调小 Ω(滑动窗容量),只留最近 1–2 条反思——代价是丢掉更早 trial 的教训,可能重蹈早期覆辙;② 让 Self-Reflection 产出更短的反思文本(约束长度),代价是定位可能变粗、修正方向变模糊;③ 调小 max_trials 提前停,代价是放弃本可在更多轮里收敛的任务。三者都在"上下文预算 vs 改进机会"之间取舍——这正是 04 章 memory 膨胀一节要展开的张力。

刚好够不着的挑战

同一句教训,为什么"放进 memory"比"原样重放 trajectory"强?

设想两种"重试"实现:实现 A 把上一次完整的 trajectory(代码 + 报错)原样塞回下一轮 prompt;实现 B 只把 Self-Reflection 提炼的那一句反思文本放进 memory、再拼进 prompt。论文消融显示 B 明显优于 A(反思额外贡献约 +8% 绝对值,而盲目重试 52% 反低于不重试的 60%)。试用本章的"credit assignment + 信息量在哪一步产生"两个概念,说清楚 A 给 Actor 的到底是什么、B 给的又是什么——为什么"更多原始上下文"反而不如"一句被诊断过的教训"。这个问题的完整机制会在 02 章 #ablation 展开,这里先尝试自己讲一遍。

Further reading