乐于分享
好东西不私藏

DeepSeek Harness 源码拆解:“一切皆插件”如何不散架

DeepSeek Harness 源码拆解:“一切皆插件”如何不散架

一个 coding agent 最终能做成什么样,不只由模型决定。

同一个模型,套上不同的系统提示词、工具、上下文管理、权限策略和执行循环,可能表现得像两个不同的产品。学术界甚至开始专门研究如何把模型贡献和 harness 贡献拆开评估,因为常见的 agent benchmark 测到的,本来就是二者的混合结果。一项针对 coding harness 的统计研究把问题说得很直接:benchmark 往往混淆底层模型与包裹它的提示词、工具和控制流。

这正是 DeepSeek Harness 值得读源码的原因。它不是又做了一个聊天界面,而是把“模型之外、agent 以内”的部分单独做成了开源运行时:模型适配器、工具、会话、持久化、沙箱、子 agent、工作流、界面,甚至 agent loop 本身,都由插件组装。

不过,真正重要的并不是 README 上的口号 “Everything is a Plugin”。插件化很容易写进首页,难的是回答后半句:插件如何安全地加入、退出和更换?依赖它的组件怎么办?运行中的会话如何保持一致?外部副作用能不能回滚?

通读代码和配套论文后,我的结论是:DeepSeek Harness 最值得借鉴的,不是“功能都能换”,而是它把可替换性拆成了一组具体约束——注册必须可撤销,依赖必须声明,模型所见必须能从日志重建,能力必须有完整的定义、实现与消费路径,隔离强度必须如实上报。

这些约束让开放架构更容易推理,但并没有神奇地证明整个产品正确。恰恰相反,Cordis 论文写得很清楚:逆操作是否真的能恢复原状态,仍然是组件作者的义务;越过系统控制边界的文件写入、网络发送和现实世界操作,也不会因为卸载插件就自动消失。理解这条边界,才算真正理解 DeepSeek Harness。


Harness 到底是什么

可以先把 agent 写成一个简化公式:

Agent = Model + Harness
Harness = 上下文组装 + 工具执行 + 循环控制 + 会话状态+ 权限与沙箱 + 持久化 + 对外接口

模型给出下一步的判断,harness 决定模型此刻能看到什么、可以调用什么、调用后如何继续,以及出错或重启后如何恢复。

这个定义并非 DeepSeek 一家的说法。Anthropic 在长时运行 agent 的工程实践中讨论过跨上下文的进度文件、增量执行和 compaction;在托管 agent 架构中,又明确拆分了 session、harness 与 sandbox,并把 session 描述为可持久化的追加式事件记录。由此可见,会话日志、执行循环和沙箱正在成为 agent 基础设施的共同部件。

DeepSeek Harness 的不同点,不是发明了这些部件,而是进一步取消了固定产品内核的特权地位官方架构文档写道,模型适配器、工具注册表、会话日志和 agent loop 都是插件;扩展系统的方式,是在现有插件旁挂载另一个插件,而不是修改一个不可替换的核心。

这句话可以从仓库结构中得到验证。整体并不只是“内核、插件”两层,而更接近下面五层:

层次主要职责代表内容
Cordis插件生命周期、服务依赖、事件与可撤销 effectvendor/cordis
产品主干会话、提示词、工具、Agent 接口、默认 loop、scopepackages/core/*
能力模块LLM、shell、fs、LSP、sandbox、web、subagent、workflow 等packages/<capability>/*
组合层profile、bundle、preset 与配置 patchpackages/bundlepackages/preset
产品表面Web、CLI、headless、ACP、JSON-RPC、Python SDKapps/python/

所谓“一切皆插件”,不是说系统里没有核心代码,而是说核心能力也通过同一套生命周期和依赖机制进入运行时。默认 agent loop 当然存在:ReactLoopAgent 就是具体实现;关键在于使用方依赖公开的 Agent 接口,配置可以替换其 provider。


Cordis:先解决“怎么拆”,再谈“怎么装”

DeepSeek Harness 底下的 Cordis,不只是一个依赖注入容器。它试图回答动态组合的两个问题。

第一个是时间维度:组件退出后,它在共享上下文里留下的修改能否撤销?Cordis 论文称之为 temporal composability,并用 revertible effects 表达。直觉上,一个 effect 不只执行“注册”,还必须同时交出对应的“注销”。运行时收集这些逆操作,在组件卸载时按相反顺序执行。

在插件代码中,这通常长这样:

ctx.effect(() => {
registry.set(nameprovider)
return () =>registry.delete(name)
})

真实实现比这个例子严密得多。Fiber.effect() 的源码会收集 effect 产生的 disposer,卸载时逆序调用;重复 dispose 是空操作,异步清理也会等待完成。工具、提示词段、模型适配器和监听器的注册,最终都应落到这套机制上。

第二个是空间维度:一个组件依赖什么服务,依赖出现、消失或换了实现时,它该如何响应?论文称之为 spatial composability,并用 reactive coeffects 表达。在 Cordis 中,插件通过 inject 声明依赖;所需服务存在时它才激活,provider 离开时相关生命周期会重新对账。加载顺序因此不必散落在一段手写 boot 脚本里,而由依赖关系推导。

服务和事件共享一个 Context 也很关键。ctx.toolsctx.llmctx.sessions 是服务入口;ctx.on()ctx.waterfall() 等则处理运行中的协作。事件的派发模式也不是实现细节:emit 用于同步观察,parallel 等待所有监听器,serial 顺序求值,waterfall 允许中间件接管或继续传递。对于 waterfall,监听器只有调用 next() 才会把控制权交给下一个;不调用就是有意短路。这个语义被写进类型、文档和生成的事件目录,而不靠团队成员默契维持。

“可证明”有明确前提

这里必须纠正一种很诱人的误读:Cordis 论文有形式化演算和元理论,不等于“装进 Cordis 的任意插件都被自动证明可逆”。

论文正文在 effect tracking 一节明确说明:ctx.effect 接受组件提供的 inverse,但运行时不会验证这个 inverse 是否真的恢复了 effect;这是组件作者必须满足的义务。形式化结果还需要组件之间的独立性、依赖图无环等假设。

论文的 system boundary 一节又划出第二条线:只有系统能独占修改并恢复的位置,才在可逆状态之内。打开文件后关闭描述符可以回收资源;已经写到外部文件的字节、发出去的网络数据、完成的付款,却不能靠卸载插件自动撤回。它们需要延迟提交或业务补偿,而且证明要在新的等价关系下重新建立。

因此,更准确的说法是:Cordis 为可逆生命周期提供了统一机制和可推理的形式基础,DeepSeek Harness 又用工程规范强迫大部分注册走这条路;它没有把所有现实世界副作用变成事务。


一次 turn 如何流过系统

要判断 agent loop 是否真的可扩展,最有效的方法不是数包,而是跟一次请求。

DeepSeek Harness 把层级定义得很清楚:一个 turn 对应一次输入从开始到耗尽;一个 turn 可以包含多个 step;每个 step 是一次模型请求,加上该请求触发的工具执行。简化后的路径如下:

turn/start
  领取输入,组装 system prompt 与 tool schemas
  agent/pre-step
  step/start
    user/message 写入 session
    从 session 推导模型历史
    agent/request -> llm/stream
    assistant/chunk* -> assistant/message
    tool/call* -> tools/pre-execute
               -> tools/execute
               -> tools/post-execute
               -> tool/result*
  step/end
  若工具或新输入要求继续,则进入下一 step
turn/end

这条路径暴露了三类扩展点。

第一类是 durable session event,例如 user/messageassistant/message 和 tool/result。它们需要跨重启保留。

第二类是 agent/* live event,例如 agent/pre-step 和 agent/request,用来拦截正在发生的工作。

第三类是能力自己的事件,例如 tools/*fs/*,用于在不导入 agent loop 的情况下挂接策略。

这比“到处都能加 hook”更有价值,因为事件属于哪个域,决定了它是否持久化、谁拥有它、能否改变结果。新增行为也因此有明确位置:模型 provider 注册到 ctx.llm,面向模型的工具注册到 ctx.tools,请求策略挂在 agent/request,文件策略挂在 fs/*。如果一项功能必须直接修改默认 loop,仓库规范要求同步更新架构文档里的扩展点地图。


能力缝:可替换的不是一个类,而是一条完整路径

DeepSeek Harness 使用 capability seam 描述一种完整能力。它至少包含三个角色:

  1. Service Definition:声明稳定接口。

  2. Service Provider:给出本地、远程或平台特定实现。

  3. Consumer:使用接口,通常把能力包装成模型可调用的工具。

shell 是最直观的例子。接口负责说明“如何执行”;本地 Bash、PowerShell 或沙箱版本提供实现;模型最终看到的是 Bash/terminal 工具。工具不应该知道底层究竟是本机进程、远程容器还是另一种执行环境。

这解释了官方文档中“替换一个 provider 会改变整个产品”的说法。文件系统、子进程、终端和 LSP 若共享同一个执行世界,把 provider 换到远程环境后,相关 consumer 应一起迁移,而不是每个工具各自维护一套远程分支。

这套设计也给“插件化”设了更高门槛。只抽一个接口不算 seam,只写一个工具也不算;定义、实现和消费路径必须一起想清楚。它迫使开发者在加功能时回答:稳定语义由谁拥有?部署差异放在哪里?模型看到的结果由谁呈现?生命周期又由谁回收?

用一个扩展需求检验分层是否真的有效

假设现在要把本地执行换成远程开发容器。一个边界含混的框架,往往会让 Bash 工具自己识别远程地址,文件工具再实现一遍上传下载,LSP 又维护第三套连接;权限、错误和清理语义由此分叉。

在 dsh 的分层里,第一步不是修改模型工具,而是确定“执行世界”由哪些 Definition 共同描述:文件访问、子进程、持久终端以及 LSP 如何指向同一环境。第二步是为这些接口提供远程 Provider。第三步才是检查现有 Consumer 是否能够不感知部署位置继续工作。若远端环境已有自己的容器隔离,本地 ctx.sandbox 也不应硬套进去;官方文档把远程执行视为整条能力缝的替代实现,正是为了避免把两种边界叠成一套说不清的策略。

然后还要沿请求与日志路径检查一次:远程工具的 schema 是否通过 ctx.tools 进入请求头;调用和结果是否进入 session;结果中哪些字段对模型可见,哪些只是 UI meta;断线发生在命令提交前还是提交后,能否区分“未开始”与“结果未知”;agent scope 销毁时,远程终端和连接是否真的到达 quiescence。

这套检查也适用于其他扩展:

需求首要落点还必须回答的问题
新增模型厂商ctx.llm adapter流式错误如何归一,provider 私有状态如何回放
新增模型工具ctx.toolsschema 如何进请求头,结果如何记录与呈现
新增请求策略agent/request 或 tools/* waterfall是观察、改写还是短路,是否必须调用 next()
新增持久状态SessionEventMap老版本能否忽略,如何投影、校验与恢复
只给某个 agent 增加能力agent preset 与 agent.ctxscope、realm、销毁与同名遮蔽是否正确

所谓“无需改内核”并不等于“只写几行注册代码”。它真正承诺的是:需求应该能落到一组已有的所有权位置;如果落不下去,开发者能够明确知道自己正在新增一种公共语义,而不是把特殊情况偷偷塞进 loop。


Session:不是保存聊天记录,而是保存请求事实

DeepSeek Harness 最扎实的一项设计,是 append-only session log。

SessionEventMap定义核心事件词汇,并允许插件通过 TypeScript declaration merging 扩展。当前核心事件包括 turn/step 边界、用户与助手消息、原始流式 chunk、工具调用与结果、待办快照、请求头、模型路由上下文和 seed 生命周期标记。这里不宜用一个固定数字宣传,因为词汇本来就是开放的,而且在 developer preview 阶段变化很快。

模型历史不是另存一份 messages 数组,而由 deriveMessages() 从日志的 message surface 推导。请求头也不只记录聊天文本:request/header 保存实际使用的模型调用配置、渲染后的 system prompt 和工具 schemas。换句话说,“模型看见了什么”不仅指 user/assistant 对话,也包含影响本次采样的请求环境。

仓库把这条规则写成:Model-visible means logged。凡是进入模型请求的输入,都应该能从 session log 重建;新增一种模型可见输入,就应新增相应 session event 或投影规则。运行时 invariant 会比较重建结果与实际请求,防止 UI、持久化和模型上下文各自维护一份逐渐漂移的状态。

这套日志至少解决了四个问题:

  • replay、resume、fork 和 transcript 可以基于同一份事件流,而不是彼此复制状态。

  • 原始 assistant/chunk 保留流式输出来源,组装后的 assistant/message 用于模型历史。

  • compaction 不删除旧事件,而是用 surfaceOp: replace 在模型可见 surface 上遮蔽一段旧节点;审计历史仍保留。

  • crash recovery 能识别未闭合 turn,并对“工具未开始”和“工具已记录但结果未知”作不同处理。

最后一点尤其重要。事件日志可以证明系统记录了什么,却不能凭空得知崩溃瞬间外部工具究竟完成没有。仓库的恢复逻辑会把这类结果标为 outcome unknown,并提示只有只读或幂等操作才适合直接重试;有副作用的调用应先检查外部状态。这个细节比“崩溃后自动恢复”更可信,因为它承认不可观测窗口确实存在。

日志还付出了一组明确成本。Session.append() 会对数据做无损 JSON 快照并冻结;undefined、BigInt、函数、循环引用、稀疏数组、负零、Map/Set/Date 和类实例等值会被拒绝。插件不能把任意进程对象塞进持久事实。未被旧版本识别且没有 ignorable: true 的事件,也应让读取方拒绝恢复,而不是静默跳过一段可能改变语义的数据。这是典型的 fail-closed:宁可无法打开,也不伪装成完整重放。


Scope 与 preset:同进程不等于同能力

在多 agent 场景中,工具注册表不能只是一个进程级全局 Map。否则给某个子 agent 增加的工具,可能意外出现在另一个会话里。

scope 包提供 per-agent 的注册身份和生命周期。全局贡献对所有 agent 可见;scoped 贡献只在精确 scope 中可见,并且可以遮蔽同名全局项。agent 销毁时,通过其 agent.ctx 注册的工具、提示词段和监听器随 scope 一起回收。

preset 则决定一个会话实际挂载哪组能力。这里有一个很有含金量的细节:子 agent 继承父 agent 时,使用 composeFrom()绑定父方正在运行的那一代组合,而不是重新按 preset id 读取配置文件。

原因并不抽象。假设父 agent 启动后,preset 文件被修改或删除;如果子 agent 重新解析 id,它拿到的工具与提示词就可能和父会话历史产生时使用的版本不同。composeFrom() 复用已经存在的插件对象、工具注册和提示词段,保证父子此刻基于同一代组合工作。

服务隔离还需要 Cordis 的 isolate realm。preset 自带的服务若发布到 root realm,就会变成进程级全局对象,同名服务会冲突,也可能被其他会话读到。仓库因此检查 preset 私有服务必须放进 isolate realm,或明确移到 host composition。这里的隔离主要解决服务可见性和生命周期,不应与操作系统级安全沙箱混为一谈。


沙箱:把“做到了多少”作为返回值

DeepSeek Harness 的 sandbox seam 有一个很好的设计习惯:不只返回包装后的命令,还返回这次隔离实际达到了什么程度。

文件效果策略分为三档:

  • read-only拒绝文件写入,仅保留 shell 所需的必要 sink。

  • workspace-write允许写工作区和后端承诺的临时目录。

  • danger-full-access绕过 confinement,直接执行原始命令。

Linux 可选择 bubblewrap 或 Landlock,macOS 使用 Seatbelt,Windows 使用 ACL restricted token。对于受限模式,ctx.sandbox.confine() 要么返回应当替代原命令的 argv,要么抛出 SANDBOX_UNAVAILABLE;不能悄悄退化成无隔离执行。

返回值中的 enforcement 只有 full 与 partial。旧 Landlock ABI 可能只覆盖部分文件访问类别;Windows ACL 也有 Everyone 权限与硬链接边界,因此后端必须报告 partial。调用方如果需要绝对保证,就不能把 partial 当 full 使用。

denialSignatures 和 runnerFailureRules 又把两类失败拆开:前者表示沙箱成功运行并阻止了命令;后者表示沙箱 runner 自己失败,命令可能根本没开始。二者如果混在一起,产品会把基础设施故障误报为一次正常的权限拒绝。

不过,sandbox 文档也写明了边界:SandboxMode 管的是文件效果,网络和进程可见性不在这套 vocabulary 内;容器、microVM 或远程执行属于整条能力缝的另一种实现。Cordis 论文同样指出,依赖声明只能约束通过 Context 访问的能力,不可信插件代码仍需要语言运行时之外的 sandbox。把这两条限制写出来,比笼统声称“安全执行”更重要。


TypeScript 在这里承担了三种结构工作

DeepSeek Harness 大量使用 TypeScript,但亮点不在 strict: true 本身,而在类型如何参与跨包结构。

第一种是 branded ID。SessionIdCallId 等在运行时都是字符串,编译期却不可互换。构造函数只做类型转换,没有额外运行时对象,却能阻止把会话 id 误传到工具调用 id 的位置。

第二种是 Map → derived union。核心包先声明可合并的 interface map,再从 keyof Map 推导事件名,从 Map[keyof Map] 推导联合。插件通过 declaration merging 增加成员,无需修改拥有该联合的核心包。

这里也有一个容易写错的结论:开放联合不能像封闭联合那样在 switch 末尾无条件 assertNever。仓库规范明确要求,封闭联合做 exhaustive check;可合并联合则保留有说明的 default。对于持久化事件,陌生且非 ignorable 的分支甚至应拒绝读取。扩展性与穷尽检查并没有“自动两全”,代码必须知道自己处理的是哪一种联合。

第三种是 Typert它从 TypeScript 声明生成远程调用所需的描述符和 codec,让 Host、Client 与 Remote 共享调用定义。strict codec 带生成的 schema;src-json 只保证 JSON 安全,不承诺恢复完整结构类型。Typert 减少了手写 RPC glue,但传输失败、取消、lookup 策略和业务异常仍然有各自的错误语义;“从类型自动长出 RPC”不等于网络边界从此没有运行时验证。


工程纪律:覆盖率不是证明,门禁也不是装饰

这种开放度必须依赖持续的机械检查,否则生命周期约束很快会退化成文档愿望。

仓库要求 packages/*/*/src 的 per-file 行覆盖率达到 100%。测试政策对这项指标的解释很克制:未覆盖行经常意味着应删除的死代码;行被执行过只是必要条件,不能证明功能在真实产品中工作。PowerShell 等依赖宿主能力的路径也有明确例外,因此不应把这个数字宣传成“没有 bug”。

覆盖率之外还有真实模型 API e2e、无 key 的协议/会话 snapshot,以及浏览器 snapshot。真实 API 测试负责发现 mock 无法暴露的 provider 差异;snapshot 固定外部协议、持久化日志与呈现;e2e 则强调重新读取文件或执行命令来验证真实世界状态,而不是在 agent 自己的回答中搜索“完成”两个字。

仓库的 defensive patterns 也值得读。它们不是抽象格言,而是把已经发生或险些发生的 bug 类别固化成规则:进程可以同时“超时”和“退出码为 0”,所以正交结果必须独立上报;dispose 不只是发出 kill,还要等待子进程真正退出;一个用户回调抛错,不能阻止后续监听器收到通知;不可信子进程拿到的环境变量要去除 key、secret、token 和 password。

Agent Note 的作用也在这里。非平凡修改要留下设计与验证记录,已归档的 note 又不被当作当前规范;真正的现行事实仍回到源码、生成目录和顶层门禁。相比用一个会快速过时的 Note 数量制造传播点,这种“决策有记录,但当前代码才是权威”的关系更值得写。


自我修改:架构入口,不是效果证明

仓库把这组能力放在 packages/extensions,并提供 demo:cordis:agent 可以检查当前 Cordis 进程,在内存中挂载或卸载模型编写的插件。Web 产品中的创造模式同样提供运行时检查、插件实验和 preset 创作入口。

从架构上看,这正好把前面的设计串起来:插件注册可撤销,依赖可声明,组合可以在运行时对账,agent 才可能把自己的 harness 当成可操作对象。若插件只能安装不能干净退出,把修改权交给模型会迅速积累不可追踪状态。官方示例同时警告,临时插件可能影响同一进程中的其他会话;这说明“能动态挂载”本身不是会话隔离保证,插件注册在哪个 realm 和 scope 仍需单独审查。

但“能修改自己的运行时”与“能够稳定自我改进”之间还有很长距离。2026 年出现的 Self-Harness等工作,把 weakness mining、候选修改和回归验证组织成闭环,并用 held-out 任务衡量改进。DeepSeek Harness 当前展示的是组合与生命周期基础设施,不是这一闭环在真实任务上的效果证明。


四个不能被口号遮住的代价

读到这里,可以更准确地评价 DeepSeek Harness。

第一,可逆性依赖作者纪律。运行时能可靠收集和执行 disposer,却不能自动证明 disposer 恢复了原状态。绕开 ctx.effect() 的副作用也不会被追踪。

第二,可逆性有系统边界。外部写入、网络发送和真实世界操作需要幂等、延迟提交或补偿协议;session log 只能记录事实,不能让事实逆转。

第三,热重载不等于无损迁移。Cordis 论文说明,旧组件的 tracked effects 会被回退,新组件从干净状态重新应用;组件自身内存状态若没有放进更长寿命的依赖,不会自动迁移。DSU 式的前向状态迁移仍是未来工作。

第四,项目还没有稳定兼容承诺。当前 SESSION_FORMAT_VERSION 仍是 0,旧日志格式可以被拒绝而不迁移;README 也明确写着 developer preview 会有 breaking changes。适合现在研究和试验,不等于可以不做版本锁定就直接托付长期生产状态。

这些限制并不削弱项目,反而让它的价值更清楚:DeepSeek Harness 没有消灭动态组合的复杂性,而是把复杂性放到能被命名、记录和检查的位置。


结语:真正值得复制的是约束

“一切皆插件”最容易让人关注可替换:今天换模型,明天换工具,后天再换 agent loop。读完源码后,更值得关注的是退出路径。

一个插件加入时注册了什么,离开时是否能逆序回收?一个 provider 消失时,依赖它的 consumer 是否会被正确处理?模型看到的提示词和工具表,能否从持久日志重建?子 agent 继承的是父 agent 当时使用的组合,还是后来重新解析出的另一个版本?沙箱只能做到 partial 时,系统会诚实上报,还是悄悄当成 full?

DeepSeek Harness 对这些问题给出了相当具体的答案:Cordis 管生命周期与依赖,capability seam 管替换边界,session log 管可重建事实,scope 与 preset 管会话级能力,sandbox 把执行强度作为数据返回,类型与测试门禁则把一部分错误提前拒绝。

它还没有证明一个开放 agent 运行时可以天然正确,也没有证明自我修改一定带来更强性能。它证明的是一件更朴素、也更有工程价值的事:当系统准备把越来越多的决定交给插件,甚至交给 agent 自己时,“怎么卸载、怎么重建、怎么承认不知道”必须和“怎么扩展”同等重要。