3824 字
约 12 分钟
18
5.5 长任务的交接:Session 不等于 Context
2026-10-25
无标签

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

三条最重要的结论:

  1. 上下文是能力天花板(承接 2.2)。提效果先管好它。
  2. 稳定性 > 容量。前缀稳、状态显式,比塞更多资料有用。
  3. 连续性靠文件,不靠会话。长任务的本质是交接设计。

八、动手:长任务交接的落盘格式

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 章就真正学会了。

长任务之所以能跑完,靠的不是更强的模型,而是更好的接缝。

5.5 长任务的交接:Session 不等于 Context
https://clxhxhhr.top/posts/4891/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。