ByteNoteByteNote
DeepSeek Harness 架构精读:事件与能力接缝
字

字节笔记本

2026年10月6日 · 约 8 分钟读完

DeepSeek Harness 架构精读:事件与能力接缝

API中转
¥120

DeepSeek 开源的 agent harness 项目 dsh(DeepSeek Harness,仓库 deepseek-ai/deepseek-harness)里,有一份写给贡献者的架构文档:动手改 packages/ 目录下的任何代码之前,先把 docs/architecture.md 读一遍。这份文档不长,却把整个产品的组织方式讲透了:没有特权核心,一切皆插件,扩展靠文档化的事件与接缝。本文按它的脉络梳理一遍,也可以把它当作阅读这类 agent harness 源码的路线图。

Cordis:插件即一切

dsh 构建在 Cordis 框架之上。插件向共享上下文贡献三样东西:服务、类型化事件,以及可回滚的副作用。产品的每个部分都是插件:模型适配器、工具注册表、会话日志,连 agent 循环本身也是插件。这意味着没有需要打补丁的特权核心:想扩展 dsh,把自己的插件挂到其他插件旁边即可;插件卸载时,它注册的一切作为副作用自动回卷消失。

profile 与 bundle:启动即组合

一个运行中的 dsh 是一棵启动时按序合成的插件树。profile 是存放在 Harness 家目录里的命名组合,列出它叠放哪些 bundle,容纳自己安装的树外插件,并保存用户自己的 cordis.patch.yml,web 与 headless 两个 profile 作为模板发行。bundle 则是 Cordis 配置行及其挂载代码的发行格式,保证它插入的内容仍能被上层改写。

DeepSeek Harness 启动组合:profile 叠 bundle 再叠三层补丁

每个 bundle 和 profile 都在自己的 package.json 里用 dsh 字段声明自己。所有 profile 的第一层都是 dsh-base:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测都在这一层;dsh-web-app 在其上添加浏览器应用,dsh-headless 则给出一个完全没有服务器的单次运行器。

分层作用于一个空的入口列表,顺序固定:先按 profile 列出的顺序叠每个 bundle,然后是 profile 的 cordis.patch.yml,再是家目录级的那份,最后是 --patch 覆盖。补丁按 id 定位一行配置,整行替换或插入新行。想知道自己的机器实际启动了什么,官方给了一条命令:dsh --profile web --dump-config,打印出来的每一行都可以被你自己的补丁替换。

核心包分工

文档列出了贡献到 Cordis 树的核心包:core/session 拥有只追加的 SessionEvent 日志和内存存储,挂在 ctx.sessions;core/system-prompt 负责提示词分段与工具 schema 的组装;core/tools 是带守卫执行管线的工具注册表;core/agent 与 core/agent-loop 分别提供 Agent 接口、活动注册表,以及实现该接口的默认驱动;core/scope 是按 agent 隔离注册的原语;llm/llm 定义消息与流式词汇表,并留出适配器接缝 ctx.llm。

三类事件域

事件是扩展点,选对域是大多数改动的第一步。session 事件是持久事实:追加进日志并通过 session/event 广播,事实需要活过一次重载就用它。agent 事件(agent/)携带活的 Agent 对象:收件箱、步骤、状态、请求、校验、续行,要观察或拦截进行中的工作就挂在这里。capability 事件(fs/、tools/、telemetry/)把策略与适配器接到接缝上,而不需要引入循环本身。

一次 turn 的完整流程

文档把一次模型请求加上它调用的工具定义为一个 step,turn 则由零个或多个 step 组成:在第一份输入被认领前打开,在不再欠任何东西时关闭。

dsh 一次 turn 的事件流:绿色为 durable 会话事件,蓝色为 live 扩展点

流程里有几处值得展开。agent/pre-step 决定模型看见什么:监听者可以改写被认领的消息,也可以直接拒绝;被拒绝或改写为空的首轮认领同样会关闭一个持久 turn,只是不花费任何 step,日志里仍会记录这次尝试。输入经由唯一收件箱抵达驱动器:有些消息会立刻唤醒它,注入的上下文则在收件箱里排队,直到下一条消息把它一并带走。

事件还分两种驱动方式:agent/pre-step、agent/request、llm/stream 与三个 tools/* 事件是瀑布,监听者必须调用 next() 把控制权交回链条;agent/turn-stopping 则是串行事件,没有 next() 可调。

会话日志:模型可见即已记录

会话日志是模型所见上下文的唯一来源:deriveMessages() 从日志投影出模型历史,原始的 assistant/chunk 事件则保住回放与界面还原度;fork、恢复、转录、遥测和持久化全部从这条流派生。文档把它写成一条运行时不变量:任何进入模型请求的内容,都必须能从日志重建。因此要给模型增加一种新输入,正确做法是新增一种会话事件:扩展 SessionEventMap,然后从日志渲染。

能力接缝:三个角色缺一不可

接缝是一个可替换的能力,由三个角色构成:声明接口的 Service Definition、给出实现的 Service Provider,以及消费它的 Consumer,最后这个角色通常是模型侧工具。一个包可以兼任多个角色,但只有一个角色不算接缝;新增能力意味着把三个角色一起设计出来。

接缝解释了为什么换一个 provider 能改变整个产品:文件系统与子进程的 provider 共享同一个执行世界,把它们指向远程沙箱,Bash、PTY 与 LSP 就跟着一起搬走,不需要任何 provider 分叉。子代理 provider 同样如此:同一个接口背后,可以从新起一个子 agent,到在另一个产品里委派一轮 turn,变化空间极大。

新行为挂在哪里

文档最后给了一张扩展点地图,节选如下:

目标机制
新增模型 provider在 ctx.llm 上注册适配器
新增模型可见能力注册到 ctx.tools,其 schema 加入提示词组装
为单个会话换能力集组合一个 agent preset,其中的服务行需要 isolate realm
拦截请求、工具或 turn用对应的 agent/* 或 tools/* 事件,agent/turn-stopping 可终止 turn
注入模型可见上下文调用 agent.inject(),落入下一个被接纳的请求
新增持久会话状态扩展 SessionEventMap,从日志渲染与回放
后台任务注册 ctx.jobs,由 job_* 工具收集或停止
分叉活动会话调用 ctx.sessions.fork()

文档还规定:新行为一律挂到这些文档化的扩展点上;如果确实要改循环本身,就得同步更新这张地图。想读懂 dsh,这份 architecture.md 是最好的入口;官方甚至建议直接派一个 agent 去探索代码库,把这份文档当作它的地图。之前我们写过它的仓库规矩、预设组合与 goal 机制,配合这份架构文档一起读,项目全貌就齐了。

相关文章

分享: