第 7 章:插件组合与 Cordis
本章解决的问题
前端开发者熟悉 React 组件组合和中间件,但不一定熟悉服务插件系统。这一章用前端类比解释 Cordis。
交互图解:Agent、Harness 与插件服务全景 · 查看图解导读
大白话理解
Cordis 是 DeepSeek Harness 底层的插件框架。
五个核心思想:
- 插件是对象或函数。
- Context 是服务仓库。
inject声明依赖。- typed events 负责通信。
- 注册是可撤销 effect。
你可以把 Context 想象成前端 DI 容器,把 waterfall 事件想象成 Koa 或 Redux 中间件。
必须掌握的概念
1. Context
Context 保存服务。插件通过 ctx.tools、ctx.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.mdpackages/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。
问:为什么插件注册必须可撤销?
回答要点:
- 热更新需要卸载旧插件。
- 卸载后不能留下工具、监听器或服务。
- 否则会出现重复注册和幽灵监听器。
自测题
inject解决了什么问题?ctx.effect()的返回值代表什么?- waterfall listener 忘记调用
next()会发生什么? - 为什么插件系统比一个大
switch更适合 Agent 平台?
本章小结
Cordis 是前端开发者理解 Agent 平台插件化的很好入口。它把依赖注入、中间件和生命周期管理组合成一个可热更新的运行时。
进一步阅读:
docs/cordis-primer.mddocs/cordis-tutorial/index.md