DeepSeek Harness 为什么需要 Cordis:从 Everything is Plugin 开始看

最近在学 Agent,也在看 DeepSeek Harness,就是 DSH。

DSH 里面有一个很核心的底层项目叫 Cordis,继续往下看 DSH 的很多实现,最后都会碰到它。

Cordis 本身的核心代码其实不算多:

text 复制代码
context.ts
registry.ts
fiber.ts
service.ts
reflect.ts
events.ts

不过还是老习惯,看代码之前先看这个项目到底要解决什么问题。

DSH 有一个很重要的设计:

Everything is Plugin.

Tool、LLM、文件系统、Shell,甚至 Agent Loop 本身,都可以通过 Plugin 的方式挂进来。

这样做的好处很明显,核心代码不用把所有能力都写死。

比如一个 Agent 可能需要:

text 复制代码
Agent
├── LLM
├── Tools
├── File System
├── Shell
├── Permission
├── MCP
├── Skills
└── Agent Loop

这些东西如果全部写在一个 Runtime 里面,后面想换、想加、想删都会越来越麻烦。

如果都是 Plugin,就简单很多:

text 复制代码
Cordis Runtime
├── LLM Plugin
├── Tool Plugin
├── File Plugin
├── Shell Plugin
├── MCP Plugin
└── Agent Loop Plugin

需要什么就装什么。

不过新的问题马上就来了:

text 复制代码
Plugin 怎么注册?

Plugin 之间怎么使用对方的能力?

Plugin A 依赖 Plugin B 怎么办?

Plugin 之间怎么通信?

Plugin 被卸载以后,
之前注册的事件、连接、Service 怎么处理?

Cordis 基本就是围绕这些问题设计出来的。


1. Plugin 先得有人管

最直接的入口就是:

ts 复制代码
ctx.plugin(plugin)

表面看就是注册一个 Plugin,但 Cordis 并不会简单地:

ts 复制代码
plugin(ctx)

执行完就算了。

Plugin 注册之后,还有自己的状态、依赖、配置,以及后面需要清理的资源。

所以这里先出现了:

text 复制代码
RegistryService

它负责管理 Plugin 的注册。

但 Registry 也不能只保存:

ts 复制代码
plugins.push(plugin)

因为 Cordis 还需要知道:

text 复制代码
这个 Plugin 当前有没有运行?

它属于哪个 Context?

它依赖哪些 Service?

它创建了哪些资源?

什么时候应该卸载?

所以每一个真正运行起来的 Plugin,Cordis 都会给它建立一个对应的 Fiber

可以先简单理解成:

Fiber 就是 Plugin 的运行实例。

比如:

text 复制代码
Root Fiber
├── LLM Plugin Fiber
├── Tool Plugin Fiber
└── Agent Plugin Fiber

Plugin 是那段代码。

Fiber 则记录这段 Plugin 现在是怎么运行的。

所以:

ts 复制代码
ctx.plugin(plugin)

背后大概就是:

text 复制代码
Registry 注册 Plugin
        ↓
创建 Fiber
        ↓
检查依赖
        ↓
依赖满足
        ↓
执行 Plugin

后面 Plugin 注册出来的 Event、Service、Effect,也都会跟这个 Fiber 产生关系。


2. Plugin 之间怎么使用能力

Plugin 能注册了,接下来还有一个问题:

Plugin 之间总得互相调用。

比如一个 Metrics Plugin 提供统计能力,其他 Plugin 想记录:

ts 复制代码
metrics.record(...)

这个 metrics 从哪来?

Cordis 的答案就是 Service

DSH 官方文档里给了一个很简单的例子:

ts 复制代码
import { Service, type Context } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  static inject = ['llm']

  constructor(ctx: Context) {
    super(ctx, 'metrics')
  }

  record(event: string, value: number) {
    // ...
  }
}

最值得注意的是:

ts 复制代码
super(ctx, 'metrics')

这里的 'metrics' 就是这个 Service 的名字。

Service 内部最终会做类似:

ts 复制代码
ctx.reflect.provide(
  'metrics',
  this,
)

也就是:

text 复制代码
当前 Plugin
提供了一个叫 metrics 的 Service

这样其他 Plugin 就可以:

ts 复制代码
export const inject = ['metrics']

export function apply(ctx: Context) {
  ctx.metrics.record('tool_call', 1)
}

这里其实就已经把 Plugin 和 Service 的关系说明白了:

text 复制代码
Metrics Plugin
      ↓
提供
      ↓
metrics Service
      ↓
其他 Plugin inject
      ↓
ctx.metrics.record()

所以 Service 可以简单理解成:

Plugin 对其他 Plugin 暴露出来的能力。

Plugin 是模块。

Service 是模块对外提供的接口。


3. inject 又是干嘛的

上面的消费方还有一句:

ts 复制代码
export const inject = ['metrics']

这个东西很重要。

它相当于告诉 Cordis:

我这个 Plugin 运行之前必须先有 metrics。

如果 metrics 还不存在,那这个 Plugin 就不能正常启动。

等 MetricsService 注册以后,依赖它的 Plugin 才能继续运行。

所以:

text 复制代码
Service

解决的是:

我能提供什么能力。

而:

text 复制代码
inject

解决的是:

我运行需要什么能力。

比如:

ts 复制代码
export const inject = ['tools']

就是:

没有 tools,我这个 Plugin 就不要启动。

DSH 也支持可选依赖。

如果只是偶尔需要一个 Service,可以不写 inject,而是在使用的时候:

ts 复制代码
export function apply(ctx: Context) {
  const metrics = ctx.get('metrics')

  metrics?.record('plugin_loaded', 1)
}

所以这两种语义是不一样的。

text 复制代码
inject
= 这是我的运行条件

ctx.get()
= 有就用,没有也无所谓

4. ctx.metrics 为什么能直接拿到 Service

这里有一个挺有意思的地方。

我们前面并没有写:

ts 复制代码
ctx.metrics = metricsService

但使用的时候却可以直接:

ts 复制代码
ctx.metrics.record(...)

这是因为 Cordis 的 Context 本身是一个 Proxy:

ts 复制代码
constructor() {
  const self = new Proxy<this>(
    this,
    ReflectService.handler,
  )
}

所以访问:

ts 复制代码
ctx.metrics

并不一定是在读取一个普通 JS 属性。

Proxy 会先拦截:

text 复制代码
get metrics

然后再去 Cordis 的 Service 系统里查。

大概可以先理解成:

text 复制代码
ctx.metrics
    ↓
Proxy get
    ↓
当前 Context / Fiber 有没有 metrics
    ↓
有
    ↓
返回 MetricsService

如果当前 Fiber 没有,还可能沿着父 Fiber 继续找。

所以 Context 更像是一个:

带作用域的 Service 容器。

ReflectService 主要就在做这些事情:

text 复制代码
Service 注册
Service 查找
Service 依赖
Context Proxy 解析

前面看到:

ts 复制代码
ctx.reflect.provide('metrics', service)

实际上就是把这个 Service 放进这套系统里面。


5. Service 不适合所有通信,所以还有 Event

Plugin 之间并不是什么事情都适合直接调用。

比如:

ts 复制代码
ctx.metrics.record(...)

这种很明确:

我就是需要 metrics 这个能力。

这时候 Service 很合适。

但如果只是:

某件事情发生了。

比如:

ts 复制代码
ctx.emit('user/created', user)

调用方其实并不关心是谁来处理。

其他 Plugin 如果关心,就自己监听:

ts 复制代码
ctx.on('user/created', (user) => {
  // ...
})

所以两者很好区分:

text 复制代码
Service
= 我明确需要一个能力

Event
= 我通知一件事情,谁关心谁处理

这也是为什么 Cordis 里同时存在:

text 复制代码
ReflectService
EventsService

EventsService 里面就是比较熟悉的:

ts 复制代码
ctx.on(...)
ctx.emit(...)

除此之外还有:

text 复制代码
parallel
serial
bail
waterfall

分别处理并行、串行、中断和中间件这种不同场景。


6. Plugin 卸载以后怎么办

这其实是 Cordis 很重要的一部分。

Plugin 启动以后可能干很多事情:

text 复制代码
注册 Service
注册 Event
创建 Timer
打开 Connection
注册子 Plugin

如果 Plugin 被卸载了,但这些东西还在,那插件系统其实就不完整。

所以 Cordis 里面经常能看到:

ts 复制代码
ctx.effect(() => {
  const connection = createConnection()

  return () => connection.close()
})

前面负责创建。

返回的函数负责清理。

这些 Effect 会跟当前 Fiber 绑定。

比如一个 Plugin Fiber 最后可能是:

text 复制代码
Plugin Fiber
├── metrics Service
├── user/created listener
├── timer
├── connection
└── child Plugin

当这个 Plugin 被卸载:

text 复制代码
Fiber dispose
    ↓
执行 disposer
    ↓
移除 Event
    ↓
移除 Service
    ↓
关闭 Connection
    ↓
卸载子 Plugin

这时候再回头看 Fiber,就会发现它其实很关键。

它把:

text 复制代码
Plugin 创建出来的东西

和:

text 复制代码
Plugin 本身的生命周期

绑在了一起。

所以 Cordis 不只是:

怎么把 Plugin 注册进来。

它更关心:

Plugin 注册以后,怎么保证它最后还能被完整地撤销。


一个前端看 Cordis 的小插曲

看这个项目的时候还有一个挺有意思的感觉:

好多东西前端开发其实都见过。

比如 EventsService

ts 复制代码
ctx.on(...)
ctx.emit(...)

这不就是以前经常写的 EventBus。

再看:

ts 复制代码
ctx.effect(() => {
  const connection = createConnection()

  return () => connection.close()
})

是不是又有点眼熟?🤣

和 React:

ts 复制代码
useEffect(() => {
  const connection = createConnection()

  return () => connection.close()
}, [])

思路其实很像。

都是:

text 复制代码
setup
  ↓
返回 cleanup
  ↓
生命周期结束
  ↓
cleanup

还有 Context:

ts 复制代码
const self = new Proxy<this>(
  this,
  ReflectService.handler,
)

Proxy 这个东西,写 Vue 的时候就更熟了。

Vue 的响应式源码里面到处都是 Proxy。

只不过 Vue 拦截:

ts 复制代码
state.xxx

主要是为了依赖收集和响应式。

Cordis 拦截:

ts 复制代码
ctx.xxx

是为了 Service 查找和 Context 作用域。

目的完全不同,但这种:

text 复制代码
表面看是普通属性访问

实际上 Proxy 在背后接管

的感觉非常像。

所以前端转过来看这种运行时框架,有些东西其实并没有想象中陌生。


最后

到这里再回来看 Cordis 的核心文件:

text 复制代码
context.ts
registry.ts
fiber.ts
service.ts
reflect.ts
events.ts

基本就能对上了。

text 复制代码
Registry
    ↓
Plugin 怎么注册

Fiber
    ↓
Plugin 怎么运行和销毁

Service / Reflect
    ↓
Plugin 怎么提供和依赖能力

Events
    ↓
Plugin 怎么松耦合通信

Effect
    ↓
Plugin 创建的资源怎么清理

整个设计其实都是从:

Everything is Plugin

这句话往后推出来的。

既然所有能力都尽量通过 Plugin 装进来,那 Cordis 就必须解决:

text 复制代码
Plugin 怎么注册
Plugin 怎么依赖
Plugin 怎么通信
Plugin 怎么提供能力
Plugin 怎么卸载

所以 Cordis 表面看是一个 Plugin Framework,实际上做的是一整套 Plugin Runtime。

而 DSH 把 LLM、Tools、Agent Loop 等能力建立在这个 Runtime 上,后面很多设计也就自然能够组合起来了。

相关推荐
炮哥聊AI1 小时前
会调工具的 Agent 只值一半:真正的业务智能体,得会"记仇"
人工智能·agent
字节跳动数据库1 小时前
火山 PostgreSQL Serverless × 飞书妙搭:把 AI 装进数据库,一句话唤醒数据智能
数据库·人工智能·后端
智购科技自动贩卖机1 小时前
自动售货机硬件主控方案选型实战:单片机、树莓派、ESP32怎么选?
人工智能·单片机·嵌入式硬件·物联网·网络协议·yolo·架构
用户1917291270831 小时前
企业 Agent 运行时护栏:独立安全层四道防线设计与落地
人工智能
beiju1 小时前
从 Meta AI Mac 版看 Agent 产品:桌面端真正解决的是交付上下文
人工智能
Scene2161 小时前
旧 REST 接口封装成 MCP 服务:完整实战指南
人工智能·后端
2301_786756601 小时前
不做分子仿真、不布局自动化实验室,垂直科研智能平台同样跑通 AI for Science 可持续变现
运维·人工智能·自动化
大数据点灯人1 小时前
【大模型】深度解答:OOV 与 Tokenizer 词汇表共用问题
人工智能·深度学习·ai·大模型·transformer
Erishen1 小时前
🤖 用 AutoGen 搭 PSE 三角色闭环:可重试、可追溯、可审计的多智能体框架
架构·开源·agent