跳转至

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 等符号,再回答下面的问题:

  1. Session ID 由谁生成;
  2. 消息何时持久化;
  3. 工具开始和结束是否分别记录;
  4. Compaction 由长度、Token 还是显式请求触发;
  5. 摘要怎样进入后续 Context;
  6. 恢复是否重新连接 Provider 或执行环境;
  7. 多客户端是否共享同一 Session 真值;
  8. 哪些状态只存在于宿主内存。

只找到数据结构还不够,因为它没有告诉你系统实际怎样恢复。继续顺着 Call 路径查,直到找到哪些函数创建、写入、读取并消费这些结构。

回到运费案例

进程是在编辑后退出的。恢复时先确认写入工具已经留下完成记录,再加载改过的工作区,并告诉模型「测试尚未运行」,让它把验证补上。如果写入是否完成还说不准,就先读文件或检查 Diff,不要直接再改一遍。

任务结束后,可以留下 Session 供以后审计。长期 Memory 则只收已经核实,而且以后还用得上的项目规则。

练习:恢复时哪些动作不能重放

Session 最后两条事件是「开始编辑 shipping.ts」和「进程意外终止」,中间没有工具完成记录。恢复后应当直接再次编辑、直接运行测试,还是先读取文件并检查差异?请说明理由。

查看核对要点 应先观察文件和差异,确认副作用究竟有没有发生。消息通常可以重放,副作用却不能盲目重放,否则可能重复插入内容,也可能覆盖其他修改。只有确认了文件状态,Harness 才能决定补做编辑还是继续测试。

现在应该能解释

Context 管模型这一轮能看到什么,Session 记任务一路怎样跑过来,Runtime State 留住当前进程还在用的事实,Memory 则把信息带到未来任务。历史太长时,Compaction 会把它收短。做恢复时,你既要找回继续干活所需的状态,也要防止副作用重复发生。只把聊天再演一遍,解决不了这些问题。

下一篇:Trace、Eval 与结果核对