Deepseek Harness 调研 - Cordis 框架核心

前几天发布了 Deepseek Harness, 我就琢磨预计这玩意可以对我们现有的 harness 项目有所帮助.

Meta

文章分类:Tech

标签:Cordis框架插件架构TypeScript事件系统Deepseek

目录

前几天发布了 Deepseek Harness, 我就琢磨预计这玩意可以对我们现有的 harness 项目有所帮助.

闲下来的时候研究了一下, 它的官网就放了对应的论文, 尝试用 AI 阅读的时候发现其的底层是一个叫做 Cordis 的插件框架, 这个框架竟然和多年前使用的 QQ 机器人 koishi 的底层是同一个 (虽然我就因为它管理太麻烦抛弃了 koisi 换用了 astroBot) . 但依然值得研究一下.

Cordis 五大核心概念

五大核心:

  • 插件 Plugin
  • 上下文 Context
  • inject 依赖 Inject Dependencies
  • 类型化事件 Typed Events
  • 可逆副作用 Reversible Side Effects

前置说明 (框架内置, 无需手动实现)

以下为 Cordis 框架自带基础类型, 所有示例基于此:

type Cleanup = () => void;

// 全局上下文容器
interface Context {
  // 四种事件分发
  emit(name: string, ...args: any[]): void;
  waterfall<T>(name: string, init: T): Promise<T>;
  parallel<T>(name: string, ...args: any[]): Promise<T[]>;
  serial<T>(name: string, ...args: any[]): Promise<T[]>;

  // 可逆副作用注册
  on(name: string, handler: Function): void;
  effect(fn: () => Cleanup): void;

  // 服务动态挂载
  [key: string]: any;
}

// 服务基类
declare class Service {
  static inject?: string[];
  async start(ctx: Context): Promise<void>;
  async stop(): Promise<void>;
}

1. 插件 Plugin (能力载体)

核心定义: 插件是承载 Service 服务的最小单元, 生命周期由 Cordis 框架托管, 支持两种写法.

核心特点

  • 两种形态: 函数式插件, Service 类插件 (语法糖)
  • 所有业务能力 (大模型, 工具, 会话) 都由插件提供
  • 无需手动实例化, 启停, 框架自动调度

极简代码示例

// 写法 1: 函数式插件 (推荐, 简洁) 
export const LLMPlugin = {
  inject: [], 
  apply(ctx: Context) {
    // 挂载 AI 服务能力
    ctx.llm = { chat: async (text: string) => `AI 回复: ${text}` };
  }
};

// 写法 2: Service 子类插件 (面向对象写法) 
export class ToolPlugin extends Service {
  static inject = [];
  async start(ctx: Context) {
    ctx.tools = { list: () => ["计算器工具"] };
  }
}

2. 上下文 Context (服务容器)

核心定义: 统一的服务容器, 所有服务挂载在 ctx.xxx 键上, 实现模块解耦.

核心特点

  • 统一入口: 所有服务通过 ctx.{key} 访问
  • 完全解耦: 不直接导入其他插件代码, 只依赖键名
  • 可无缝替换: 更换服务实现, 上层业务代码无需改动

极简代码示例

// 不需要 import 导入 LLM 实现, 直接从上下文获取
export const AgentPlugin = {
  inject: ["llm"], 
  apply(ctx: Context) {
    const res = await ctx.llm.chat("你好");
    console.log(res);
  }
};

3. Inject 依赖声明 (自动启停调度)

核心定义: 插件声明依赖的服务, 框架自动拓扑排序, 等待依赖就绪后再启动当前插件.

核心特点

  • 无需手动编排插件启动顺序
  • 依赖未就绪时, 插件会等待
  • 解决多模块加载顺序混乱问题

极简代码示例

export const AgentPlugin = {
  // 声明依赖 llm, tools
  inject: ["llm", "tools"], 
  async apply(ctx: Context) {
    // 框架保证: 运行到这里, ctx.llm, ctx.tools 已经就绪
    console.log("工具列表: ", ctx.tools.list());
  }
};

4. 类型化事件 (模块通信机制)

核心定义: 插件之间通信方式, 使用 ctx.on 注册监听, 提供四种分发策略.

核心特点

  • ctx.on 统一注册监听器; 行为差异由触发函数决定
  • 支持 TS 声明合并, 实现事件类型安全
  • 四种模式适配不同业务场景

极简代码示例

export const EventPlugin = {
  inject: [], 
  async apply(ctx: Context) {
    // 注册监听
    ctx.on("prompt.modify", (text: string) => {
      return `『系统前缀』${text}` ;
    });

    // 1. emit: 广播通知, 丢弃返回值
    ctx.emit("prompt.modify", "原始提问");

    // 2. waterfall: 流水线, 数值依次传递, 返回单个结果
    const finalText = await ctx.waterfall("prompt.modify", "原始提问");

    // 3. parallel: 并发执行, 返回结果数组
    const parallelResult = await ctx.parallel("collect.tool", {});

    // 4. serial: 顺序排队执行, 返回结果数组
    const serialResult = await ctx.serial("post.process", "回答内容");
  }
};

5. 可逆副作用 (资源自动清理)

核心定义: 通过 ctx.on / ctx.effect 注册资源, 插件卸载/热重载时自动执行清理逻辑.

核心特点

  • 避免手动解绑造成内存泄漏
  • 所有副作用归属当前插件上下文
  • 是框架支持安全热重载的基础

极简代码示例

export const LogPlugin = {
  inject: [], 
  apply(ctx: Context) {
    // 方式 1: ctx.on 自动托管监听, 卸载自动移除
    ctx.on("user.message", (msg) => console.log("收到消息: ", msg));

    // 方式 2: ctx.effect 自定义资源, 返回清理函数
    ctx.effect(() => {
      console.log("日志连接已打开");
      // 返回清理逻辑
      return () => {
        console.log("日志连接关闭");
      };
    });
  }
};

Cordis 四种事件分发模式

基础规则:

  1. ctx.on(事件名, 处理函数) : 统一用于注册监听器
  2. emit / waterfall / parallel / serial 用于触发事件, 行为差异仅由触发方法决定
  3. 通过 ctx.on 注册的监听器属于当前插件上下文, 插件销毁/重载时自动解绑

1. ctx.emit() 广播通知

  • 所有监听器接收原始入参
  • 监听器返回值全部丢弃, 不会收集结果
  • 适合: 日志埋点, 单纯事件通知, 不需要修改与获取数据
// 注册监听
ctx.on("user.message", (text) => {
  console.log("收到消息: ", text);
  return "无用返回值"; // 返回值直接丢弃
});

// 触发事件
ctx.emit("user.message", "你好");

2. ctx.waterfall() 瀑布流水线

  • 首个监听器接收初始值, 上一个返回值作为下一个的入参
  • 顺序执行, 最终返回最后一个处理器的单个结果
  • 适合: 请求中间件, 逐层加工提示词, 链式数据修改
// 注册监听
ctx.on("prompt.transform", (text) => {
  return `『系统前缀』${text}` ;
});
ctx.on("prompt.transform", (text) => {
  return `${text}\n 简洁回答` ;
});

// 触发事件
const result = await ctx.waterfall("prompt.transform", "原始提问");
console.log(result);

3. ctx.parallel() 并行执行

  • 所有监听器使用同一个原始参数, 数据互不传递
  • 所有监听器并发同时运行
  • 返回数组, 收集全部监听器返回结果
  • 适合: 并行收集插件能力, 并发调用多个独立资源
const sleep = (ms) => new Promise(res => setTimeout(res, ms));

// 注册监听
ctx.on("collect.tools", async () => {
  await sleep(100);
  return "计算器工具";
});
ctx.on("collect.tools", async () => {
  await sleep(200);
  return "天气查询工具";
});

// 触发事件
const list = await ctx.parallel("collect.tools", {});
console.log(list);

4. ctx.serial() 串行排队执行

  • 所有监听器使用同一个原始参数, 数据互不传递
  • 严格顺序执行, 上一个完成才运行下一个
  • 返回数组, 收集全部监听器返回结果
  • 适合: 有序后置处理, 有依赖顺序的任务队列
const sleep = (ms) => new Promise(res => setTimeout(res, ms));

// 注册监听
ctx.on("post.handle", async (content) => {
  await sleep(200);
  return "阶段 A 处理完成";
});
ctx.on("post.handle", async (content) => {
  await sleep(200);
  return "阶段 B 处理完成";
});

// 触发事件
const outputs = await ctx.serial("post.handle", "AI 原始回答");
console.log(outputs);

⚠️ 重点注意事项

  • waterfall vs serial 极易混淆:
    • waterfall: 数据向后传递, 输出单个值
    • serial: 仅排队执行, 所有处理器共用原始参数, 输出数组
  • parallel 任意监听器抛出异常, 整体直接失败;
  • ctx.on 属于可逆副作用, 不要手动维护解绑逻辑, 框架自动清理;
  • 同一个事件名称可以注册多个监听器, 执行逻辑完全由触发函数决定;
  • emit 不等待异步监听器全部完成; 其余三种分发方法均需要 await 等待执行完毕.

简要对比速查表

方法参数传递返回类型执行模式
emit不传递返回值丢弃广播通知
waterfall依次传递单个值顺序流水线
parallel共用原始参数数组并发同时执行
serial共用原始参数数组顺序排队执行