DeepSeek Harness 架构(三):历史不能丢,模型又不能看全部历史
用 Turn、Step、Session Event 与上下文投影,理解恢复、压缩和多视图一致性。
最近修订:2026.09.05
阅读导航 · 第 3 / 5 篇
DeepSeek Harness 架构系列:1 · 系统全景 · 2 · 组合与生命周期 · 3 · 状态与上下文 · 4 · 执行与安全 · 5 · 深入思考
阅读时间为估计,包含图表理解;动手实验另计。
版本边界:本系列沿用官方源码快照
76fda72。它是 developer preview;以下解读不是稳定接口或生产安全承诺。
工单插件已经装好,Agent 修改了文件,测试运行到一半进程退出。重启后,它该继续哪一步?如果只保留一句“正在修复”,就无法区分未执行、已执行但未确认,以及执行失败。
这一篇讨论事实如何进入日志,又怎样变成模型能读的有限上下文。
Agent Loop:Turn 是业务承诺,Step 是一次模型调用
很多 Agent 框架把循环写成一个看似简单的 while (toolCalls.length)。DeepSeek Harness 的循环更像一个事件溯源状态机。
它明确区分:
- Step:一次模型请求,以及该响应触发的一组工具执行;
- Turn:从一条用户输入被认领,到所有工具债务和中途追加输入都被处理完,包含零个或多个 Step。
一个 Turn 的主流程如下:
sequenceDiagram
participant U as Inbox / User
participant A as Agent Loop
participant S as Session Log
participant M as Model Adapter
participant T as Tool Runtime
A->>S: turn/start
A->>U: claim next-step + one next-turn message
A->>A: agent/pre-step
A->>S: step/start + user/message
A->>A: assemble prompt + derive history
A->>M: agent/request → llm/stream
M-->>S: assistant/chunk* → assistant/message
loop each tool-call group
A->>S: tool/call*
A->>T: pre-execute → guard → execute → post-execute
T-->>S: tool/result*
end
A->>S: step/end
alt tools or steering owe another request
A->>A: next step
else nothing owed
A->>A: agent/turn-stopping
A->>S: turn/end
end
图 1|一个 Turn 内的模型请求、工具结果与继续条件。竖线表示参与者,实线箭头表示请求,虚线箭头表示返回;按从上到下的时间顺序阅读。图为教学抽象,不代表全部实现细节。
这套定义解决了几个容易被忽略的边界问题:
- 用户在 Agent 工作时追加的 Steering,可以在最近的 Step 边界进入,而不必粗暴终止整个任务;
- 仅注入 Context 不会唤醒空闲 Agent,它等待下一条真正输入;
- 被策略拒绝的第一步仍会写入
turn/start/turn/end,所以“尝试过但没有调用模型”也是可审计事实; - 取消会携带
user、parent、hook或disposed原因,并最终进入 Turn 的结束记录; - 并行安全工具进入有界并发池,Exclusive 工具则形成顺序屏障。
默认 agent-loop 只负责“调用模型、执行工具、继续循环”。重试、Compaction、目标推进和停止规则都通过事件或 Capability 插件挂上去。官方agent-loop 文档甚至把自己称为系统中唯一的具体 Loop 实现——“唯一”并不表示不可替换,而是其他包不应偷偷再实现第二套循环语义。
Session:不是聊天记录,而是运行时的事实账本
DeepSeek Harness 最值得借鉴的设计之一,是把 Session 定义为追加写入的 SessionEvent 日志。模型历史不是另一份可变数组,而是从日志投影得到:
Durable Session Events
│
├── deriveMessages() ──> Model History
├── projection fold ───> UI / Task / Permission / Token State
├── replay ────────────> Resume / Fork
└── export ────────────> Transcript / Telemetry / Audit
它遵循一条很强的约束:Model-visible means logged。 任何进入模型请求的信息,都必须能由持久日志重建;运行时 Invariant 会检查请求是否可重构。原始 assistant/chunk 也被保留,因此 UI 可以重放流式输出,而不是只得到最终文本。
事件大致分三类:
turn/*、step/*、user/message、assistant/*、tool/*:模型交互事实;approval/*、compaction/*、agent/inbox/*等:运行时控制与审计事实;request/header、request/context:模型路由、上下文容量和请求系列的可重构快照。
这比“保存 messages 数组”复杂得多,却带来四个关键收益:
- 恢复:进程崩溃后可以识别未闭合 Turn,并在恢复层补写中断语义;
- Fork:子 Session 可以从确定的事件边界继承历史;
- 多视图一致性:模型上下文、UI、遥测和审计来自同一事实源;
- 插件扩展:新能力通过 Declaration Merging 增加 Event 类型,而不必篡改一份中心状态对象。
它也意味着存储不再只是 I/O 细节。事件顺序、身份、提交边界、Log-only 事件与 Model Surface 的区分,全部成为公共架构。详见官方 Session 子系统说明。
上下文工程:日志保持完整,Surface 可以被替换
事件溯源系统会遇到一个现实冲突:审计要求历史完整,模型上下文要求历史变短。DeepSeek Harness 用“完整 Log + 可变 Surface Projection”解决。
Compaction 不删除旧事件,而是:
- 写入
compaction/start,形成可检测的事务锁; - 选取工具调用/结果配对完整的一段 Surface;
- 生成摘要并写入
compaction/summary; - 追加一个带
replace操作的user/message,在模型视图中替换旧区间; - 最后写入
compaction/end。
如果进程在中间崩溃,存在 Start 而没有 End,恢复逻辑就知道这不是一次成功完成的压缩。旧内容仍然存在于 Canonical Log,只是在当前模型 Surface 中被摘要遮蔽。
在生成摘要前,系统还可以对超长 Tool Result 做确定性的头/尾保留与中段裁剪;随后重新测量 Token 压力,只有必要时才进行模型摘要。完整机制见 Compaction 子系统。
Prompt 装配也服务于缓存稳定性。固定 Identity、Persona、Prompt Sections 和工具 Schema 按确定顺序形成前缀;运行时状态则作为有来源的 User-role Snapshot 进入历史。Preset 在 Session 生命周期内保持不变,同一 Preset 的 Agent 共享稳定组合,这有利于 KV Cache 复用。官方 System Prompt 文档也明确记录每项贡献对 Token 和 Cache 的影响,见 dsh-system-prompt。
这套策略的核心不是“尽量压缩”,而是把三种需要分开:
- 审计需要无损事实;
- 模型需要有限、高信号的当前视图;
- Provider Cache 需要稳定前缀。
实践:用中断点检验恢复语义
| 中断点 | 恢复时必须回答 |
|---|---|
| 有 tool/call,没有 tool/result | 是否已产生外部效果?先对账还是可安全重试? |
| 有 compaction/start,没有 end | 摘要事务是否完成?旧证据是否仍可恢复? |
| 工具被拒绝,模型未运行 | 是否仍能解释本次 Turn 为什么结束? |
| 摘要成功,但当前模型换了 | 采用的模型、上下文与配置版本能否追溯? |
这些是设计验收项,不意味着日志本身提供 exactly-once 执行。对“请求已成功但回包丢失”的外部操作,恢复层仍需幂等键或独立状态查询;不能仅因为日志缺 result 就重新执行。
从三份历史回到一个事实源
把这次修复的原始事件、模型上下文与 UI 展示放在一起,检查同一个工具调用的参数和结果是否一致。允许 UI 只显示摘要,但摘要必须能指回同一个事件。
完整日志也不是无限保留的许可:生产系统应按数据类别配置访问、保留与删除策略,并处理备份和派生视图。这里的 append-only 是运行时记录语义,不是对所有业务数据永久不可删除的要求。
读完这部分,应能解释“历史完整”与“上下文变短”为什么不矛盾:它们服务不同消费者,却需要共同的来源与版本约束。
上一篇:组合与生命周期。
状态可以回放,不代表动作就被正确限制。下一篇沿着真正产生副作用的工具边界,检查参数、并发、PTC 和多 Agent 的风险。