跳转至

第 11 章 长程作业:目标与记忆(跨上下文窗口的"换班")

这是本书的"皇冠章"。前面所有层,讲的都是 agent 在一个上下文窗口里怎么干活。但真正有价值的任务——造一个完整应用、重构一个大模块——往往一个窗口装不下,要跨好几个小时、好几次"重开"才能干完。

这一章的问题是:当 agent 必须"换班"时,怎么让它不失忆地接力? 这是长程 agent 最难、也最能拉开差距的地方——Anthropic 把它直接称为"an open problem(尚未解决的开放问题)"。

引子:每个新班次的工程师,都失忆

Anthropic 有一个绝妙的比喻:让 agent 干长活,就像一个软件项目由一批轮班的工程师来做——而每个接班的人,都对上一班发生过什么毫无记忆。 用原文的话说:长程 agent 的核心难题是"它们必须在一段段离散的会话里工作,而每个新会话开始时都对之前发生的一切毫无记忆(each new session begins with no memory of what came before)"。

为什么?因为上下文窗口是有限的。一个大任务做到一半,窗口满了,就得开一个新会话——而新会话是一张白纸,它不知道前面已经干了什么、为什么这么干、下一步该干嘛。

更微妙的是,这个限制不只是"装不下"那么简单。Anthropic 在《Effective context engineering for AI agents》里给它起了个名字——上下文腐烂(context rot):随着上下文变长,模型性能本身会下降,因为 Transformer 里每个 token 都要"注意"到其他每个 token,上下文越长,注意力预算被摊得越薄。换句话说,就算窗口物理上还塞得下,塞太满本身也会让模型变笨。 这就是为什么"主动管理上下文"不是优化项、而是长程 agent 的生死线。

Anthropic 观察到这种"失忆"会导致两种典型翻车:

  1. 想一口吃成胖子:agent 试图在一个窗口里"一把梭"把整个应用做完,结果做到一半窗口耗尽,留下一个半成品、还没文档——下一班接手时一脸懵,得花大量时间猜"这是啥、怎么又跑不起来了"。
  2. 看到有进展就宣布完工:后面某一班的 agent 扫了一眼,看到"好像做了不少了",就直接宣布"任务完成"——原文描述得很传神:"a later agent instance would look around, see that progress had been made, and declare the job done(后来的 agent 实例环顾四周,看到有进展,就宣布大功告成)"。

这两种翻车的根子是同一个:没有一套让"班次之间"传递状态的机制。 第 5 章讲的压缩,解决的是"同一个班次内窗口快满了"的续命问题;这一章讲的另外两件装备——目标、记忆——加上落地它们的版本化进度工件,解决的是"换了班也不失忆"。

〔配图 1:换班的工程师(封面级)〕——画一个"交接班"场景:左边一个累瘫的 agent 工程师(上一班)正要离开,手里把三样东西交给右边刚到岗的 agent(下一班):一份"进度记录(progress)"、一串"git 提交链"、一张"目标/待办清单"。两人中间一道"上下文窗口"的门。配字:"每个新班次都失忆——所以必须留下交接物。"

一、目标:把"要干嘛、还剩多少预算"做成一等工件

第 5 章的压缩腾的是"地方",但腾完地方,agent 还得知道"我们到底要干成什么",不能每班各干各的。Codex 把"目标"做成了一等公民(codex-rs/core/src/goals.rs + tools/handlers/goal/ 下的 create_goal / get_goal / update_goal + 模板 codex-rs/prompts/templates/goals/,另有一份在 ext/goal/templates/goals/)。它管三件事,对应三份模板:objective_updated.mdbudget_limit.mdcontinuation.md

预算:只有 token_budget,而且只是"提醒"不是"硬墙"

很容易以为预算同时按 "token / 步数" 两种口径来记,但没有"步数"这回事goals.rs 里的字段就一个:

pub(crate) token_budget: Option<i64>,
// 校验也只认它:
pub(crate) fn validate_goal_budget(value: Option<i64>) -> anyhow::Result<()> {
    if let Some(value) = value && value <= 0 {
        anyhow::bail!("goal budgets must be positive when provided");
    }
    Ok(())
}

预算是按 token 记的,统计口径是 goal_token_delta_for_usage(非缓存输入 + 输出 token)。超了之后会发生什么?不是硬性中止,而是"steering(引导)"——这是 goals 系统最精的设计。

BudgetLimitSteering:Allowed / Suppressed,决定"要不要提醒它超预算了"

goals.rs 里有一个不起眼但关键的枚举:

enum BudgetLimitSteering { Allowed, Suppressed }

它控制的是:当一次工具调用结束、账记完、发现目标状态变成了 BudgetLimited(超预算)时,要不要把"你超预算了"这条引导信息推给模型。规则是:

  • 普通工具完成(ToolCompleted)→ Allowed:正常干活时超了预算,应该提醒模型——让它知道该收敛了,对照目标决定是继续还是收尾。
  • "更新目标"工具本身完成(ToolCompletedGoal)→ Suppressed:agent 刚刚在调 update_goal 调整目标,这时候用旧预算去打断它——否则会出现"它正想把预算调大,你却拿旧预算去喷它"的荒诞场景。

而且系统会记一个 budget_limit_reported_goal_id同一个目标的超预算只引导一次!budget_limit_was_already_reported),不会每轮都念叨。这就把"预算"从一道生硬的墙,变成了一次恰到好处的提醒——既治住"在一个子任务上无限投入",又不会因为机械中止而毁掉一次本来快成功的长任务。这正呼应第 10 章那条:"对 agent 的约束要可引导、可恢复,而不是一刀切。"

Continuation:接力棒的真实运行时——Semaphore、plan mode、空续作抑制

把 continuation 比作"接力棒",方向对,但容易停在比喻层。真实机制在 goals.rs 里是一套带并发控制的自动续跑goal_runtime_apply 那段 doc comment 几乎是这套语义的官方说明书:

"plan mode ignores continuations, turn starts capture the active goal and token baseline, tool completions account usage and may inject budget steering, … continuation turns with no counted autonomous activity suppress the next automatic continuation until user/tool/external activity resets it."

拆成三条工程约束:

  1. Semaphore 锁(continuation_lock: Semaphore::new(1):续跑只能有一个在途。maybe_start_goal_continuation_turn 一上来就 continuation_lock.acquire(),拿不到就退出。这防的是"同一个目标被并发拉起两条续跑 turn"——长程 agent 最怕的就是自己和自己打架,一个信号量把它焊死成串行。
  2. plan mode 忽略 continuationshould_ignore_goal_for_mode(mode) 在 plan 模式下直接返回、不续跑(源码日志:"skipping active goal continuation while plan mode is active")。因为 plan 模式(第 9 章)是"只想、不动手",自动续跑会破坏这个契约。
  3. 空续作抑制下一次自动续跑:如果一次续跑 turn 跑完没有任何"被计入的自主活动"(既没调工具、也没产生实质进展),系统会抑制下一次自动续跑,直到有用户/工具/外部活动把它重置。这是防"空转死循环"的关键——不然一个 active 目标会永远自己拉自己续跑,烧光预算什么也没干成。

续跑真正发起时,注入的是 continuation_prompt(&goal)(来自 continuation.md 模板)包装成的一条上下文输入项。整条链路还层层设了"逃生阀":目标不是 Active、已有 turn 在跑、mailbox 里有待处理输入、ephemeral 线程……任一成立就不续跑。接力棒不是"无脑往下递",是一套"确认下一棒该不该跑、能不能跑"的运行时协议。

有了显式目标 + 这套 continuation,"看到有进展就宣布完工"那种翻车就被治住了——因为 agent 不是凭感觉收尾,而是由"目标状态机 + 续跑判定"来决定还要不要接着干。

〔配图 2:目标三件套(落到运行时)〕——三张卡片并排:① objective(一面旗子,"最终目标,跨班不变")② token_budget(一个油表,下面小字"超了不是断电,是 BudgetLimitSteering: Allowed/Suppressed 引导一次")③ continuation(一支接力棒,棒上缠着一把锁🔒"Semaphore=1",旁注"plan mode 不接力 / 空续作抑制下一棒")。底部:"接力棒不是无脑递——是一套'该不该跑、能不能跑'的运行时协议。"

二、记忆:跨会话的持久工作记忆

压缩和目标管的是"一趟任务之内"的连续性。但有些东西,你希望 agent 跨任务、跨天都记得——你的偏好、这个项目的特殊约定、上次踩过的坑。这就是记忆(memories)。这正是 Anthropic 列出的另一根上下文工程杠杆——"结构化记笔记(structured note-taking)":把高信号信息持久化到上下文窗口之外,需要时再拉回来。

Codex 有专门的记忆子系统:codex-rs/memories/ 下的 read/write 两个 crate,加上 codex-rs/ext/memories。它把记忆持久化到 ~/.codex/memorieslocal.rs 里就是 codex_home.join("memories"))。还记得第 10 章吗?这个目录在 workspace-write 模式下默认可写,正是为了让 agent 维护记忆不必每次审批。

两阶段写入 + consolidation:归并去重 + 渐进式披露

记忆不是"看到啥记啥"那么糙,它是个两阶段流水线memories/write/src/phase1.rs / phase2.rs):

  • Phase 1:从一次次 rollout(会话记录)里抽出"原始记忆(raw memories)",汇成临时的 raw_memories.md。这一步还会做安全清洗——redact_secrets 把 token/key/密码替换成 [REDACTED_SECRET],绝不把密钥写进记忆。
  • Phase 2(consolidation,整合):拿 raw_memories.md 当输入,按 codex-rs/memories/write/templates/memories/consolidation.md 这份提示词,把零散条目归并、去重、提炼进一个结构化的记忆文件夹

consolidation 模板里把这个文件夹的结构写得清清楚楚,而且显式以"渐进式披露(progressive disclosure)"为设计目标——原文:"consolidate raw memories and rollout summaries into a local, file-based 'agent memory' folder that supports progressive disclosure." 这套分层是:

  • memory_summary.md:永远加载进系统提示,第一行必须是 v1,要求"dense, highly navigational, and discriminative(密集、强导航性、有区分度)"——它是索引/地图,不是内容本身。
  • MEMORY.md:手册条目,靠 grep 关键词命中;聚合洞见,并指向更详细的 rollout 摘要。
  • rollout_summaries/<slug>.md:单次会话的精炼复盘(教训、可复用知识、关键错误片段),是"按需才拉取"的深层内容。
  • skills/<name>/:可复用流程,入口 SKILL.md——这和第 8 章的 skills 渐进加载是同一套思想:先给目录,用时才展开。

这套"summary 当地图、详情按需拉"的结构,和第 4 章讲的"对 agent 可见的才存在、但别一次全塞给它"、以及第 8 章 skills 的"渐进加载"完全互文:记忆若不做 consolidation,就会膨胀成又一本'千页手册'(第 4 章那个陷阱,在记忆这里同样成立)。 模板里甚至有反"凑数"的硬规则:「No-op content updates are allowed and preferred when there is no meaningful, reusable learning worth saving(没有值得保存的可复用学习时,宁可什么都不改)」——主动选择"不记",是记忆质量的一部分。

记忆和压缩的区别值得点一句:压缩是"为了腾地方而浓缩当前任务的历史",记忆是"为了跨任务复用而沉淀的长期知识"。 一个是短期工作记忆的管理(活在一趟任务里),一个是长期记忆的积累(活在 ~/.codex/memories 里、跨任务跨天)。人脑也是这么分的——压缩像"整理今天的草稿纸",记忆像"把经验写进笔记本"。

〔配图 3:记忆的两阶段与渐进披露〕——左半"Phase 1":一摞 rollout 会话被漏斗筛出 raw_memories.md,漏斗上贴"redact 密钥"。右半"Phase 2 consolidation":raw 经过"归并/去重/提炼",落进一个文件夹塔:顶层 memory_summary.md(标"永远加载·只是地图")、中层 MEMORY.md(标"grep 命中")、底层 rollout_summaries/skills/(标"按需拉取")。底部:"summary 当地图,详情按需拉——和第 8 章 skills 同一套渐进披露。"

三、进度工件:让"交接"真的可行

压缩、目标、记忆,最终都要落到一组写在仓库里、下一班能读到的工件上——否则"换班"就是空话。这正是 Anthropic 那篇长程 agent 文章的核心招数:它给 agent 配了一个两 agent harness(初始化 agent + 编码 agent),并留下三样"交接物":

  • init.sh:一键把开发环境/服务器跑起来的脚本。下一班一上来先跑它,立刻有个能跑的环境,不用从头摸索"这项目怎么启动"。
  • claude-progress.txt:一份进度日志,记录"已经干了什么、现在到哪了"。Anthropic 点名这是"关键洞见"——让 agent 在全新上下文窗口里快速搞清工作状态,靠的就是 progress 文件 + git 历史这对组合。
  • feature_list.json:一份结构化的功能清单,每个功能初始标 "passes": false,做完一个翻成 true。这就把"整个任务"拆成了一格格可勾选的进度——直接治住了"一口吃成胖子"和"提前宣布完工"两种翻车

这里有两个极其精到的工程细节,值得每个做长程 agent 的人记住,而且都有原文为证:

  1. 为什么用 JSON 而不是 Markdown 存功能清单? 原文:「we landed on using JSON for this, as the model is less likely to inappropriately change or overwrite JSON files compared to Markdown files(用 JSON,因为相比 Markdown,模型更不容易不恰当地改写或覆盖 JSON 文件)」。Markdown 太"自由",agent 容易顺手改乱;JSON 的结构约束让它只敢动 passes 字段。
  2. 用强硬措辞防止 agent 偷工:清单的说明里写着近乎严厉的话——原文:「It is unacceptable to remove or edit tests because this could lead to missing or buggy functionality(删改测试项是不可接受的,因为这会导致功能缺失或带 bug)」。因为 agent 有"走捷径"的倾向(测试过不了?那就把测试删了!),必须明令禁止。

Codex 这边的对应物是什么? 这里有一处容易想当然的地方:看上去 Codex 似乎会用 docs/exec-plans/(active / completed / tech-debt)当进度工件——但当前 HEAD(ad2012d6)的仓库里根本没有这个目录。Codex 真实的"版本化进度面",是前面几件装备本身

  • goals(objective / token_budget / continuation)扮演 feature_list.json 的角色——它就是那份"结构化、可机械判定、不靠感觉"的目标/进度状态机;
  • memories(持久到 ~/.codex/memories、经 consolidation 提炼)扮演 claude-progress.txt + 经验沉淀的角色;
  • git 历史(由 codex-rs/git-utils 这个 crate 管理)扮演不可篡改的进度证据链——init.sh 那种"先跑起来"的环境约定,则落在仓库根的 AGENTS.md / 启动脚本里(第 6、7 章)。

形式不同,思路一致:把"进度和决策"写进版本化的仓库/状态工件,让任何一班(人或 agent)都能从仓库本身读到前情——这又回到第 4 章那条魂:对 agent 可见的,才存在。 装在上一班 agent"脑子"(上下文)里的东西,一换班就没了;只有写进仓库/状态库的,才传得下去。

〔配图 4:feature_list 翻牌进度墙〕——一面墙上贴着一排功能卡("新建对话""发送消息""主题切换"…),每张卡有个开关:初始全是 ✗ passes:false(灰),随着干活一张张翻成 ✓ passes:true(绿)。墙角一行强硬告示:"不许删改测试项!"。旁边标"用 JSON 不用 Markdown——模型更不敢乱改",并画一条线把这面墙连到 Codex 的 goals 卡片,标"Codex 的对应物:goals + memories + git,不是 exec-plans"。

四、双轨对照:实验室原型 ↔ Codex 工程化

把这一章(连同第 5 章的压缩)收成一张对照表——左边是 Anthropic 那篇文章里偏"实验室原型"的做法,右边是 Codex 把同样思路做成的"生产工程":

要解决的 实验室原型(Anthropic) Codex 的工程化对应
腾上下文(第 5 章) 上下文重置 + 交接(治 context anxiety,Opus 4.5 后删除) 主循环内 run_auto_compactinline / remote v1 / remote v2 三路分发compact*.rs),触发条件 token_limit_reached && needs_follow_up
判断"该不该腾"(第 5 章) "快满了"直觉 auto_compact_token_status 多信号(scope tokens/limit、full window、prefill 基线)
知道要干嘛 一开始写好的 spec / feature_list.json goals(objective / 只有 token_budget / continuation)+ BudgetLimitSteering 引导
防自我打架/空转 (隐式) continuation 的 Semaphore 锁 + plan-mode 忽略 + 空续作抑制
跨会话记忆 claude-progress.txt memories(read/write 两 crate + 两阶段 consolidation + 渐进披露,持久到 ~/.codex/memories
交接可行 init.sh + claude-progress.txt + feature_list.json goals(状态机)+ memories(沉淀)+ git 历史(git-utils)+ AGENTS.md
防偷工 "不许删测试" + 用 JSON 强约束工件 + 机械校验(第 10 章 / 第 13 章)

〔配图 5:双轨对照〕——左右两条平行轨道。左轨"实验室原型"画 init.sh / progress.txt / feature_list.json 三个文件图标;右轨"Codex 生产"画 compact(带三个小分叉 inline/remote/v2)/ goals(带锁)/ memories(带分层塔)三个模块。中间一条条横线把对应的连起来。底部一行:"博客观点 → 生产代码:思路一致,但 Codex 把每件都拆成了带并发/选路/分层的运行时机制。"

而"换班前先读什么"也被规范成了固定动作(Anthropic 的 "get up to speed" 步骤):每一班一上来,先 pwd 看自己在哪、读 git log 和进度文件看前情、读功能清单挑"优先级最高且未完成"的那个、跑 init.sh 确认环境没坏。把"接班该做的事"写成清单,agent 就不会上来就瞎干。 这条在 Codex 里的对应物,就是启动时拉起 goals 状态 + memories 摘要 + git log 的那套引导。

本章小结

  • 长程作业的根本难题是"换班失忆":上下文窗口有限,大任务要跨多个会话;每个新会话是白纸,没有机制就会"一口吃成胖子"或"提前宣布完工"。而且不只是"装不下"——上下文太长本身会引发 context rot,主动管理上下文是生死线,不是优化项。
  • 目标 = 一等工件 + 运行时机制:预算只有 token_budget(无步数),且超预算不是断电、是 BudgetLimitSteering(Allowed/Suppressed) 的一次性引导;continuation 这支接力棒落到 Semaphore 串行锁 + plan-mode 忽略 + 空续作抑制,是一套"该不该跑、能不能跑"的协议,不是无脑往下递。
  • 记忆 = 跨会话长期知识:两阶段写入(抽取→consolidation 归并去重提炼)+ 渐进式披露(summary 当地图、详情按需拉),持久到 ~/.codex/memories,和第 4 章上下文、第 8 章 skills 同一套思想;区别于压缩(第 5 章)的短期浓缩。
  • 进度工件是交接的关键init.sh / 进度日志 / 结构化功能清单(JSON 防乱改、强约束防偷工)。Codex 的对应物不是(不存在的)exec-plans,而是 goals + memories + git 历史——把进度与决策写进版本化的仓库/状态库,因为对 agent 可见的才存在

下一章,我们看另一种"一个 agent 不够用"的解法:不是让一个 agent 跨时间接力,而是让多个 agent 同时协作——多代理编排。


参考来源

解剖标本(codex 源码,HEAD = ad2012d6)

  • codex-rs/core/src/goals.rs — 目标、预算、continuation
  • codex-rs/core/src/tools/handlers/goal/ — goal 工具处理器
  • codex-rs/prompts/templates/goals/ — 目标模板
  • codex-rs/memories/ — 记忆 read/write 两 crate、两阶段写入
  • codex-rs/memories/write/templates/memories/consolidation.md — consolidation 模板
  • codex-rs/git-utils — git 进度工件

方法论

注:压缩的触发判定与三路实现已独立成章(第 5 章);目标与记忆子系统仍有更多细节(如 goals 的 wall-clock 计时),本章聚焦"换班接力"主干与"实验室↔生产"对照;符号/路径以 HEAD=ad2012d6 为准。

留言