DeepSeek Harness 系列第六篇。Cordis 原本是 Koishi(QQ 机器人框架)的底层——一个国人开发的"小众"插件框架。DeepSeek 为什么选它来支撑 50+ 包的 Agent 运行时?

背景:Cordis 是什么

Cordis 是 cordiverse/cordis 项目,最初为 Koishi(一个 QQ/Discord/Telegram 机器人框架)设计的插件运行时。它的核心只有约 2000 行 TypeScript,提供:

  • Service:命名的 ctx 键,任何插件可以提供/消费
  • Fiber:插件的生命周期状态机(PENDING → LOADING → ACTIVE → DISPOSED)
  • Effect:注册即 disposer 的副作用管理
  • inject:声明式依赖等待
  • Events:类型化的 emit/waterfall/serial/parallel 事件

Harness 将其 vendor 进仓库(vendor/cordis,版本 4.0.0-rc.7),rescope 为 @deepseek-ai/cordis,并追加了 18 项本地修改(生命周期加固、事务性配置加载、JSDoc 等)。

为什么不用"主流"方案

方案 A:InversifyJS / tsyringe / NestJS

传统 DI 容器做的事:

// InversifyJS 风格
@injectable()
class ToolRegistry {
  constructor(@inject('LLM') private llm: LLMService) {}
}
container.bind('ToolRegistry').to(ToolRegistry)
const registry = container.get('ToolRegistry')

问题:

  1. 没有自动 dispose:你 bind 了一个 service,谁来负责 unbind + cleanup?传统 DI 要么不管(leak),要么需要手动 lifecycle hook
  2. 没有 dependency-driven reload:如果 LLM provider 热替换了,依赖它的 ToolRegistry 应该自动重启——传统 DI 做不到
  3. 配置和实例化分离cordis.yml 一行就是一个 plugin 实例,改配置触发 HMR——传统 DI 的配置通常是启动时一次性的

Cordis 的关键差异:它不只是 IoC,它是一个有 lifecycle + reactivity 的 plugin orchestrator。

方案 B:自己写一个

50+ 包的 monorepo,如果从头设计:

  • Plugin registry + lifecycle state machine
  • Dependency graph + topological sort
  • Hot reload protocol
  • Effect tracking + ordered disposal
  • Typed event system with waterfall semantics
  • YAML config loader + patch/overlay composition

这大概是 6000-8000 行基础设施代码,加上持续的 edge case 修复。Cordis 把这些已经做了,而且在 Koishi 生态中经过了真实的多插件并发运行验证。

方案 C:不用 DI,直接 import

// 直接 import 方式
import { toolRegistry } from './tool-registry'
import { llmService } from './llm-service'
// 每个模块直接引用具体实现

这在 Agent 框架中是不可行的,因为 Harness 的核心卖点就是可替换性——用户必须能通过配置换掉任何一层。直接 import 把依赖硬编码到了编译时。

Cordis 解决了什么 Agent 特有问题

1. Service 消失时的 graceful degradation

AI Agent 运行时有一个独特场景:service 可以运行时消失

  • MCP Server 崩溃 → ctx.tools 中的 MCP tool 被注销
  • LLM Provider 因为 rate limit 暂时不可用
  • 文件系统 watcher 被操作系统杀死

传统 DI 的假设是:一旦 bind,service 就一直在。Cordis 的假设是:service 可以随时出现和消失

export const inject = ['tools']

export function apply(ctx: Context) {
  // 如果 ctx.tools 的 provider 被卸载(比如 HMR 替换):
  // 1. 本插件自动进入 DISPOSED
  // 2. 所有通过 ctx 注册的 effect 逆序执行
  // 3. 当新的 tools provider 加载后,本插件自动重新 apply
}

这个行为是 Cordis 框架级保证的——每个使用 inject 的插件都自动具备这个能力,不需要手动写重连逻辑。

2. 声明式组合 + HMR

AI Agent 的开发循环要求极快的反馈:改了一个 tool 的 prompt,想立刻看效果。

Cordis 的 cordis.yml + HMR plugin 提供了这个能力:

- id: my-tool
  name: './src/my-tool.ts'
  config:
    temperature: 0.7

修改 temperature → Cordis loader 检测变化 → 旧 fiber dispose(所有 effect 清理)→ 新 fiber 用新 config 加载。整个过程不重启进程,不丢失其他 session 状态。

传统 DI 框架没有"文件变化 → 局部热替换 → 依赖自动重建"这条路径。你要么重启整个进程,要么自己写一套 HMR 协议。

3. 配置层的 patch/overlay 组合

Harness 的 Profile + Bundle 系统:

base bundle → plugin bundles → profile patch → home patch → CLI overlay

每一层都是 Cordis 配置的一组 patch 操作。这个组合模型直接来自 Cordis loader 的 Include + patch 机制——Harness 只是定义了层的优先级规则。

如果自己实现,这个"多层配置合并 + 引用已安装 npm 包中的 patch 文件"的逻辑相当复杂。

4. Effect-scoped 资源管理

Agent 运行时有大量需要生命周期管理的资源:

  • MCP 连接(要断线重连、要清理 tool 注册)
  • PTY session(要维护状态、要在 Agent 结束时 kill)
  • File watcher(要在 workspace 切换时重建)
  • Background job(要在 session 结束时取消)

全都通过同一个模式管理:

ctx.effect(() => {
  const resource = acquire()
  return () => resource.release()
})

不需要为每种资源设计不同的 lifecycle hook。这比 NestJS 的 OnModuleInit / OnModuleDestroy 更通用——因为 effect 可以嵌套、可以条件性执行、可以在运行时动态添加。

Cordis Waterfall:around-middleware for events

Cordis 的 waterfall 事件是 Harness 架构中大量使用的模式:

// tools/pre-execute 是 waterfall 事件
ctx.waterfall('tools/pre-execute', async (toolName, args, next) => {
  // 在 tool 执行前做权限检查
  if (!hasPermission(toolName)) {
    return { denied: true, reason: 'No permission' }  // 短路,不调 next()
  }
  // 放行
  return next()  // 必须调用,否则后续 listener 不执行
})

这个模型和 Koa/Express 的 middleware 类似,但应用在事件系统中:

  • agent/pre-step:决定模型这一步看到什么
  • agent/request:拦截或修改即将发出的 LLM 请求
  • llm/stream:处理流式响应
  • tools/pre-execute / tools/execute / tools/post-execute:工具执行管道

每个 waterfall 的 listener 可以选择:

  • next() 放行(大多数情况)
  • 不调 next() 短路(策略拒绝)
  • next() 但修改参数或包装返回值(around advice)

这比纯 emit/subscribe 强大得多——它允许多个独立插件组成一个决策链,而不需要知道彼此的存在。

Vendor 的代价和收益

代价

Harness 做了 18 项本地修改,维护成本不低:

  • Fiber lifecycle hardening:修复了 3 个 reentrant disposal 的 race condition
  • Transactional config reload:让配置变更失败时能回滚
  • Lazy config resolution!!js 表达式在 inject 激活后才求值
  • Include patch semantics:让后一个 patch 能覆盖前一个 patch 插入的行

每次 upstream 更新,都要 re-apply 这些修改。

收益

  • 完全可审计:vendor 在仓库里,不依赖外部注册表的版本
  • 可 patch:遇到 upstream 不接受的修改,不需要 fork + 长期维护
  • 版本锁定:不会被上游 breaking change 意外破坏
  • 性能调优:可以针对 Harness 的使用模式优化热路径

为什么"小众"不是问题

Cordis 在 npm 上的下载量可能不如 InversifyJS,但:

  1. 代码量小:核心 ~2000 行,可以完整 review
  2. 语义明确:5 个核心概念(plugin, service, inject, effect, event),没有 magic
  3. TypeScript-first:完整的类型推导,declaration merging 让扩展点 type-safe
  4. 实战验证:Koishi 生态有上百个插件并发运行的经验
  5. DeepSeek 自己维护:vendor 后由 Harness 团队负责,不依赖外部 maintainer

对于一个 AI Agent 运行时来说,最重要的框架特性不是"生态大",而是:

  • 插件可以安全地注册/注销资源 ✓
  • Service 消失时依赖方自动重启 ✓
  • 声明式配置组合 + 热替换 ✓
  • Around-middleware 事件管道 ✓

Cordis 恰好把这四件事做好了,而"主流"DI 框架做的是另一组事情(HTTP 路由、请求生命周期、模块化)。

对 Agent 框架选型的启示

如果你也在构建 Agent 运行时,Cordis 的设计给出几个值得借鉴的方向:

  1. DI 不够,需要 lifecycle:传统 IoC 解决"谁创建谁"的问题,但 Agent 运行时需要"谁什么时候活着"的管理
  2. 配置即组合:Agent 的能力集应该是配置决定的,不是代码决定的
  3. Effect tracking 比 manual cleanup 可靠:资源泄露是 long-running Agent 的大敌
  4. Around-middleware 比 hook callback 强:决策链(permission → approval → timeout → execution)需要有序短路能力

参考链接


DeepSeek Harness 系列文章:

  • 第六篇:为什么用 Cordis 做 AI Agent 运行时(本文)
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐