DeepSeek Harness: 能力三角色

最后更新:2026-08-31

Cordis 的能力系统是"一切皆插件"的底层支撑。它将一个功能拆成三个角色:定义接口的人、实现接口的人、使用接口的人。这种分离让替换实现变得像换电池一样简单——拔掉旧的,插上新的,系统正常运行。

💡 提示:能力三角色的核心价值是"可替换性"——替换一个 Provider 就替换了整个产品行为,而 Definition 和 Consumer 完全不用改。这就是接缝(seam)的力量。

📋 前置知识:已完成 19-service.md,理解 Service 基类

1. 你将学到


2. 能力系统概述

能力三角色模型

(1) 为什么需要三角色

没有能力系统时,功能直接硬编码在代码中:

TYPESCRIPT
// ❌ 硬编码实现
class FileAnalyzer {
  analyze(path: string) {
    const stat = fs.statSync(path)  // 只能用本地文件系统
    return { size: stat.size, type: 'local' }
  }
}

有了能力系统,功能被拆成三层:

TYPESCRIPT
// ✅ 三角色分离
// Definition: 声明接口
interface FileStats {
  getSize(path: string): Promise<number>
  getType(path: string): Promise<string>
}

// Provider: 实现(本地)
class LocalFileStats implements FileStats { ... }

// Provider: 实现(远程沙箱)
class SandboxFileStats implements FileStats { ... }

// Consumer: 使用(不关心具体实现)
class FileAnalyzer {
  constructor(private stats: FileStats) {}
  analyze(path: string) {
    const size = await this.stats.getSize(path)
    return { size }
  }
}

(2) ▶ 示例 2

100%
graph LR
    DEF[Definition<br/>声明接口] --> PROV[Provider<br/>实现接口]
    PROV --> CON[Consumer<br/>使用接口]
    CON --> DEF

(3) 类比

角色 类比 现实对应
Definition 电源插座标准 国标插座规格
Provider 插座实现 墙上的插座
Consumer 用电设备 电视机、冰箱

电视机不关心电从哪个发电站来,只关心插座符合标准。同样,Consumer 不关心 Provider 是谁,只关心 Definition 定义的接口。


3. Definition:声明接口

(1) 定义能力

Definition 声明能力的接口——不包含实现,只描述"这个能力能做什么":

TYPESCRIPT
import { defineCapability } from '@deepseek-ai/cordis'

export const FileStats = defineCapability({
  name: 'file-stats',
  description: 'File statistics and metadata access',
  interface: {
    getSize(path: string): Promise<number>
    getType(path: string): Promise<string>
    exists(path: string): Promise<boolean>
    list(dir: string): Promise<string[]>
  }
})

(2) Definition 的要素

要素 说明
name 能力标识,全局唯一
description 能力描述
interface TypeScript 接口定义

(3) 为什么分开定义

Definition 独立于 Provider 的好处:

(4) 约定

Definition 通常放在独立文件中,与 Provider 分离:

TEXT 📖 仅展示
capabilities/
├── file-stats/
│   ├── definition.ts    ← Definition
│   ├── local.ts         ← Provider (本地实现)
│   └── sandbox.ts       ← Provider (沙箱实现)

4. Provider:实现接口

(1) 实现能力

Provider 实现 Definition 声明的接口:

TYPESCRIPT
import { FileStats } from './definition'

export default class LocalFileStatsProvider extends Service {
  static inject = ['fs']

  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    ctx.implement(FileStats, {
      async getSize(path: string) {
        const stat = await ctx.fs.stat(path)
        return stat.size
      },
      async getType(path: string) {
        const stat = await ctx.fs.stat(path)
        return stat.isDirectory ? 'directory' : 'file'
      },
      async exists(path: string) {
        try {
          await ctx.fs.stat(path)
          return true
        } catch {
          return false
        }
      },
      async list(dir: string) {
        const entries = await ctx.fs.readdir(dir)
        return entries.map(e => e.name)
      }
    })
  }
}

(2) ctx.implement()

ctx.implement(capability, implementation) 将实现注册到能力:

TYPESCRIPT
ctx.implement(FileStats, {
  getSize: async (path) => { ... },
  getType: async (path) => { ... },
  // 必须实现 interface 的所有方法
})

如果缺少方法,TypeScript 编译时报错。

(3) ▶ 示例 3

本地 Provider:

TYPESCRIPT
export default class LocalFileStatsProvider extends Service {
  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    ctx.implement(FileStats, {
      async getSize(path) {
        const stat = await ctx.fs.stat(path)
        return stat.size
      },
      async getType(path) {
        return (await ctx.fs.stat(path)).isDirectory ? 'directory' : 'file'
      },
      async exists(path) {
        try { await ctx.fs.stat(path); return true } catch { return false }
      },
      async list(dir) {
        return (await ctx.fs.readdir(dir)).map(e => e.name)
      }
    })
  }
}

远程沙箱 Provider:

TYPESCRIPT
export default class SandboxFileStatsProvider extends Service {
  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    ctx.implement(FileStats, {
      async getSize(path) {
        const resp = await fetch(`http://sandbox:8080/stat?path=${path}`)
        return (await resp.json()).size
      },
      async getType(path) {
        const resp = await fetch(`http://sandbox:8080/stat?path=${path}`)
        return (await resp.json()).type
      },
      async exists(path) {
        const resp = await fetch(`http://sandbox:8080/exists?path=${path}`)
        return (await resp.json()).exists
      },
      async list(dir) {
        const resp = await fetch(`http://sandbox:8080/ls?dir=${dir}`)
        return (await resp.json()).entries
      }
    })
  }
}

5. Consumer:使用接口

(1) 消费能力

Consumer 通过 inject 声明依赖,通过 ctx 使用能力:

TYPESCRIPT
export const inject = ['file-stats']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'file_info',
    description: 'Get file information',
    parameters: {
      type: 'object',
      properties: {
        path: { type: 'string', description: 'File path' }
      },
      required: ['path']
    },
    async execute({ path }, ctx) {
      const size = await ctx['file-stats'].getSize(path)
      const type = await ctx['file-stats'].getType(path)
      return { path, size, type }
    }
  }))
}

(2) Consumer 不知道 Provider 是谁

Consumer 只依赖 Definition 定义的接口,不关心具体实现:

TYPESCRIPT
// Consumer 代码完全相同,无论底层是 Local 还是 Sandbox
const size = await ctx['file-stats'].getSize(path)

(3) ▶ 示例 3

TYPESCRIPT
// 通过类型扩展
declare module '@deepseek-ai/cordis' {
  interface Context {
    'file-stats': {
      getSize(path: string): Promise<number>
      getType(path: string): Promise<string>
      exists(path: string): Promise<boolean>
      list(dir: string): Promise<string[]>
    }
  }
}

6. seam(接缝)概念

(1) 什么是 seam

seam 是 Definition 和 Provider 之间的接合点——它是系统中可以"拆开替换"的地方:

100%
graph LR
    CON[Consumer] -->|依赖| DEF[Definition<br/>seam 接缝]
    DEF -->|实现| PROV_A[Provider A<br/>本地实现]
    DEF -.->|替换为| PROV_B[Provider B<br/>沙箱实现]

(2) seam 的价值

TEXT 📖 仅展示
没有 seam:
  Consumer → Provider A (硬编码,无法替换)

有 seam:
  Consumer → Definition (seam) → Provider A
                        (seam) → Provider B (替换!)

seam 让系统在每个能力点都是可替换的。

(3) 识别 seam

判断一个接口是否是好的 seam:

标准 好 seam 差 seam
抽象度 恰到好处 过细或过粗
实现数 可能有多种实现 只有一种可能
变更频率 实现可能变化 实现永远不变
依赖方向 Consumer 依赖接口 Consumer 依赖实现

(4) seam 的粒度

TEXT 📖 仅展示
粗粒度 seam: FileSystem(整个文件系统可替换)
中粒度 seam: FileStats(文件统计可替换)
细粒度 seam: FileSize(文件大小查询可替换)

粒度太粗 → 替换代价大;粒度太细 → 接口碎片化。选择中等粒度。


7. 能力注册图

(1) 注册流程

100%
graph TB
    DEF[defineCapability<br/>声明接口] --> REG[注册 Definition]
    REG --> PROV1[Provider A implement]
    REG --> PROV2[Provider B implement]
    PROV1 --> ACTIVE_A[当前激活: A]
    PROV2 --> WAIT_B[等待中: B]
    ACTIVE_A --> CONSUMER[Consumer 使用]

(2) 替换流程

100%
graph LR
    OLD[Provider A<br/>当前激活] -->|卸载| INACTIVE_A[已停用]
    NEW[Provider B<br/>新注册] -->|implement| ACTIVE_B[当前激活]
    ACTIVE_B --> CONSUMER[Consumer<br/>自动切换]

(3) 多能力协作

100%
graph TB
    FS_DEF[FileStats Definition] --> FS_PROV[FileStats Provider]
    DB_DEF[Database Definition] --> DB_PROV[Database Provider]
    
    FS_PROV --> TOOL[file_info Tool]
    DB_PROV --> TOOL
    TOOL --> AGENT[Agent]

8. 替换一个 Provider = 替换整个产品行为

(1) 核心价值

这是能力系统最强大的特性:

TEXT 📖 仅展示
场景: 从本地开发切换到沙箱执行

1. 卸载 LocalFileStatsProvider
2. 加载 SandboxFileStatsProvider
3. 所有 Consumer 自动使用沙箱实现
4. Consumer 代码零修改

(2) 配置切换

YAML
# 本地开发
plugins:
  file-stats:
    $insert: ./providers/local-file-stats

# 沙箱环境(只需改这一行)
plugins:
  file-stats:
    $replace: ./providers/sandbox-file-stats

(3) 运行时切换

TYPESCRIPT
// 通过 $replace 动态切换
ctx.on('config/updated', (config) => {
  if (config.environment === 'sandbox') {
    // 框架自动重载,切换到 Sandbox Provider
  }
})

(4) A/B 测试

YAML
# A 组: 本地实现
realms:
  group-a:
    plugins:
      file-stats:
        $insert: ./providers/local-file-stats
  
  group-b:
    plugins:
      file-stats:
        $insert: ./providers/sandbox-file-stats

❓ 常见问题

Q Definition 必须是 interface 吗?
A 是的。Definition 描述的是纯接口(只有方法签名,没有实现)。如果需要共享代码,放在独立的工具模块中。
Q 一个 Definition 可以有多个 Provider 同时活跃吗?
A 默认不行,后注册的覆盖先注册的。如果需要共存,使用 realm 隔离。
Q Provider 替换时,Consumer 会中断吗?
A 会有短暂中断。旧 Provider 卸载后、新 Provider 加载前,能力暂时不可用。Consumer 应该处理 undefined 的情况。
Q 如何判断一个功能是否需要抽象为能力?
A 问自己:这个功能将来可能有不同实现吗?如果有,抽象为能力;如果确定只有一种实现,直接用 Service。
Q seam 和 inject 有什么区别?
A inject 是依赖声明机制("我需要 X"),seam 是可替换性设计("X 可以被替换")。inject 是 seam 的前提——Consumer 通过 inject 声明依赖,seam 保证依赖可替换。
Q 能力系统增加了代码复杂度,值得吗?
A 对于简单项目可能不值得。但当项目有多种部署环境(本地/沙箱/远程)或需要 A/B 测试时,能力系统的收益远大于成本。 ---

📖 小节


📝 作业

1. ⭐ 基础题:定义一个 TimeService 能力(Definition),包含 now(): number 方法。实现一个 LocalTimeProvider,注册后通过 Consumer 插件调用。

2. ⭐⭐ 进阶题:再实现一个 MockTimeProvider(返回固定时间戳),用 $replace 切换 Provider。验证 Consumer 的调用结果从真实时间变为固定时间,Consumer 代码无需修改。

3. ⭐⭐⭐ 挑战题:设计一个 SearchEngine 能力,定义 search(query: string): Promise<string[]> 接口。实现两个 Provider:LocalGrepProvider(用 grep 搜索)和 RemoteAPIProvider(调用搜索 API)。用 realm 配置让两个 Agent 分别使用不同的搜索实现,验证隔离效果。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏