任务没跑完,进度去哪了:任务级交接文档
Skill 卡片解决了新会话启动时 Agent 不知道自己是谁、能做什么的问题,给了它角色定义和行为边界。然而任务并不总能在单次会话内完成;当执行被中断、由新会话接续时,另一个问题随之出现:新会话的 Agent 不知道这个任务已经做到哪里。
任务无法在单次会话内完成听起来是偶发情况,实则不然。04b 节的任务分解假设每个子任务都能在单次会话内完成,但这个假设的失败概率远比直觉预期的高。
上下文消耗不可预测是第一个原因。一个自包含的任务规约定义了 Agent 需要读取的信息范围,但执行过程中,Agent 会主动读取规约未明确要求的外部文档——为了理解一个接口去查看它的实现,为了确认一个数据格式去翻阅上游模块的代码。这些是合理的工程行为,但每一次读取都在消耗上下文窗口。外部文档的实际长度往往超出估计,一个本应在窗口容量 60% 处完成的任务可能在 90% 时仍未结束。
模型输出的不确定性是第二个原因。大语言模型基于概率生成,同一份输入的十次执行结果并不相同。大多数时候 Agent 会顺利完成任务,偶尔它会走弯路——尝试了不必要的方案、重复解决已解决的问题、在某个分支上过度探索。这种不确定性是模型的本质属性,无法通过更好的规约消除,只能通过容错机制承接。
供给侧的不稳定是第三个原因。API 提供商的服务质量不是常量:模型可能在高峰期被静默降级,实际可用的上下文窗口可能小于文档标称的长度,速率限制可能在长任务的中途触发。这些因素在设计任务分解时完全不可控。
这些原因共同导致一个结果:任务很可能无法在不触发压缩的情况下于单次会话内完成。一旦上下文接近饱和,大部分工具会触发自动压缩——对历史内容进行摘要或截断以腾出空间。压缩是不可逆的信息损失:早期的设计决策、接口约定、已完成工作的细节被不可预测地丢弃。04a 描述的上下文之墙是质量的缓慢下降,压缩是一次性的断崖。压缩之后,Agent 的行为确定性无法保证,此时继续执行不如终止当前会话、开启新会话接续。
单次会话无法完成任务,意味着新会话必须从某个中间状态继续,而不是从零开始重做。从零重做面临同样的约束:上下文消耗、模型不确定性、供给侧波动依然存在,新会话很可能在同一个位置再次中断。只有从上次停下的地方接续,任务才能随着会话的累积逐步推进,最终完成。新会话没有前序上下文,唯一能依赖的是记录到文档的信息。这就是任务级交接文档存在的理由。
一份交接文档的目标是让一个完全没有前序上下文的 Agent 能够接续任务。但"接续"不等于继承上一个 session 的思路——新 session 的价值恰恰在于它是独立的判断者,能够发现前任走偏的地方。
这引出了交接文档的核心设计原则:只记录事实,不记录推断。
事实是已经发生的事:哪些代码已经写了,做了什么选择,遇到了什么阻塞。这些信息可以对照实际代码和测试结果验证。推断是对未来的判断:下一步应该做什么,建议用什么方案。这些信息无法验证,只能被信任或质疑。
如果上一个 session 的执行路径有偏差,推断类信息会把这个偏差编码进交接文档。新 session 看到一份"下一步计划",很可能直接沿着这条路径继续,丧失独立纠偏的机会。这是共谋问题(03b)在会话间的变体:上一个 session 既执行任务又规划后续,自己给自己定方向。交接文档里的推断类信息也是一种推理污染(04c):前一个 session 的推理产物进入新 session 的上下文,干扰独立判断。
因此,交接文档里没有 next_steps 字段。新 session 拿到事实信息后,结合原始规约(Ring 0 信任源,02e),自行推导下一步。
一份最小的交接文档包含以下字段:
spec_ref: "/specs/auth-module.md"
status:
completed:
- "JWT 签发逻辑(RS256)— 已通过单元测试"
- "登录端点 /api/login — 已通过集成测试"
in_progress:
- "Token 刷新端点 /api/refresh — 签发完成,吊销逻辑未开始"
pending:
- "登出端点 /api/logout"
- "Token 黑名单机制"
decisions:
- what: "选择 RS256 而非 HS256"
why: "需要跨服务验证,非对称签名允许服务只持有公钥"
- what: "refresh token 存数据库而非 Redis"
why: "当前阶段用户量不需要 Redis,降低基础设施复杂度"
blockers:
- "user 表缺少 refresh_token_hash 字段,需要先跑 migration"
- "发现现有 session 中间件和 JWT 逻辑有冲突,需确认是否替换"
files_changed:
- "src/auth/jwt.py — 新建"
- "src/auth/login.py — 新建"
- "tests/auth/test_jwt.py — 新建"
- "tests/auth/test_login.py — 新建"
spec_ref 指向原始规约,不在交接文档中重复规约内容。重复意味着两份文档可能出现不一致,规约腐烂(02f)的问题同样适用于交接文档。新 session 以规约为权威信息源,交接文档只提供执行状态;两者冲突时以规约为准。
status 用三态而非简单的完成/未完成。in_progress 是最关键的状态,标记的是做了一半的工作。一个 subtask 要么完全没做,要么完全做完,都好处理。做了一半最危险——新 session 如果不知道哪些部分已实现、哪些还没有,可能重复实现已有逻辑,或者在半成品上继续构建导致不一致。
decisions 记录已经做出的选择和选择的理由。这里的 why 和被刻意排除的 next_steps 有本质区别:decisions 描述的是已存在的代码事实(代码已经用了 RS256),why 帮助新 session 判断这个选择是否需要推翻,是回顾性的解释,不是前瞻性的指令。未被记录的决策理由等于不存在(02d),新 session 可能直接推翻一个有充分理由的设计选择。
blockers 记录执行过程中发现的阻塞。没有被写下来的 blocker,新 session 可能直接跳过继续实现,产出的代码在集成时必然失败。
files_changed 提供变更的事实清单,让新 session 能够核对实际代码状态,不必盲目信任 status 字段的声明。
交接文档只在任务无法在单次会话内完成时产生,是容错机制的产物,不是常规流程的一环。任务顺利完成时不需要它,产出直接进入后续的验证流程。
问题在于:谁来判断什么时候该写入?Agent 本身不是可靠的触发源,原因有两个。其一,Agent 目前普遍缺乏对自身上下文消耗的感知能力,不知道窗口还剩多少空间。其二,即使模型能够感知,大语言模型基于概率生成,同一状态下的判断结果并不稳定。写入交接文档是一项确定性任务——必须在正确的时机发生,不能依赖概率性的保证。因此,触发必须来自外部的确定性工具,而不是模型本身。
一种方式是通过工具框架的 hook 机制:配置在 token 用量超过特定阈值时向 Agent 注入指令,让它停止当前工作、按交接文档的结构保存进度。另一种方式是用独立的监测进程持续跟踪 token 使用量,在接近上限时注入 prompt。前者与特定工具框架耦合,后者需要额外的基础设施,但适用范围更广。两种方式的共同点是,感知和触发的逻辑都由外部确定性系统承担,与模型的概率特性解耦。
无论哪种方式,触发必须发生在自动压缩之前。一旦压缩开始,Agent 对已完成工作的记忆就已经不完整了,此时写出的交接文档无法反映真实状态。触发阈值需要留出足够的窗口空间,让 Agent 在上下文完整时完成写入。
交接文档写入后,当前 session 终止。新 session 加载原始规约和交接文档,基于事实独立规划,继续执行未完成的部分。任务最终完成后,交接文档归档保留,供后续复盘参考。