Deepseek Harness 调研 - Cordis 框架核心
前几天发布了 Deepseek Harness, 我就琢磨预计这玩意可以对我们现有的 harness 项目有所帮助.
目录
前几天发布了 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 四种事件分发模式
基础规则:
ctx.on(事件名, 处理函数): 统一用于注册监听器emit / waterfall / parallel / serial用于触发事件, 行为差异仅由触发方法决定- 通过
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);
⚠️ 重点注意事项
waterfallvsserial极易混淆:- waterfall: 数据向后传递, 输出单个值
- serial: 仅排队执行, 所有处理器共用原始参数, 输出数组
parallel任意监听器抛出异常, 整体直接失败;ctx.on属于可逆副作用, 不要手动维护解绑逻辑, 框架自动清理;- 同一个事件名称可以注册多个监听器, 执行逻辑完全由触发函数决定;
emit不等待异步监听器全部完成; 其余三种分发方法均需要await等待执行完毕.
简要对比速查表
| 方法 | 参数传递 | 返回类型 | 执行模式 |
|---|---|---|---|
| emit | 不传递 | 返回值丢弃 | 广播通知 |
| waterfall | 依次传递 | 单个值 | 顺序流水线 |
| parallel | 共用原始参数 | 数组 | 并发同时执行 |
| serial | 共用原始参数 | 数组 | 顺序排队执行 |