会话与消息模型
本教程共 32 篇 · 第 10 篇 · 更新于 2026-08-15 · 约 6 分钟阅读
本节目标:理解会话(Session)是「仅追加事件日志 + 内存存储」,学会 turn/step 边界与消息词汇,知道模型历史是怎么从日志派生的。
你有没有想过:模型看到的那段对话历史,到底存在哪里?dsh 的答案很特别:没有专门的地方存。历史从一份仅追加的会话事件日志里派生出来。这就是本节要讲的核心模型。
会话:一份仅追加的事件日志
会话是一份由类型化 SessionEvent 组成的仅追加日志,由 core/session 包提供,通过 ctx.sessions 访问(内存存储)。它是 Agent 完整交互历史的唯一真源。LLM 消息历史从日志派生,从不单独存储;回放就是重新从同一组事件派生历史。可以想成监控录像:画面不是另存的截图,而是每次从录像带重新剪出来的。
日志里的每个事件都有单调递增的 seq(seq = log.length)和毫秒级 time。所有事件数据都必须能无损序列化为 JSON,Session.append 在源头强制这一点。
为什么用仅追加设计?因为派生性:恢复、fork(分叉)、transcript(文字记录)、遥测、持久化全部从这条事件流派生。日志是账本,谁也不能改账——只能往后记。会话日志里混进脏数据,比代码 bug 难查得多,所以 dsh 把「只能追加」做成了硬约束。
事件词汇:日志里记什么
SessionEventMap 是合并可扩展的事件词汇表。最常用的一批:
| 事件 | 携带数据 | 含义 |
|---|---|---|
turn/start / turn/end | { turn } / { turn, reason } | 轮次开闭,reason 如 completed / aborted / max-tokens |
step/start / step/end | { turn, step } | 步骤开闭 |
user/message | UserMessage | 用户消息:直接提示、注入上下文、steering |
assistant/chunk | { turn, step, chunk } | 原始流式分片,token 级回放保真 |
assistant/message | { turn, step, message, usage? } | 组装后的 assistant 消息(派生历史用它) |
tool/call | { turn, step, callId, name, arguments } | 模型请求的一次工具调用 |
tool/result | { turn, step, message, error?, meta? } | 工具调用的模型可见结果 |
插件可以扩展这个词汇表:通过 TypeScript 声明合并声明新事件类型。例如 compaction seam 添加了 compaction/start、compaction/summary、compaction/end。
turn 与 step:执行边界
一个 step(步骤)是一次模型请求加上它调用的工具。 一个 turn(轮次)包含零个或多个 step:它在领取首条输入之前打开,在不再欠下任何工作时关闭。
turn/start
claim next-step input
assemble prompt sections + tool schemas
step/start
user/message → agent/request → llm/stream → assistant/chunk* → assistant/message
tool/call* → tools/pre-execute → tools/execute → tools/post-execute → tool/result*
step/end
turn/end
注意:轮次包围一次模型循环执行,不是整个会话。一个「帮我看代码 → 读文件 → 改文件 → 再跑测试」的完整任务,可能是多轮、多 step 的组合。
模型历史:从日志派生
Session.deriveMessages() 把事件日志投影成模型看到的 Message[]。投影规则很直接:
| 事件 | 投影为 |
|---|---|
user/message | 一条 user 消息 |
assistant/message | 一条 assistant 消息 |
tool/result | 一条带 tool-result 块的 user 角色消息 |
assistant/chunk | 跳过(属于回放/UI 数据) |
turn/*、step/* | 跳过(结构信息) |
由此得到 dsh 最重要的一条运行时不变量:
模型可见即已记录。 抵达模型请求的一切都必须能从日志重建。因此,新增一项模型可见输入,就需要新增一个会话事件——扩展
SessionEventMap并从日志渲染。
人话版:模型看到什么,完全由投影规则决定。想给模型加一种新输入,就要先在日志里给它一个位置。
这条不变量由运行时不变量服务断言。它的推论很实用:你没法悄悄塞给模型一段不出现在日志里的上下文。凡是声称影响后续模型的数据,都必须能由持久日志重建。
消息词汇:Message 与内容块
模型请求与持久历史共用同一套消息表示。一条 Message 带稳定 id、角色(system / user / assistant)和一个内容块数组。内容块类型可合并扩展:
text(文本) | reasoning(思考) | image(图片附件)
| tool-call(工具调用) | tool-result(工具结果)
assistant 消息还携带来源信息(AssistantProvenance):产生它的 provider 与 model,以及可选的适配器私有回放数据。也就是说,一条 assistant 消息自带「身世」,回放时知道找谁还原。
inbox:输入怎么到达驱动器
所有输入通过同一个 inbox 到达 agent loop 驱动器。有些消息会立即唤醒它。注入的上下文(文件变更通知、AGENTS.md、skill 内容、定时提醒)会留在 inbox 里,等另一条消息来唤醒。agent/pre-step 决定模型看到什么:监听器可以改写已领取的消息,也可以直接拒绝。首次领取被拒绝或被改写为空时,仍会关闭一个不含 step 的持久轮次——日志会记录这次尝试。
会话事件如何广播
会话事件追加进日志后,还会通过 Cordis 事件 session/event 广播。需要实时反应的消费方(UI 渲染、持久化、遥测、标题生成)监听这个事件;需要回放 transcript 的 SDK 用户也消费这条事件流。持久化到 JSONL / SQLite 是另一个 seam(ctx.sessionPersistence),细节在第 18 章展开。
Tip初学者常问「模型看到的历史和 Trajectory 视图怎么不一样」。原因就在投影规则:模型只看
user/message、assistant/message、tool/result三类 surface 事件;assistant/chunk、边界事件、请求上下文是观测与回放数据,不进模型历史。被记录 ≠ 模型可见。
Warning版本基线
@deepseek-ai/dsh0.1.0-rc.6 处于 Developer Preview,事件词汇会随版本增减。完整SessionEventMap以官方 session 子系统文档为准。
小结
- 会话是仅追加的类型化事件日志,是交互历史的唯一真源。
- 模型历史由
deriveMessages()从日志投影,从不单独存储。 - 常用事件词汇:
turn/*、step/*、user/message、assistant/*、tool/call、tool/result。 - 「模型可见即已记录」:新增模型可见输入,就要新增会话事件。
- 日志通过
session/event广播;持久化是另一个 seam(第 18 章展开)。