note
DSH 轨迹视图学习笔记
拆解 DSH 轨迹视图的信息框架(Turn→Group→Record 三层、Request 档案)、记录设计背后的诊断动线,以及「收集了但没展示」的数据缺口——取证与判断的边界在哪里。
一、信息框架:一次 agent 运行被拆成了什么
这部分看的材料是 dsh-client-ui-trajectory 这个包:21 个类型定义文件,加一个 7371 行的编译产物(没压缩,能直接读),再对照着 GUI 里实际点了一遍。
Turn → Group → Record 三层
整个视图是一个三级结构,每层有明确的切分依据:
- Turn:按「用户开口」切。你发一条消息,到 agent 给出回答,中间发生的一切算一个 Turn。一个 Turn 里可能有几十次模型调用,但它们共享同一个边界。
- Group:按「模型请求」切,只有两种。Message 组装输入,你的消息和系统注入的上下文合并在一起;Step N 组装第 N 次模型请求的全部产物。两种组性质完全不同,一个纯展示,一个对应一次真实调用。
- Record:按「一件发生的事」切,界面上的一行,共 7 种类型。
一个 Step 组里的结构是固定的:
Step N
├─ requestOnly 分隔行(只有请求头,无可见内容)
├─ ASSISTANT 记录(模型这次响应的完整内容:文本 + 思考 + 工具调用指令原文)
└─ TOOL 记录 ×k(这次响应发起的每个工具调用各一条)
闭环是这样的:某次响应里没有工具调用,Turn 结束;有的话,工具结果填回上下文,触发 Step N+1。
Request 不是一层,是 Step 的心脏
这一点我读错过。原以为 Request 是 Turn 和 Group 中间的一层,实际它是贴在 Step 组第一条记录上的小标记,编号会话全局,比如 Request #42。点开是这次模型调用的完整档案:哪个 provider 哪个模型、请求配置、重试了几次、本次和累计的 token 用量(输入、缓存读、缓存写、输出、思考,五桶分开计)。
「这一步为什么慢、为什么贵」,答案都在这个档案里。
关键发现:每个 Step 都是全量重发
DSH 是无状态请求。每个 Step 发给模型的都是系统提示词加截止目前的全部历史,不是增量。也就是说 input token 随 Step 数近似平方地涨,而且放大器是巨型工具结果:一次读回几千行文件,这些内容进入历史后,之后每个 Step 都要为它们再付一次 input。
我自己那次调研会话,单个 Turn 跑了三十多个 Step,累加 input 812 万 token。真正的新信息很少,大头是同一个上下文被反复重发。
所以请求级的 usage 统计不是摆设。没有这两个数(用量和缓存命中),一次运行的成本结构根本无从谈起。
二、每种记录里放了什么,为什么这么放
先看呈现方式。每条记录的信息分两层给:行内一层,只放序号、类型徽章、一行摘要和耗时,供快速扫读;点开之后进检查器,放全文和指标,供细查。这个分层不新鲜,但 DSH 在「每层放什么」上的取舍值得细看。
检查器的 tab 不是固定的,是按记录类型分派的
这是读源码时第一个让我觉得「想过」的地方。检查器没有一套通用 tab,而是每种记录一组:
| 记录类型 | tab 组 |
|---|---|
| USER / CONTEXT / ASSISTANT | Summary / Preview / Raw / Source |
| TOOL / SUBTOOL | Summary / Payload / Result / Schema / Timing |
| COMPACTED | Summary / Raw Output |
| SYSTEM | System Prompt / Tools(有变更时多一个 Diff) |
为什么这么做,看 TOOL 的那组 tab 就明白了。
TOOL 的五个 tab 是一条完整的排查动线
Payload 是模型传给工具的参数原文,Result 是工具返回的完整内容,这两个好理解。值得注意的是另外两个。
Schema 放的是调用那一刻模型可见的工具定义。工具集和 schema 在会话中途是会变的(插件启停、权限变化),模型乱调工具的时候,不先核对它当时看到的定义,就只能猜。这个快照把「模型为什么这样调」从玄学变成了可查的事实。
Timing 除了起止和耗时,还有一行小字:Timing source: Session timestamps。它主动声明这个耗时是从会话时间戳推算的,不是专门插桩测的。进行中或缺数据的时候显示 Not available,而不是编一个数。这种「不虚构」的诚实在整个视图里是一贯的。
Summary 页本身也有讲究:除了状态,还有一行 Hierarchy,标明这条工具调用是哪个 ASSISTANT 响应发起的,点击直接跳过去。SUBTOOL(工具内部分派的子调用,比如 subagent 的活动)的 Hierarchy 会多一层父工具。从结果回溯决策,靠的就是这一行。
五个 tab 连起来读,其实是一串问题:谁决定调它 → 传了什么 → 它以为这工具是什么 → 拿回了什么 → 花了多久。检查器是按诊断动线组织的,不是按数据字段堆的。
ASSISTANT 记录:完整响应,外加一组性能指标
ASSISTANT 记录装的是模型这次响应的全部:文本正文、思考内容、工具调用指令原文(在 sourceBlocks 里按原顺序保留)。所以它行内显示「(tool call only)」时并不是空的,只是没写文本,指令原文在检查器里能看到。
除了内容,Summary 页还有一组别处没有的指标:TTFT(首 token 时间)、Generation(生成时长)、Throughput(吞吐 tok/s)。慢在等首 token 还是慢在解码,看一眼就知道。
顺带一提工具调用指令和 TOOL 记录的关系:指令原文在 ASSISTANT 里,执行和结果在 TOOL 里,两边用 callId 互链。同一件事记两份,各有用途。
USER / CONTEXT:多一个 Source tab
这两类记录的 tab 组里有个 Source,放的是这条内容的生产者信息。谁注入的,在排查上下文膨胀时是直接的归因入口。Source 只在有生产者信息时才出现,没有就少一个 tab,不占位置。
图片单独说。上传的图片被提取成独立的 block,协议白名单只放行 data:image、blob 和 http(s),渲染在 Preview tab 和 Summary 预览区;Raw tab 只有文本,看不到图。Raw 保真模型视角,Preview 管人读体验,两份呈现共用同一份数据。
SYSTEM 和 COMPACTED:管「模型看到的世界」
这两类跟事件流关系不大,管的是环境。
SYSTEM 记录带着完整的系统提示词和当时的工具目录快照,中途有变更就生成新记录,还能 Diff 前后版本。COMPACTED 记录带着上下文压缩后的摘要全文。压缩之后模型看到的世界就是这份摘要,不看它,模型后续的很多行为解释不了。
为什么这么放
把所有记录的字段摆在一起看,能归纳出三层逻辑:
- 行内负责定位:这是什么事件、干了什么、花了多久、错了没有,一行扫完
- 检查器内容区负责取证:Input/Output/Thinking 全部是无损原文,诊断不能建立在摘要上
- 指标区负责归因:TTFT 拆时间、token 五桶拆成本、Schema 和 Prompt 快照拆环境
三层各回答一类问题,互不越界。这是我觉得这套视图最值得学的地方:它不是「能展示的都展示了」,而是每个字段都对应一个具体的追问。
三、收集了但没展示的数据,以及为什么
这部分的找法很简单:把数据模型里有的字段列一遍,把界面上渲染的字段列一遍,取差集。差集比想象的大,而且方向很集中。
记录级:input 三桶被藏起来了
每条 ASSISTANT 记录的 cell 里其实存了完整的 token 五桶,包括 input、缓存读、缓存写。但记录检查器只渲染 output 侧(总输出、思考、正文)。想看这一步读了多少上下文,得跳一层,去点 Step 组首的 Request #N,进请求检查器的 Usage tab。
数据在,展示差一层。「这一步读了多少」恰恰是排查上下文膨胀时最高频的问题。
Turn 级:数据在,Turn 头却是空的
快照里有每个 Turn 的起止时间(turnTimings),但 Turn 头部只渲染了「Turn N」三个字。折叠后的摘要也只有「N steps · M tool calls」,没有耗时,没有 token。
「哪个 Turn 最贵、最慢」这个用户视角最自然的问题,在整个视图里没有入口。
会话级:有,但不在 Trajectory 里
会话级聚合不在 Trajectory 标签页,在 Chat 页输入框下方的一行统计条:轮数、步数、LLM 耗时、工具耗时、首 token 平均、吞吐、缓存命中率、输入输出总量。附近还有一个「上下文已用 X%」的占用指示,带系统提示词、工具、对话消息三段分解。
这行统计的数据源是全量投影,不受加载窗口限制,压缩重写历史也不丢数,比看上去周全。
真正缺的只剩一样:趋势。每个请求的用量都有,累计也有,但没有任何地方画得出「input 从哪一步开始抬头」。想找到失控的起点,只能逐个请求点开看。
一个写完但没交付的视角
时间轴的投影模式,代码里有四种:等宽、实际时长、完整墙钟,外加组合。但工具栏里「实际时间」开关的源码上写着 hidden: true,永远不可见。文案、状态、投影逻辑全都是写好的,就差一个开关。
也就是说「保留空闲间隙的墙钟视角」,也就是「用户实际等了多久」这个维度,是一个完成了但没交付的功能。为什么藏着,只能猜:空闲间隙的语义有争议,人盯着屏幕思考的那几分钟算不算耗时,大概他们自己也没想好。
窗口之外:什么都没有
选择、搜索、时间轴,全部只覆盖已加载的那段历史,README 里明说。更早的记录要手动往上翻页加载。跨会话的聚合则完全不存在,Host 侧有数据,没有任何视图去接。
为什么不展示
把缺口放在一起看,能看出来不是做不到,是有意不做:
- 数据全都采集了,缺的只是聚合和展示,工程量很小
- 完成度很高的功能(墙钟视角)被一行
hidden: true按住不放 - README 里有一节专门的「已知限制与暂缓事项」,措辞精确
我的解读:这个视图的定位是取证,不是判断。它把一次运行的证据收得很齐,但拒绝替你下任何结论。「这个 Turn 是否异常」「这次调用该不该发生」「token 从哪一步开始失控」,全是留给使用者的。
这个克制本身就值得记下来:一个观测工具该在哪里停下来,DSH 给的答案是停在证据上,一步都不多走。至于证据之后的那一步该不该有人做、怎么做,那是我自己项目要回答的问题,也是这篇笔记记到这里的原因。