07 会话日志与可观测性#
DeepSeek Harness 有一条贯穿整个设计的硬性规则:模型看到的一切,必须能从日志里还原出来。基于这条规则设计的会话日志系统,以及配套的 Trajectory 轨迹视图,是 dsh 区别于其他闭源 Agent 产品最显著的特征之一,被 Hacker News 社区评价为「killer feature」。
硬性规则:模型看到的必须写进日志#
这不是一个建议,也不是一个可选功能——这是写死在框架里的强制约束。
规则内容#
任何进入模型请求的内容,都必须先写入会话日志;任何从模型输出的内容,也必须完整记录在日志里。具体来说,必须记录的内容包括:
类别 |
必须记录的内容 |
|---|---|
系统提示词 |
所有系统消息、角色设定、能力说明、格式要求 |
用户输入 |
原始用户消息、附件内容、工作区上下文注入 |
思维链 |
模型的 Chain of Thought 推理过程(如果模型输出) |
工具调用 |
每一次工具调用的名称、参数、调用时间 |
工具结果 |
工具返回的完整输出、错误信息、退出码 |
子 Agent 调度 |
委派给子 Agent 的任务、子 Agent 的执行轨迹、返回结果 |
上下文注入 |
每一次 RAG 检索结果、记忆模块注入的内容、计划/目标状态 |
流式输出 |
完整的 |
元数据 |
每一步的模型版本、参数设置、Token 用量、耗时 |
为什么这是硬性规则#
这条规则的核心目的是保证可观测性和可复现性:
你可以精确知道模型为什么做出某个决策,因为你看到了它看到的一切
任何会话都可以精确回放,因为所有输入输出都有完整记录
出问题时可以精准定位——是提示词有问题?工具返回错了?还是模型理解错了?
没有「黑盒」,没有隐藏的上下文注入,没有「暗箱操作」
对比许多闭源 Agent 产品:
你看不到系统提示词是什么
你不知道它偷偷注入了什么额外上下文
工具调用的完整参数和返回结果可能被隐藏
出错时你只能猜测「它为什么这么做」
没有办法回放和复盘整个过程
dsh 的设计哲学是:Agent 做的每一件事都应该是可审计、可解释、可复现的。
Append-only SessionEvent 流设计#
会话日志的底层是一份仅追加(append-only)的 SessionEvent 流。
什么是 append-only#
Append-only 意味着:
事件只能被追加到日志末尾,不能修改、删除已有事件
日志是不可变的(immutable),一旦写入就不会改变
新事件总是按发生顺序追加,有单调递增的序号和时间戳
这种设计看似简单,但带来了几个极其重要的特性:
没有并发冲突:写入只需要追加,不需要锁和复杂的事务
天然支持审计:你无法「篡改历史」,所有操作都留下永久记录
时间旅行调试:你可以回到任意时间点的状态
派生能力统一:所有功能(回放、分叉、视图)都从同一条事件流派生
SessionEvent 结构#
每个 SessionEvent 是一个结构化的记录,典型字段包括:
interface SessionEvent {
id: string; // 事件唯一 ID
sequence: number; // 单调递增序号
timestamp: number; // 事件发生时间戳
type: string; // 事件类型(user/assistant/tool-call/tool-result/...)
payload: unknown; // 事件内容(类型由 type 决定)
metadata: {
stepId?: string; // 所属 Step ID
turnId?: string; // 所属 Turn ID
parentEventId?: string; // 父事件(如子 Agent 事件)
tokens?: { input: number; output: number };
duration?: number;
};
}
事件类型枚举#
核心事件类型包括:
事件类型 |
说明 |
|---|---|
|
会话开始 |
|
用户消息 |
|
系统提示词注入 |
|
模型流式输出的一个 chunk |
|
模型完整消息(聚合后的) |
|
模型发起工具调用 |
|
工具执行结果 |
|
子 Agent 被创建 |
|
子 Agent 完成并返回结果 |
|
外部上下文被注入(RAG、记忆等) |
|
检查点创建 |
|
会话结束 |
所有事件严格按发生时间顺序排列在流中。
deriveMessages() 投射机制#
原始 SessionEvent 流是「发生了什么」的完整事实记录,但模型实际看到的是按特定格式组装的消息列表(OpenAI 格式的 messages 数组)。deriveMessages() 函数负责从事件流中投射出模型在任意 Step 看到的消息历史。
投射过程#
deriveMessages(events, upToStepId) 的工作是:
接收完整的 append-only 事件流
接收一个目标点(通常是某个 Step 的 ID)
按顺序重放事件,直到目标点
将事件转换为模型 API 要求的消息格式(system/user/assistant/tool)
返回组装好的 messages 数组
这个投射过程是纯函数——给定相同的事件流和相同的目标点,总是返回完全相同的 messages 数组,没有副作用。
为什么用投射而不是快照#
很多框架选择「定期保存消息历史快照」,但 dsh 选择从事件流投射,原因是:
单一真相源:只有事件流是真实的,快照只是派生品,不会出现「快照和真实状态不一致」
任意时间点:你可以投射出任意 Step 时模型看到的历史,而不仅仅是保存快照的几个点
天然支持分叉:从任意事件点分叉,只需要从该点开始追加新事件,不需要复制整个历史
容错性好:如果进程崩溃,只需要从最后一个事件开始重放,不需要担心快照损坏
原始事件流是唯一的真相来源,所有其他视图都是从它派生出来的。 这是 dsh 会话日志设计最核心的洞见。
Trajectory 轨迹视图使用方法#
Trajectory(轨迹)视图是 dsh Web UI 中基于事件流提供的可视化工具,也是普通用户最常接触的可观测性功能。
如何打开 Trajectory 视图#
有两种打开方式:
单条消息轨迹:在任意一条 Agent 回复消息的右上角,点击 Trajectory 按钮,查看这条消息对应的执行轨迹
整个会话轨迹:在会话侧边栏或会话菜单中,选择 View Full Trajectory,查看整个会话从开始到现在的完整轨迹
Trajectory 里能看到什么#
打开 Trajectory 视图后,你会看到按时间顺序展开的完整执行树,每一个节点都是一个事件:
节点类型 |
展示内容 |
|---|---|
系统提示词 |
可以展开查看完整的系统提示词内容,包括所有能力说明、格式要求、角色设定 |
用户输入 |
原始用户消息,以及自动注入的工作区上下文 |
思维链(CoT) |
如果模型输出了推理过程,这里会完整展示(通常是折叠的,点击展开) |
工具调用 |
每次工具调用的名称、完整参数、调用时间、耗时 |
工具结果 |
工具返回的完整输出,包括 stdout/stderr/退出码/错误信息 |
上下文注入 |
RAG 检索到的文档片段、记忆模块召回的内容、自动注入的其他上下文 |
子 Agent |
子 Agent 的独立轨迹可以展开查看,形成嵌套树结构 |
流式输出 |
可以回放模型输出的流式过程,逐字看到回答是如何生成的 |
元数据面板 |
每个节点都有元数据面板,显示 Token 用量、模型版本、耗时等信息 |
每个节点旁边标注了来源(是哪个插件注入的、哪个工具产生的),让你清楚知道每一段内容从何而来。
Trajectory 里能做什么#
在轨迹视图中,你可以执行以下操作:
检视(Inspect):点击任意节点展开查看完整内容,长内容不会被截断
搜索(Search):在轨迹中搜索关键词,快速定位到特定事件
过滤(Filter):按事件类型过滤(比如只看工具调用、只看错误)
分叉(Fork):从任意节点分叉出一个新会话(详见下一节)
恢复(Resume):跳回历史上的任意节点,从那里继续执行(相当于「时间旅行」)
回放(Replay):自动回放整个执行过程,按原时间间隔展示事件如何一步步发生
导出(Export):将轨迹导出为 JSON 或 Markdown 格式,用于分享或离线分析
Fork 分叉 / Resume 恢复 / 回放机制#
基于同一条 append-only 事件流,dsh 实现了三个强大的调试功能。
Fork(分叉)#
分叉允许你从历史上的任意一个节点开始,创建一条独立的新会话分支:
使用场景:
Agent 走了一条错误的路,你想「如果当时我给它补充点信息,会怎么样?」
你想试验不同的选择——「如果当时它选了工具 A 而不是工具 B,结果会怎样?」
你想基于某个中间状态尝试不同的提示词策略
操作方法:
在 Trajectory 视图中找到你想要分叉的节点
点击节点菜单,选择 Fork from here
dsh 会创建一个新会话,历史到该节点为止与原会话完全一致
从这个点开始,你可以输入新的指令,新的事件会追加在分叉后的事件流中,不影响原会话
分叉是极其轻量的——因为事件流是 append-only 的,分叉不需要复制任何历史事件,只需要记录「新分支从事件 X 开始」即可。你可以从同一个点分叉出无数个分支做试验,几乎不占额外存储空间。
Resume(恢复)#
恢复允许你跳回当前会话历史上的任意节点,从那里继续执行:
与分叉的区别:
分叉:创建新会话,原会话保持不变
恢复:在当前会话中「回退」到某个点,后续事件会追加在该点之后,该点之后原有事件被标记为废弃但不删除
使用场景:
Agent 已经走了 10 步,你发现第 3 步就错了,不想从头重新输入,直接从第 3 步重新来
你想打断 Agent 的当前执行,调整一下提示词,然后让它从打断点继续
修复了某个工具的 bug,想从工具调用失败的点重试,不需要重跑前面的步骤
操作方法:
在 Trajectory 视图中找到你想要恢复到的节点
点击节点菜单,选择 Resume from here
确认后,会话会回退到该节点的状态
输入新的指令,或者让 Agent 重新执行后续步骤
被「回退」掉的事件并没有真正删除——它们仍然在事件流中,只是被标记为 deprecated: true,不会被 deriveMessages() 投射给模型。你随时可以再回到原来的分支。
Replay(回放)#
回放允许你按原时间顺序重新「播放」一遍整个会话的执行过程:
使用场景:
向同事演示 Agent 是如何一步步解决问题的
录制教程或演示视频
仔细观察某个容易被忽略的中间步骤
复盘长时间运行的任务,不需要实时盯着
操作方法:
打开 Trajectory 视图
点击顶部的 Replay 按钮
可以调整回放速度(0.5x / 1x / 2x / 5x)
事件会按原始发生的时间间隔依次展示,流式输出会逐字回放,就像你重新跑了一遍一样
回放也可以从任意节点开始播放,不需要从头开始。
调试价值:为什么这很重要#
这套可观测性设计在实际使用中价值巨大,尤其体现在以下场景:
长任务 Debug#
对于需要运行几十步、几十分钟的长任务:
没有 Trajectory:如果最后一步失败了,你可能完全不知道中间哪一步出了问题,只能从头重跑一遍,再试一次
有 Trajectory:你可以回到失败点,查看当时工具返回了什么、模型看到了什么上下文,直接定位问题原因,从失败点重试而不用重跑前面所有步骤
长任务调试的时间成本可以降低一个数量级。
决策复盘#
当 Agent 做出了一个意料之外的错误决策时:
没有 Trajectory:你只能猜测「它为什么会这么想」,可能是提示词问题、可能是工具返回错了、可能是上下文太长它忘了前面的内容——你无法确定
有 Trajectory:你可以展开决策前的系统提示词、查看当时注入了什么上下文、看工具返回的完整结果、甚至看它的思维链,精确理解决策是怎么一步步产生的
这对于改进提示词、修复工具 bug、优化 Agent 行为至关重要。
失败分析与迭代#
当你在开发自己的插件或优化提示词时:
每一次失败都是一次「测试用例」,被完整记录下来
你可以对比修复前后的轨迹差异,确认问题是否真的解决了
你可以收集失败案例,建立测试集,做回归测试
你可以把失败轨迹分享给别人,请求帮助排查问题——对方不需要重跑,看轨迹就能知道发生了什么
对于严肃的 Agent 开发工作,这种级别的可观测性不是「nice to have」,而是「must have」。
Hacker News 评价:Killer Feature#
DeepSeek Harness 开源后,Trajectory 和会话日志设计在 Hacker News 上引发了大量讨论,被许多开发者称为「真正的 killer feature」。
讨论中被反复提及的对比点:
方面 |
许多美国厂商闭源产品 |
dsh |
|---|---|---|
系统提示词 |
作为商业秘密隐藏,用户看不到 |
完全开放,Trajectory 里随时可以看 |
工具调用参数 |
可能被截断或隐藏部分字段 |
完整记录,一字不差 |
工具返回结果 |
经常只展示摘要,原始数据不公开 |
完整 stdout/stderr 都记录 |
上下文注入 |
偷偷注入 RAG 结果或内部指令,用户不知道 |
每一次注入都在事件流中有记录,标注来源 |
思维链 |
很多产品不展示,或者只展示「整理后」的版本 |
完整记录,原样展示 |
分叉/回放 |
不支持,或者只支持简单的「重新生成」 |
从任意点分叉、恢复、回放,轻量高效 |
可复现性 |
同一条指令多次运行结果可能不同且无法解释 |
相同事件流一定投射出相同消息历史,执行过程完全可复现 |
正如一位开发者在评论中所说:
「当你用 Claude Code 或 Cursor 遇到一个奇怪的 bug,你花了两个小时还搞不清它为什么要删掉你的重要文件时,你就会明白——能看到模型看到的一切,不是一个功能,这是基本人权。」
DeepSeek Harness 选择把一切都摊开给你看,而不是把用户当不需要知道细节的「傻瓜用户」。这种透明化的设计选择,本身就是对开发者用户最大的尊重。
理解了会话日志和 Trajectory,你就掌握了 dsh 调试和复盘能力的核心。下一章我们将介绍如何在 dsh 中配置和切换不同模型。
← 06 Agent 循环 | → 08 模型配置