如果只是把 DeepSeek Harness 当成一个现成的 AI 编程工具来用,其实有点可惜。DSH 更有意思的地方,在于它本身是一套 Harness。
模型只是其中的一环,在它外围还有 Prompt、Context、Tool、Session、工作区、插件、工作流以及各种扩展机制。也正因为如此,它拥有很高的 DIY 空间。
对于一些比较固定的个人开发流程来说,如果把模型、工具、上下文和扩展能力搭配得足够合适,最终得到的体验未必会逊色于一些封装成熟的通用 Agent,甚至可能更贴合自己的工作习惯。
于是我产生了一个想法:与其单纯阅读 DeepSeek Harness 的源码,不如真正给它做一个插件。
但问题也随之而来。我对 DSH 内部架构并不熟悉,不知道一个插件应该放在哪里,也不清楚它的数据从哪里获取、页面怎么注册、前后端如何通信。
那能不能换一种方式?
让 Agent 帮我阅读源码、寻找扩展点、完成开发,而我负责提出需求、判断方案、处理问题和最终验收。
这篇文章记录的,就是这样一次真实的 Harness DIY。
一、先看成果:Session Insights
Session Insights 是一个会话统计页面,它给每个 Session 增加了一个新的「会话统计」标签,用来集中查看 Agent 在当前会话中的运行情况。

页面目前可以统计用户对话轮次、模型调用次数和失败次数、Tool 调用总数及成功失败情况、当前模型、Session 总耗时、模型与 Tool 平均耗时,以及 Input、Output、Cache Read、Cache Write Token。下方还会按照调用次数对 Tool 进行排行。
每一个 Tool 都可以继续展开,查看自己的调用次数、失败次数、成功率、累计耗时和平均耗时。

除此之外,页面还可以将当前统计结果直接导出成 Markdown 报告。
功能本身其实并不复杂,但做完以后我反而觉得,它非常适合作为第一个 DSH 插件。因为一个看起来只是“增加统计页面”的功能,背后刚好会经过 Client Package、Slot、SessionEvent、Projection、前后端数据传输、Bundle 和测试等一整条扩展链路。
换句话说,它不算难,但足够让我们顺着一个真实功能去理解 DSH 的插件体系。Session Insights 本身也是一个只读消费者,不会额外调用模型,也不会向 Session 中产生新的事件。
二、Session Insights 到底是怎么工作的?
整个插件最核心的数据流,其实可以压缩成下面这一条:

理解这条链,基本就理解了这个插件的大部分实现。
数据从哪里来?
DSH 在 Agent 工作过程中,本来就会不断记录 SessionEvent,例如 step/start、step/end、llm/retry、tool/call、tool/result、assistant/message 等。
这些事件已经足够回答很多问题:模型什么时候被调用、是否发生过重试、Tool 调用了多少次、调用是否成功、每一步花了多长时间。
所以 Session Insights 并没有重新设计一套日志系统,而是直接建立在 DSH 已经存在的 SessionEvent 之上。
为什么不直接统计「轨迹」?
刚开始很容易想到一个更简单的方案:轨迹里不是已经包含各种调用信息了吗?直接让前端把轨迹扫一遍,然后统计不就可以了?
问题在于,轨迹页面通常只持有当前已经加载的窗口,历史数据还可能分页。假设当前只加载了一部分记录,Tool Calls 显示 80 次;等继续向前加载历史以后,突然又变成 154 次,这显然不适合作为“整个 Session”的统计结果。
因此这里用到了 DSH 已经存在的一个重要机制:
Projection。
Projection 可以简单理解为:Host 持续读取 SessionEvent,并将这些事件折叠成一个不断更新的状态。
例如 step/start 到来时增加模型调用次数,llm/retry 到来时记录失败,tool/call 记录一次 Tool 调用,而 tool/result 再结算成功、失败以及耗时。
这样一来,统计结果与前端加载了多少轨迹无关,而是始终面向整个 Session。
Token 也采用同样的思路。DSH 原本已经有 tokenUsage Projection,因此这次并没有自己重新解析 Token,而是直接复用已有结果。
Host 的数据怎么到浏览器?
Projection 在 Host 侧计算完成后,DSH 已经有一套通用的 Session Projection Transport,可以把结果同步给 Web Client。
Transport 本身并不知道什么是模型调用次数、Tool 排行或者 Cache Token,它只负责传递 key、value、seq 这样的 Projection 数据。初始进入 Session 时会得到一份基线,之后运行过程中再通过实时推送持续更新。
因此到了 React 页面这一层,事情反而变得很简单:
const sessionStats = useProjection('sessionStats')const tokenUsage = useProjection('tokenUsage')
插件不需要自己监听全部 SessionEvent,也不需要重新做一套 Client Store 或自定义 RPC,而是直接订阅 DSH 原本已经提供的数据能力。
为什么中间还需要一个 InsightsModel?
即使已经拿到了 Projection,也不建议直接把所有底层字段塞进 React 页面。
因此 Session Insights 中间又增加了一层 InsightsModel,负责把 Projection 和必要的 trajectory fallback 整理成页面真正需要的展示数据,例如 Tool 排名、成功率、平均耗时、时间格式化,以及运行中的 Session 如何持续更新总耗时。
最后 SessionInsightsView 和 Markdown Report 都消费同一个 InsightsModel。
这个设计很简单,但很实用:页面显示的数据和导出的 Markdown 使用的是同一套统计逻辑。
三、如果从零开始,这个插件应该怎么做?
实际做完以后再回头看,我认为新手第一次开发 DSH 插件,最好不要一上来就要求 Agent 同时把 UI、数据层、插件注册和工程配置全部做完。
更合理的方式,是先让 Agent 搜索仓库中已经存在的相邻实现。例如先看看 ui-trajectory 是怎样注册页面的、现有的 Client Package 目录是什么结构、conversation.view 又是怎么被其他页面使用的。
确定扩展点以后,可以先创建一个最小的 Client Package,只注册一个空白的 Session Insights 页面。这个阶段的目标只有一个:确认插件能够被 DSH 正确加载,页面能够进入原有会话标签体系。
随后再开始梳理数据。哪些统计 DSH 已经存在,哪些必须扩展 Host Projection,哪些只是前端显示时计算的派生值,要先分清楚。像 Token、已有 Session 统计可以直接复用;模型失败次数、逐 Tool 排行等数据则需要进一步补充;成功率、平均耗时和排序则更适合留在展示模型中。
等数据链打通以后,再去完善 UI、Markdown 导出和实时更新,最后把 Package 接入 Web Bundle,执行完整的 Build、TypeScript 和测试。
整体过程可以简单概括为:
寻找已有插件 ↓找到扩展点 ↓先挂出空页面 ↓确认所需数据 ↓复用 / 扩展 Projection ↓useProjection() 接入 Client ↓InsightsModel ↓UI 与 Markdown Export ↓Bundle + Build + Test
这套顺序也基本符合最终从真实源码中还原出来的实现过程。
四、我是怎么把这个任务交给 Agent 的?
这次我并没有在 Prompt 里告诉 Agent 应该修改哪个文件、哪个函数,因为这些内容我原本就不知道。
我给它的是 目标、约束和验收标准。
核心 Prompt 大致如下:
请在当前 DeepSeek Harness 仓库中实现一个Session Insights(会话统计)插件,用于查看当前 Agent Session 的运行统计。开始编码前,请先阅读仓库现有插件/Client Package、Session、Trajectory、Model Call、Tool Call 等相关实现,找到正确的数据来源、扩展点和 UI 注册方式,严格遵循现有 DSH 插件架构和代码风格。功能包括:用户轮次、模型调用/失败、Tool 调用统计与排行、Token、Cache、Session 与模型/Tool 耗时、当前模型,并支持自动刷新和 Markdown Export。要求:1. 必须读取 DSH 已有真实 Session 数据,不允许 Mock。2. 尽量以独立 DSH Plugin / Package 形式实现。3. 优先复用已有组件、类型、事件和数据解析逻辑。4. 不修改无关功能。5. 完成后实际执行 build、test、lint 等验证。6. 最后汇报数据来源、数据流、新增修改文件和当前限制。如果实际仓库结构与设想不同,以真实源码架构为准。
这里最重要的并不是那一串功能列表,而是几条限制。
首先是“先阅读现有架构”。如果不加这一句,Agent 很容易按照自己熟悉的方式重新创造一套看似合理的插件结构,而不是寻找项目本身已有的扩展方式。
其次是“不允许 Mock”。否则很容易先得到一个 UI 看起来已经完成、里面数字却全部是临时数据的 Demo。
最后是“完成后汇报数据来源和数据流”。这一条对学习尤其有帮助,因为 Agent 不只是告诉你“做完了”,还必须解释 Tool 次数从什么事件统计、Token 从哪里获得、页面又为什么能够注册到这里。
到了这个阶段,Agent 就不只是帮忙写代码,也开始承担一部分源码讲解的作用。
五、真实开发并没有“一句话完成”
如果只看最后的成果,很容易产生一种错觉:Prompt 发过去,等一会儿,一个插件就自动完成了。
实际上并没有这么顺利。
其中一个很典型的问题就是 Client Bundle。源码已经写好,React 测试甚至可能通过,但 DSH Client Loader 真正加载的是构建出来的 lib/client.js。如果这个文件没有正确生成,Harness 启动时依然会直接报 MissingClientBundleError。
也正因为这样,后面除了源码级测试,还专门增加了真实构建产物的 Bundle 装配测试。
另一个问题来自 Monorepo 的工程配置。Agent 一开始已经把新 Package 加进 tsconfig.client.json,但源码模式下 Loader 依然找不到它。最后才发现 tsconfig.base.json 还需要对应的 workspace path mapping。
这让我第一次比较直观地理解:“参与编译”和“源码模式下能够根据 Package 名找到它”是两件不同的事情。
更麻烦的是:页面有数字,不代表数字就是对的
这次还出现过一个更加隐蔽的问题。
最初,最终失败的模型调用没有正确计入平均模型耗时。原因并不在 UI,而在 Agent Loop 的真实事件顺序:step/end 会先出现,之后才是 turn/end(error)。
如果 Projection 在 step/end 时就把当前模型尝试的起始时间清掉,那么等真正收到最终错误事件时,就已经无法计算这次失败调用到底持续了多久。
最后只能重新确认真实事件生命周期,再调整 Projection 的状态清理时机,并增加一个专门的回归测试。
这个问题让我觉得很有代表性。
对于统计类功能来说,“有数字”只是最低要求,数字到底是怎么来的才是真正需要确认的东西。
类似的情况在 Tool 上也存在。Tool Call 一旦开始,就已经会计入总调用次数;但只有 Tool Result 返回以后,才会进一步进入成功、失败和耗时统计。因此某个 Tool 正在运行时,总调用数并不一定等于成功数加失败数。
这些细节,如果只是看最终 UI,其实很难发现。
六、Agent 说“完成”之后,还需要做什么?
这一次最终执行了 Session Stats、Session Insights、Client Fixture 等相关测试,一共 6 个测试文件、88 个测试,同时还通过了 TypeScript Build、完整 pnpm run build、Client Bundle、lint、package invariant 和 Web built bundle 装配测试。
但即使这样,我依然认为最后必须真正把 Harness 打开,再人工跑一遍。
例如切换 Session 后统计有没有串数据,成功和失败的 Tool 是否正确变化,Token 有没有重复累计,长 Session 是否仍然按照全量数据统计,以及 Markdown 导出的数值是否和页面一致。
因为“Agent 告诉你测试通过”和“这个功能真的符合你的使用预期”,始终不是完全相同的事情。
七、做完这个插件以后,我反而更容易理解 DSH 了
这是这次实验里我觉得最有意思的一点。
如果一开始直接告诉我,要先学习 Client Package、Slot、Cordis、SessionEvent、Projection、Transport、useProjection()、Bundle 和 Invariant,再开始写插件,大概率看一会儿就会失去方向。
但现在顺着 Session Insights 反推,一切都会自然很多。
为什么页面能出现在「对话」「轨迹」旁边?因为有 conversation.view Slot。
为什么统计不直接扫描轨迹?因为存在 Projection。
为什么前端不需要自己处理所有 SessionEvent?因为 Host 已经通过 Projection 把全 Session 状态整理好了。
为什么 React 里只有两行 useProjection() 就能拿到数据?因为前面的 Registry、Transport 和 Store 已经把这条链铺好了。
为什么 UI 和 Markdown 报告能够保持一致?因为二者共享同一个 InsightsModel。
这些原本看起来比较抽象的架构名词,因为都对应到了一个真实功能,突然就容易理解很多。
八、Agent 真正帮我的,其实不只是写代码
以前进入一个陌生的大型仓库,往往需要自己不停全文搜索、跟引用、判断某个目录到底有没有关系,再慢慢拼出整体结构。
现在这部分工作可以先交给 Agent。
先提出一个明确需求,让它去搜索相邻实现、定位扩展点、解释项目已有机制,然后完成第一版实现。接着再通过真实 Build、报错和测试不断验证它的判断,最后根据已经跑通的实现反过来阅读源码。
我觉得 Agent 最大的帮助,并不是“替我写了多少行代码”。
而是明显降低了第一次进入陌生大型代码库时的搜索成本。
但这并不意味着我们可以完全不理解源码。
如果最后只是:
Agent 写完 → 能跑 → 完事
那真正学到的可能只是“这段 Prompt 可以生成一个插件”。
更有价值的是继续追问:为什么这里使用 Projection?为什么注册到这个 Slot?为什么需要 Client Bundle?为什么某个统计一开始会算错?
当这些问题能够解释清楚以后,Agent 写出来的代码才逐渐变成自己的知识。
九、这可能是一种更适合 Agent 时代的开源学习方式
做完这次 Session Insights 以后,我反而越来越喜欢一种思路:
不要为了读源码而读源码,先给自己找一个足够小、但真实的需求。
比如这一次就是“给 DSH 增加一个会话统计页面”。
然后围绕这个需求不断去追:功能应该挂在哪里?数据从哪里拿?项目本来提供了什么机制?出现错误以后为什么?最后又该怎么验证?
最终虽然没有从 DSH 仓库第一行一直读到最后一行,却已经顺着一条真实调用链理解了不少核心模块。
对于今天越来越庞大的开源项目来说,我觉得这比“先把整个项目源码全部看懂,再开始开发”现实得多。
十、下一步:把它真正做成第三方插件
目前的 Session Insights 已经能够正常使用,不过它仍然属于 DSH Monorepo 内部的 Client Package,同时还依赖这次对 sessionStats 的扩展。
所以接下来还有一个更有意思的问题:
能不能把 Session Insights 真正从 DSH 源码中“拔出来”?
也就是把它做成一个独立 GitHub 仓库,通过 DSH 正式的外部插件机制安装,而不是跟着整个 Monorepo 一起编译。
到时候就需要继续搞清楚 DSH 怎样加载仓库外插件、Profile 怎样安装 Client Package、外部插件能不能自行注册 Projection,以及最终能不能做到一条命令安装和升级。
这部分就留到下一篇继续折腾。
写在最后
这一次最开始只是想做一个简单的会话统计页面。
但真正走完以后,经历的其实是:
需求 ↓Agent 搜索源码 ↓定位扩展点 ↓实现 ↓Build / Debug ↓测试 ↓人工验收 ↓反向理解源码
所以与其说这篇文章是在讲“如何让 AI 帮你写一个 DSH 插件”,我更愿意把它看成一次真实的 Harness DIY:
一个原本并不熟悉 DSH 内部架构的人,如何借助 Agent 完成需求、源码定位、实现、Debug 和测试,并最终反过来理解 DSH 的插件系统。
AI 的确让进入陌生项目这件事变得容易了很多,但真正值得掌握的,可能从来都不是某一句万能 Prompt。
而是如何提出一个清楚的问题,如何让 Agent 沿着项目原本的架构工作,如何判断它做得对不对,以及最后怎样把它写出来的代码重新变成自己的知识。
夜雨聆风