第 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 观察到这种"失忆"会导致两种典型翻车:
- 想一口吃成胖子:agent 试图在一个窗口里"一把梭"把整个应用做完,结果做到一半窗口耗尽,留下一个半成品、还没文档——下一班接手时一脸懵,得花大量时间猜"这是啥、怎么又跑不起来了"。
- 看到有进展就宣布完工:后面某一班的 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.md、budget_limit.md、continuation.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 里有一个不起眼但关键的枚举:
它控制的是:当一次工具调用结束、账记完、发现目标状态变成了 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."
拆成三条工程约束:
- Semaphore 锁(
continuation_lock: Semaphore::new(1)):续跑只能有一个在途。maybe_start_goal_continuation_turn一上来就continuation_lock.acquire(),拿不到就退出。这防的是"同一个目标被并发拉起两条续跑 turn"——长程 agent 最怕的就是自己和自己打架,一个信号量把它焊死成串行。 - plan mode 忽略 continuation:
should_ignore_goal_for_mode(mode)在 plan 模式下直接返回、不续跑(源码日志:"skipping active goal continuation while plan mode is active")。因为 plan 模式(第 9 章)是"只想、不动手",自动续跑会破坏这个契约。 - 空续作抑制下一次自动续跑:如果一次续跑 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/memories(local.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 的人记住,而且都有原文为证:
- 为什么用 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字段。 - 用强硬措辞防止 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_compact,inline / 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— 目标、预算、continuationcodex-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 进度工件
方法论
- Anthropic — Effective harnesses for long-running agents
- Anthropic — Harness design for long-running application development
- Anthropic — Effective context engineering for AI agents
- OpenAI — Harness engineering: leveraging Codex in an agent-first world
注:压缩的触发判定与三路实现已独立成章(第 5 章);目标与记忆子系统仍有更多细节(如 goals 的 wall-clock 计时),本章聚焦"换班接力"主干与"实验室↔生产"对照;符号/路径以 HEAD=ad2012d6 为准。