DeepSeek Harness 架构解析:MCP 和 Skill 如何被统一为 Cordis 插件

本文是 DeepSeek Harness 系列博文的第三篇。面向已有 Agent 框架开发经验的读者,深入源码分析 Harness 如何用统一的插件抽象消解 MCP、Skill 等独立概念。

问题:协议碎片化带来的工程复杂度

主流 Agent 框架的一个结构性问题是:每引入一种外部协议,就引入一套独立的生命周期管理。

以 MCP 为例,Claude Code 的处理方式是:

  • 独立的 mcp.json 配置文件
  • 独立的连接状态机(connecting → connected → reconnecting → failed)
  • 独立的 tool 注册/注销路径
  • 独立的错误处理和重试逻辑

再加上 Skill/Rules、LLM Adapter、File System 各自一套,框架内部变成了多套并行的生命周期管理器,行为模式各异。

DeepSeek Harness 的解法是:让所有能力走同一条注册路径,复用同一个生命周期引擎------Cordis。

Cordis 的核心机制:Effect-scoped Registration

在进入 MCP/Skill 的具体实现之前,先理解 Cordis 提供的基础设施。

Cordis 的注册模型只有一个规则:通过 ctx 发起的任何注册,在该 ctx 对应的插件 fiber 卸载时自动撤销。

typescript 复制代码
export function apply(ctx: Context) {
  // 这个 register 调用返回一个 disposer
  // 但你不需要手动管理它------Cordis 在插件卸载时自动调用
  ctx.tools.register(defineTool({ /* ... */ }))
  
  // 对于非 Cordis-native 的资源,用 ctx.effect() 显式绑定
  ctx.effect(() => {
    const connection = connectExternal()
    return () => connection.close() // 卸载时执行
  })
}

服务依赖通过 inject 声明:

typescript 复制代码
export const inject = ['tools']
// Cordis 保证:在 apply 执行时 ctx.tools 已就绪
// 如果 ctx.tools 的 provider 被热替换,本插件自动 dispose 并重新 apply

这两个机制组合起来,意味着任何外部协议的集成只需要做一件事:把协议的资源映射到 ctx 上的注册调用。

MCP Bridge 源码分析:一个 170 行的 apply

@deepseek-ai/dsh-mcp-client 的入口(packages/mcp/mcp-client/src/index.ts)是一个标准的 Cordis 函数插件:

typescript 复制代码
export const name = 'mcp-client'
export const inject = ['tools']

export async function apply(ctx: Context, config: Config): Promise<void> {
  // 1. 校验 reconnect 配置
  const reconnect = resolveReconnectPolicy(config.reconnect, ...)

  // 2. 抢占 serverName 命名空间(effect-scoped,卸载自动释放)
  ctx.effect(() => {
    names.add(config.serverName)
    return () => void names.delete(config.serverName)
  }, 'mcp-client.serverName')

  // 3. 启动连接 supervisor
  const connection = startConnection(ctx, config, reconnect)

  // 4. 绑定连接到 fiber 生命周期
  ctx.effect(() => {
    return () => connection.dispose()
  }, 'mcp-client.connection')

  // 5. 阻塞 fiber 激活直到首次 tool 发现完成
  const outcome = await connection.ready
  if (outcome.error && config.failOnStartupError) throw ...
}

关键设计决策:

serverName 命名空间是 effect-scoped 的。 通过 ctx.effect() 注册,如果两个实例用了相同的 serverName,后加载的那个在 apply 阶段就会 throw------不是运行时静默覆盖。而当插件被 HMR 热替换时,旧的 namespace reservation 自动释放,新实例可以用相同的名字。

连接 supervisor 是 Cordis-oblivious 的。 startConnection() 返回一个普通对象 { ready, dispose() }------它不知道 Cordis 的存在。Cordis 集成只发生在 ctx.effect(() => () => connection.dispose()) 这一行------把 supervisor 的生命周期绑到 fiber 上。

Tool 注册:从 MCP 协议到 ctx.tools

tools.ts 中的 syncTools 函数执行两阶段切换:

typescript 复制代码
export async function syncTools(
  client: Client,
  ctx: Context,
  opts: ToolBridgeOptions,
  previous: ToolDisposers,  // 上一代 tool 的 disposer 集合
): Promise<ToolDisposers> {
  // Phase 1: Fetch --- 分页遍历 MCP 的 tools/list,构建 ToolDefinition
  const definitions = new Map<string, ToolDefinition>()
  let cursor: string | undefined
  do {
    const response = await listToolsUncached(client, cursor)
    for (const tool of response.tools) {
      const publicName = publicToolName(opts.serverName, tool.name)
      definitions.set(publicName, {
        name: publicName,
        description: tool.description ?? '',
        parameters: tool.inputSchema,
        output: createOutput(tool.name, supportedOutputSchema(tool.outputSchema)),
        execute: createExecutor(client, tool.name, ...),
      })
    }
    cursor = response.nextCursor
  } while (cursor)

  // Phase 2: Swap --- 原子替换 tool 注册
  for (const dispose of previous.values()) dispose()
  const disposers: ToolDisposers = new Map()
  try {
    for (const [publicName, definition] of definitions) {
      disposers.set(publicName, ctx.tools.register(definition))
    }
  } catch (error) {
    // 冲突回滚:全部注销,不留 partial set
    for (const dispose of disposers.values()) dispose()
    return new Map()
  }
  return disposers
}

这段代码的核心洞察:MCP tool 注册和原生 tool 注册走完全相同的 ctx.tools.register() 路径。 这意味着:

  • 模型看到的 tool schema 格式完全一致
  • tools/pre-executetools/executetools/post-execute 事件管道完全一致
  • 权限策略、timeout、compaction 行为完全一致
  • 不存在"MCP tool 是特殊的"这种概念

命名规范化:确定性函数

公共名 mcp__<serverName>__<rawName> 的生成是纯函数:

typescript 复制代码
export function publicToolName(serverName: string, rawName: string): string {
  const joined = `mcp__${serverName}__${rawName}`
  const normalized = joined.replace(INVALID_NAME_CHARS, '_')
  if (normalized === joined && normalized.length <= MAX_PUBLIC_NAME_LENGTH) 
    return normalized
  // 有损规范化时,附加 12 字符 SHA-256 hash 防碰撞
  const hash = createHash('sha256')
    .update(`${serverName}\0${rawName}`)
    .digest('hex').slice(0, HASH_LENGTH)
  return `${normalized.slice(0, MAX_PUBLIC_NAME_LENGTH - HASH_LENGTH - 1)}_${hash}`
}

不依赖连接顺序、不依赖其他 server 的存在------相同的 (serverName, rawName) 输入永远产生相同的公共名。

Reconnect Supervisor:有限状态机

连接管理实现了一个标准的断线重连状态机,核心语义:

scss 复制代码
connecting → connected → (transport close) → backoff → connecting → ...
                                                    └→ (budget exhausted) → disabled

关键参数:

  • initialDelayMs:首次重连延迟(默认 500ms),指数递增
  • maxDelayMs:延迟上限(默认 30s),同时作为"稳定连接"的判定阈值
  • maxAttempts:连续失败次数预算(默认 10)

一个连接存活超过 maxDelayMs 会重置预算------这意味着偶尔崩溃的 server 可以无限恢复,但 crash-loop 会被正确终止。

整个 reconnect 逻辑是通用的连接管理------和 MCP 协议本身无关。 如果未来出现另一种需要 stdio/HTTP 连接的协议,这段逻辑可以被提取复用。

Skill 系统源码分析:Scoped Layered Registry

Skill 的架构比 MCP bridge 更复杂,因为它不只是桥接一个外部协议,而是一个完整的 Capability Seam

Service Definition:SkillRegistry

typescript 复制代码
declare module '@deepseek-ai/cordis' {
  interface Context {
    skills: SkillRegistry
  }
  interface Events {
    'skills/change'(): void
  }
}

export class SkillRegistry extends Service {
  // 分层注册表:全局层 + 每个 scope(agent preset)一层
  private readonly layers = new ScopedLayers<SkillLayer>(...)
  
  registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void
  register(skill: SkillRegistration): () => void
  async list(options: SkillViewOptions = {}): Promise<SkillSummary[]>
  async snapshot(options: SkillViewOptions = {}): Promise<SkillCatalogSnapshot>
  async get(name: string, options: SkillViewOptions = {}): Promise<SkillDefinition | undefined>
}

核心数据结构是 ScopedLayers<SkillLayer>------和 ctx.tools 使用同一种分层模型。一个注册根据调用者的 scope 落入对应的层:

  • 宿主行和仓库插件 → 全局层
  • Agent preset 的 standing composition → 该 preset 的 scope 层
  • 读取时,全局层 + 查看者 scope 链逐层合并,近层同名 entry 直接覆盖远层

Provider 接口

typescript 复制代码
export interface SkillProvider {
  readonly name: string
  readonly list: (options: SkillLookupOptions) => 
    Promise<readonly SkillCandidate[] | SkillProviderObservation>
  readonly get: (candidate: SkillCandidate, options: SkillLookupOptions) => 
    Promise<SkillDefinition | undefined>
}

export interface SkillProviderControl {
  readonly signal: AbortSignal        // 注册被销毁时 abort
  readonly invalidate: () => void     // 通知 registry 清除缓存
}

Provider 注册时拿到一个 SkillProviderControl

  • signal 在精确的这个注册被 dispose 时 abort------provider 可以用它取消进行中的发现工作
  • invalidate() 通知 registry 刷新缓存------只在注册仍然存活时生效

这个设计让 provider 是无状态可替换的:一个文件系统 provider、一个 Redis provider、一个 HTTP registry provider 都实现同一个接口。

Consumer:dsh-tool-skill

Consumer 做的事:

  1. 在每个 agent/pre-step 注入初始 skill catalog(name + description 列表)
  2. 每步之前检查 catalog 是否变化,变了就注入更新
  3. 提供 skill({ name }) tool 给模型主动加载完整 skill body

Skill body 是按需加载 的------catalog 只暴露 name 和 description(控制 token 开销),模型决定是否调用 skill tool 获取完整内容。

运行时注册

除了 provider 发现的 skill,任何插件也可以直接注册 runtime skill:

typescript 复制代码
export const inject = ['skills']

export function apply(ctx: Context) {
  ctx.skills.register({
    name: 'my-runtime-skill',
    description: 'Dynamic skill contributed by a plugin',
    source: 'runtime',
    content: '# Instructions\n...',
  })
  // 插件卸载时自动从 registry 移除
}

和 provider 发现的 skill 在同一个 registry 中合并------优先级由 rank 决定(runtime rank = 250,介于 project 和 user 之间)。

对比:三种能力的注册路径

MCP Tool 原生 Tool Skill
注册目标 ctx.tools.register() ctx.tools.register() ctx.skills.register()registerProvider()
生命周期 fiber dispose → syncTools 回滚 fiber dispose → auto fiber dispose → auto
配置入口 cordis.yml 行 cordis.yml 行 cordis.yml 行
隔离机制 isolate: { tools: true } isolate: { tools: true } scope chain
HMR dispose old → re-apply → re-sync dispose old → re-apply dispose old → re-apply → re-discover
模型可见性 tool schema in prompt tool schema in prompt catalog message + skill tool

三者共享的关键路径:

  1. 插件加载 :Cordis fiber 激活 → apply() 执行
  2. 注册 :调用 ctx.<service>.register() → 返回 disposer
  3. 依赖等待inject 声明 → 框架保证服务就绪
  4. 卸载清理:fiber dispose → 所有 effect 逆序执行
  5. 热替换:配置变更 → dispose old fiber → create new fiber → re-apply

Capability Seam 模式:以 Shell 为参照

MCP 和 Skill 都不是最完整的 Capability Seam 示例。最经典的是 Shell(Bash 执行):

scss 复制代码
dsh-shell (Service Definition)
  ├── dsh-bash-local (Provider: 本机执行)
  ├── dsh-bash-e2b (Provider: E2B 沙箱执行)
  └── dsh-tool-bash (Consumer: 模型可见 tool)

cordis.yml 中切换执行环境:

yaml 复制代码
# 本地执行
- name: '@deepseek-ai/dsh-bash-local'

# 换成 E2B 沙箱------只改这一行
# - name: '@deepseek-ai/dsh-bash-e2b'
#   config:
#     sandboxId: '...'

Consumer (dsh-tool-bash) 完全不变。它只 inject: ['shell'],不知道也不关心后面是本机进程还是远程沙箱。

MCP 在这个框架中的位置:它是 ctx.tools 的一种 Provider,和 defineTool 手写的 tool、和 Skill tool consumer 注册的 tool 共享同一个 Service Definition。

Skill 的位置:ctx.skills 是独立的 Service Definition,有自己的 Provider(filesystem/runtime/badge),Consumer 通过 ctx.tools.register() 暴露 skill tool 给模型。

实际影响:开发一个自定义 Skill Provider

假设你要从公司内部 Git 仓库发现 skill,实现如下:

typescript 复制代码
import type { Context } from '@deepseek-ai/cordis'
import type { SkillProvider, SkillProviderControl } from '@deepseek-ai/dsh-skill'

export const name = 'skill-git-remote'
export const inject = ['skills']

export function apply(ctx: Context) {
  ctx.skills.registerProvider((control: SkillProviderControl) => ({
    name: 'git-remote',
    
    async list(options) {
      // control.signal 在注册被 dispose 时 abort
      const response = await fetch('https://internal.git/api/skills', {
        signal: options.signal ?? control.signal,
      })
      const skills = await response.json()
      return skills.map(s => ({
        name: s.name,
        description: s.description,
        invocation: { modelInvocable: true, userInvocable: true },
        source: 'custom' as const,
        provider: 'git-remote',
        rank: 350,  // 介于 custom dirs(300) 和 user-dsh(400) 之间
        locator: s.url,  // 不透明句柄,get() 时回传
      }))
    },

    async get(candidate, options) {
      const response = await fetch(candidate.locator as string, {
        signal: options.signal ?? control.signal,
      })
      return {
        ...candidate,
        content: await response.text(),
      }
    },
  }))
}

cordis.yml 中挂载:

yaml 复制代码
- id: skill-git-remote
  name: './plugins/skill-git-remote.ts'

完成。不需要修改框架代码,不需要了解 skill catalog 的渲染逻辑,不需要处理 HMR------registerProvider 的 effect-scoped 语义保证了热替换时旧 provider 自动注销、缓存自动失效。

总结:统一抽象的工程价值

Harness 的做法不是"弱化"MCP 和 Skill------MCP bridge 有完整的 reconnect 状态机和两阶段原子 swap,Skill registry 有分层合并和增量 catalog 推送------功能一点不少。

区别在于,这些复杂性被封装在各自的插件内部 ,对外只暴露统一的 ctx.tools.register()ctx.skills.registerProvider()。框架层面不存在"MCP 连接管理器"和"Skill 发现引擎"这种并行的基础设施------有的只是 Cordis fiber 的 effect-scoped lifecycle。

工程价值:

  1. 一套 HMR 规则覆盖所有子系统 ------改 cordis.yml 配置,对应 fiber 热替换
  2. 一套隔离机制覆盖所有服务 ------isolate: { tools: true } 同时隔离原生 tool 和 MCP tool
  3. 一套错误传播语义------inject 的服务消失 → 依赖它的 fiber 自动 dispose
  4. 一套测试基础设施 ------mock ctx.tools 就能测所有 tool 注册者,不区分来源

这是 Cordis 作为 plugin orchestration 框架的核心卖点:把"连接管理"、"发现机制"、"注册/注销"这些每个子系统都要做的事情收敛到一个统一的 effect 模型中。

参考


DeepSeek Harness 系列文章:

  • 第一篇:DeepSeek Harness 框架介绍
  • 第二篇:插件开发实战
  • 第三篇:MCP 和 Skill 如何被统一为 Cordis 插件(本文)
相关推荐
用户337922545681 小时前
Event-Sourced Session:AI Agent 的“会话即事件流“设计
人工智能
cxr8281 小时前
上下文工程框架之11 模块与优先级链和冲突消解、淘汰与版本
人工智能·架构
Cosolar1 小时前
DeepSeek Harness 理解 Harness 的设计哲学 - 可组合的插件运行时
人工智能·设计模式·架构
DFT计算杂谈1 小时前
Janus单层Cr2SSe中的应变可调多压电效应与谷电子学
人工智能·算法·机器学习
今天AI了吗1 小时前
从 LLM 到 Agent Skill:把 AI 底层概念串起来
数据库·人工智能·sql·深度学习·神经网络·算法·机器学习
DS随心转小程序1 小时前
巧用 AI 导出鸭攻克各类难题完善 ChatGPT 输出 word 文档转化工作
人工智能·chatgpt·aigc·word·豆包·deepseek·ai导出鸭
梦想的旅途22 小时前
企微 API 二次开发:结合 AI 打造考勤打卡与报表智能分析系统
人工智能·企业微信
Kari112 小时前
腾讯云 ADP 实施问题解析:回答异常时企业如何组织排查与支持协同?
人工智能
刘新洲2 小时前
别再只做会聊天的 Agent:我用 1 天把工具调用做成了可验证、可评测的工程系统
人工智能·python·openai