ByteNoteByteNote
DeepSeek Harness 能力的三种角色设计
字

字节笔记本

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

DeepSeek Harness 能力的三种角色设计

API中转
¥120

DeepSeek Harness 是 DeepSeek 开源的 agent 框架,架构口号是把一切做成插件,底层由 Cordis 插件框架驱动。插件写多了就会撞上一个现实问题:有些能力足够通用,注定要支持可替换的实现,比如执行一条 Bash 命令,本地能跑,将来也可能换一种执行环境。对这类能力,harness 给出了固定的组织方式:把一项能力拆成 Service Definition、Service Provider、Consumer 三种角色。本文先讲清这套拆分的设计逻辑,再完整走一遍实现流程。

三种角色各管一段

角色需要独立演进或替换时,就放进不同的包;没有这种需求的话,一个包完全可以同时承担多个角色。以 Bash 执行能力为例,官方把它拆成了三个包:

  • dsh-shell:Service Definition,定义 Cordis 服务以及 Bash 请求和结果类型,它是一份契约,不含任何实现。
  • dsh-bash-local:Service Provider,在本地计算机上执行命令,是可被替换的具体实现。
  • dsh-tool-bash:Consumer,把这项能力公开为模型可调用的工具。

三种角色与依赖方向

有一个判断值得记牢:完整的能力才构成接缝(seam),任何单一角色都不是接缝。接缝覆盖的是契约、实现加工具入口的整条链路,而不是其中某一块。

拆开之后能得到什么

提供方可替换。 同一个 Service Definition 可以挂多个提供方,在 cordis.yml 里改一行配置即可切换。更换提供方时,服务定义和工具两层保持不变,调用方感知不到任何变化。

三方独立演进。 调用方一旦依赖了服务定义的约定,这份契约就很少再改动;提供方可以专心优化性能和安全性;消费方则能自由调整能力向模型呈现的方式,三层互不牵扯。

依赖彻底解耦。 提供方依赖定义,消费方也依赖定义,但提供方和消费方互不依赖。执行实现升级一版,工具层不需要跟着适配;工具层改了呈现方式,执行层也毫不知情。至于 harness 内置了哪些能力接缝、分别对应哪些包,官方维护了一份 seam 目录可以按图索骥。

动手实现:三步拆出一个能力

下面用一个自造的 my-cap 能力把三种角色各写一遍,前提是你已经掌握 Cordis 插件与服务的基本写法。包结构对照真实项目,可以直接照搬。

三步开发流程与设计要点

第一步:编写 Service Definition

ts
// packages/my-cap/my-cap/src/index.ts
import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    myCap: MyCapService
  }
}

export abstract class MyCapService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'myCap')
  }

  /** Execute the capability. */
  abstract execute(request: MyCapRequest): Promise<MyCapResult>
}

export interface MyCapRequest {
  input: string
}

export interface MyCapResult {
  output: string
}

这一步产出两样东西:一个挂在 Context 上的抽象服务类,以及请求、结果两个接口。类型归定义所有,是整套拆分的关键约定,后面两层都从这里引用类型。

第二步:编写 Service Provider

ts
// packages/my-cap/my-cap-local/src/index.ts
import type { Context } from '@deepseek-ai/cordis'
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'

class MyCapLocal extends MyCapService {
  async execute(request: MyCapRequest): Promise<MyCapResult> {
    // Local provider behavior.
    return { output: request.input.toUpperCase() }
  }
}

export const name = 'my-cap-local'

export function apply(ctx: Context) {
  ctx.plugin(MyCapLocal)
}

提供方继承抽象服务类并实现 execute 方法,再以插件形式把自己挂进框架。示例实现只是把输入转成大写,真实场景里这里才是真正干活的地方,性能与安全的优化也都发生在这层。

第三步:编写消费方

ts
// packages/my-cap/tool-my-cap/src/index.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'tool-my-cap'
export const inject = ['tools', 'myCap']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'my_cap',
    description: 'Execute my capability.',
    parameters: {
      input: { type: 'string', required: true },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      const result = await ctx.myCap.execute({ input: args.input })
      return result.output
    },
  }))
}

消费方同时注入 tools 和 myCap 两个服务,用 defineTool 把能力注册成名为 my_cap 的模型工具,执行时转发给服务。模型看到的是工具,真正干活的是服务,两层各管各的。

在 cordis.yml 中组合

yaml
- name: '@deepseek-ai/dsh-my-cap-local'
- name: '@deepseek-ai/dsh-tool-my-cap'

两行配置,能力上线。哪天想换实现,只替换提供方那一行,定义和工具原样不动。

三条设计要点

第一,不要预防性拆分。只有当角色确实需要独立演进时才拆包,简单的工具插件一个包写完反而更好维护。第二,请求与结果类型归 Service Definition 所有,提供方和消费方都只依赖定义包,这是依赖解耦的前提。第三,显式优于隐式:默认值应当通过显式的 resolve(request): Spec 步骤处理,而不是藏在 run() 里用兜底默认值糊弄过去,否则契约里看不出行为的真实形状。

顺着同一套模式,还可以为 LLM 这类外部依赖编写适配器,把模型提供方也纳入同样的接缝管理。动手之前先想清楚自己处在哪种角色、要不要拆包,通常能省掉日后大量的重构。

相关文章

分享: