Skip to content
签到签到

第 7 章:插件组合与 Cordis

本章解决的问题

前端开发者熟悉 React 组件组合和中间件,但不一定熟悉服务插件系统。这一章用前端类比解释 Cordis。

交互图解:Agent、Harness 与插件服务全景 · 查看图解导读

大白话理解

Cordis 是 DeepSeek Harness 底层的插件框架。

五个核心思想:

  1. 插件是对象或函数。
  2. Context 是服务仓库。
  3. inject 声明依赖。
  4. typed events 负责通信。
  5. 注册是可撤销 effect。

你可以把 Context 想象成前端 DI 容器,把 waterfall 事件想象成 Koa 或 Redux 中间件。

必须掌握的概念

1. Context

Context 保存服务。插件通过 ctx.toolsctx.llm 等键访问服务,而不是直接 import 具体实现。

2. Service

Service 是声明了稳定键的插件。它可能在构造时注册其他能力。

3. inject

插件声明:

ts
export const inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']

Loader 等这些服务存在后才激活插件,因此不需要手写启动顺序。

4. effect

注册必须可撤销。插件卸载时,它注册的 prompt、工具、监听器都会清理。

5. Typed Events

事件不是任意字符串。TypeScript declaration merging 定义事件名和参数。

四种派发模式:

模式是否等待是否有返回值用途
emit通知
waterfall可包装、可短路
parallel并行处理
serial按顺序处理

DeepSeek Harness 对应实现

AgentLoop 是一个真实 Service:

ts
static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']

它注册自己为 Agent factory:

ts
ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()')

这表示:

  • 它不是直接暴露具体 class。
  • 它通过 ctx.agents 注册创建能力。
  • 卸载时可以撤销。

进一步阅读:

  • docs/cordis-primer.md
  • packages/core/agent-loop/src/index.ts

代码示例

简化 Context 和 Plugin

ts
interface Plugin {
  name: string
  inject?: string[]
  apply(ctx: Context): () => void
}

class Context {
  private services = new Map<string, unknown>()

  provide<T>(key: string, service: T): () => void {
    this.services.set(key, service)
    return () => {
      this.services.delete(key)
    }
  }

  get<T>(key: string): T {
    const service = this.services.get(key)
    if (!service) throw new Error(`missing service "${key}"`)
    return service as T
  }
}

const toolPlugin: Plugin = {
  name: 'clock-tool',
  inject: ['tools'],
  apply(ctx) {
    const tools = ctx.get<{ register(tool: object): () => void }>('tools')
    return tools.register({
      name: 'clock',
      description: 'Get current time',
    })
  },
}

Waterfall 中间件

ts
type Next<T> = () => Promise<T>
type WaterfallListener<T, R> = (value: T, next: Next<R>) => Promise<R>

async function runWaterfall<T>(
  listeners: Array<WaterfallListener<T, T>>,
  value: T,
): Promise<T> {
  let index = 0

  const next = (): Promise<T> => {
    const listener = listeners[index]
    index += 1
    if (!listener) return Promise.resolve(value)
    return listener(value, next)
  }

  return next()
}

waterfall 的关键是:调用 next() 委托给下一层,不调用则短路。

面试怎么问

问:依赖注入和直接 import 有什么区别?

回答要点:

  • 直接 import 把调用方绑定到具体实现。
  • 依赖注入让实现可替换。
  • Agent 平台需要替换模型、沙箱、工具和 UI。
  • Cordis 还负责等待依赖和卸载清理。

问:emit 和 waterfall 有什么区别?

回答要点:

  • emit 只通知,无返回值。
  • waterfall 返回一个值,listener 可以包装或短路。
  • 策略类事件通常用 waterfall。
  • 观察类事件通常用 emit。

问:为什么插件注册必须可撤销?

回答要点:

  • 热更新需要卸载旧插件。
  • 卸载后不能留下工具、监听器或服务。
  • 否则会出现重复注册和幽灵监听器。

自测题

  1. inject 解决了什么问题?
  2. ctx.effect() 的返回值代表什么?
  3. waterfall listener 忘记调用 next() 会发生什么?
  4. 为什么插件系统比一个大 switch 更适合 Agent 平台?

本章小结

Cordis 是前端开发者理解 Agent 平台插件化的很好入口。它把依赖注入、中间件和生命周期管理组合成一个可热更新的运行时。

进一步阅读:

  • docs/cordis-primer.md
  • docs/cordis-tutorial/index.md