前言¶
在 DSH 里调智能体,最常遇到的一类问题是:模型这一轮到底发了什么出去。上下文里被塞进了一堆自动注入的信息,工具调用来回追加,多步之后请求已经和用户输入差得很远。想排查「为什么上下文涨这么快」「这个参数是谁设的」,通常只能去日志里翻事件记录,过程既慢也容易漏。
下面介绍一个插件 shengpj/dsh-payload。它的做法是直接读客户端已经持有的会话日志,离线重建出「产生某条回复的那次调用」实际发给模型的完整请求,在界面上一步就能看到,不重发请求、不改服务端。插件分类是「模型推理」,目录页显示 GitHub 星标 1,维护者 shengpj,当前版本 1.9.5(出自仓库 package.json),许可证 MIT。
这是什么¶
一句话定位:一个纯客户端的 DSH Web GUI 插件,给每条 assistant 回复配上一个「查看请求 JSON」的入口,弹框里展示这次调用的 model、调用参数、messages、tools、token 拆解和上下文来源占比。
它和「看日志」的区别是:日志是事件流,需要自己按 step/start、step/end 去拼;这个插件按调用边界把事件折叠成一份请求快照,并且把本轮新增和历史分开,直接回答「这次多发了什么」。
入口位置:每条 assistant 回复下方的操作行里(和「复制」「好的回答」同一排)多一个 { } 图标按钮。注意只有已定稿的回复才有这个操作行,正在流式输出的那条没有。
核心功能¶
精确到某一条回复。 每条 assistant 消息的按钮指向产生它的那次调用,而不是「这一轮的最后一次」。一轮里有四十次调用时,也能找到中间某条回复对应的那一次。
按调用分组,一套入口。 一个 step 就是一次调用,一次调用一份快照。不管一次还是四十次调用,切换入口都只有左侧 step 列表,每行显示 #k、token 和 step n · m 条消息,右侧 JSON 占满剩余宽度。界面形状不随调用次数变,不存在「少的时候用卡片、多的时候换列表」两套心智。
本轮与历史分开。 messages 里本轮(这条回复所属的 Turn)新产生的消息默认展开,靠一条品牌色竖线标出;更早的历史合并成 历史消息 (n) 一行折叠,数组标题上同时给出 本轮 n / 历史 n 两个计数。折叠只是显示层,下标仍是数组里的真实下标,复制出来的 JSON 一字不差。
新增消息就地标记。 同一轮里第 2 次及以后的调用,messages 中「这次才出现」的消息直接在 JSON 里标出来:左侧换成绿色实线轨道,行底色铺一层绿,行首挂「新增」胶囊,新增段之前有一条虚线分隔,写着「上一次调用已有 1933 条 · 以下为本次新增 18 条」。判定方式是拿两次调用的消息数组做前缀比较(prefix 之外的即新增),所以「内容相同但顺序挪了」不会被误判成新增。发生变化的事实用胶囊标在消息条数旁边:新增 18、消失 4(有消息消失,通常意味着发生了压缩)、原地改写、参数变化。一轮的第一次调用没有标记,因为「上一次调用」还不存在。
工具调用卡片。 tool_calls 数组里的每条调用行显示 工具名(参数摘要) 加状态徽标(成功 · 21ms / 失败 · 1.3s / 未返回),悬停看原始参数 JSON,点徽标展开完整的参数与结果正文。状态、耗时、结果字节都来自本轮日志里配对的 tool/call + tool/result,不是解析参数猜的。本次请求里出现但本轮日志没有记录的调用(历史被截断时会有)降级成 未返回,不会凭空消失。
上下文来源占比。 弹框底部把 messages 按来源分成五类并给出条数与占比:你的输入、harness 自动注入(时间上下文、运行时上下文、技能目录、系统提示、后台任务回报等)、模型回复、工具往返。点某一类才展开它由哪些小类组成。旁边标出「本轮新增」占多少,用来回答「上下文为什么涨这么快」。占比按消息字节算,不是 token。
本轮时间线。 一轮有多步时,头部多一条时间线,每步一段,宽度按该步实际消耗的 token(输入 + 缓存 + 输出)算,点哪段切到哪次调用,右端是这一轮的总 token。它和 step 列表分工不重复:列表回答「选第几次调用」,时间线只回答「占比」。
请求与响应分离。 右侧 JSON 区右上角有 请求 | 响应 切换。响应侧同样可折叠、可复制,包含 结束原因(要求调用工具 / 正常结束 / 失败 / 被中断 等)、失败重试的每一次尝试(code + status + message,成功的那次不计入重试)、首字节 / 总时长 / 输出字符 / 推理字符 / 响应体 字节,以及按 OpenAI chat-completion 形态重建的响应体(choices[0].message + finish_reason + usage,带推理内容时额外给出 reasoning_content)。这一步彻底失败、没有任何回复时,响应侧直接写「日志里没有这次调用的回复记录」,并列出失败尝试,不伪造一份空回复;此时 响应 按钮变灰。
JSON 与对话互链。 messages 里每条消息的行上常驻一个 对话 ↗ 标签,点它关掉弹框并把对话滚动到这条消息自己的气泡(精确到 data-chat-node-key,气泡在折叠的轮次里没挂载时退回滚到该轮 data-chat-turn)。反方向同理:从某条回复的按钮打开弹框时,那份 JSON 会直接停在这条回复对应的那一行并高亮,不用在几百条消息里找。
复制与导航。 复制当前那一次调用,或「复制全部调用」导出成请求数组。JSON 面板右侧是常驻加宽的滚动条,头部另有「↑ 顶部 / ↓ 末尾」按钮,切换调用或展开收起后自动回到顶部。Esc 或点击遮罩关闭弹框,弹框高度固定 88vh,只有 JSON 区自己滚动。
安装与启用¶
插件是一个 Cordis bundle(cordis.patch.yml + 一个纯客户端 client.js),装进 DSH profile 后刷新界面即可,不需要重装服务端。
方式一(从 GitHub 安装,让 DSH 里的 agent 执行,或自己调 plugin_manager 的 install_bundle):
git+https://github.com/shengpj/dsh-payload.git#<commit>
<commit> 用 40 位 SHA,当前值用下面这条命令取:
git ls-remote https://github.com/shengpj/dsh-payload.git refs/heads/main
方式二(走插件市场索引)。仓库里带了一份 market/index.json(DSH 插件市场 format 1)。启用 plugin-market 后把这个地址加成索引:
https://raw.githubusercontent.com/shengpj/dsh-payload/main/market/index.json
之后在 Discover 里搜 dsh-payload 就能一键安装。索引里的版本指向 commit-pinned 的 git 源,装到的是仓库里对应的那一版。发新版本时用 node tools/build-market-index.mjs --commit <40 位 SHA> --version <新版本号> 往索引里加一条,旧版本条目会保留。
方式三(改代码时更方便):把仓库拉到本地,然后用 link:<绝对路径> 安装这个目录,后续改动不需要重装。三者装好后的效果一致——在 profile 的 bundles 列表里能看到 dsh-payload,刷新界面(桌面端 Ctrl+R,改不动就退出重开),回复下方就出现图标。
典型用法¶
1、找到某条 assistant 回复,把鼠标移到操作行的 { } 上,气泡显示「查看请求 JSON」。
2、点击,弹框打开并直接停在产生这条回复的那次调用上。
3、messages 默认只展开本轮新产生的消息,更早的历史收在 历史消息 (n) 这一行里,数组标题上的 本轮 n / 历史 n 告诉你各有多少条。
4、若这条回复所在的 Turn 调用了很多次,用左侧 step 列表切换 step。右侧 JSON 右上角是 请求 | 响应:前者是发出去的东西,后者是收回了什么。
5、tool_calls 里的行是工具调用卡片:徽标给出成败与往返耗时,悬停看原始参数,点徽标展开参数与结果正文。
6、「复制」拿走当前这一屏的内容(请求侧是请求,响应侧是重建出来的响应体),「复制全部调用」拿走整轮所有请求组成的数组。
7、想知道模型回了什么,点 响应:结束原因 说明它为什么停下,重试 列出这一步失败过的尝试,下面是重建后的 chat-completion JSON。
数据来源与 token 口径¶
插件不重发请求、不新增服务端接口,完全离线重建,数据全部来自 Client 已经持有的会话日志:
| 需要的字段 | 日志来源 |
|---|---|
| 调用边界(每次调用一条) | step/start + step/end 的 {turn, step} |
model、provider |
该 step 的 assistant/message.data.message.source,缺失时退回 request/header 的 header.config |
| temperature / maxTokens 等调用参数 | request/header 的 header.config,稀疏,需向前继承 |
tools |
request/header 的 header.tools,同样需向前继承 |
messages |
该 step 首个响应事件之前的所有 surface 事件 |
| 本轮 / 历史的切分 | 这条回复所属 Turn 的 turn/start.seq |
| token 用量 | 该 step 的 assistant/message.data.usage |
| 工具调用的状态/耗时/结果 | tool/call 与 tool/result 按 callId 配对 |
request/header 只在调用配置发生变化时才写一条(reason 为 initial / change / resume / series),不是每次调用都有,所以分组锚点用 step/start,配置从不超过该 step 的最近一条 header 继承。
token 拆解的字段口径如下,全部来自 provider 上报的 assistant/message.data.usage:
| 字段 | 含义 |
|---|---|
inputTokens |
本次实际发出的 prompt tokens 中未命中缓存的部分 |
cacheReadTokens |
命中 prompt cache 的 tokens |
outputTokens |
模型生成的 tokens |
totalTokens |
provider 的合计,等于 inputTokens + cacheReadTokens + outputTokens |
命中率按 cacheReadTokens / (inputTokens + cacheReadTokens) 计算。三个容易混的量在 README 里被明确区分:「输入」chip 是未命中缓存的 prompt tokens,不是 JSON 大小;「缓存」chip 是命中缓存的部分,输入加缓存才是完整 prompt;「N 条消息」「请求体 X MB」是消息条数与重建 JSON 的字节数,字节不等于 token。
README 还给了一个实测差异的例子:某次调用 input 1009 / cache 80,640 / output 1,425 / total 83,074,而日志重建出 1078 条消息、3.70 MB、约 754k tokens,provider 只按 81,649 prompt tokens 计费,差 9.2 倍。原因是请求发出前会先经上下文压缩把旧区间折叠成摘要,日志保留原文、模型只读摘要。因此 token 一律以 assistant/message.data.usage 为准,条数和字节只作「这份快照有多大」的参考。相邻两次调用有时只差 2 条,是因为一步的产物正好是「一条 assistant 消息(带 tool_calls)+ 一条对应的 tool 结果」。
适用场景与注意¶
适合在 DSH 图形界面里做智能体行为的事后排查:上下文为什么涨、哪一步花掉了最多的 token、工具调用是成功还是失败、模型为什么结束、参数是哪次调用变掉的。它不是一个跨端的日志工具。
几点需要注意:
- 只支持 DSH 的图形界面客户端,TUI/CLI 下没有入口。插件是纯客户端实现,没有 Host 半边的服务注册。
- 插件以当前 DSH 进程的权限运行,安装前应当检查源码与许可证(MIT,见仓库
LICENSE)。 - 首次打开某个 Turn 时需要把会话窗口往前翻完(
loadOlder()),翻页次数和会话长度成正比,同一 Turn 的结果会缓存,再开很快。重建本身只折叠一次窗口,170 次调用的 Turn 在 Node 里约 100 ms。 - provider 没上报
usage的调用会显示「未记录到用量」,其余部分照常展示。 - 已验证环境是 DSH
0.2.1-rc.3桌面端。
结尾¶
dsh-payload 的价值在于把「这次请求到底是什么」从一个要翻日志拼答案的问题,变成一个弹框点开就有的问题。如果你在 DSH 里做智能体调试,值得装上试一轮长对话,重点看上下文来源占比和本轮时间线这两块。
- 社区目录页:https://www.skillhub.cn/plugins/shengpj/dsh-payload
- 仓库:https://github.com/shengpj/dsh-payload