5.5 长任务的交接:Session 不等于 Context
一句话结论:长任务的核心挑战不是"做",而是"交接"。一个人换班时失忆,工作就断了——Agent 也一样。所以长任务的关键设计是:让每一次交接都留下干净、可续的状态。
读这篇之前
需要先读 5.2(压缩)、2.2(状态外显)。这一篇收束第 5 章,也回答了一个很实际的问题:为什么 Agent 干不了大活。
一、先看两种典型失败
给 Agent 一个高层提示:"帮我做一个完整的聊天应用"。这需要认证、对话管理、流式输出、文件上传、Markdown 渲染、多轮历史……加起来 200+ 个功能点。
现实中会怎么失败?两种模式:[原文 4631]
失败模式一:One-shotting(一口气做太多)
Agent 试图在一次会话中完成所有功能,结果:
- 上下文窗口在实现到一半时被用完
- 下一个 Agent 接手时,面对半成品代码,只能猜测前任做了什么
- 大量时间浪费在"让基本功能重新跑起来",新功能反而没时间做
- 即使有压缩(5.2),压缩后的指令不够清晰,新 Agent 仍然迷路
典型时间线:
Agent 1 开始写 → 写了 50% 功能 → 上下文用完
Agent 2 接手 → 花时间修复半成品 → 上下文又用完
Agent 3 接手 → 重复……
每次接手都忙于修复,推进陷入停滞。
失败模式二:过早宣布完成(Premature Completion)
Agent 看到已经实现了一些功能,就认为项目基本完成:
- 声明完成,实际只完成核心功能的 30%
- 缺乏任务清单,不知道还差什么
- 没有验证机制,以为做完了但没有端到端测试证明
一个精准的类比
想象一个项目,每个工程师只上一个班次,换班时完全失忆:不知道前一个人做了什么、为什么做、下一步该做什么。每个人坐下打开一堆半成品代码,只能从零开始理解。
这就是没有交接机制的长运行 Agent 的真实状态。[原文 4631]
核心洞察:Agent 并不缺乏能力,它缺的是在上下文断裂时维持连续性的机制。
二、关键区分:Session 不等于 Context
这是本篇最重要的一句话:
| Session(会话) | Context(上下文) | |
|---|---|---|
| 是什么 | 一段连续的交互过程 | 模型在某一轮能看到的全部内容 |
| 会不会断 | 长任务必然要断 | 每轮都在变 |
| 断的后果 | —— | 模型"失忆" |
"Session 不等于 Context"的含义:即便你保持同一个会话,上下文也会因为压缩、截断而不断丢失细节;反过来,跨会话也能维持连续性,只要状态被正确外置。
结论:长任务的连续性不能靠"一个超长会话"来维持,必须靠外部的状态文件。
三、方案一:双角色(Initializer + Coding)
核心思路:把一个 Agent 拆成两个角色,一个负责规划,一个负责执行。每次交接都留下干净的状态。[原文 4631]
Initializer Agent(只跑第一轮,负责从零到有)
| 职责 | 产出 |
|---|---|
| 搭环境 | init.sh 脚本 |
| 写进度文件 | claude-progress.txt |
| 把高层提示展开成详细功能清单 | JSON 格式清单 |
| 做第一次提交 | 干净的 git commit |
Coding Agent(每轮都跑,负责从有到多)
| 职责 | 做法 |
|---|---|
| 读进度文件 | 了解当前状态 |
| 一次只做一个功能 | 不贪多 |
| 完成后更新进度文件 | 留下状态 |
| git commit | 写清楚做了什么 |
这两个角色的分工,本质上是把"规划"和"执行"分开——这正好对应 1.2 讲的四模块里那个容易被忽略的「规划」。
四、功能清单:为什么必须用 JSON
一个非常具体、非常有价值的经验:
用 JSON 格式记录功能清单,而不要用 Markdown。原因是:模型更不容易错误修改结构化的 JSON,而 Markdown 容易被模型顺手重写。[原文 4631]
{
"features": [
{
"category": "authentication",
"description": "邮箱密码登录与会话管理",
"steps": [
"创建登录表单组件",
"实现认证 API",
"添加 session cookie 处理",
"写端到端测试"
],
"passes": false
},
{
"category": "chat",
"description": "与模型的流式对话",
"steps": ["..."],
"passes": true
}
]
}
为什么 JSON 更好? 因为它有强结构——模型"顺手重写"时会被结构卡住,而 Markdown 的松散结构允许它自由改写,一改就丢失了原始约束。
一个必须加的保护
在提示中明确告诉 Agent:「不允许删除或修改已有的测试内容」。
否则 Agent 会为了让测试通过而降低测试标准——这是"过早宣布完成"的一种变体,也是第 13 章评测要防的作弊模式。
五、方案二与方案三:脑手分离、双角色
原文还提到另外两种交接方案:[原文 4631]
| 方案 | 做法 | 适用 |
|---|---|---|
| 脑手分离 | 一个模型负责规划(脑),一个负责执行(手),通过状态文件中转 | 规划复杂、执行机械的任务 |
| 双角色(已述) | Initializer 规划一次,Coding 逐步执行 | 长周期、多功能的构建任务 |
三者共同的核心:状态外置到文件,而不是留在上下文里。
六、把"交接"设计成一种能力
综合起来,长任务要回答四个问题:
| # | 问题 | 手段 |
|---|---|---|
| 1 | 现在做到哪了? | 进度文件 + passes 标记 |
| 2 | 为什么这么做? | 关键决策记录(不是流水账) |
| 3 | 下一步做什么? | 功能清单里第一条 passes: false |
| 4 | 怎么验证做对了? | 测试 + 明确的验收标准 |
第 2 条最容易被忽略。"为什么"比"做了什么"更难重建——代码能看出做了什么,但看不出为什么不用另一种方案。
一个最小可用的进度文件
{
"goal": "实现聊天应用的认证模块",
"done": ["登录表单", "认证 API"],
"in_progress": {"item": "session cookie", "note": "已写入但未验证过期逻辑"},
"decisions": [
{"what": "用 JWT 而非 session 表", "why": "无状态,便于横向扩展"}
],
"next": "补充 session 过期与刷新的测试",
"blocked": []
}
这份文件极小(几百 Token),但它让任何"新接手者"能在 10 秒内进入状态。
七、第 5 章小结
五篇讲完,把上下文这一层压缩成一张表:
| 主题 | 核心结论 | 篇 |
|---|---|---|
| 装什么 | 六类内容;历史是最大膨胀源;装错比装满更糟 | 5.1 |
| 压缩 | 把"要算的"变成"能查的";防溢出更防腐化 | 5.2 |
| 缓存 | 前缀必须稳定;动态内容追加末尾 | 5.3 |
| 污染与遗忘 | 内容可以是指令;错误会被继承 | 5.4 |
| 交接 | Session ≠ Context;状态必须外置 | 5.5 |
三条最重要的结论:
- 上下文是能力天花板(承接 2.2)。提效果先管好它。
- 稳定性 > 容量。前缀稳、状态显式,比塞更多资料有用。
- 连续性靠文件,不靠会话。长任务的本质是交接设计。
八、动手:长任务交接的落盘格式
5.5 讲了"状态外置"。这里给出一个可直接用的文件格式。
{
"task": "修复登录超时 bug",
"started_at": "2026-10-25T10:00:00Z",
"turn": 7,
"progress": {
"done": [
"读取日志,确认超时错误",
"定位到 src/auth/session.ts:42",
"确认配置 timeout=5s 过短"
],
"in_progress": null,
"next": "修改配置为 30s 并跑测试"
},
"decisions": [
{
"what": "改配置而非改代码",
"why": "默认值本身不合理,不是逻辑 bug",
"at_turn": 5
}
],
"side_effects": [
{"action": "write_file", "target": "src/auth/session.ts", "at_turn": 6}
],
"resources": ["src/auth/session.ts", "logs/app.log"],
"constraints": ["不要修改测试文件"]
}
五个字段的作用
| 字段 | 恢复时怎么用 |
|---|---|
progress |
直接告诉模型"做到哪、下一步做什么" |
decisions |
防止模型推翻已定的方向 |
side_effects |
防止重复执行(发邮件发两封) |
resources |
不必重新搜索文件 |
constraints |
防止压掉关键约束(5.2 的压缩风险) |
最后两个字段最容易被省
side_effects:没有它,恢复后可能把"发邮件"再做一遍。
constraints:压缩时最容易丢"不要做什么"这类否定约束(5.2 已强调)。
恢复时的注入方式
【会话恢复】
任务:{task}
已完成:{done 列表}
下一步:{next}
注意:{side_effects} 已执行过,不要重复。
持续约束:{constraints}
不显式声明"这是恢复的",模型可能会从头开始整个任务。
一个完整的长任务案例:8 小时重构
一个真实的对照:Agent 重构一个模块,跑了 8 小时、40 多轮。
没有交接机制(失败):
| 阶段 | 发生了什么 |
|---|---|
| 第 1-15 轮 | 正常推进 |
| 第 16 轮 | 上下文满,触发截断 |
| 第 17 轮 | 模型"忘了"之前的方案,重新开始设计 |
| 第 18-25 轮 | 在新方案上推进,与前 15 轮的改动冲突 |
| 之后的轮次 | 一直在修补冲突,直到预算耗尽 |
结局:40 轮后项目处于"改了一半、编译不过"的状态,没有任何检查点可用。
有交接机制(成功):
| 阶段 | 做法 |
|---|---|
| 每完成一个子目标 | 写 progress.json + git commit |
| 上下文接近阈值 | 从检查点重建上下文(不是截断) |
| 发现方向错 | 回滚到上个检查点,重来 |
结局:同样 40 轮,但产出了可编译、有测试、可交付的结果。
两个唯一的差别
| 差别 | 没有交接 | 有交接 |
|---|---|---|
| 子目标完成时 | 继续往下做 | 写检查点 |
| 上下文满时 | 截断(丢信息) | 从检查点重建 |
"写检查点"只是多花 30 秒,但它决定了 8 小时的工作是否白做。
一个可以直接抄的做法
每轮结束前问一句:
我这次完成了一个"可验证的子目标"吗?
是 → 写检查点(progress.json + commit)
否 → 继续
这个习惯的成本极低,但它是长任务能不能跑完的分水岭。
九、动手:一个可运行的检查点管理器
5.5 给了格式。这里给出读写逻辑。
import json, os, time
class Checkpoint:
def __init__(self, path=".agent/progress.json"):
self.path = path
def load(self):
if not os.path.exists(self.path):
return None
return json.load(open(self.path, encoding="utf-8"))
def save(self, task, turn, done, next_step, decisions,
side_effects, constraints, resources):
state = {
"task": task, "turn": turn, "saved_at": time.time(),
"progress": {"done": done, "next": next_step},
"decisions": decisions,
"side_effects": side_effects,
"resources": resources,
"constraints": constraints,
}
os.makedirs(os.path.dirname(self.path), exist_ok=True)
json.dump(state, open(self.path, "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
def render(self):
"""把检查点渲染成给模型看的恢复文本"""
s = self.load()
if not s:
return ""
return (
"【会话恢复】\n"
f"任务:{s['task']}\n"
f"已完成:{';'.join(s['progress']['done'])}\n"
f"下一步:{s['progress']['next']}\n"
f"已执行过的动作(不要重复):"
f"{';'.join(x['action'] + ':' + x['target'] for x in s['side_effects'])}\n"
f"持续约束:{';'.join(s['constraints'])}\n"
)
三个设计要点
| 要点 | 理由 |
|---|---|
render() 单独一个方法 |
恢复时直接拼进上下文,不用手写 |
side_effects 带 target |
能精确说"哪个动作做过" |
constraints 单独存 |
防止压缩时丢掉(5.2 的风险) |
一个常见的实现错误
错:save() 里用 turn 做文件名(progress_7.json)
→ 恢复时不知道用哪个
对:固定文件名 progress.json,内部记录 turn
→ 永远能拿到"最新"
检查点是"覆盖",不是"归档" —— 归档需求用 git commit 解决。
十、下一篇
上下文解决的是"一次任务内记得住"。但还有一个更大的问题:跨任务、跨会话,怎么让 Agent 记得你?
这就是记忆系统。它和上下文不是一回事——上一篇已经说清了 Session ≠ Context,而记忆比上下文又更靠外一层。
下一篇从三层记忆讲起。
一个反例:只存对话历史
错:检查点只存 messages
后果:
① 恢复后模型不知道"下一步做什么" → 重新规划,可能走错方向
② 不知道哪些副作用已执行 → 重复发邮件
③ 不知道已定的决策 → 推翻之前的选择
对话历史是"发生了什么",不是"接下来做什么"。 两者必须分开存。
一句话总结
上下文工程的四条判据:装对、压对、省对、防对。
- 装对:三段分离,稳定在前
- 压对:保约束,丢过程
- 省对:前缀稳定,缓存命中
- 防对:来源标记,执行兜底
四条都做到,上下文就不再是瓶颈。
一句话
长任务不是"一个很长的任务",是"很多个短任务的串联"。 每一个短任务结束时留一个干净的接缝,长任务才跑得完。
一个补充:检查点写太频繁也有代价
| 频率 | 问题 |
|---|---|
| 每轮都写 | 磁盘 IO + 注意力分散到"记录"上 |
| 每 5 轮写一次 | 可能丢掉最近 4 轮 |
| 语义边界写 | ✅ 最优 |
"语义边界"指:
定位到问题 → ✅ 写
改完代码 → ✅ 写
测试通过 → ✅ 写
读了一个文件 → ❌ 不写
判据:"如果现在进程崩了,重做这件事要多久?" 超过 5 分钟,就值得写检查点。
这一条判断标准很好用:超过 5 分钟能重做的,就值得存一次。
第 5 章收尾
上下文这一章的全部结论可以压成一句:装对、压对、省对、防对,然后留好接缝。 前四个字管"这一轮做好",最后一个管"下一轮接得上"。
记住这句,第 5 章就真正学会了。
长任务之所以能跑完,靠的不是更强的模型,而是更好的接缝。