第 2 卷
复制代码 1*// user/message:消息本身就是 data*data: { content, source, role, id }
2 3*// assistant/message:消息装进 message,外面另带 turn 和 step*data: { turn, step, me
一个把消息摊平摆在 data 上,另一个把消息装进盒子、外面再贴两个标签。
接着看几条事件怎么串起来:turn 套 step。日志本身是平的,8 条事件平铺一列,没有任何嵌套。但按 turn/*、step/* 这几条标记事件的起止去读,层次就出来了:
复制代码
| 1 | turn/start | seq0 | 一轮开始 |
|---|---|---|---|
| 2 | step/start | seq1 | 一次模型调用开始 |
| 3 | user/message | seq2 | |
| 4 | assistant/message | seq3 | |
| 5 | tool/call | seq4 | |
| 6 | tool/result | seq5 | |
| 7 | step/end | seq6 | |
| 8 | turn/end | seq7 | 一轮结束 |
一个 turn 是一轮对话,一个 step 是一次模型调用 + 它要求的那些工具执行。示例里模型调一次就答完了,所以只有一个 step;如果它看完工具结果还想再想一步,同一轮里就会接着
出现 step 2、step 3,一个 turn 里可以有多个 step(step/*、tool/call 这些事件的 data 里都带着 turn 和 step 两个编号,就是为这个)。
还有一点:tool/call 和 tool/result 是两条独立事件,模型要求调用和调用完了,在日志里分得清清楚楚。反过来也成立,一个 turn 也可能一个 step 都没有:官方明确说,没进
过 step 的 turn 不会留下 step/start、step/end,比如这轮在进模型之前就结束了。所以 turn 和 step 是两层各自独立的编号,不是一轮必然配一步。
面)就是模型看得见的那一层。标是的只有三种,原因第三节从模型的视角交代:
| 事件 | 记的是什么 | 进surface |
|---|---|---|
| turn/start / turn/end | 一轮的起止,以及结束原因 | 否 |
| step/start / step/end | 一次模型调用及其工具执行的起止 | 否 |
用户说的话。三种都记在这儿:人说的一句、 agent.inject() 补进来的上下文(文件变更通知、子目
user/message是录 AGENTS.md、技能内容、定时通知……)、目标
续接的一轮——三者原样投影,靠 source 区分
原始流式分片,一个分片一条,为的是 token 级的回 assistant/chunk否放保真
一个 step 组装好的模型消息——派生历史用的就是 assistant/message它,不是上面那些分片;适配器上报了用量才带是
usage
| tool/call | 模型要求的一次调用(原始参数字符串) | 否 |
|---|---|---|
| tool/result | 一次调用的模型可见结果 | 是 |
| request/header | 发给模型的请求快照 | 否 |
表格里有一行值得多看一眼:request/header。模型每次开口前,程序都得给它配好一套设定——用哪个模型、什么参数、提示词写的什么、给哪些工具。消息历史记的是说了什么,
这套设定则是在什么条件下说的,它不在对话里,却决定模型怎么答。request/header 就是把这套设定拍张快照存进日志:过后想知道当时给它看了什么,翻日志即是原文。而且不是
每次请求都拍,跑起来先拍一张,之后设定变了才追加下一张。
顺带提一嘴,这本账记在什么纸上是可换的:dsh-session 自己只认那一串事件,落在哪里是另一层的事。官方就给了两种后端,JSONL 存成一份文本文件,SQLite 每个事件存一行;
行字段与事件 1:1 映射,所以没有需要保持同步的并行持久化 schema,换的是摆法,不是账。怎么落盘不在这一讲的范围。
最后,回头看只能追加这四个字。为什么不允许涂改?因为下面三件事,全都压在同一个前提上——写下的就改不动:
崩溃之后接着跑。进程随时会死,重启后要接着干,得先答两个问题:断在哪一条、哪些是真的。只追加的日志答案很干脆,就是最后那一条。它要么是完整写下的,要么压根不在
日志里,不存在“写了一半被覆盖”这种中间状态(官方专门有一节叫“崩溃恢复保留被中断的轮次”)。
同一份会话在几处同时打开。几方看同一段历史,得有唯一一份说了算。只追加就没有“谁的版本更新”这个问题:差异只可能是尾巴长短,长的一方更新。
事后追责与复盘。要能证明这段历史没被人动过。只追加天然满足——任何改动都只能表现为新增,而新增本身又是账上明明白白的一条。
而只追加是这个前提最经典的实现:任何一条都改不了,于是这本账永远只有一个出处。至于从它能算出多少种视图,那是第四节的事,真源只有一份,视图随便多少份。
回头看这一节看过的四件事:一串事件、一条事件里的四个字段、turn 套 step 的层次、以及十种事件类型——这是日志的形。
形看完了。可形只是它长什么样,它凭什么当得起唯一真源这四个字?
日志的底线:三条硬约束
既然是唯一真源,就容不得半点含糊:它得保证你读回来的,就是当时写下的那本。下面这两