DeepSeek Harness 实战:写一个可替换的能力 菜鸟教程
上一章节讲了三角色的概念,这一篇我们将按照官方教程把它写成代码。
我们要实现一个叫 myCap 的能力:输入一段文本,输出全部大写,它小到一眼看懂,又完整覆盖 Definition、Provider、Consumer 三个包。
三步走:Service Definition(抽象类 + 类型)→ Service Provider(实现子类)→ Consumer(defineTool)。
最后在 cordis.yml 里把 Provider 与 Consumer 组合加载。
第一步:编写 Service Definition
Service Definition 声明能力本身:服务叫什么、怎么调用、请求与结果的类型是什么。
它不包含任何实现逻辑,只有一个抽象方法和两个接口。
抽象类 MyCapService 继承自 Service,通过 super(ctx, ‘myCap’) 注册为命名服务。
declare module 声明合并让 ctx.myCap 拥有类型,这个技巧在第 14 篇讲过。
实例
// 文件路径:packages/my-cap/my-cap/src/index.ts
import { Service, type Context } from ‘@deepseek-ai/cordis’
// 声明合并:让 ctx.myCap 在 TypeScript 里有类型提示
declare module ‘@deepseek-ai/cordis’ {
interface Context {
myCap: MyCapService
}
}
// 抽象类:Definition 包只声明契约,不写实现
export abstract class MyCapService extends Service {
constructor(ctx: Context) {
super(ctx, ‘myCap’) // 注册为命名服务 ctx.myCap
}
/** Execute the capability. */
abstract execute(request: MyCapRequest): Promise<MyCapResult>
}
// 请求类型:调用方必须提供 input
export interface MyCapRequest {
input: string
}
// 结果类型:能力返回 output
export interface MyCapResult {
output: string
}
abstract execute 是唯一的抽象方法,Provider 必须实现它。
MyCapRequest 拥有 input 字段,MyCapResult 拥有 output 字段。
请求与结果类型由 Definition 包拥有,Provider 与 Consumer 都从它导入。
第二步:编写 Service Provider
Service Provider 继承抽象类,填上真正的行为。
MyCapLocal 把 input 变成大写后返回。
export const name 声明插件名,apply 里通过 ctx.plugin(MyCapLocal) 注册服务实现。
实例
// 文件路径: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’
// 实现类:只依赖 Definition 包
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.myCap 的实际服务
ctx.plugin(MyCapLocal)
}
Provider 只依赖 Definition 包里的抽象类和类型。
它不关心模型怎么调用,也不关心工具长什么样。
将来想换一种实现,例如在远程服务器上执行,只需再写一个 Provider 子类。
第三步:编写消费方 Consumer
Consumer 把能力包装成模型可以调用的工具。
inject 声明依赖 tools 与 myCap,保证这两个服务就绪后再执行 apply。
defineTool 声明工具 schema,模型看到 my_cap 工具后就会按 parameters 传参。
execute 里调用 ctx.myCap.execute,把结果作为工具输出返回。
实例
// 文件路径: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’
// 依赖声明:tools 提供注册入口,myCap 提供服务实现
export const inject = [’tools’, ‘myCap’]
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: ‘my_cap’, // 面向模型的工具名
description: ‘Execute my capability.’,
// 参数 schema:模型按它生成参数
parameters: {
input: { type: ‘string’, required: true },
},
// 输出 schema 与渲染方式
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
},
}))
}
模型不直接认识 ctx.myCap,它只认识 my_cap 工具。
Consumer 是两者之间的桥梁,也负责把结果渲染成模型可见的文本。
myCap 三角色结构与数据流
把三个包拼在一起,数据从模型出发,流经 Consumer、Definition,落到 Provider。
模型先调用 my_cap 工具,这一步只发生在 Consumer 层。
Consumer 把参数包装成 MyCapRequest,交给 ctx.myCap.execute(request)。
Definition 把调用委托给当前加载的 Provider 实现。
Provider 算出结果后,沿原路把 MyCapResult 返回给 Consumer。
Consumer 把 output 渲染成文本,作为工具结果交给模型。
整条链路里,Provider 被换掉时,Consumer 与模型都无感知。
在 cordis.yml 中组合
三个包写完只是零件,还要在 cordis.yml 里把 Provider 与 Consumer 一起加载。
加载 Provider,ctx.myCap 才有实现;加载 Consumer,模型才有 my_cap 工具。
实例
# 文件路径:cordis.yml
# 先加载 Provider,让 ctx.myCap 有实现
- name: ‘@deepseek-ai/dsh-my-cap-local’
# 再加载 Consumer,让模型能用 my_cap 工具
- name: ‘@deepseek-ai/dsh-tool-my-cap’
加载顺序上,Cordis 会按依赖自动排序,因此 Provider 与 Consumer 谁先谁后并不关键。
想换成别的 Provider 时,只改第一行,Consumer 保持不变。
设计要点
官方文档给了三条设计要点,写能力之前最好先记住。
第一条,不要预防性拆分。
只有角色需要独立演进时,才把它们放进不同的包。
简单的工具插件不需要拆分,一个包承担多个角色完全合法。
第二条,Service Definition 拥有 Request/Result 类型。
Provider 和 Consumer 只依赖 Definition 包,因此请求与结果类型必须由 Definition 定义。
第三条,显式优于隐式。
实现应通过显式的 resolve(request) 步骤处理默认值,而不是在 run() 里隐藏 ?? default。
Bash seam 就是例子:ShellExecRequest 的 workdir、timeoutMs 是可选的,工具层先调用 ctx.shell.resolve(request) 得到全部字段必填的 ShellExecSpec,再交给 run()。
不要在 run() 内部悄悄补默认值;先 resolve,再执行,边界才清晰。
小结与自测
一句话总结:写一个可替换能力 = Definition 抽象类 + Provider 子类 + Consumer 工具,再加一份 cordis.yml。
自测题一:MyCapRequest 和 MyCapResult 应该定义在哪个包里?
自测题二:Provider 与 Consumer 之间有没有依赖?
自测题三:为什么简单的工具插件不建议预防性拆分?
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/gpt/post/20260818/DeepSeek-Harness-%E5%AE%9E%E6%88%98%E5%86%99%E4%B8%80%E4%B8%AA%E5%8F%AF%E6%9B%BF%E6%8D%A2%E7%9A%84%E8%83%BD%E5%8A%9B-%E8%8F%9C%E9%B8%9F%E6%95%99%E7%A8%8B/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com