DeepSeek Harness 实战:写一个可替换的能力

2026-09-08 00:00    #AI   #工具   #DeepSeek  

实现一个叫 myCap 的能力:输入一段文本,输出全部大写。三步走:

Service Definition(抽象类 + 类型)→ Service Provider(实现子类)→ Consumer(defineTool),最后在 cordis.yml 里把 Provider 与 Consumer 组合加载。

第一步:编写 Service Definition

声明能力本身:服务叫什么、怎么调用、请求与结果的类型。不含实现逻辑,只有一个抽象方法和两个接口。

 1// 文件路径:packages/my-cap/my-cap/src/index.ts
 2import { Service, type Context } from '@deepseek-ai/cordis'
 3
 4// 声明合并:让 ctx.myCap 在 TypeScript 里有类型提示
 5declare module '@deepseek-ai/cordis' {
 6  interface Context {
 7    myCap: MyCapService
 8  }
 9}
10
11// 抽象类:Definition 包只声明契约,不写实现
12export abstract class MyCapService extends Service {
13  constructor(ctx: Context) {
14    super(ctx, 'myCap') // 注册为命名服务 ctx.myCap
15  }
16
17  /** Execute the capability. */
18  abstract execute(request: MyCapRequest): Promise<MyCapResult>
19}
20
21// 请求类型:调用方必须提供 input
22export interface MyCapRequest {
23  input: string
24}
25
26// 结果类型:能力返回 output
27export interface MyCapResult {
28  output: string
29}

第二步:编写 Service Provider

Provider 继承抽象类,填上真正的行为。

 1// 文件路径:packages/my-cap/my-cap-local/src/index.ts
 2import type { Context } from '@deepseek-ai/cordis'
 3import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
 4
 5// 实现类:只依赖 Definition 包
 6class MyCapLocal extends MyCapService {
 7  async execute(request: MyCapRequest): Promise<MyCapResult> {
 8    // Local provider behavior.
 9    return { output: request.input.toUpperCase() }
10  }
11}
12
13export const name = 'my-cap-local'
14
15export function apply(ctx: Context) {
16  // 把实现类作为插件加载,注册成 ctx.myCap 的实际服务
17  ctx.plugin(MyCapLocal)
18}

Provider 只依赖 Definition 包里的抽象类和类型,不关心模型怎么调用、工具长什么样。将来想换实现(如远程服务器),只需再写一个 Provider 子类。

第三步:编写消费方 Consumer

Consumer 把能力包装成模型可调用的工具。

 1// 文件路径:packages/my-cap/tool-my-cap/src/index.ts
 2import type { Context } from '@deepseek-ai/cordis'
 3import { defineTool } from '@deepseek-ai/dsh-tools'
 4
 5export const name = 'tool-my-cap'
 6// 依赖声明:tools 提供注册入口,myCap 提供服务实现
 7export const inject = ['tools', 'myCap']
 8
 9export function apply(ctx: Context) {
10  ctx.tools.register(defineTool({
11    name: 'my_cap', // 面向模型的工具名
12    description: 'Execute my capability.',
13    parameters: {
14      input: { type: 'string', required: true },
15    },
16    output: {
17      schema: { type: 'string' },
18      render: (_args, value) => [{ type: 'text', text: value }],
19    },
20    async execute(args) {
21      const result = await ctx.myCap.execute({ input: args.input })
22      return result.output
23    },
24  }))
25}

模型不直接认识 ctx.myCap,它只认识 my_cap 工具。Consumer 是两者之间的桥梁,也负责把结果渲染成模型可见的文本。

数据流

模型 → my_cap 工具(Consumer 层)→ Consumer 包装参数为 MyCapRequest 交给 ctx.myCap.execute(request) → Definition 委托给当前加载的 Provider → Provider 返回 MyCapResult → Consumer 把 output 渲染成文本作为工具结果交给模型。整条链路里 Provider 被换掉时,Consumer 与模型都无感知

在 cordis.yml 中组合

1# 文件路径:cordis.yml
2# 先加载 Provider,让 ctx.myCap 有实现
3- name: '@deepseek-ai/dsh-my-cap-local'
4
5# 再加载 Consumer,让模型能用 my_cap 工具
6- name: '@deepseek-ai/dsh-tool-my-cap'

加载顺序上 Cordis 会按依赖自动排序,Provider 与 Consumer 谁先谁后不关键。想换 Provider 时只改第一行。

设计要点(三条)

  1. 不要预防性拆分:只有角色需要独立演进时才拆包。简单的工具插件不需要拆分,一个包承担多个角色完全合法。
  2. Service Definition 拥有 Request/Result 类型:Provider 和 Consumer 只依赖 Definition 包,因此请求与结果类型必须由 Definition 定义。
  3. 显式优于隐式:实现应通过显式的 resolve(request) 步骤处理默认值,而不是在 run() 里隐藏默认值。Bash seam 就是例子:ShellExecRequest 的 workdir、timeoutMs 可选,工具层先调用 ctx.shell.resolve(request) 得到全部字段必填的 ShellExecSpec,再交给 run()
不要在 run() 内部悄悄补默认值;先 resolve,再执行,边界才清晰。