第 1 卷
张嘉熙 · DeepSeek Harness 前沿工程实践



你好,我是张嘉熙。
前面几讲我们盯的都是一次调用,工具调用怎么走流水线、被守卫拒绝、被沙箱约束能碰哪些文件,Code Mode 里的子调用也一样。但一个真实任务往往要在模型与工具之间来回几十
趟,模型说一句、调个工具、看看结果、再说一句……这些来回攒下的状态,存在哪儿?
dsh 的回答很朴素:一份只追加的日志(Append-Only Log)。这些来回,都发生在同一个会话里:会话里每发生一件事就记一条,一个会话一份这样的事件流——这就是会话日志。
这里还有一层设计容易被忽略:我们发给模型的那段消息历史,不是另外找地方存了一份,而是每次从日志里算出来的。这一讲把“从日志算出来的东⻄”叫投影(Projection)。投影不是
真相,它只是真相算出来的结果,所以它不需要单独存,也不存在“跟日志对不上”这回事。日志多了一条,重新算一遍,投影就跟着变。你想改投影,不能直接改它,只能往日志里追加一
条;你想重放,就是把日志从头再算一遍。
- 日志长什么样;
- 日志凭什么可信;
- 第一个投影:消息历史;
- 投影怎么写;
- 投影不必每次从头算。
前两节说日志这个因,后三节说从它算出的果。
关于环境:本讲新增一个包 @deepseek-ai/dsh-session-projection(投影机制的定义与框架驱动),已加入本项目的 package.json,npm install --legacy-
peer-deps 装上即可。
关于示例:npm run start:ch08 跑日志,npm run start:ch08:projection 跑投影。
日志:一串只能追加的事件
一次会话(Session)就是一份仅追加日志:由一串带类型的事件(SessionEvent)组成,是这次会话全部交互历史的唯一真源——要查发生过什么,只认这一份,别处没有第二
份。(官方的说法是“agent 完整交互历史的唯一真源”,那是对正在跑的那个 agent 而言:
它打开的就是这一个会话。)
先看它长什么样,跑 npm run start:ch08,第一段是记完一次最简交互之后的日志(顶格那句小结是程序打的总结,不属于日志):
复制代码 1--- ① 日志:一条条追加,seq 连号 ---
| 2 | seq0 | turn/start |
|---|---|---|
| 3 | seq1 | step/start |
| 4 | seq2 | user/message |
先看这一串:号是连着的。 8 条事件,seq 从 0 到 7,一个挨一个,它等于这条事件被追加那一刻日志里已有的条数。连号不是装饰,是契约:读回来只要发现中间缺了一个,比如 0、
1、2、4,就知道这段日志不完整,不会把它当完整历史接着用。所以事件一旦写下就抽不掉:抽一条,号就断了。
再看一条事件里面有什么。上面只打了类型;我们用程序把 seq 2 这条原样打出来:
复制代码 1{"type": "user/message","seq": 2,"time": 1788744295522,"data": {"content": [{"typ
一条事件必有这四个字段:
type:它的种类
seq:它在会话内的序号,等于被追加那一刻日志里已有的条数
time:追加那一刻的毫秒时间戳,每次运行都不同
data:这件事的载荷,里面的 id 是框架给每条消息生成的身份,每次运行都不一样
尾巴上还多一个 surfaceOp,这条事件进不进模型视野,就看它。注意,它是条件必填,不是可带可不带:进得了 surface 的那几种类型,追加时必须声明自己怎么进,不声明就报错;
进不了的类型不许带,带了一样报错。第三节专门讲。
官方把事件的这层结构叫信封(envelope):type、seq、time 是信封上写的信息,两个可选字段 surfaceOp(第三节讲)、ignorable(第二节讲)也是;data 是装在信封里
的信。这个分法下面第二节要用。