别看基模榜单了,DeepSeek 真值钱的功法在这里

浅论 Cordis 与动态系统的代数骨架

DeepSeek Harness(简称 dsh)一经发布,旋即议论纷纷。不过关注我的读者,应该不会止于表面的粉黑噪声,或者最新版模型权重打榜又刷了几个点,还是更想听些真有价值、且能经得住历史考验的信息。

先发个暴论,抛开一切外部组件不谈,抛开基座模型不谈,目前唯有其核心插件框架 Cordis 最值得关注

如今,各厂商从基模大战卷到 Agent 大战,普通用户的体感却往往和榜单并不一致。为什么?

真正决定实际工况下能不能干活的,往往是模型之外的 harness。工具怎么挂进去,记忆怎么保存,权限怎么收束,沙箱怎么隔离,失败以后怎么回滚。Prompt 当然重要,但只靠 prompt 层创新来组织复杂系统,约等于胶带古法修飞机,临时撑一段尚可,长期这样,就纯属逆天而行了。

Cordis 及其背后论文的最大贡献就在这里,表面上写插件系统、动态组件、热更新;实际上,讨论的是一个更底层的问题:

工程系统,如何在部件来来往往后,仍保持组合意义上的秩序。

这个问题为什么重要?

因为现代软件已经离一段静态程序的上古形态很远了,演化得更像一个营业的城市,服务会扩缩容,模型会灰度,特征会变更,插件会热更,Agent 会临时挂载工具,训练任务会申请资源又释放资源……而过去常见的工程习惯,是给每个模块写生命周期回调。

apply(ctx)
dispose()

既朴素,又危险,因为 dispose 到底能不能擦干净,全靠作者自觉、测试覆盖、祖传经验。

系统一复杂,最常见的事故往往不是组件完全挂掉(经常 debug 的朋友都知道,这种纯黑纯白的问题一般好办),而是半挂。

比如监听器还在,插件已经没了;服务注册还在,提供者已经退了;GPU 还占着,训练容器已经挂了;依赖已经换了,下游浑然不觉。

Cordis 对此的判断是,动态组合应有自己的程序语义

为此,问题需拆成两个维度。

一个是空间可组合性。组件在时,依赖什么、提供什么,必须能被系统声明、解析、响应。

一个是时间可组合性。组件走时,造成的副作用必须撤销。

不严谨地以直觉论,效应 effect 管“我如何改变世界”,共效应 coeffect 管“我要世界给我什么”,上下文则是二者的舞台。

比较严谨地讲,从范畴论看,对象是上下文类型 Γ,态射是状态变换,所有的自态射构成一个幺半群,也可以看成只有一个对象的小范畴,普通 effect 就是这个范畴里的箭头,而组合 effect 就是态射复合。

具体地,一个插件注册路由、事件监听器、数据库连接、命令处理器,这些都是 effect。它把旧世界变成新世界。论文用很朴素的数学写法表示:

\[ f: Γ \to Γ \]

这里的 Γ 就是上下文,系统当前的世界状态。

麻烦在于,只记录 f 不够。动态系统关心的不是“你干嘛~”,而是“你走后怎么恢复”。所以 Cordis 要求 effect 携带逆操作。

\[ g \circ f = id \]

先做,再撤,能还原。只要求左逆,不要求右逆——因为现实如此,注册一个进程再杀掉,能回到原状;先杀一个不存在的进程再注册它,结果是……???

另一个关键动作,是把撤销凭证放进 Context。从而,effect context 定义为当前状态 + 回滚日志,每做一次 effect,状态向前走一步,回滚函数追加一笔。恢复时,系统执行累积的回滚函数,然后清空日志。原文具体如何通过定义 effect context 二元组实现,此处不赘。

这听起来像工程里的 undo stack,但论文的野心更大,不仅做了,还证明这种追踪是保组合结构的,作为“时间可组合性”的代数核心,对得起原文标题中“编程范式”级的革新。

至此,范畴论已成必需,唯一要事就是东西如何组合、组合以后什么结构不变。

原文证明,track 是一个幺半群同态

因为 Effect 是上下文上的箭头。多个 effect 的执行是箭头复合。加载顺序是正向复合,卸载顺序天然反过来。于是论文定义了带逆操作的扭曲组合,是为栈式回滚

听着吓人,不过大家早已熟练掌握,已知你先穿袜子再穿鞋,那么脱鞋脱袜子的顺序,应该不用我教。

但,真实系统不会永远按栈来先进后出。插件 A、B、C 依次加载,后来只想卸载 A,怎么办?这时就需要 effect independence。两个 effect 如果互不干扰,它们对应的状态变换就应该交换。

\[ f \circ h = h \circ f \]

这个条件一旦成立,系统就可以在交错加载、交错卸载中保持一致性,是为保结构 Structure-preserving。(当然,如不能交换,就不能假装独立,必须通过依赖关系、生命周期或作用域把顺序显式表达出来,由依赖图约束。)

这便是范畴论化形于工程世界的一面。所谓交换图 commuting diagram,即问不同执行路径走到最后,是不是同一个世界;如果忘掉回滚日志,只看当前状态,track 后看到的结果,还是不是原来的 effect 结果。如果一组 effect 两两独立,那么它们的撤销顺序就不重要,即组件可以交错加载、交错卸载,只要它们声明和实际行为满足独立性。

光有 effect 还不够,组件还需要依赖。一个 Agent 工具可能需要浏览器沙箱、文件系统、密钥、模型连接、向量库。一个插件可能需要数据库、logger、router。

过去这类东西常常散落在文档、配置、启动脚本和隐式约定里。

系统启动失败后,算法同学就原地大小哭,喊来开发、运维灰头土脸从日志里考古。

如今 Cordis 把它统一叫 coeffect,类似“上下文索引的计算”,因为以范畴论的直觉,计算不是单独存在的,而是总立足于某个上下文之上

于是,一个组件不再只是“运行一段代码”,而是声明它需要什么、提供什么、改变什么、如何撤销、世界变化时如何响应,构成了 reactive coeffects。依赖不是启动时检查一次就完事,Context 每次变化,组件都要根据自己的依赖声明重新判断。依赖满足,可以激活;依赖消失,必须停用;上游撤销,下游也要跟着撤销自己的 effect。

比如 A 提供 database,B 依赖 database 并提供 user-service,C 依赖 user-service 并注册用户路由。A 卸载后,B 的依赖失效,B 停用,user-service 消失,C 也停用,路由撤销。这个过程不应该再靠人肉写一串回调,而应该由 Context 的依赖语义推出。

这就是空间可组合性,依赖图能自动维护。

论文在此还补了两个很工程的细节。Isolation 处理“同一个逻辑依赖,在不同作用域里解析到不同值”,如测试库和生产库都叫 database,但不能串台;Interception 处理“访问依赖时附加横切行为”,如 logger 还是 logger,只是某个子上下文读到它时自动带上 traceId、权限、租户或局部配置。前者像作用域隔离,后者像依赖访问上的中间件,二者都不改共享表本身,而是改“这个组件眼里的世界”。

此处,最核心的设计论断是,Cordis 把 effect 和 coeffect 统一进同个 Context,因为真实工程里,我提供的,是别人的依赖;我依赖的,又决定我能否提供新的东西;别人见我亦如是。副作用与依赖不是两张表,而是同一枚硬币的正反面。(这并非理所当然;传统 Spring/IoC 容器的核心思路就并非如此。)

一旦 Context 成为中心,组件就不是野生函数,而是挂在上下文上的纤维 fiber,虽然它是工程上的组件实例,但直觉就和 fibration 很接近。它有生命周期,有依赖声明,有提供能力,有回滚记录,上下文变化,fiber 的状态也随之变化。

论文后面的形式化证明,则是在维护几个朴素但关键的不变量,证明这些机制组合起来依然奏效。听起来像废话,但无数工程事故往往就出在这些废话上。

具体而言,Preservation 确保系统原本良构,走一步仍然良构。Temporal composability 确保组件卸载后不留垃圾。Spatial composability 确保活跃组件凡有依赖,必被满足。Progress 确保系统不会无故卡死,有路可走就能继续前进。Confluence 确保互不相关的操作,不能因调度顺序不同而分道扬镳。


总之,Cordis 不只是发明了一套优雅的插件 API,更是在声明动态软件系统不能再把生命周期管理当杂活、且需要一套自洽的代数骨架,而以上这些问题还必须进入系统本身,而不是留给会议纪要和人工记忆。

Agent harness 某种意义上,是暴露这个问题的风口浪尖,因为 Agent 从设计上就需要动态挂工具、换模型、换 memory、换权限、换沙箱,它把过去插件系统里的各种隐患放大了。模型越强,harness 越不能拖后腿。

这并不意味着业界其他领先 Agent 没有类似设计。如 Claude Code 的可见思路:分层记忆、分作用域组装上下文、只读探索到写入执行的权限递进、子 Agent 上下文隔离、worktree 隔离、命令风险分级、单一职责工具、确定性生命周期 hooks。它解决的也是同一组痛点,只是语言更工程,不讲代数。Codex 一类写码 Agent 也大抵如此,依赖 sandbox、审批、diff、测试、git 回滚、容器隔离来控制副作用。至于 Hermes 等系统,如也是 agentic harness,通常也逃不开这几件事:工具注册、能力白名单、上下文裁剪、权限边界、执行隔离、审计日志。大家都在处理 effect 和 coeffect,只是多数系统把它识别为安全、沙箱、权限、工作流等等。Cordis 的特别,就在于它试图把这些工艺提炼为一套统一语义

再横向看,范畴论进入工程也并非第一次。成功者如 Haskell,把副作用变成可组合的值;如Scala 的 Cats Effect、ZIO,把这套思路做成生产级并发运行时;如Kestrel 的 Specware 系统,更用范畴论做软件规格说明、合成、维护,证明其在工业级开发中的可行性。当然反例也不少,早期 FRP、过度 Monad Transformer、过度 tagless-final,论文里应有尽有,落地后败给调试、性能、招聘、交付压力等各种问题。

若由此多想一步,Cordis-like 机制的想象力还可以从 harness 向外走出多远呢?以下谨以笔者略知一二的领域,尝试展望一二。

在搜广推系统浩瀚无边的陈年屎山中,召回、粗排、精排、重排、过滤、混排、实验、特征、模型版本、日志回流、冷备恢复……本质上都是带依赖的动态组件。一个策略上线改变流量,一个模型依赖特征 schema,一个实验要求特定 embedding 服务和数据新鲜度,个中滋味,一言难尽。设想有一个 Cordis-like 机制,如能用于控制面和生命周期管理,就不会只问“配置有没有发出去”,而会追问每个策略提供什么、依赖什么、改变什么、如何回滚。毕竟推荐系统缺的不是又一个 if else,而是能管理策略、特征、模型、实验之间动态一致性的 Context。当然,线上请求热路径看到的仍应是已编译、不可变、局部缓存的执行图。

在调度科学计算作业的 HPC 超算、调度虚拟机的 IaaS 云、编排负载的 K8s、乃至如今把训练任务当一等公民的所谓 ML 智算平台,一个合理的思路是让这套语言成为分布式控制面的语义骨架。(但不可替代分布式一致性协议,也许可作为Saga模式的补偿协议,因为单机 Context 的代数逆,在跨进程、跨机器的分布式环境里会退化为带幂等、重试和补偿的恢复协议,并不能替代共识系统。)Cordis 式系统可以把资源、数据、通信、监控、恢复拆成可组合、可撤销、可观测的组件。当然,Lambda 类平台连“工作负载”的持续运行都抽象掉了。用户只提交“函数”,也算是这方面起步较早的探索。

而在量子计算中,抛开顶层应用层及底层的物理实现和控制层不谈,至少在量子计算的形式化与编译优化上,范畴论值得押注。编译优化是离线低频任务,不卡热路径,却足以承担更复杂的计算和决策,又有丰富的上下文信息可感知,复杂度刚好付得起抽象税。如何用类似的机制在运行时动态管理组件依赖、控制噪声,或者直接用辫状幺半范畴的容错性做容错量子计算,都是可探讨的方向。

总之,在现代工程,谁改变世界、谁依赖世界、改变能否撤销、依赖能否重算、组合后的系统还是否遵守承诺,所有这些处理不慎,都可能是未来的技术债,在 AI vibe coding 遍地走的时代更是如此。代数骨架固然不能完全替代工程打灰,但至少能避免让泥巴糊得每层都是。

可以期待的是,必然会有其他领域开发者意识到这一点,以 Cordis-like 的代数骨架重构原本的系统,其外溢影响,会比特定领域内的二次开发要大得多。

后记

起初看到论文,我就想到 Bartosz Milewski 的《程序员的范畴论》。当然就我能看到的论文文本和参考文献,二者没有直接引用关系,姑且理解为某种趋同演化,在此也推荐工程背景的朋友了解。

Milewski 在书一开头就指出:组合是范畴论的根基,也是编程的本质;副作用本身不邪恶,真正的问题在于副作用被隐藏后无法扩展。于是,范畴论用类型、函子、单子、Kleisli 箭头这些工具,把“脏东西”重新纳入可组合结构。

而 Cordis 做的事,则把这个问题推进到更复杂的运行时,回答一个会加载、会卸载、会依赖、会失败的组件该怎样组合,并把这套眼光从纸面上的函数做到真正工程落地,是范畴论在应用方向上又一次大胆的尝试。

参考文献

  • Cordiverse. A Programming Paradigm for Spatiotemporal Composability. paper.pdf.