创见博客
DeepSeek Harness 架构解析:从源码看「一切皆插件」是怎么实现的
七崽爱吃小饼干2026/09/21阅读 0

几周时间,200k+ star。

这个数字放在 agent 框架这个已经挤满选手的赛道里,有点反常。模型能力各家大差不差,一个 harness 想靠功能差异化出圈很难。DeepSeek Harness 的差异化不在功能列表上,而在它的一句设计口号里:Everything is a plugin。

这篇文章不聊怎么用它,只拆源码:插件长什么样、按下启动命令后发生了什么、一堆插件之间靠什么互相通信。

一、DeepSeek Harness 是什么

DeepSeek Harness(简称 dsh)是 DeepSeek AI 开源的 agent harness(智能体框架),底层由 Cordis 驱动。

Cordis 是一套以插件为中心的运行时。dsh 把从 UI 界面到最底层 agent loop 的所有能力,全部拆成了独立的插件。换个说法:它没有不可替换的核心,每个环节都可以被你的插件接管。

跑起来有两种方式。

第一种,直接跑:

bash
npx @deepseek-ai/dsh web

第二种,从源码启动:

bash
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web

启动后配置好 key 就能用,界面和交互跟 codex 桌面版很像。但真正值得看的是源码里的这套插件机制。下面从目录结构开始。

二、源码分析

目录结构

bash
deepseek-harness/
├── apps/        # 最终可运行的应用
├── packages/    # dsh 的插件包
├── vendor/      # Vendored Cordis 源码
├── python/      # Python SDK 和运行时
├── native/      # 原生扩展
├── examples/    # 用户可运行的 cordis.yml 示例
├── docs/        # 架构、子系统和开发文档
├── website/     # VitePress 文档站
├── scripts/     # 构建、检查、生成器
└── .agents/     # Agent 工作流和 Agent Notes

packages/ 是仓库的主体。每个子目录是一个独立的 npm workspace package,命名统一为 @deepseek-ai/dsh-<name>,并按能力分组:

text
packages/
├── core/    # 核心 agent 能力
├── api/
├── llm/
├── shell/
├── client/
└── ...

注意 vendor/cordis:Cordis 不是当依赖装的,而是直接把源码 vendored 进仓库。所以下面讲的 Context、Fiber、Loader 这些机制,都在你本地仓库里能翻到。

插件写法

插件本质上就是一个导出 apply 函数的 TypeScript 模块。框架加载它时调用 apply,传入一个上下文对象 ctx,插件通过 ctx 注册自己的能力。

有三种写法。

函数形式,最常见,够用:

typescript
import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  // 必需的依赖在 apply 执行前就已经就绪
  console.log('[hello-plugin] plugin loaded!')
}

对象形式,需要声明依赖时更方便:

typescript
import type { Context } from '@deepseek-ai/cordis'

export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx: Context) {
    // ...
  },
}

类形式,当这个插件要对外提供服务时使用:

typescript
import { Service, type Context } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  static inject = ['tools']

  constructor(ctx: Context) {
    super(ctx, 'myService')
    // 同步初始化放在构造函数里
  }
}

写法本身没什么门槛。真正的问题在后面:这些插件是怎么被组织成一棵树、又是怎么互相调用的。

启动流程:从一条命令到一棵插件树

从启动命令 pnpm dsh --profile web 反查入口。package.json 里:

json
"scripts": {
  "dsh": "node --import tsx/esm apps/cli/src/bin.ts"
}

入口是 apps/cli/src/bin.ts。里面按 mode 分发:

typescript
switch (invocation.mode) {
  case 'profile': {
    const { runProfile } = await import('./profile-boot.ts')
    await runProfile({
      environment: loadLayeredEnv('dsh'),
      profile: invocation.profile,
      patchFiles: invocation.patches,
      args: invocation.args,
    })
    break
  }
}

runProfile 本身很薄,核心就是调 boot:

typescript
import { boot } from '@deepseek-ai/dsh-app-boot'

export async function runProfile() {
  const ctx = await boot(/* ... */)
}

boot 在 packages/boot/app-boot 里,去掉中间过程后只剩三步:

typescript
export async function boot(...) {
  const ctx = new Context()

  await ctx.plugin(Loader)

  await mountRootInclude(ctx, absoluteConfigPath, patches)

  return ctx
}

三步分别对应:

  1. new Context():Cordis 的初始化,负责管理上下文与插件。
  2. ctx.plugin(Loader):把 Loader 挂到上下文上。
  3. mountRootInclude(...):用 Loader 加载 packages 下的插件,组装成插件树。

启动 = 建 Context → 挂 Loader → 按配置长出插件树。 没有更长的主流程,也没有硬编码的能力清单。

Fiber:插件的生命周期单元

插件树上的每个节点,在 Cordis 里对应一个 Fiber。它跟 React Fiber 的思路很像:Fiber 是插件运行实例的生命周期管理单元,所有 Fiber 组成一棵可暂停、可更新、可销毁的树。

每个 Fiber 负责四件事:

  1. 保存插件自己的 Context 和配置
  2. 等待 inject 声明的依赖满足
  3. 执行插件代码
  4. 卸载时清理插件资源

插件通过 ctx.plugin() 创建 Fiber,源码在 vendor/cordis/src/registry.ts:

typescript
const fiber = new Fiber(
  this.ctx,
  config,
  Inject.resolve(plugin.inject),
  runtime,
  getOuterStack,
)

这一步也解释了 inject 的意义:它不是加载顺序的提示,而是 Fiber 在真正执行 apply 之前必须等到的前置条件。依赖没满足,这个 Fiber 就停在等待态,不会执行。

插件通信

插件拆散之后,下一个问题就是它们怎么协作。dsh 里主要有三种方式。

1. 服务注入 / 服务调用:一个插件提供服务,另一个通过 inject 拿到并调用。

Provider 注册服务:

typescript
ctx.provide('llm', llmService)

Consumer 声明依赖:

typescript
export default {
  inject: ['llm'],

  apply(ctx) {
    ctx.llm.stream(request)
  },
}

2. 事件总线:发送方广播,任何插件都能监听,属于一对多。

发送方:

typescript
ctx.emit('session/event', session, event)

接收方:

typescript
ctx.on('session/event', (session, event) => {
  // 处理事件
})

3. Waterfall 扩展链:多个插件按顺序处理同一个事件,监听器必须调用 next() 才能把处理权交给下一个。

typescript
ctx.on('tools/execute', async (exec, next) => {
  const result = await next()
  return transform(result)
})

它的调用顺序是洋葱式的:每个插件都能在真正的执行器前后插入逻辑。

text
Plugin A
  ↓ next()
Plugin B
  ↓ next()
实际执行器
  ↓
Plugin B 后处理
  ↓
Plugin A 后处理

这三种方式覆盖了绝大多数协作场景:需要强依赖就走服务注入,需要广播就走事件,需要拦截或改写一条处理链就用 Waterfall。前面两个负责「连起来」,Waterfall 负责「插进去」——想在工具真正执行前做校验、执行后加工结果,靠的就是它。

三、总结

这篇文章没有展开 dsh 作为 agent 本身的能力,因为那部分各家已经趋同。dsh 能出圈,靠的是它把整个系统拆成插件后带来的组合能力:主流程只有「建 Context、挂 Loader、长插件树」三步,剩下的全交给插件;插件之间用服务注入、事件、Waterfall 三种方式协作,任何一环都能被替换或拦截。

换句话说,它的价值不在于替你写好了多少能力,而在于把改写的成本降到了写一个插件。

评论
0/100