第一次打开 DeepSeek Harness 的轨迹界面(Trajectory),我想到的是“Agent 的工作录像”:这一轮模型看到了什么、调用了哪个工具、工具返回了什么、子任务如何展开,似乎都能沿时间线继续检查。

但真正值得拆解的不是这张表格,而是它下面的数据结构。轨迹界面(Trajectory)不是在 Agent 跑完以后临时拼出来的一份调试报告。它建立在同一份会话事件日志上;聊天、模型上下文、统计、恢复和分叉,也都从这份日志派生。

本文的核心判断

DeepSeek Harness 最有价值的技术选择,不是“展示更多过程”,而是先把 Agent 运行定义成可追加、可派生、可回放的事件事实,再让不同界面按自己的目的读取它。

这篇文章不做模型能力或代码质量测评,只回答一个更具体的问题:DSH 如何把一次 Agent 运行,变成可以观察、恢复、分叉和重新解释的事件流?

先把后面会反复出现的术语翻译成一条更直观的链路:

中文对象源码术语它解决的问题
运行事实Session Event这次 Agent 实际发生了什么
模型可见历史Surface下一次模型调用应该看到哪些消息
面向不同用途的派生视图Projection聊天、统计和调试界面分别怎样读取同一份事实
轨迹检查界面Trajectory开发者怎样按轮次、步骤和工具调用下钻
恢复与分叉Resume / Fork怎样从稳定历史继续,或创建另一条运行分支

一、会话事件日志:不是聊天记录,而是唯一真源

在 DSH 当前的会话模型里,Session 是由类型化 SessionEvent 组成的仅追加日志。LLM 消息历史并不单独保存,而是从日志派生;恢复或回放也不是读取另一份“对话快照”,而是再次折叠同一组事件。

这和普通聊天产品的直觉不一样。普通聊天记录往往只关心“用户说了什么、助手回答了什么”;Agent Runtime 还必须记录一轮执行如何展开:

事件族记录的事实为什么重要
turn/start / turn/end一轮请求从何处开始、因何结束区分完成、取消、阻塞、错误、截断和崩溃中断
step/start / step/end一次模型调用及其请求的工具执行把长任务拆成可定位的运行单元
user/message用户输入或被注入的上下文保留“这段信息从哪里进入模型”的来源差异
assistant/chunk / assistant/message流式分片与组装后的最终消息同时支持流式回放与稳定的消息历史
tool/call / tool/result工具名、原始参数、结果、错误与展示元数据把一次动作和它的结果可靠配对
request/header模型配置、系统提示词和工具 schema让一次模型请求具备可重建条件

把它简化成一段事件流,大致是这样:

seq 41  turn/start        { turn: 3 }
seq 42  user/message      { source: "user", content: [...] }
seq 43  step/start        { turn: 3, step: 1 }
seq 44  request/header    { model, system, tools }
seq 45  assistant/chunk   { ... }
seq 46  tool/call         { callId: "c1", name: "search", arguments: "..." }
seq 47  tool/result       { callId: "c1", message: [...] }
seq 48  assistant/message { message: [...], usage: {...} }
seq 49  step/end          { turn: 3, step: 1 }
seq 50  turn/end          { turn: 3, reason: "completed" }

这里的 seq 是会话内连续递增的位置。事件数据必须能无损序列化为 JSON,连流式 chunk 也不能在规范日志里随意过滤。它带来的直接好处是:持久化层拿到的不是一份“总结过的运行结果”,而是可以原样重建的事实序列。

这也是为什么“展示一条 Trace”和“拥有事件溯源的 Session”不是一回事。前者可能只是事后写入的观测日志;后者直接成为系统状态的来源。

二、轮次、步骤与事件:三层粒度分别解决什么

DSH 把执行边界拆成三层。一个轮次(Turn)包围一次用户请求驱动的模型循环;一个轮次可以包含多个步骤(Step);一个步骤对应一次模型调用,以及该模型调用所请求的工具执行;最细的事实再落成事件(Event)

flowchart LR
    T["第 3 轮"] --> S1["步骤 1 · 模型调用"]
    S1 --> C1["tool/call"]
    C1 --> R1["tool/result"]
    R1 --> S2["步骤 2 · 再次模型调用"]
    S2 --> A["assistant/message"]
    A --> E["turn/end"]

这三个粒度让不同问题有了稳定坐标:

  • 用户说“这一轮为什么失败”,可以先看轮次的结束原因。
  • 模型为什么在工具返回后改变判断,可以定位到前后两个步骤。
  • 具体哪次参数错误、哪个工具被拒绝,则继续下钻到单条事件。

turn/end 也不是简单的 success / failure。当前事件词汇会区分完成、取消、阻塞、错误、达到输出上限,以及持久化层在崩溃恢复时补写的 interrupted。这个细节很重要:恢复系统不能把“进程没了”伪装成“任务完成了”。

三、模型可见历史:日志很完整,但模型不该看到全部日志

事件日志是唯一真源,不代表其中每条记录都要进入下一次模型上下文。DSH 为此引入了模型可见历史(Surface):从完整日志里派生出当前模型应该看到的有序消息。

核心事件中,只有三类会生成模型消息:

  • user/message:用户输入或合成注入的 user-role 上下文。
  • assistant/message:一个 Step 组装完成后的助手消息。
  • tool/result:作为后续模型输入的工具结果。

assistant/chunk、Turn / Step 边界、请求头、统计与其他插件事件仍保留在日志中,但不会直接变成消息历史。换句话说,DSH 同时保留了两种不同的完整性:

日志完整性回答“系统实际发生过什么”;上下文完整性回答“下一次模型调用需要看到什么”。两者不能混为一谈。

Surface 还支持两种进入方式:正常的 append,以及对既有消息区间执行 replace。后者可以被上下文压缩使用:压缩摘要替换模型可见面上的一段旧历史,但旧事件并没有从日志里消失。

flowchart TB
    L["仅追加的会话事件日志"] --> O1["原始消息与工具结果仍保留"]
    L --> S["折叠出模型可见历史"]
    S --> N1["当前消息"]
    S --> N2["压缩摘要 · replace(range)"]
    S --> N3["最新工具结果"]
    N1 --> M["下一次 Model Context"]
    N2 --> M
    N3 --> M

这解决了长任务里的一个关键矛盾:为了继续运行,模型上下文可能必须压缩;为了调试、审计和重放,系统又不能把压缩前的事实抹掉。模型可见历史负责“现在怎么读”,事件日志负责“当时发生过什么”。

四、派生视图:轨迹界面不是另一份真相

如果会话事件是统一的写入事实,那么聊天、轨迹、统计和任务状态就是不同的派生视图(Projection)。DSH 的会话投影机制把这种关系做得很明确:领域插件注册纯同步的 init → apply(event) → view 单元,框架把每个已提交事件折叠进去,再向客户端提供当前完整值。

state0 = init()
state1 = apply(state0, event[0])
state2 = apply(state1, event[1])
...
clientView = view(stateN)

同一个 Session 因此可以有多种视图,而不需要每个 UI 自己解析原始事件:

flowchart LR
    E["会话事件流"] --> P1["模型消息历史"]
    E --> P2["对话视图"]
    E --> P3["轨迹检查视图"]
    E --> P4["待办 / 统计 / 遥测视图"]
    P1 --> M["Model"]
    P2 --> U1["聊天界面"]
    P3 --> U2["开发者轨迹界面"]
    P4 --> U3["状态与可观测系统"]

投影快照还带有统一的 asOfSeq 水位线,说明所有值共同反映到哪一条事件。对前端来说,这比“分别请求消息、统计、状态,然后祈祷它们碰巧来自同一时刻”可靠得多。

当前 Trajectory 界面也并非把 JSONL 原样倾倒出来。它按 Turn 组织记录,支持选择用户、助手、工具和嵌套子工具;局部检查器再展开 token、耗时、输入、输出和计时。长会话会从尾部打开,向上按页加载更早历史,只挂载当前可见窗口;用户向上检查旧记录时,自动跟随尾部会暂停,避免新事件把视线抢走。

所以,更准确的说法不是“DeepSeek 把后台日志放到了前台”,而是:它把事件流投影成了一种面向 Harness 开发者的检查界面。

五、恢复与分叉:为什么能共用同一套底座

当历史是连续事件流时,恢复和分叉就可以退化成两个清晰动作:选择一段稳定前缀,把它作为新 Session 的 seed,再从边界之后继续追加事件。

DSH 的普通 Session fork 会选择到某个事件序号为止的前缀,并要求边界不能停在一个尚未闭合的 Turn 内。这项约束并不花哨,却决定了分叉后拿到的是一致状态,而不是一半工具调用、一半模型回复的损坏现场。

带 seed 的 Session 会写入 session/end-seed 边界,用来区分:

  • 哪些事件来自恢复、fork 或 replay 继承的历史;
  • 哪些事件是当前生命周期真正新写入的工作。

再加上日志里的 request/header——模型、调用配置、系统提示词和工具 schema——一次请求就不只是“有答案”,而是尽量保留了重建输入所需的运行条件。

flowchart LR
    A["Parent Session\nseq 0…120"] -->|"选择稳定边界 96"| B["Seed\nseq 0…96"]
    B --> C["session/end-seed"]
    C --> D["Child Session\n继续追加 98…"]
    A -->|"从尾部恢复"| E["恢复后的会话"]

当然,“可重建”不等于“确定性复现”。外部 API、文件系统、模型输出和时间都会变化。事件溯源保证的是输入、边界与既有结果可追溯;如果要做严格 Replay,还需要对外部副作用、工具响应和模型采样另行控制。

六、「一切皆插件」与事件流为什么必须一起看

DSH 的另一个显眼口号是 Everything is a Plugin。单独看,它很容易被理解成“组件都能替换”。真正有工程价值的地方在于:插件不只提供能力,也能扩展事件词汇和投影。

例如,压缩机制可以贡献 compaction/startcompaction/summarycompaction/end;Hook 桥接可以记录 hook/invokedhook/result。这些插件事件保留在日志中,但不必进入模型 Surface。插件还可以注册自己的投影,把事件折叠成客户端需要的领域状态。

这形成了一条更完整的替换链:

  1. 替换一个 Loop、工具、压缩策略或 UI 插件。
  2. 插件继续把自己的运行事实写入统一会话日志。
  3. Trajectory、Telemetry 或领域投影用同一序号空间观察变化。
  4. 比较替换前后的失败位置、耗时、调用路径和最终结果。

只有插件化,很难证明替换后的系统究竟发生了什么;只有轨迹,又很难把发现的问题变成可替换的工程边界。DSH 把“可组合”和“可观察”放在同一套运行结构里,这才是它比一张漂亮 Trace 更值得研究的地方。

它对未知事件也采取了偏保守的策略:纯信息记录可以显式标为可忽略;遇到无法识别、且可能影响重建语义的必需事件,读取方应拒绝重建,而不是悄悄跳过。这种选择牺牲了一点兼容便利,换来的是不会在缺失关键状态时假装恢复成功。

七、从技术可观测性到产品可理解性,还差一次翻译

到这里可以看见,DSH 的“透明”至少包含四个层次:

层次DSH 提供的基础它支持的判断
事实连续、无损、可扩展的 Session Event当时发生了什么
结构Turn / Step / callId / source / seq事件属于哪轮、哪步、哪次调用
视图模型可见历史与多种派生视图模型、聊天界面或调试界面分别该读什么
操作恢复、分叉、分页检查与时间线定位从哪里继续、在哪里比较、如何下钻

但这些仍主要是 Harness 开发者的语言。普通任务用户未必需要知道 step 4 发出了两个 Tool Call;他可能只需要知道“正在核对两份来源,其中一份访问失败,结论暂时降级”。

因此我更倾向于把 Agent 可见性分成三层:

  • 任务层:目标、阶段、阻塞、当前产出和下一步动作,默认可见。
  • 证据层:来源、文件、关键依据、检查结果与不确定性,按需展开。
  • 运行层:Tool Call、Context、Session Event、完整 Trace、重试与回放,服务开发、评测、审计和排障。

这三层不是三份数据。更好的实现是从同一事件事实源生成不同语义密度的投影。DSH 已经把底层条件铺得很完整,真正的产品工作,是继续缩短 Runtime 状态与用户判断之间的距离。

八、这套架构的代价与边界

事件溯源并不是免费的午餐。

  • 日志会持续增长。长 Session 需要分页、虚拟化、压缩编码和投影缓存,DSH 当前代码也已经在处理这些问题。
  • Schema 演进更谨慎。事件一旦进入持久化格式,修改字段与重建语义就可能成为破坏性变更。
  • Trace 可能包含敏感内容。模型输入、工具参数、文件差异和结果元数据不能因为“方便调试”就默认永久保存或对所有角色开放。
  • 可观察不等于可评测。有完整事件流,只是具备归因素材;是否完成任务、证据是否充分,仍需要 Eval 与业务标准。
  • 运行记录不等于模型思维链。产品可以展示可核验的输入、动作、结果和状态变化,不应把隐藏推理过程当作透明度目标。

而且 DSH 目前仍处于 developer preview,官方明确提醒未来会有破坏兼容性的变化。本文描述的是 2026 年 8 月 17 日核对到的主分支设计,不应被当作稳定 API 合约。

九、如果你想亲手观察这条事件流

官方当前提供的最短启动方式是:

npx @deepseek-ai/dsh web

默认 Web UI 地址是 http://127.0.0.1:3080。进入一次真实会话后,不要先盯着最终答案,可以沿 Trajectory 依次检查:

  1. 一个 Turn 内为什么出现多个 Step。
  2. 上下文注入与用户原始输入如何区分来源。
  3. tool/calltool/result 如何配对。
  4. 模型输入、工具结果、耗时与 token 分别在哪一层展开。
  5. 恢复或分叉后,继承历史和新运行的边界在哪里。

如果只把它当成一款 Coding Agent,最有意思的部分反而会被错过。DSH 更像一个用真实产品界面暴露 Agent Runtime 结构的开发者预览。

结语:先保存事实,再决定让谁看见什么

DSH 给我的最大启发,不是“Agent 应该展示全部过程”。恰恰相反:正因为底层事件保存得足够完整,上层才有条件克制地展示。

完整日志负责追溯,模型可见历史负责下一次模型上下文,派生视图负责不同产品界面,轨迹视图负责专业检查,任务界面则应该继续翻译成用户能采取行动的语言。

Agent 的可见性,不是把后台搬到前台,而是让同一份运行事实,在正确的时刻帮助不同的人做出正确判断。

参考资料