DeepSeek Harness · 会话与循环

Model-visible ⟺ logged:一条会崩给你看的不变量

模型看到的一切都要能从日志重建,发请求前还要现场验一遍,验不过直接崩。

课程目标读完你能说清三件事:为什么 DSH 规定对话只能有一份真相,模型看到的一切都必须能从会话日志重建;发请求之前它怎么现场重建一遍、逐字节比对一遍;比对不过时它为什么选择当场崩溃,连一行告警都懒得打。
交互演示 · 日志篡改实验室

先玩再讲。左边是一条只能往后追加的事件日志,右边是从日志重建出来的消息数组,和即将发给模型的请求。点播放看事件流入。播完你可以当一次坏人:删一条日志,或者绕过日志直接改请求,再点「发起下一次请求」,看比对怎么逐条打钩、在分歧处打红叉、然后当场崩给你看。

只追加的事件日志session-1
(空日志)
从日志重建的消息数组deriveMessages()
(还没有要给模型看的内容)
即将发给模型的请求
(请求尚未构建)
点「播放」,看事件逐条流入日志。

实验台的比对顺序与源码一致,红条里的报错保留源码原文:比对逻辑在 packages/core/agent-loop/src/invariant.ts 第 31 至 42 行,报错前缀拼接在 packages/runtime-diagnostics/invariants/src/index.ts 第 62 行。核对日期 2026-08-13。

思路一 · 只留一份真相

它解决什么问题。大多数聊天程序都有两份对话:内存里一份数组,磁盘上一份存档,各写各的。某天进程崩了,你从存档恢复会话,恢复出来的历史比模型当时实际看到的少了一条工具结果。模型接下来的回答全对不上号,你还查不出原因,因为两份状态谁也证明不了谁。只要真相有两份,它们迟早漂移。

思路是什么。DSH 把真相压缩到一份。规矩写在仓库根部的 AGENTS.md 第 107 行:

Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event. 出处:deepseek-harness-master 仓库 AGENTS.md 第 107 行,核对日期 2026-08-13

拆开说。会话日志是一份只追加的事件流,任何想让模型看到的内容,必须先变成一条事件写进日志。消息历史从日志派生,官方文档的说法是「从不单独存储」。所以在 DSH 里,日志就是对话本身,系统里没有第二份对话状态。

出处:docs/subsystems/session.zh.md 第 5 行,核对日期 2026-08-13。

然后是标题里那个双向箭头 ⟺。日志推得出请求,请求也必须能被日志解释。光写日志做不到这一点,还得有人在发请求的路口站岗。每次请求出站前,DSH 从日志现场重新派生一份应有的消息数组,和请求里实际带的那份做全量字符串比对;系统提示词、模型名、采样参数、工具清单也要和日志里的请求头快照逐字段对上。全对上才放行。

只追加的事件日志 请求头快照 用户消息 助手消息 工具调用 · 工具结果 唯一真相,别处没有副本 现场重建 从日志派生应有的请求 消息数组 + 请求头快照 循环实际构建的请求 深度冻结,过检后改不了 出站口逐字节比对 一致,放行发出 不一致,当场崩
教学化结构图:节点与连线用于解释源码关系,内容经过课程化整理。

站岗的核心就四行,短到可以当金句贴墙上。它在证明一件事:每次派发前,DSH 真的会从日志重新派生一份消息,和实际请求做全量字符串比对,对不上就地失败:

packages/core/agent-loop/src/invariant.ts第 39 至 42 行
    const expected = session.deriveMessages()
    if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
      fail(`llm request for session "${String(session.id)}" diverges from the dispatch-time durable derivation (log-reconstruction desync)`)
    }
源码快照说明:依据本地仓库 deepseek-harness-master,核对文件 packages/core/agent-loop/src/invariant.ts,核对日期 2026-08-13。代码块保留源码原文。

还有一个细节。检查用的派生函数,和恢复、回放用的是同一套公开函数,检查者和被检查者共享同一套重建规则,谁也没有私货。请求头的重建也只是一个七行的纯函数,扫一遍事件、取最后一个快照。

出处:完整检查顺序在 packages/core/agent-loop/src/invariant.ts 第 22 至 52 行;请求头重建在 packages/core/session/src/request-header.ts 第 65 至 71 行。核对日期 2026-08-13。

为什么长期成立。single source of truth(单一事实来源)是数据库领域用了五十年的老道理:账本只能有一本,其余全是账本的视图,视图可以随便丢、随便重建。DSH 只是把这条纪律搬进了 agent 的对话管理。哪天源码重写成 Rust、事件类型翻三倍,账本只有一本这件事照样成立。

日志就是对话本身,其余都是视图。
思路二 · 验不过就崩,不要带病运行

它解决什么问题。设想站岗的发现对不上,只打一行告警会怎样。告警意味着违规请求已经发给了模型:某个插件绕过日志偷偷改了消息,模型看到的和日志记的从这一刻起是两回事。日志接着记,记的全是错账。三天后有人拿这份日志回放排查,怎么都复现不出线上的怪行为。静默偏差比崩溃可怕,它把排查成本悄悄转嫁给了未来。

思路是什么。所以 DSH 选了最硬的那条路:比对不过,抛异常,这次请求当场作废,根本发不出去。设计笔记里明确否决过温和方案,对「比较连续请求,发散时告警」这条备选的判词是「因违规必须在接口层面不可表达而否决」。

出处:.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md「曾考虑的替代方案」一节,核对日期 2026-08-13。

配套还有两个小机关。检查器插在事件监听队列的队头,防止别的监听器提前短路、把检查静默跳过,站岗的人要第一个看到请求。然后请求对象和消息数组必须深度冻结,堵住先过检、再改内容的后门。

出处:队头注册见 invariant.ts 第 20 至 21 行与第 54 行,冻结与会话检查见第 22 至 29 行。核对日期 2026-08-13。

告警方案(被设计笔记否决) 发现请求和日志有分歧 打一行告警 违规请求照样发出 日志从此记的是错账 回放、审计全部失真 DSH 的选择 发现请求和日志有分歧 抛异常,当场崩 请求作废,发不出去 日志保持干净 修好 bug 重跑即可
同一个分歧,两种处理方式的后果对比。教学化示意图。

为什么长期成立。这就是 fail-fast(快速失败)。崩溃把损失锁在零:日志里没有被污染的轮次,修好 bug 重跑就行。带病运行的成本是复利,跑得越久坏数据越多,最后连哪天开始坏的都查不出来。在边界上崩一次,比在三天后的诡异现象里考古便宜得多。这条判断和语言、框架都没有关系。

请求发出去的那一刻,日志就再也解释不了模型。
思路三 · 一份日志买到全家桶

前两个思路各花一份力气,回报在这里一起结算。日志既然是唯一真相,围绕它能白拿一整套能力:

连 Compaction(压缩,把过长的历史换成摘要)都被这条不变量罩着:压缩产生的摘要同样以事件形式写进日志,压缩之后的请求照样要过出站比对,压缩实现写出 bug 会当场崩,逃不掉。

为什么长期成立。这套玩法有个学名,event sourcing(事件溯源),银行和会计系统用了很多年:不存余额,只存流水,余额永远从流水算出来。DSH 只是把交易流水换成了对话事件。只要事件流还是唯一真相,这些能力就一直是免费的副产品。

横向对比 · 都写日志,谁在反向校验

会话持久化几乎是编码 agent 的标配,差别在方向。Grok Build(xAI 开源的 Rust 编码 agent)的持久化是一条单行道:内存里的对话状态是主,落盘是从。每来一条消息,把内存条目拷一份丢进落盘通道,发送结果直接丢弃,落盘失败也不打断对话;压缩时还允许把整份落盘历史一次性替换掉。这是合理的产品取舍,只是持久层不承担正确性职责:没有任何路径把日志反向派生回来、和出站请求比对。

出处:grok-build-main 仓库 crates/codegen/xai-grok-shell/src/session/chat_persistence.rs 第 30 至 38 行(persist_messagereplace_history),核对日期 2026-08-13。

Claude Code 闭源,公开可见的是 ~/.claude 下的 .jsonl 会话日志和恢复功能。基于已公开证据,它属于事后记录型持久化;没有公开材料表明存在派发时刻从日志重建请求并比对的运行时断言,内部有没有等价机制属于未知。

所以这里只比机制方向:Grok Build 和 Claude Code 把持久化当恢复手段,DSH 把日志升格为需要运行时证明的第一性原理。

课堂练习
01

推演一次触发路径

某插件监听请求出站事件,想在发出前往消息数组里塞一条系统提示。按本课的检查顺序推演两种情况:它直接改那个被冻结的数组会发生什么?它先克隆整个请求、改克隆体再转发,又会在哪一步被拦下?提示:冻结检查和逐字节比对各自守住什么。

Takeaway:DSH 的会话日志是对话的唯一真相,恢复、分叉、回放、审计共用同一份事件流。每次请求出站前从日志现场重建并逐字节比对,对不上当场抛异常,请求发不出去。崩溃优于告警,因为请求发出去的那一刻,日志就再也解释不了模型的行为。