Session、Context、Memory 与恢复:状态究竟保存在哪里¶
上一篇:工具、权限与执行边界 · 返回学习入口 · 下一篇:Trace、Eval 与结果核对
人们常把 Session、Context 和 Memory(记忆)混着说,读源码时却得分清它们各自管什么。模型这一次请求能看到的输入叫 Context。Session 记下一项任务怎样一轮轮跑到现在,Memory 留下以后做别的任务还用得上的信息。至于 Compaction(上下文压缩),它要解决的是历史太长怎么办,也就是怎样把原始记录收短。
用中断后的任务观察差异¶
假设运费修复已经读完文件并应用编辑,可进程在测试尚未运行时突然退出,那么系统重新启动后就需要回答:
- 用户原始目标是什么;
- 哪个文件已经修改;
- 编辑工具是否完成;
- 下一步应该运行什么测试;
- 之前的模型消息是否全部重新发送;
- 这次任务结束后,有哪些信息值得跨任务保存。
这些问题听起来都像在追问「之前发生了什么」,可你顺着实现查下去,会分别落到 Session、工具状态、Context 怎么拼,以及 Memory 要留什么。
Context:当前这一轮模型能看见什么¶
Harness(位于模型外部,负责输入装配、工具、权限、状态和产品表面的程序)通常会拿下面几类内容来拼 Context:
稳定部分:系统指令、工具定义、项目规则
任务部分:用户目标、附件、初始环境信息
历史部分:模型消息、工具请求和工具结果
动态部分:工作目录、剩余预算、任务进展、权限模式
检索部分:按需加载的文件、记忆和外部知识
Context 并不会照搬数据库里的整段 Session。Harness 还得看 Model 的窗口有多大、Cache 怎么用、任务跑到了哪一步,再挑出这一轮真正要发送的内容。
回到运费案例,恢复后至少要告诉模型三件事:原始目标是什么,文件已经怎么改了,哪个测试还没跑。要是只发最近那句「编辑成功」,模型既弄不清这次为什么要改,也选不出该跑哪个测试。
Session:任务的连续运行记录¶
Session 会把多轮 Message 和执行事实挂到同一项 Task 上。你若想以后还能追查并恢复这项任务,通常得在 Session 里留下这些内容:
- 稳定标识;
- 创建和更新时间;
- 用户消息与模型消息;
- 工具调用、决定和结果;
- 当前运行配置;
- 取消、错误和结束原因;
- 压缩摘要及其覆盖范围;
- 外部产物引用。
Session Store(会话存储)可以落在文件、数据库或 Server 里,也可以由 Host(宿主)应用提供接口。存到哪里并不关键。它必须说清哪些 Event 已经写稳,以及恢复时该从哪个提交点往下走。
界面只是投影。聊天窗口只把 Session 的一部分展示给你,内部还可能留着控制事件、Token 统计、Approval(审批)记录和 Tool 的原始输出。屏幕上的消息并不是完整记录。
Runtime State:尚未成为消息的运行事实¶
有些状态不适合直接写入消息历史,例如:
- 当前正在执行的工具;
- 取消令牌;
- 流式响应已经接收的片段;
- 并行工具的完成集合;
- 尚未持久化的事件;
- 当前工作区句柄或进程 ID。
这类状态可能只活在 Runtime(运行时)的内存里。进程一退出,凡是还没写过持久化接口的状态,恢复代码只能把它们标成未知,不能装作已经全都找回来了。未知也得明确记下来。
所以读源码时,你要查清哪些状态能恢复,哪些状态出了当前进程就没了。看到 resume(session_id) 这个 API,也不能顺势认定正在发生的副作用都能原样接上。
Memory:跨任务保留的信息¶
Memory 是留给未来任务用的,并不会把每段 Session 永久塞进模型输入。它通常留下这些信息:
- 用户偏好;
- 项目约定;
- 已确认的环境事实;
- 经过整理的解决方案;
- 可复用 Skill 或指令;
- 外部知识索引。
运费修复时具体调过哪一次工具,通常没必要长期记住。若项目已经定下「金额单位使用整数分」或「提交前运行 shipping 测试」,这类规则稳定,也能反复使用,更适合写进项目文档或 Memory。
往 Memory 里写内容时,必须同时交代来源,也要规定以后怎么更新。未经核实的模型猜测一旦跨会话留存,后面的任务就会反复受它误导。
Compaction:历史太长时保留什么¶
当消息历史接近模型窗口或成本预算,Harness 可以:
- 丢弃可重新获取的旧工具输出;
- 截断长日志并保留文件引用;
- 把早期消息总结成摘要;
- 保留最近若干轮原文;
- 固定关键任务状态和未完成步骤;
- 重新建立稳定前缀。
Compaction 把历史收短以后,仍得留下会影响后续判断的事实。只顾删旧消息,任务很可能接不下去。Summary 也一定会漏掉信息,所以用户目标、已经发生的副作用、没做完的步骤、约束和关键错误都要保住。
在运费案例里,下面这份教学摘要已经足够让模型继续干活:
目标:修复金额 100 时仍收费的问题。
已读取:src/shipping.ts。
已修改:条件 total > 100 改为 total >= 100。
尚未完成:运行 tests/shipping.test.ts 并确认边界断言。
如果摘要只剩一句「已修复运费问题」,模型很可能直接跳过测试,还没拿到证据就宣布结束。
恢复流程的四个检查点¶
1. 加载 Session¶
先确认 Session 确实存在且版本兼容,然后找到最后一个完整持久化的事件。
2. 重建可恢复状态¶
恢复消息、配置、工作区引用和压缩摘要,而进程句柄等不可恢复对象,则要重新创建或者标记失效。
3. 处理未完成副作用¶
如果工具只留下开始事件,却没有结束事件,就要根据工具类型查询实际结果、请求用户判断,或者执行补偿。不能统一重跑。
4. 构造下一轮 Context¶
把原目标、已完成工作、未完成步骤和新环境状态交给模型,只要这些事实足以支持安全继续,恢复就无须回放全部历史。
消息重放与副作用重放不同¶
把旧消息再发给模型,通常不会动到外部世界。旧工具若再执行一次,却可能把副作用也再做一遍。因此,恢复 Session 时必须把这两类动作分开:
| 对象 | 是否通常可以重建 | 主要风险 |
|---|---|---|
| 用户和模型文本 | 可以 | 版本差异导致解释变化 |
| 工具结果消息 | 可以从存储恢复 | 原始输出可能已被截断 |
| 只读工具 | 可能重新执行 | 环境已经变化,结果不再相同 |
| 写入、支付、发送等工具 | 不能盲目重跑 | 重复副作用 |
| 进程内流式片段 | 常常不完整 | 无法知道 Provider 是否已结束 |
幂等键、外部操作 ID 和事务记录可以减少重复执行的风险,但前提是每个工具先说清自己会造成什么副作用。这些机制只能降低重复风险,无法保证每次都能挡住重复动作。
在源码里怎么找¶
你可以从公开入口查起,搜索 create、load、resume、Fork、compact、History、store 等符号,再回答下面的问题:
- Session ID 由谁生成;
- 消息何时持久化;
- 工具开始和结束是否分别记录;
- Compaction 由长度、Token 还是显式请求触发;
- 摘要怎样进入后续 Context;
- 恢复是否重新连接 Provider 或执行环境;
- 多客户端是否共享同一 Session 真值;
- 哪些状态只存在于宿主内存。
只找到数据结构还不够,因为它没有告诉你系统实际怎样恢复。继续顺着 Call 路径查,直到找到哪些函数创建、写入、读取并消费这些结构。
回到运费案例¶
进程是在编辑后退出的。恢复时先确认写入工具已经留下完成记录,再加载改过的工作区,并告诉模型「测试尚未运行」,让它把验证补上。如果写入是否完成还说不准,就先读文件或检查 Diff,不要直接再改一遍。
任务结束后,可以留下 Session 供以后审计。长期 Memory 则只收已经核实,而且以后还用得上的项目规则。
练习:恢复时哪些动作不能重放¶
Session 最后两条事件是「开始编辑 shipping.ts」和「进程意外终止」,中间没有工具完成记录。恢复后应当直接再次编辑、直接运行测试,还是先读取文件并检查差异?请说明理由。
查看核对要点
应先观察文件和差异,确认副作用究竟有没有发生。消息通常可以重放,副作用却不能盲目重放,否则可能重复插入内容,也可能覆盖其他修改。只有确认了文件状态,Harness 才能决定补做编辑还是继续测试。现在应该能解释¶
Context 管模型这一轮能看到什么,Session 记任务一路怎样跑过来,Runtime State 留住当前进程还在用的事实,Memory 则把信息带到未来任务。历史太长时,Compaction 会把它收短。做恢复时,你既要找回继续干活所需的状态,也要防止副作用重复发生。只把聊天再演一遍,解决不了这些问题。