上周把 DeepSeek 开源的 Harness 源码教程从头翻完,十五个章节,有一句话我在三个地方都看到了它的影子:「模型所见,必入日志」。初看平平无奇,翻到后面才明白,这句话是整套设计的地基,也是它敢喊出「能上生产」的底气所在。

先理解一个问题:agent 出事了,你拿什么复盘
企业用 AI 最怕的不是它笨,是它干了什么你完全不知道。
传统程序每一步都在代码里写着,出错能查日志、能断点、能回放。agent 不一样,它的每一步都是模型现场决定的,今天这么干明天那么干,你根本没法预判。要是它半夜自己跑了个什么操作,改了个不该改的文件,第二天你除了看最终结果,中间过程全是黑箱。
市面上大多数 agent 框架对这件事的答案是:存对话记录。你翻聊天记录,能看到它说了什么,但看不到它「当时脑子里装了什么、为什么这么想、调用了哪些工具、工具返回了什么」。
dsh 把这件事换了个做法:不存「对话」,存「日志」。
这俩有什么区别?区别大了。
日志不是聊天记录,是唯一真源
dsh 的会话层(packages/session)有一个核心设计:一段对话不是一个「消息列表」,而是一份只能追加的事件日志。
模型说的每一句话、每一步思考、每一次工具调用、工具的返回结果,全部是这条日志上的一条事件。turn/start、assistant/chunk、tool/call、tool/result,各有各的事件类型,按时间顺序一条条追加,只增不改。
关键在这一句。模型的对话历史不是单独存的,是每次需要时从这份日志现场「投影」出来的。源码里叫 deriveMessages()——从日志推导出模型要看的消息。
这意味着什么?日志才是唯一的真相,对话历史只是它的一次投影。你随时可以从这份日志重新投影出任意时刻的模型视角,这就是 fork、恢复、回放这些功能的地基。市面上大多数框架是「存一份对话」,dsh 是「存一份流水账,对话随取随用」。
那这条流水账,凭什么保证真实?
那条不变式,才是真正的狠活
到这里你会说:这不就是事件溯源吗,微服务早就用烂了。
区别在后面这句。dsh 定了一条运行时不变式,而且用代码断言保证:凡是进入模型请求的内容,必须能从日志里重建出来。
反向说更清楚:你看到日志里有什么,模型就看到了什么;模型看到的东西,日志里一定找得到。两者画等号,中间没有漏网之鱼。
这条不变式对插件作者是个硬约束——你想往模型上下文里塞点新东西,不能直接塞,必须先在日志上定义一个新事件类型,再让这个事件能被投影成模型看到的文本。麻烦是麻烦。但换来一个极大的好处:任何时候,你都能精确知道模型当时看到了什么。
出事了,翻日志,一步步还原模型当时的完整视角——它看过哪些文件、被注入了什么上下文、工具返回了什么、它在什么状态下做的决定。这不是事后补的审计报告,是本来就在那儿的、一分不少的真实记录。
那这些记录能拿来干嘛?
重放、分叉、恢复:日志派生的一切
因为对话历史是从日志投影的,一系列「高级功能」就成了顺水推舟:
回放。把日志重放一遍,等于把 agent 干过的活重演一遍,UI 上能看到每一步的原始 chunk 流,不是事后整理的摘要。
分叉(fork)。把一份日志复制一份,从某个节点开始改一个分支继续跑。对比两个方案,各跑各的,互不干扰。这比「复制整个对话」轻量得多,因为日志是结构化的,不是一大坨文本。
恢复。进程挂了,日志还在,下次启动从日志重建会话,agent 记得自己干到哪了。fork、resume、transcript、遥测,全是这条流上长出来的。
对企业来说,这三个能力对应三个实际场景:出了事故能回放定位(审计)、改方案能分叉对比(实验)、跑挂了能无缝恢复(容错)。
说句公道话
这套设计不是没代价。每条事件都要定义类型、写投影逻辑,插件作者的学习成本比「往 messages 里 push 一条」高得多。日志只增不改,长期跑会有体积膨胀的问题,所以 dsh 还配套了压缩(compaction)机制,把老日志折叠成摘要。
但取舍很明确:为了「可审计、可回放、可恢复」这三件事,多付的这点复杂度是值得的。尤其企业场景,AI 一旦开始碰文件、跑命令、改配置,可审计性就不是可选项,是底线。
翻到第 9 章我还看到一层设计,审批没人应答时默认拒绝(fail-closed),跟日志这套是同一个思路:宁可多留痕迹、多拦一步,也不放过一次不可追踪的操作。上生产这件事,dsh 把「留痕」和「把关」两件事都做了,这是它跟那些玩具级框架最大的分水岭。
完整教程在 https://tool-ix.cn/tutorials/deepseek-harness
首发于公众号「键盘漫笔」
