DSH 为什么敢「万物皆插件」:会话日志是唯一真相

一套能稳定运行的插件化体系,不仅仅靠一套能替换的插件系统,更重要的是一个不动的真相源。DeepSeek Harness 用仅追加的 Session 事件日志,把可插拔建立在可重建之上。

最近在 DSH 上写一个工具,我要算 TTFT 和吞吐量。但是查看轨迹页面时发现,界面上首token时间不可用。

拉日志

第一反应是:这个数据是不是压根没记?那就把日志拉下来看。

日志是 JSONL,一行一个事件,带 seq / time / type / data。我要的东西在 assistant/message 事件的 data.stream 里。

但它不是我预期的那种「一行一个 token」的裸增量,而是压缩批

json 复制代码
{"type": "reasoning-chunks", "time0": 1787797260198, "index": 0,
 "dt": [147, 49, 1, 0, 0, ...],
 "texts": ["The", " user", " wants", " me", ...]}
  • texts 是逐 token 的文本
  • dt 是每个 token 相对上一个的时间增量
  • time0 是这一批的基准时间

写了个脚本,发现数据都在

time0 逐项累加 dt[i-1],就能还原出每个 token 的绝对时间;首个非空文本或推理成员的时间,就是首 token 时间。

跑完之后,得到一份真实会话的账单:

  • 175 个请求 ,TTFT 平均 3.47s(最小 0.69s,最大 17.86s)
  • 总生成 2795.2s ,输出 307,128 token ,平均吞吐 109.9 tok/s
  • 其中 207,949 token 是推理 ,占 68%

信息全都在日志里。 并不需要额外采集。

而且我注意到,推理占了输出的三分之二,这和我当前会话的思考深度有关。

但同一个界面里,有的能算,有的不能

数据既然在,那问题就变成了:谁没用上它。

继续往下看,轨迹面板的首 token 时间来自一个状态字段 firstTokenTime,而它唯一的赋值点 是处理实时增量块时的判断------只有块类型是 text-delta / reasoning-delta / tool-call-delta 时才会被设置:

ts 复制代码
// /packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts
...isTokenDelta(chunk) && state.firstTokenTime === undefined
  ? { firstTokenTime: time }
  : {}

而它消费的 chunk 来自客户端合成的 assistant/live-chunk 事件------这个事件只在流式进行中存在 。会话结算之后,往 firstTokenTime 赋值的那条实时路径断了。

这会导致现象:直播的时候正常,历史会看就是undefind

我找了个对照组:

消费者 是否解码压缩批 TTFT
dsh-session-stats ✅ 调 assistantStreamFirstTokenTime 正常
dsh-client-connection ✅ 本地 expandAssistantStream 正常
dsh-client-ui-trajectory ❌ 只认裸 delta 不可用

同一份日志,有的reader解码了,有的没解码。

我在Discussions看了下,之前的问题已经有人在讨论,分析也透彻,感兴趣的可以看下#6129,这篇不重复那些答案。

而且我翻阅了源码note,发现这是一次处于性能优化导致的缺口,那么为什么一次为性能做的优化,能让一个能力静默消失?

仓库里有一份 2026-09-06 的 Agent Note:Embedded Assistant stream consumers read compact records。它解释了这次改动的动机。

所有折叠嵌入流的消费者都先调 expandAssistantStream()(这是个会把整个压缩批完整物化成成员数组)。可消费者常常只需要小量数据(找首 token、找最后一个 usage、拼一段文本),却付出了 O(members) 的分配和时间:每个成员大约多两个对象。

改后的收益很可观:

阶段 改前 改后
首次打开 · 投影 28.0 ms 5.9 ms
首次打开 · 总计 76.9 ms 53.8 ms
首次打开 · 峰值内存 137.2 MB 94.6 MB

dsh-llm 直接支持从压缩记录回答问题,并造了一批提前退出 的记录级读取器------runFirstTokenTimeassistantStreamFirstTokenTimelastAssistantStreamChunk 等等。只取一个事实,不展开整个数组。

同一份 note 里还有一句记录:

Chat and Trajectory sections settled directly from it.

也就是说,Chat 和 Trajectory 改成直接从嵌入流结算,不再先物化后折叠。

所以首token时间的数据源没了,这也是为什么新版本出现了这个问题。

我翻了提交记录,commit 84c11c7243 有对测试的改动。

它把轨迹面板的一个断言,从「期望有值」改成了「期望为空」:

diff

scss 复制代码
- expect(finalNode?.timing?.firstTokenTime).toBe(1_999)
+   timing: { firstTokenTime: null },

同时新增了一个用例,名字里写了:

sql 复制代码
uses live Assistant deltas without replaying settled embedded streams

这意味着:

  1. 行为变化是有意的,「不重放已结算的流」是这次提交的目标,这是有实质收益。
  2. 空值被编码成了期望,测试不再报警。至于产品上显示「不可用」是未被识别的副作用,还是被接受的取舍,单看 diff 推不出来。

这才是这类问题最难被发现的地方:当测试跟着实现一起改,测试用例不能保证用户可见的结果。

最后,落到 DSH 的设计思路

这不是一次孤立的疏忽,而是一套设计取向的必然副产品。

第一,解码被定义成「读者的责任」。 Agent Note Session format v2 里写着:

Ordinary Session restoration validates the settlement fields needed by the runtime without expanding every historical stream; consumers that expand a compact stream validate its records when they read it.

这条设计的合理性很明显:如果恢复会话时展开所有历史流,成本会随会话长度线性增长;而绝大多数时候,打开一个会话根本用不到每一段流。所以「谁要读,谁自己解码」是省资源的选择。

第二,时间信息原本有一个更持久的归属,但被推迟了。 2026-08-04 那份(现已归档的)Note Web turn and window latency/throughput metrics 里,有一个被推迟的方案:

A durable session projection (token-meter shape) ... would survive compaction and window paging and cover the whole log. Deferred, not rejected.

一个持久化投影,能让时序活得比加载窗口久、覆盖整条日志。它被推迟了,理由是投影状态必须保持 O(1)------存均值,而不是分位数。

于是现在的时序,完全挂在节点上的 firstTokenTime 上。而节点,是实时路径的产物。

第三,规则被集中了,谁必须调用这些规则,仍然没人强制。之前依赖的部分没有被完全收束。 同一份 09-06 的 note 里还有这句:

The token, visibility, and visible-text rules have one home in dsh-llm, so a record reader and the accumulator's packing rules cannot drift apart.

「打包规则」和「读取规则」被放进了同一个归属,这防止了它们互相漂移。

DSH 的设计

我能说的是这个架构层面的观察:

可插拔建立在可重建之上。但「可重建」不是一个保证,是一条纪律。

它由每一个消费者各自履行,而没有任何机制强制。当一次优化让某个消费者不再履行,必然会出现副作用,但这套纪律正是保证这套插件系统稳定运行的基础。

这不是 DSH 独有的问题。任何把「状态从日志推出来」当基础的体系,都会长成这个样子:能力分散在每个消费者手里,而它们的义务已经写在文档里,需要自己遵守。

就这一次缺口而言,结算时补一次回退解码就够------#6129 已经给出了修法。但它修的是「这个读者忘了履行纪律」。若要让时序指标活得比加载窗口久、不跟着某个 UI 折叠一起漂移,归属还得回到那份被推迟的持久化投影。

这次的遇到的bug,本身也是这个理念一次侧面证明。

排查时顺着 Agent Note 往下读,很像在和开发团队隔空对话:动机、取舍、推迟项都写在仓库里,谁都能看见。这种「取舍可被后来者读懂」的体验,大概就是我喜欢 DSH 的原因。

相关推荐
weixin_416660073 小时前
DeepSeek、豆包生成的表格怎么导出Excel,避免内容挤在一列?
word·豆包·deepseek
舒灿4 小时前
全网都在测鹈鹕,我用 DeepSeek-V4.1-Flash 跑了50+小时、超5000MTokens消耗的真实开发任务
chatgpt·ai编程·deepseek
程序员老赵4 小时前
Docker 部署 DeepSeek Harness:轻松搭建本地 AI Agent 运行时平台
docker·agent·deepseek
冬奇Lab15 小时前
DeepSeek Harness 系列(06):System Prompt 组装——动态提示词的工程实现
人工智能·deepseek
蔡俊锋18 小时前
DeepSeek 语音对话灰度上线:四种音色背后的端侧 AI 交互架构与商业逻辑
架构·大模型·语音交互·deepseek·端侧ai
大模型真好玩19 小时前
DeepSeek Harness 入门很简单(四)——DeepSeek Harness接入插件
人工智能·agent·deepseek
JudithHuang20 小时前
Claude 桌面端入门:Claude Desktop + cc-switch + DeepSeek
claude·deepseek
Rocky Ding*1 天前
【三年面试五年模拟】2026-09-08 DeepSeek AI Agent开发岗社招一面:20道问题与系统设计题全解析
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·deepseek