引言#
东方 Project 中一个有着灰绿色头发的少女古明地恋、群聊中的聊天机器人和 Deepseek Harness,这些无关的词被 Cordis 串了起来。
近日发布的 Deepseek Harness,在文档中处处可见 Cordis,要怎么理解它,这是一个框架、一个理论还是别的东西。
但是追溯来源,可以发现我们在群聊中很常见的聊天机器人和一个开源聊天机器人社区 Koishi(日语中“恋”的发音,名字来源即为东方中的古明地恋)。
Cordis 是 Koishi 用于解决聊天机器人插件的底层框架,想要了解它,故事需要从聊天机器人讲起。
聊天机器人#
2015 年,telegram 和 discord 开放了机器人开发平台。与我们现在熟悉的 AI Agent 的多轮对话不同,一个群组中与机器人交互流程通常是:
@bot 签到
→ 今日签到成功,积分 +10
@bot ban @张三 10m
→ 管理员将张三禁言十分钟
@bot steam 123456
→ 查询玩家状态
@bot 色图猫猫
→ 调用图片插件
新人进群
→ 自动欢迎 + 分配权限
从交互上来看,它是群中一个头像,就像与真人交流一样。从本质来看,它更像是一个长期运行的服务器,与它对话就像前端调用后端的接口。
在即时通讯产品中,Telegram 侧重用户聊天与群组,Discord 侧重社区,Slack 专注于企业办公,QQ 则有一些国内特色,在熟人社交的基础上发展出了大型群组。
产品基因的不同,决定了这些产品中机器人的需求也不同,Telegram 和 Slack 希望把机器人做成聊天中随处可用的工具,作为聊天窗口中一个通向互联网的窗口,比如查寻天气、发送报告。
Discord 则希望解决大型社区运营的问题,Discord 的群组通常规模更大,它们的机器人的典型用途就是运营,为新入群的用户分配频道权限,自动禁言等。
QQ 的机器人生态比上面三者更受平台控制和规则变化影响。因此中国的聊天机器人社区会出现大量解决方案,开发者对 QQ 群自动化的需求很强,而官方开放能力与社区实际需求之间长期存在一定距离。
现在仅 Telegram 上就有超过一千万个机器人。无论是哪个平台的用户,其实都有定制化的长尾需求,开放的机器人平台给了他们实现的机会。
此时的难点在于各家生态不统一,很多用户也不是程序出身,并不会编程。这些难点推动社区不断完善,解决平台适配、插件、配置等问题并形成了开发框架。Cordis 的作者 Shigma 就是其中之一。
Shigma 在 2019 年开始编写他自己第一个机器人,他浏览各种框架但是没有能满足他需求的,于是他决定自己编写一个(这点和 pi-agent 的作者 Mario 很像)。一开始这个机器人只包含了很少的功能,但随着其更多功能的加入,他开始调整底层架构,并逐步将其开源出来,这就是 Koishi。
Koishi 的发展整体是由需求驱动的
最早的 Koishi 主要满足基于聊天机器人产品形态的需求。比如我们前面提到的需要接入不同平台、一个机器人有很多功能、不同机器人存在长尾需求等。
对此 Koishi 的做法是创建专门的协议层来统一聊天平台(Satori,名字来源是古明地觉),把功能拆成插件,每个插件独立开发、安装和配置。然后建立插件生态和市场,由更多开发者去匹配长尾需求。
生态扩大以后,需求更多是工程需求。这些需求不是因为聊天机器人这个业务本身需要,而是因为前面的产品形态被真正实现以后,软件工程上需要解决。
聊天机器人大多由个人部署和维护,相比互联网企业数百上千的部署实例,个人可能只有单一实例。如果希望机器人长期在线,就需要支持插件的加载、卸载和热重载。
在插件有更新后,还需要满足不能留下垃圾状态。
插件数量多了之后,还会出现插件间互相依赖,不同开发者的插件有所差异的问题。
可以把这些问题归类到时间上和空间上两类。时间维度是生命周期问题,空间维度是依赖关系问题。
计算机科学与技术对这类问题最普遍的解决方法是加一层抽象,于是 Cordis 在 2022 年从 Koishi 分离出来,作为一套通用机制,管理动态组件对系统产生的影响,以及组件之间的依赖关系。
Cordis#
Cordis 从可热插拔的插件出发,一个插件可以被安装,就应该能完全卸载干净。
这是一个从插件这种应用层一直下穿到底层代码的软件工程问题。
在 C 语言和早期的 C++ 中,用 malloc() 分配内存后,要使用对应的函数来释放掉。因为要记得手动释放,因此使用 C 和 C++ 很容易写出内存泄露的代码。因此在大多数现代的编程语言中,使用垃圾回收来统一释放,也有像现代 C++ 和 Rust 这种使用智能指针和所有权系统的零成本抽象。
Cordis 要做的就和这些编程语言一样。作者 Shigma 把这种特性称之为可逆性。放到 Koishi 的背景中,即任何一个 Koishi 实例,任意进行加载和卸载插件操作后,最终行为仅与最终启用的插件相关;与中间是否重复加载过插件、插件之间的加载或卸载顺序都无关。
Cordis 通过 context 来实现。当一个插件被加载时,Cordis 并不是把所有插件都放进同一个全局环境,而是从父级 Context 派生出一个新的子 Context。
这样 Cordis 就知道插件与哪些资源相关,在卸载时将资源全部回收。
这些资源不只是内存,也包括事件监听器、定时任务、路由和 Service。Cordis 的做法是让这些操作同时返回对应的清理函数,并记录在 Context 中。
注册 Listener
→ 记录如何删除 Listener
启动 Timer
→ 记录如何停止 Timer
注册 Service
→ 记录如何移除 Service
插件被卸载时,Cordis 销毁对应的 Context,并依次执行这些清理操作。这样插件对系统产生的影响就可以被撤销。
这解决了插件在时间维度上的问题。但插件之间还存在依赖。
例如一个统计插件需要数据库,但它并不应该关心用户使用的是 SQLite 还是 MySQL。Koishi 因此把数据库抽象成一个 database Service,数据库插件负责提供它,统计插件只声明自己需要它。
SQLite ──┐
├── database ──→ Statistics
MySQL ───┘
这种方式解决了不同实现的替换问题,但在面对热重载时,会出现以下场景:
Database V1
↓
卸载
↓
database 消失
↓
Database V2
↓
database 再次出现
此时依赖 database 的 Statistics 不能继续假装数据库还存在。Cordis 因此不只记录“谁提供了什么”,还记录“谁需要什么”。
当某个 Service 发生变化时,Cordis 会找到依赖它的组件,重新检查这些组件当前是否还满足运行条件。
论文:一种用于“时空可组合性”的编程范式#
伴随 Deepseek Harness 发布的还有一篇论文《A Programming Paradigm for Spatiotemporal Composability》
Cordis 在工程上做到这些之后,论文进一步问了一个问题:这些机制是不是只适用于 Koishi,还是描述了一类更普遍的软件问题。
当软件组件可以动态安装、卸载、替换、依赖其他组件时,怎么保证整个系统还能正确组合?
论文把这个问题拆成了时间和空间。
时间维度关注组件对系统产生的影响。论文借用了编程语言中的 Effect 概念,并进一步提出 Revertible Effect,即可逆 Effect。
一个 Effect 不只是:
状态 A
↓
执行操作
↓
状态 B
而是同时存在一个反向操作:
状态 A
↓ Effect
状态 B
↓ Reverse
状态 A
Cordis 中记录插件清理函数的机制,因此被抽象成了一个更加一般的性质:如果一个组件离开系统以后,它产生的影响能够被完整撤销,那么这个组件具有 Temporal Composability,时间可组合性。
空间维度则关注组件与周围环境之间的关系。
论文使用 Coeffect 来描述一个组件对环境提出的要求:
Effect
组件 → 环境
我改变了什么
Coeffect
环境 → 组件
我需要什么
Cordis 中的 Service 和依赖声明就可以放进这个模型。一个组件声明自己需要 database,当 database 出现、消失或者被替换时,组件的生命周期也随之变化。
论文将这种机制称为 Reactive Coeffect,并进一步定义为 Spatial Composability,空间可组合性。
论文接下来又把 Effect 和 Coeffect 所操作的环境统一成同一种 Context,再把它们组合成 Component,并给出了一套描述组件动态加入和离开的形式演算。作者希望证明的也不再只是“一个插件能够正确卸载”,而是当许多组件交错地加载、卸载和互相依赖时,单个组件的这些性质仍然能够传递到整个系统。
Next Reads