DeepSeek Harness 为什么不用 messages 数组保存一切

很多 Agent 最初只有一个 messages[]:用户消息追加进去,模型回答追加进去,工具结果也追加进去。上下文太长时,删掉前几条或塞入一段摘要。
这个方案很快会遇到一个无法回避的问题:被删掉的历史还需要审计吗?UI 要展示原始流式输出,模型却只应看到压缩后的摘要。恢复时到底相信原始事件、当前 messages,还是某个内存快照?如果三者不一致,下一次模型请求就无法解释。
DeepSeek Harness 的固定源码采用另一种分层:Session 保存追加式事件,Surface 表达模型当前可见的消息面,请求头另外记录系统提示、工具 Schema 和调用配置,Compaction 则通过新的 replacement 节点替代某段 Surface,而不删除原始事件。
本文只回答一个问题:原始 Event Log、模型可见 Surface 与压缩后的 replacement,怎样同时成立?
核心结论是:append-only 描述事实如何保存,Surface 描述模型此刻看到什么,Compaction 描述如何用新节点遮蔽旧区间。三者分开以后,审计、回放和上下文缩减可以共享一个事实源。代价是系统必须明确投影、持久化、压缩事务和恢复边界。

一个 messages 数组承担不了三种职责
假设一次工具任务产生:
user prompt
assistant reasoning stream
assistant tool call
tool result
assistant final message
UI 可能需要逐块回放流式输出,模型下一轮只需要稳定的 assistant message 与 tool result,审计系统还要知道工具调用的原始参数和发生顺序。
若这些需求都压进同一个 messages 数组,团队通常会在每条消息上不断添加临时字段:是否展示、是否送模型、是否被摘要、来源是谁、对应哪个工具调用。随后压缩又原地改写数组,原始事实和当前视图混成同一个可变对象。
DeepSeek Harness 的 Session 先保留事实,再从事实派生不同用途的视图。这里最重要的不是“用了 Event Sourcing”这个标签,而是三项具体合同:事件不可原地改写、模型可见类型是有限集合、Surface replacement 必须引用被遮蔽的来源。
Session Log:事实源,但不自动等于磁盘持久化
固定 Session 文档将 Session 定义为 typed SessionEvent 的 append-only log。每个事件有连续 seq、时间、类型和 JSON payload。模型历史通过 deriveMessages() 从日志派生,不另存一份权威 messages 数组。
事件词汇包含 Turn/Step 边界、用户消息、流式 chunk、最终 assistant message、tool call/result、request header 等。它们承担的职责不同:有些是模型可见消息,有些是执行边界,有些只用于重放、计费或请求重建。
但“Session 是事实源”不能偷换成“事件已经安全落盘”。固定 Session 文档明确说明,turn/end 本身不等待 flush。每次请求的耐久检查点由独立策略负责,需要在读取存储前确认 flush。JSONL、SQLite 或其他后端属于 Persistence Seam。
因此,至少要区分:
- append 成功:事件已进入当前 Session;
- checkpoint/flush 成功:事件已满足该后端的持久边界;
- 恢复成功:新生命周期能从存储重建并通过校验。
这三个状态不能用一个“已保存”概括。

Surface:只有三类事件直接成为模型消息
固定源码把 SurfaceEventType 限定为三类:
user/message;assistant/message;tool/result。
Turn、Step、raw chunk、tool call、usage 和 request header 都可以留在日志中,但不会直接变成模型历史消息。
这条边界解释了为什么 raw log 与模型上下文长度不是同一个指标。一次 assistant response 可以包含大量 chunk,日志需要它们维持回放精度。模型下一轮只消费组装后的 assistant message。Tool call 的原始参数属于执行事实,模型表面则通过 assistant message 中的调用块与对应 tool result 保持语义。
“不进入 Surface”也不等于事件无关紧要。它可能决定回放、恢复、UI、计费或请求重建,只是不应作为一条对话消息重复喂给模型。

SurfaceOp:追加式日志如何表达替换
Surface 节点并非只能尾部追加。每个模型可见事件都声明 surfaceOp:
append:把消息加到当前可见面的尾部;replace(start, end):用新节点替换当前 Surface 中从 start 到 end 的位置区间。
replacement 节点还通过 sourceEventSeqs 指出它引用或遮蔽的原始 Surface 节点。旧事件仍在 append-only log 中,新事件描述当前模型视图如何变化。
于是,下面两件事可以同时为真:
raw log: U1 A1 T1 U2 A2 ... C1
surface: [C1 replacement] ... recent tail
transcript: 仍可读取原始 append-origin 对话
这里有个容易踩坑的细节:replace 的 start/end 是 Surface 位置边界,不是简单的数值 seq 区间。经历过前一次 replacement 后,一个新摘要节点可能拥有更大的 seq,却出现在更早的位置。真正权威的是按 Surface 顺序记录的 shadowedSeqs。

Compaction:控制事件不直接等于摘要消息
DeepSeek Harness 把 Compaction 设计为可替换能力接缝,而不是 Agent Loop 里的固定算法。固定文档把一次压缩的持久记录分成四类。摘要生成与范围选择发生在 compaction/start 之后、后续记录之前:
compaction/start:记录操作开始并取得日志锁;compaction/summary:记录摘要、模型调用、被遮蔽范围和 token 信息;- 一个带
replace的user/message:真正进入模型 Surface 的摘要检查点; compaction/end:最后释放锁并记录成功或失败。
compaction/start、compaction/summary 和 compaction/end 都是 log-only 控制事件,不会直接加入模型 Surface。模型真正看到的是单独的 replacement user message。
这一区分避免同一摘要既作为控制记录又作为模型消息重复出现,也让系统能记录更多审计字段,而不把 Provider、usage、锁或错误细节暴露给模型。
为什么 start/end 要包住整个操作
compaction/start 先写,compaction/end 最后写。若进程在摘要或 replacement 中途崩溃,日志会留下没有匹配 end 的 start,恢复逻辑至少能检测到“上一次压缩没有完整结束”。
反过来,如果先写 end 再落 replacement,日志可能虚假声称压缩已完成。
固定文档还保留了更细的失败分类:busy、cancelled、changed、summary、commit、persistence。某些失败不会改变 Surface,却仍会留下关闭后的尝试记录。commit 阶段失败还可能发生在部分内存变更之后。
这说明“压缩失败”不是一个原子布尔值。系统要知道失败发生在选区、摘要、提交还是持久化,才能决定原 Surface 是否仍可信、是否允许重试。
但孤儿锁可检测不等于所有副作用都自动恢复。摘要调用产生的外部成本、正在进行的 Provider 请求或存储后端故障仍需要各自的取消、幂等与恢复合同。
摘要不是把旧消息拼成更短文本
固定实现允许 pressure 或 provider-confirmed context overflow 触发自动压缩,也允许空闲时手动压缩。可选的 tool-result pruner 还可以先对过长工具结果做确定性缩减,再决定是否需要模型摘要。
选区必须尊重工具调用与结果的配对边界。若摘要区间只包含 tool call、不包含结果,或反过来,新的 Surface 会形成不完整对话。因此固定 Compaction 接缝提供前后配对检查,并允许在一个超长 Turn 中选择已经闭合的早期 Step,而不是机械要求整 Turn 一起压缩。
这比“保留最近 N 条消息”更精确,但也说明摘要质量不是唯一问题。压缩首先要选对合法区间,再生成能保留约束的 replacement,最后完成日志与持久化事务。

请求重建不只依赖对话历史
模型请求还包含 Provider、Model、系统提示、工具 Schema 和容量信息。固定 Session 文档使用 request/header 保存每个 Loop 生命周期起点及发生变化时的完整请求头快照。最新快照可以重建有效 header。
System Prompt 也不是启动时拼好后永远不变。Prompt Section、动态 Context 和 Tool Schema 在每个 Step 装配。动态 Context 的完整快照会在变化或压缩移除后重新写入日志。
Provider/Model 的 context window 等路由容量则放在独立 request/context,因为容量描述的是路由,而不是模型输入本身。它不会因为容量变化就伪装成请求内容变化。
因此,恢复“一次模型请求”不能只重放 messages。至少还要能重建:
- 当前 Surface 消息;
- 有效系统提示;
- 有效工具 Schema;
- Provider/Model 与请求配置;
- 对应的容量与版本边界。

append-only 的收益和代价
分开 raw log 与 Surface 带来几个明显收益:
- UI、模型、审计和查询可以使用不同投影,但共享同一事件事实;
- Compaction 不必删除历史即可缩短当前模型上下文;
- 请求 header、chunk 和模型消息能建立来源关系;
- 失败的压缩尝试、锁和 replacement 可以被恢复逻辑识别。
代价同样具体:
- 事件 schema 必须迁移,未知必需事件不能被静默丢弃;
- 日志持续增长,需要保留、归档和存储策略;
- replacement 后 Surface 顺序不再等于 seq 数值顺序;
- append-only 与敏感数据删除、附件清理和合规保留存在张力;
- 投影、checkpoint 和后端恢复必须保持一致。
所以 Event Log 不是把状态问题“解决掉”,而是把隐含可变状态转换成显式事件、投影和迁移合同。

压缩与恢复应该怎样验收
只报告“节省了多少 token”远远不够。一个可执行的验收表至少包含:
| 验收项 | 需要记录 | 失败信号 |
|---|---|---|
| 原始事实完整性 | replacement 前后的 raw event seq 与来源关系 | 原事件丢失、seq 断裂、未知必需事件被忽略 |
| Surface 合法性 | replace 范围、shadowedSeqs、工具 call/result 配对 | 孤立调用、孤立结果、区间位置错误 |
| 约束保持 | 文件状态、用户限制、未完成目标与关键决定 | 摘要后遗忘约束或重复已完成动作 |
| 请求可重建 | header、system、tools、provider/model 与 context | 恢复后首个请求无法解释或配置漂移 |
| 持久化 | checkpoint/flush、后端写入与恢复读取 | turn 已结束但存储缺事件 |
| 失败恢复 | orphan lock、changed/summary/commit/persistence 分类 | 失败后 Surface 不确定却继续执行 |
| 预算收益 | raw events、Surface messages、system/tool tokens | 只缩短消息,却被工具 Schema 吞回预算 |
这张表是本文的主要产物。它把“可以压缩”与“压缩后仍可用、可解释、可恢复”分成不同门槛。

结论
DeepSeek Harness 不用一个可变 messages[] 保存一切,是因为原始事实、模型当前视图和请求重建承担不同职责。Session Event Log 保存追加式事实,Surface 从有限的消息事件派生当前模型历史,replacement 用新节点遮蔽旧区间,Compaction 的控制记录则留在日志中完成锁、范围、摘要和失败审计。
这套分层使 append-only 与上下文替换不再矛盾,也让 UI、模型和恢复共享事实源。但它不自动带来耐久性:Turn end 不等于 flush,replacement 不等于摘要等价,孤儿锁可检测也不等于外部副作用已恢复。
现阶段最值得采用的不是“Event Sourcing”这个标签,而是它要求团队明确回答三件事:原始事实是什么、模型现在看到什么、系统凭什么证明两者之间的转换可以重建。
参考资料
- 固定 Commit Session Subsystem:https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/session.md
- 固定 Commit Compaction Subsystem:https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/compaction.md
- 固定 Commit Persistence Subsystem:https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/persistence.md
- 固定 Commit System Prompt:https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/system-prompt.md
- 固定 Commit Architecture:https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/architecture.md
证据与推导边界
- Session Event、Surface、SurfaceOp、request header/context:固定 Commit 官方文档事实。
- Compaction 锁、log-only 事件、replacement 与失败分类:固定 Commit 官方文档事实。
- 压缩与恢复验收表:基于上述合同的作者工程建议。
- 研究材料的聚焦测试:只说明局部路径曾被执行,不构成长会话或断电恢复 E2E。
- 摘要等价、跨版本迁移、隐私删除和生产稳定性:尚未验证,不作结论。
400

被折叠的 条评论
为什么被折叠?



