Skip to content

CH 10 · 会话日志即真相源

全文字数2338 字预估耗时约 12 分钟前置CH 09 消息流转难度理解篇

本章目标

CH 09 讲过"记账本"(session),这一章把它讲透:为什么它是整台机器的真相源(Source of Truth)——模型记住什么、你看的轨迹、能导出的转录、能开的新分支,全都从这一份日志派生,而它自己只增不改。

一句话:会话日志 = agent 的账本、记忆和档案

官方对 Session 的定义很硬核,拆开就三条:

  1. 追加式:只往尾部加记录,不修改、不删除旧的。
  2. 类型化事件:每一行不是自由文本,而是一种"事件"——turn/startuser/messageassistant/messagetool/resultturn/end……
  3. 单一真相源:整个 agent 交互历史,只有这一份是真的,其他全是它的投影。

把 CH 09 串起来看会特别顺:你在上一章看的那条"消息流转",每一步其实都是在往这个日志里写一条事件——turn/start 打开轮次、step/start 开始步骤、user/message 记下你发的、assistant/message 记下模型回的、tool/result 记下工具跑的、turn/end 关闭轮次。

一句话记法:流转是"正在发生的事",日志是"已经发生过的记录",两者一一对应。

为什么它是"真相源"

官方原话:

模型消息历史是从日志派生的,从不单独存储。

意思是:dsh 里不存在第二份对话记录。你以为"模型记得前面说过的话",其实模型看到的历史是系统从日志投影出来的;你看到的轨迹视图、能导出的转录、能开的 fork,也都是从这同一条日志渲染出来的:

会话日志 = 真相源:一切从它派生(示意)

  • 模型对话历史deriveMessages() 从日志投影出模型看到的 Message[]——所以"模型记得什么"= "日志里有什么";
  • 轨迹视图:CH 04 你看到的 ASSISTANT / TOOL 时间线,就是日志的可视化;
  • 转录 / 导出:整轮对话文本,从日志重放;
  • fork 分叉:在某个历史节点长出一条新会话;
  • 遥测 / 统计:token 用量、耗时,从日志算出来;
  • 持久化文件$DSH_HOME/sessions 里那份 session.jsonl.zstd

为什么非要这样设计?因为只有一份真相,就不会出现"界面上显示的、和模型记的、和导出的,三份对不上"。想要别的视图,都是从同一份日志投影,投影规则一致,永远一致。

"模型可见即已记录":设计轴心

这是 dsh 的一条硬规则,上一章提过,这里展开:

任何到达模型请求的东西,都必须能从日志重建,而且运行时会用一项不变式去检查。

它带来两个直接推论:

  1. 想给模型加新东西,必须新增一种事件类型。比如你想让 Agent 看到一段注入的上下文,不能绕过日志直接塞进请求——而是定义一种新的会话事件,把它写进日志,再从日志投影出去。这让每一步都有迹可循,也正是首页那句"每次运行可追溯"的底层保证。
  2. 日志无损。连模型返回的原始流式 chunk 都保留(assistant/chunk),所以重放能做到逐 token 保真,UI 才能原样还原。

一句话:这个设计让"可追溯"不是口号,而是架构上绕不开的事实。

顺带和 CH 04 对上号:你用过的那条 /compact 压缩上下文命令,底层就是往日志里记下"我压缩了"这个动作(compaction/* 事件),再从日志换一种更精炼的方式投影给模型。它不改写历史——原始事件还在日志里,只是模型看到的投影被重排了。这也是为什么压缩后 Agent"记得的"更精炼,但原始记录依然完整。

真实的样子:本机会话日志

回到你自己的机器看——以我这台电脑为例(CH 05 跑过 headless 之后,这里就有了):

C:\Users\mortal\.dsh\
├─ profiles\            ← profile 清单(CH 08 说的"菜单单")
├─ sessions\            ← 会话日志在这
│  ├─ --E-software-workspace-DeepSeek~0020harness~0020demo--\
│  │  └─ session-307edce2-...\session.jsonl.zstd   ← CH 05 headless 实操那次的记录
│  └─ --E-software-workspace-doubaowork-DeepSeekHarnessGuide--\
│     └─ session-f417b4dd-...\session.jsonl.zstd   ← 这个项目里用过的会话
├─ storages\
├─ settings.yaml
└─ .credentials.yaml

几个要点:

  • 按工作区建目录:目录名是工作区路径的转义(空格变成 ~0020),一眼能看出哪个会话是在哪干活时产生的;
  • 每个会话一个文件夹,里面是 session.jsonl.zstd——JSONL 逐行追加 + zstd 压缩的持久化文件;
  • 呼应 CH 05 的"未分组":这些文件按工作区存,但 Web UI 的会话列表把 headless 跑出来的归在未分组,两个维度别搞混;
  • 这些文件是会话的"记忆档案",别随手删。删了,那个会话的"记忆"就真没了。

fork:从日志长出新会话

日志既然能完整重放,自然就能"从某个位置重新长"——官方把它叫 fork(分叉):把某个稳定位置之前的事件完整克隆,作为一条新会话的开头,之后各走各的。用途很直接:想在某个历史节点开一条新路试试,而不动原会话。具体怎么用,等后面实操章节到了再说,这里先知道"它存在、且它之所以能存在,正因为日志可以完整重放"。

这一章你学到了什么

  • [ ] 能说出会话日志的三个特征(追加式 / 类型化事件 / 单一真相源)
  • [ ] 能把 CH 09 的消息流转和日志事件一一对上(turn/start、user/message、assistant/message、tool/result、turn/end)
  • [ ] 能解释"模型历史从日志派生、从不单独存储",以及为什么这样不会有三份对不上
  • [ ] 能说出"模型可见即已记录"的两个推论(加新东西=加新事件类型;日志无损可逐 token 重放)
  • [ ] 知道本机会话日志在哪($DSH_HOME/sessions 按工作区建目录,session.jsonl.zstd),以及和 Web UI"未分组"的区别

Open Source · MIT · Community Driven