DeepSeek Harness 可追溯性实战:会话日志的 resume、fork 与 replay
调试 Agent 最痛苦的是什么?是「它刚才为什么这么做,我事后查不出来」。你只能看到最终结果,看不到它中途看到了什么、调了哪个工具、模型到底返回了什么。
DeepSeek Harness(dsh)给出的解法,是一个贯穿始终的设计原则------「模型可见即已记录」 。这篇我们就实战一下:这条原则怎么落地,resume、fork、replay 到底怎么用。
一、先理解事件流:会话日志的骨架
DSH 把一个会话的过程,记成一条只增不改(append-only)的事件流。前几篇已经见过这些事件名,这里我们按层级重新梳理:
turn/start # 一个轮次开始
├─ agent/pre-step # 实时扩展点:决定模型看到什么
├─ step/start # 一个步骤开始
│ ├─ user/message # 追加用户消息(持久事件)
│ ├─ agent/request # 实时扩展点:即将发请求
│ ├─ llm/stream # 实时:模型流式输出
│ ├─ assistant/chunk* # 持久:模型的每个 token 块
│ ├─ assistant/message # 持久:完整回复
│ ├─ tool/call* # 持久:模型发起工具调用
│ ├─ tools/pre-execute # 实时:工具执行前(可拦截)
│ ├─ tools/execute # 实时:工具执行中
│ ├─ tools/post-execute # 实时:工具执行后
│ ├─ tool/result* # 持久:工具返回结果
│ └─ step/end
├─ agent/turn-stopping # 实时:轮次收尾
└─ turn/end # 持久
留意标注:持久事件 (带 * 的)写进日志,是「事后可重建」的依据;实时扩展点是「当时可干预」的钩子。两条线分工明确------前者是黑匣子,后者是方向盘。

二、「模型可见即已记录」到底强在哪
这句话不是口号,而是一条运行时不变式(invariant):
凡是抵达模型请求里的内容,都必须能从日志中重建。
换句话说,模型看到的每一个字,日志里都有据可查;反过来,日志里没有的,模型就一定没看到过。这堵死了 Agent 里最隐蔽的一类 bug------「模型其实看到了 A,但你排查时以为它看到的是 B」。
为了维持这条不变式,DSH 有一个关键设计:deriveMessages() 能从日志投影出模型的完整历史 。也就是说,你不需要单独存一份「模型历史」,它随时能从事件流里算出来。而原始的 assistant/chunk 事件,则保证了回放和 UI 渲染的逐字保真(连流式输出的抖动都能还原)。

三、实战一:resume------中断后接着跑
Agent 跑长任务,最怕中途断掉重来。DSH 的 resume 让「接着上次跑」变成可能。
sh
# 列出一个会话
dsh session list
# 从某个会话继续
dsh session resume <session-id>
底层逻辑很简单:会话日志是 append-only 的,resume 就是把日志重新投影成模型历史,然后接着往下追加。模型看到的是「之前完整发生过的一切」,所以它的回答能无缝衔接,而不是失忆。
对开发者来说,这解决了长任务的断点续跑;对评测来说,它意味着你可以从任意一个历史节点精确复现,而不是「大概重跑一遍」。
四、实战二:fork------从一个点分出对照实验
做 Agent 评测时,一个经典需求是:在同一个起点,测试「给不同的提示 / 用不同的工具 / 换不同的模型」会有什么不同结果。
fork 就是干这个的:
sh
# 从某个会话的某个点,分出一个新分支
dsh session fork <session-id> --at <step-id>
分叉之后,你得到一个和原会话共享前缀、但之后各自独立的新会话。于是你可以:
- 分支 A:继续用模型 X;
- 分支 B:从同一点切到模型 Y;
- 分支 C:同一点但注入一段不同的 system prompt。
最后对比三个分支的走向,就是一次干净的对照实验(A/B test)。没有 fork 的话,你只能手搓三份输入,还保证不了「起点完全一致」。

五、实战三:replay------精确重现一次执行
resume 是「接着跑」,replay 则是「原样重放」。
当你复现一个 bug 时,最想要的是「一模一样地再来一次」。但 Agent 有随机性(temperature、工具执行时机),直接重跑往往对不上。replay 借助日志里的 assistant/chunk 等持久事件,把一次执行逐字重放出来:
sh
dsh session replay <session-id>
它的价值在于:把「偶发问题」变成「可稳定复现的问题」。一旦能稳定复现,bug 就解决了一半。
六、实战四:telemetry------把事件流变成数据
日志不只是给人看的,更是喂给分析系统的。DSH 的 telemetry 就是「基于事件流做统计」的能力。
有了这条完整的事件流,你可以轻松算出:
- 一次任务烧了多少 token、流式延迟多少;
- 哪些工具被高频调用、哪些从来没人用;
- 模型在哪一步反复空转(同一工具连续调用 N 次);
- 一个任务失败前发生了什么(往前翻事件流即可)。
配合上一篇文章讲的类型化事件,你可以写一个 telemetry 插件,订阅 assistant/message、tool/result 等持久事件,把数据落到你自己的分析管线里------这又回到了「一切皆插件」:可追溯性本身,也是可扩展的。
七、一条硬约束:新增能力,就要新增事件
最后说一个容易忽略的点。因为「模型可见即已记录」是条不变式,所以它反过来对你(插件开发者)提出了一个要求:
你每新增一项「模型可见」的能力,就必须同时新增一个对应的会话事件。
举个例子:如果你写了个插件,往请求里注入了一段「检索到的知识」,那你就不能只注入、不记录。你必须同时 emit 一个会话事件,把「这段知识是什么、注入到了哪一步」写进日志。否则,一旦模型因为这段知识做出了错误决策,事后你永远查不到原因------因为日志里根本没有它。
这条约束看似麻烦,实则是在从根上杜绝「模型看到了什么,日志里却查不到」的黑箱。理解了它,你就理解了 DSH 为什么敢把可追溯性当卖点。
八、小结
DSH 的会话日志不是一个「顺手记一下」的附属功能,而是它的数据底座 :resume、fork、replay、telemetry 全部派生自同一条 append-only 事件流,而「模型可见即已记录」这条不变式保证了这条流的可信。
对要调试 Agent、复现 bug、做评测的开发者来说,这一套的可迁移价值很高------你不再是「猜它为什么这么做」,而是「回放现场看它到底怎么做」。
下一篇,聊一个 Agent 绕不开的话题:安全。DSH 的沙箱机制(Landlock / Seatbelt / ACL)是怎么在系统层面把 Agent 关进笼子的。
参考:deepseek-ai/deepseek-harness 会话与可追溯性文档。