2026 年,写一个能调用工具的 Agent 并不难:一个循环,几次模型请求,若干函数调用。难的是把它做成一个可以长期使用、可以被改造、可以托管多种产品的运行时。DeepSeek Harness(命令行里叫 dsh)是 DeepSeek 开源的后者。这个专栏用 14 篇的篇幅逐层读它的源码,回答一个问题:一个"一切皆插件"的 Agent 运行时,是如何被设计出来的。
本文是开篇。我们只解决三件事:Harness 这个词究竟指什么,为什么 dsh 选择"一切皆插件",以及这个系列接下来怎么走。本文基于 2026 年 10 月的 master 分支(commit 5badb15,版本 0.2.1-alpha.1),所有路径与行号都可以在源码里对照。
先建立一个直观印象。装好 Node.js 后,npx @deepseek-ai/dsh web 会在 http://127.0.0.1:3080 启动一个 Web 界面(本地启动还会自动打开浏览器),这就是 dsh 的默认形态。但同一行命令换几个词,它又能变成别的东西:dsh headless 是一次性运行器,dsh --profile sdk 是 SDK 服务器,dsh --profile acp 是自动化协议服务器。一个命令,多个产品面——这不是打包了多个程序,而是同一棵插件树按不同组装配方长出来的不同形态。组装配方如何写、树如何长,正是前两篇的内容。本系列的读法是:不看源码也能理解设计,看完能上手改源码,所以每篇都会带你在仓库里走到具体的位置。
什么是 Harness:模型之外的运行时脚手架
打开仓库根目录的 README.md,第一句话说:dsh 是 "an open-source agent harness developed by DeepSeek AI",并且 "built on an everything-is-a-plugin architecture and powered by Cordis"。仓库的引用条目还给出了另一个名字:DeepSeek Harness: Everything is a Plugin。这两个短语——Harness 与 Everything is a Plugin——就是理解整个项目的两把钥匙。本篇先讲第一把。
Harness 的本义是挽具:套在马身上、把马的力气传导到车辕上的那套器具。在 Agent 语境里,它指模型之外的全部运行时。模型本身只负责一件事:根据输入产生输出。但一个能干活儿的 Agent 还需要:把工具清单交给模型并执行模型选中的调用,读写工作区的文件,把每一轮对话持久化下来以便恢复和审计,对危险操作做审批,以及一个让人(或另一个程序)与 Agent 交互的界面。这些都是 Harness 的职责。用一句话概括这个系列的基本视角:
模型 + Harness = Agent。
这个等式在 dsh 里不是比喻,是代码结构。模型请求由 LLM 适配层发出,工具注册在工具注册表里,会话写入只追加的日志,而驱动轮次的 agent loop(智能体循环)——在多数框架里是写死的内核——在 dsh 里同样是一个可以替换的插件。下图是这个分层的直观表示。
flowchart TD
subgraph M["模型层(本系列不展开)"]
LLM["大语言模型:DeepSeek 及其他提供方"]
end
subgraph H["Harness 层:一棵可组装的插件树"]
LOOP["agent loop:轮次与步骤的驱动器"]
TOOLS["工具注册表与执行流水线"]
SESS["会话日志:模型所见即已记录"]
ADAPT["LLM 适配器 seam"]
FACE["应用面:Web / Headless / SDK / ACP"]
LOOP -->|"调用"| TOOLS
LOOP -->|"读写"| SESS
LOOP -->|"发起请求"| ADAPT
FACE -->|"观察与注入"| LOOP
end
subgraph R["运行时支撑"]
FS["文件系统与进程"]
SBX["沙箱与审批策略"]
STORE["持久化存储"]
end
TOOLS --> FS
TOOLS --> SBX
SESS --> STORE
LLM <-->|"流式请求与响应"| ADAPT注意这张图里 Harness 层的每个框都不是"框架内置的功能",而是"树上挂的一个插件"。这意味着框与框之间没有硬编码的调用,只有经服务接口建立的依赖。改任何一个框,都不需要碰其他框的代码。这就是下一节的主角。
补一句具体的。dsh 开箱即用的能力清单,按架构文档对共享底座 dsh-base 的描述,包括:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。这份清单里的每一项,后面你都会在某一个插件包里找到它的实现,并且每一项都能被你的补丁替换或禁用。这是"一切皆插件"承诺的覆盖面,而不仅是口号。
一切皆插件:没有特权内核
多数框架的结构是"内核 + 扩展点":内核写死启动流程、内存模型和扩展契约,预留若干钩子或回调供使用者填充。你要改行为,就在既定的位置挂回调;位置之外的世界是封闭的。dsh 走了一条相反的路。仓库的架构文档 docs/architecture.zh.md 在 Cordis 一节写道:
产品的每一部分都是插件,包括模型适配器、工具注册表、会话日志,以及 agent loop 本身,因此每个都可以从配置替换。不存在需要打补丁的特权内核。
这句话里有两个值得停下来的断言。
"每一部分都是插件" 意味着系统的最核心不存在特例。agent loop 是插件,你可以不改动任何一行"内核"代码,换上一个自己的轮次驱动器——比如换一个支持不同取消策略的 loop,只要在同一个服务名下提供同一接口。模型适配器是插件,接入一个新提供方是注册一个新插件,而不是在内核里加一条 if 分支。工具、提示词组装、会话存储,无一例外。扩展的产物与内置的能力,形态上完全没有区别。
"不存在特权内核" 说的是扩展的方向。传统框架的扩展是向内:你在内核规定的缝隙里填东西。dsh 的扩展是平铺的:架构文档的原话是"把插件挂载到其他插件旁边"。配套的还有一条规则——各项注册都是副作用,会在其插件卸载时撤销。装上是一个动作,拆下是逆动作,树回到挂载之前的样子。这条规则的实现很具体:服务基类的构造函数本身就是注册(vendor/cordis/src/service.ts:11),注册跟随所属 fiber(插件的运行时载体)的生命周期,卸载时自动注销;其余注册——监听器、提示词片段、工具 schema——一律经 ctx.effect() 或 ctx.on() 安装,并随插件返回的 disposer 撤销。Cordis 的入门文档 docs/cordis-primer.zh.md 把这类约定总结为一句话:注册是可逆的副作用。
"可逆"不是洁癖,而是后续一切机制的地基。配置热重载要安全地换掉一棵树,依赖的是旧插件卸载时把注册撤干净;插件管理要能启停任意插件,依赖的也是同一件事。如果注册会漏,重载就会把树搞得越来越脏——第 10 篇讲插件管理时我们会再看到这条规则在事务里如何被保护。
支撑这一切的底层框架是 Cordis(dsh 以 vendor 方式引入并 rescope 为 @deepseek-ai/cordis 的插件框架,标记为 private、不单独发布),其设计见论文《A Programming Paradigm for Spatiotemporal Composability》——从标题就能看出野心:插件可以在空间(树的哪个位置)与时间(生命周期的哪个阶段)两个维度自由组合。在仓库里,它的源码在 vendor/cordis/src/,同目录还有 loader、include、group、timer、hmr、logger-console、schemastery 等配套包,启动链路与补丁叠加都建立在它们之上。Cordis 的几个核心概念我们会一直用到,这里只给最低限度的一句解释:Context(上下文)是服务的容器,每个服务占据一个稳定的 ctx.<key>;插件用 inject 声明依赖,谁先启动由服务依赖决定,而非代码顺序;fiber 是插件的状态机,从挂起到激活到卸载。另外三个作用域原语——extend 造子上下文、isolate 给服务名换作用域、intercept 给子树合并配置(vendor/cordis/src/context.ts:99、:121、:141)——是理解会话与 preset 机制的基础,第 4 篇再展开。
作为读者,你暂时只需要记住一件事:在 dsh 的世界里,没有"应用"和"插件"的等级之分,只有"这棵树装了什么"。
把两种架构放到同一个改动面前,差别就显形了。假设你要让 Agent 的每一轮在发起模型请求前都经过一道自定义审查。在"内核 + 扩展点"的框架里,你得先找到内核恰好留出的那个钩子;没留,就只能 fork。在 dsh 里,审查逻辑是一个监听事件的插件,挂到树上是注册,拆下来是撤销,而事件分发本身有明确的语义约定——是观察、包装还是拦截,由事件的分发模式决定(vendor/cordis/src/events.ts:31 定义了 emit、parallel、serial、bail、waterfall 五种)。你不需要任何人的许可,因为内核根本不设防——它把扩展所需的全部原语都开放成了与内置能力同级的插件接口。
与 Claude Code、OpenAI Agents SDK 的关系
读到这里,一个很自然的问题是:dsh 和 Claude Code、OpenAI Agents SDK 有什么区别?三者常被放在一起比较,但它们是不同层面的东西,先把层面分清,比较才有意义。
Claude Code 是一个产品:一个打磨完整的终端编码助手。你使用它提供的交互方式,扩展它的手段是它开放的钩子、命令与配置。它的内部实现不开源,可改造的边界由产品决定,这对"用起来"是优点,对"改到底"是边界。
OpenAI Agents SDK 是一个库:你在自己的进程里 import 它,把 agent、工具和防护装配进你的应用。它很轻,嵌入成本低,但运行时本身不在你的控制之下——它的循环、它的并发模型,是 SDK 的实现细节。
dsh 是一个可自组装的开源运行时。它自身也以插件化方式交付:以 profile(具名组装)加组合包(bundle)组装,同一套内核可以组装成 Web 应用、无界面的一次性运行器、SDK JSON-RPC 服务器或 ACP 服务器,每一层组装结果都可以再被用户的补丁文件按行替换。换句话说,前两者给你的是一个成品或一套 API;dsh 给你的是组装成品的那台机器,以及机器的图纸——从启动链路到 agent loop,每一层都既是使用者也是改造对象。
这不是价值判断,而是取舍。要一个开箱即用的助手,产品形态更合适;要在自己代码里嵌一个 agent,SDK 更顺手;要研究、改造、自托管一整个 Agent 运行时,才需要 dsh 这种把自身也完全插件化的架构。这个系列假设你是第三种读者。
"可自组装"再具体一点:在 dsh 里,换一个模型提供方是注册一个适配器插件,加一个面向模型的能力是在工具注册表上登记,让某个会话拥有不同的能力集合是组装一个 agent preset——架构文档甚至维护着一张"新行为的归属位置"表,把每类常见改动指向它该去的扩展点。你面对的从来不是"能不能改",而是"改在哪一层最合身"。这个系列会把每一层的入口都走一遍。
系列路线图
全系列 14 篇,从宏观到微观:前三篇讲设计与组装,然后是内核与智能体核心,最后是扩展机制与各应用面。
flowchart LR
subgraph G1["设计与架构"]
P1["01 开篇"]
P2["02 架构全景"]
end
subgraph G2["启动与内核"]
P3["03 启动链路"]
P4["04 Cordis 内核"]
end
subgraph G3["智能体核心"]
P5["05 会话日志"]
P6["06 Agent Loop 轮次流程"]
P7["07 工具流水线"]
P8["08 LLM 适配与流式"]
end
subgraph G4["扩展与应用"]
P9["09 能力 seam"]
P10["10 Profile 与插件管理"]
P11["11 Web 客户端"]
P12["12 Headless/SDK/ACP"]
P13["13 沙箱、审批与 Hook"]
P14["14 遥测与收尾"]
end
P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7 --> P8 --> P9 --> P10 --> P11 --> P12 --> P13 --> P14第 1、2 篇是地图:Harness 的定位、monorepo 的包结构、插件树如何被 profile 与组合包组装出来,以及补丁层的叠加规则。第 3、4 篇进入启动与内核:从 npx dsh web 按下回车到插件树沉降,再到 Cordis 的 Context、Fiber 与事件分发。第 5 至 8 篇是智能体的心脏:只追加的会话日志、轮次与步骤的流程、工具执行流水线、模型流式协议。第 9 篇起转向"怎么改":能力 seam 的替换机制、profile 与插件管理的边界,最后遍历 Web 客户端、Headless/SDK/ACP 应用面与沙箱审批。篇目为规划,可能随源码演进微调,以实际发布为准。
一个必要的提醒:dsh 目前处于 developer preview 阶段,README 明确写道 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES"。本文及后续各篇描述的细节都可能随版本变化,阅读时请以你手头的源码为准——这也是本系列坚持每个论断都落到具体文件与行号的原因。
小结
我们给出了三个判断。一,Harness 是模型之外的运行时脚手架,模型加 Harness 才是 Agent;dsh 把这一等式直接做成了代码结构。二,dsh 选择"一切皆插件":没有特权内核,agent loop、模型适配器、工具、会话日志全是插件,注册皆副作用、卸载即撤销,这是它与"内核 + 扩展点"式框架的根本区别。三,dsh 与 Claude Code、OpenAI Agents SDK 不在同一层面:后两者是产品与 SDK,dsh 是可自组装的开源运行时。下一篇我们从空中落地,给出 packages/ 下 54 个包组的一页地图,并讲清这些包如何被 profile 与组合包组装成一棵插件树。