DeepSeek Harness 深度拆解 · 第二篇 · 2026年8月16日
一切皆插件:拆解 DeepSeek Harness 的 Cordis 架构
作者:AISTOC产品团队
第一篇我们讲了 Harness 是什么、谁做的、为什么现在炸。这一篇往下走一层:当 README 说"everything is a plugin"时,它到底在说什么?一个 agent 循环怎么能做成插件?插件被卸载后怎么保证不留下内存泄漏和悬空监听?这一切的工程基础是 Cordis——一个在 Koishi 上有 4 年生产记录的插件元框架。
一、先从具象问题开始
任何用过插件系统的开发者都遇到过这些问题:插件 A 依赖插件 B,卸载 B 时 A 怎么办?插件注册了事件监听器,卸载时忘了清理,内存泄漏谁管?两个插件都想修改同一个配置项,谁覆盖谁?运行时想热替换一个插件,怎么保证状态干净?
大多数框架的答案是"约定":插件作者记得在 dispose 里清理,框架按依赖顺序加载和卸载。但约定会被违反——尤其是 Agent 场景下,加载/卸载决策由模型在运行时做出,没有代码审查、没有测试覆盖,你不能指望模型记住"这个工具注册了三个监听器,卸载时要一一撤销"。机制保证不是锦上添花,是底线。
Cordis 的答案是机制,不是约定。
二、Cordis 的两个维度
根据 Cordis 团队 8 月 13 日同步发布的论文 draft《A Programming Paradigm for Spatiotemporal Composability》,编者将其核心主张理解为:动态系统的组合有两个正交维度。需要说明的是,论文目前为 draft,形式化证明部分尚未经同行评审,以下描述基于论文摘要与 Floatboat 技术解析,属于编者解读。
空间维度:谁依赖谁。插件通过 inject 声明依赖,运行时自动解析依赖图。服务就绪时自动加载依赖方,服务停止前先卸载依赖方,服务失败时依赖方不会启动。这不是新鲜事——DI 容器做了很多年。但 Cordis 的响应式依赖管理意味着依赖关系可以在运行时变化,不需要重启整个应用。
时间维度:来过,走了,不留痕迹。这才是 Cordis 真正不同的地方。每个插件在加载时注册的副作用——事件监听器、定时器、文件句柄、网络连接——都通过 ctx.effect() 注册,携带对应的清理函数。插件卸载时,运行时按注册逆序自动执行全部清理。类比 C++ 的 RAII 或 Rust 的 Drop——区别在于 RAII/Drop 绑定词法作用域,Cordis 绑定插件生命周期——但在运行时层实现,不需要编程语言的所有权系统支撑。除了 ctx.effect(),Cordis 还暴露 ready、dispose、fork 三个用户可见生命周期事件,分别处理加载、卸载、子上下文派生——其中 fork 正是 dsh 会话 fork 在内核层的对应钩子,第三篇会展开。
这两个维度组合出一个关键特性:路径无关性。按论文 draft 的主张,应用最终状态只取决于启用了哪些插件,与加载/卸载顺序无关。想象一个 WordPress 插件系统:启用 A 再 B,和先 B 后 A,可能得到不同结果,因为插件通过隐式全局状态相互影响。Cordis 消除了这种耦合。路径无关性是"可回滚"和"可 fork"的前提——若状态依赖路径,就无法保证回滚到干净状态,也无法保证 fork 分支字节一致。需区分:这里的"路径无关"指副作用层面(任意顺序加载/卸载都能干净回滚),配置解析是另一回事——Patch 有明确层叠优先级,后应用覆盖先应用,这是有意的可预测性设计,下一节展开。
Cordis 不是实验项目。过去 4 年它是聊天机器人框架 Koishi 的底层内核(GitHub 约 6,000 star),在 Discord、Telegram 等平台生产运行。但要诚实:dsh 用的 v4.0.0-rc.7 自身也是发布候选版,4 年记录对应的是 Koishi 上的早期版本,v4 的核心 API 可能仍有调整。dsh 以 vendored 方式引入,打了 18 个本地补丁,重命名到 @deepseek-ai 作用域。Cordis 核心作者施一帆(shigma)的 GitHub 主页标注了 DeepSeek,但其入职状态尚未经官方或本人确认。
三、两根支柱:横向的生命周期,纵向的执行历史
理解 dsh 架构,最关键的认知是:它有两根支柱,不在同一层,不要混为一谈。
第一根支柱(横向):Cordis revertible effects。这是插件生命周期机制。插件加载时注册的副作用在卸载时自动清理,保证不留残留。它解决的问题是"组件怎么安全地加入和退出运行时"——在空间维度上,组件可加入可退出;在时间维度上,退出后不留痕迹。
第二根支柱(纵向):append-only session log。这是 dsh 自己的会话持久化机制。每一次 Agent 执行都作为不可变事件追加到日志,你可以从任意时间点 fork 出新的会话分支,重放、对比。它解决的问题是"Agent 的执行历史怎么审计、回溯和分叉"——历史不可篡改,但可以从任意点分叉出新的时间线。
打个比方:revertible effects 像是 C++ RAII——对象析构时自动释放资源,不过它回滚的是运行时副作用(监听器、定时器、连接),不是数据库事务那种数据变更;append-only log 像是 Git 版本控制——每次提交都永久记录,可以 checkout 到任意历史点。两者互补但不是同一回事:前者让运行时保持干净,后者让执行历史可追溯。
两根支柱有一个交汇点:当 Agent 在运行时通过 revertible effects 加载或卸载插件时,这个动作本身也会作为会话事件写入 append-only 日志。也就是说,Agent 对自身运行时的修改,既是可回滚的副作用,也是可审计的历史事件。具体机制第三篇展开。
记住这两根支柱。接下来的插件树属于第一根(空间组合),执行流、不变量、沙箱属于第二根(时间保证)。
四、插件树:Profile、Bundle、Patch
在 Cordis 之上,dsh 用三层模型组织插件。Profile 是命名配置集(官方自带 web 和 headless 两个模板);Bundle 是可分发的插件包(基础三层:dsh-base 包含模型适配器、工具、沙箱、审批、凭证等,dsh-web-app 加浏览器界面,dsh-headless 加一次性运行器);Patch 按 id 定位配置行,替换或插入。
用一个具体场景理解:你想让 dsh 用 Claude 模型跑在自定义沙箱里,不需要 fork 源码——写一个 patch 替换模型适配器配置行指向 Claude endpoint,再写一个 patch 替换沙箱提供者指向远程沙箱,启动即可。层的应用顺序从下到上:bundle 按序堆叠 → Profile 的 patch → home 目录 patch → 命令行 --patch 覆盖。dsh --dump-config 可打印完整插件树,任何打印出的行都能被 patch 替换。
需要精确:在应用层——从模型适配器到 Agent 循环到 Web UI——没有特权组件,任何一行都能 patch。Cordis 内核本身(effect 追踪、依赖解析、patch 机制)是让这一切成为可能的底座,不在可替换范围内。你不能用插件系统替换插件系统本身,这不是缺陷,是任何可扩展系统的必然结构。
五、Turn/Step:一次 Agent 执行内部发生了什么
在插件树之上,dsh 的执行模型分两个粒度。Step 是一次模型请求加上它调用的所有工具;Turn 是零个或多个 Step,在第一个输入被认领(claim,即一个 Agent 取得该输入的排他处理权)前打开,在没有待处理工作时关闭。一个 Turn 可能只有一个 Step,也可能包含多个 Step(模型搜文件、跑测试、改代码,每个工具调用后继续请求模型,直到干完)。Turn 也可能有零个 Step——比如 agent/pre-step 拦截并拒绝执行时,Turn 打开又关闭,不产生任何 Step。
一个 Turn 的完整事件流如下(来自官方架构文档):

这里有一个关键洞察:agent/pre-step、agent/request、llm/stream 和三个 tools/* 事件是瀑布式的——监听器必须调用 next() 才能继续。这意味着插件可以在模型被调用之前就拦截——不消耗一个 token。审批插件可以在 tools/pre-execute 暂停等人放行。agent/turn-stopping 串行但没有 next()——插件不能推迟 turn 关闭,防止 turn 永远关不掉。
事件分三类:
| 会话事件 | session/event | |
| Agent 事件 | agent/* | |
| 能力事件 | fs/*tools/* |
六、模型所见的对话消息即被记录
dsh 有一个运行时强制的不变量:任何到达模型请求的对话消息都必须能从会话日志字节级重建。精确地说,packages/core/agent-loop/src/invariant.ts 断言的是发出请求的 messages 与 session.deriveMessages() 从日志派生的投影字节匹配。system prompt 片段和工具 schema 在组装阶段是否同样被不变量覆盖,调研包未明确说明,因此这里不做全称判断——记录的是对话消息,这是代码里确凿断言的范围。
一层之下,packages/core/session/src/surface.ts 在追加事件时如果发现产生消息的事件没有标记它如何投影到模型历史,直接抛错。代价是每次生产请求都把完整消息历史序列化两遍做检查——DeepSeek 认为可审计性值得这个热路径开销。
这不止是调试便利。在一个 Agent 可能自动加载工具、修改配置、执行命令的系统里,"模型到底看到了什么"这个问题必须有确定答案——你不能在出了事故之后去拼凑当时发了什么 prompt。更深层地说,这是一种认识论层面的诚实:Agent 不允许有"未记录的知觉"。当 Agent 代表你读了邮件、扫了代码库、执行了命令,你有权知道它每一步判断基于什么信息。dsh 把这个权利固化成了运行时不变量——不是事后补日志,而是字节级断言。
如果说第六节的信条是"记录一切看到的",那么第七节的信条就是"拒绝一切未隔离的"。这是同一个默认不信任、强制可验证的安全哲学的两面。
七、Fail-closed:没有沙箱就拒绝执行
dsh 的安全模型遵循一个原则:缺少保护机制时,默认拒绝,而不是降级运行。工具执行按每次调用解析沙箱策略(read-only、workspace-write、danger-full-access),通过 argv 包装限制命令。各平台后端在启动时探测可用机制:
| Linux | 真隔离(两档强度不同) | |
| macOS | 真隔离 | |
| Windows | 仅限制写 |
Windows 后端只限制写,读、网络和进程可见性不隔离,官方文档诚实标注了这一局限。关键失败模式:如果请求了受限模式但启动时探测不到可用沙箱后端,运行时抛出 SANDBOX_UNAVAILABLE,拒绝以非受限模式运行命令。缺少审批服务同样意味着拒绝,而不是挂起等待或静默放行。dsh 不替你做"算了先跑吧"的决定。
能力接缝(capability seams)是另一个关键设计。文件系统和子进程提供者共享同一个执行世界——把它们从本地磁盘指向远程沙箱,Bash、PTY、LSP 不需要改一行代码就全部跟随到远程执行。子 Agent 提供者同样在一个接口后变化,从全新子 Agent 到另一个产品中的委托轮次。
八、和主流框架的根本差异
市面上的 Agent 框架可以按"谁拥有循环"分三类。这三类是理想类型,实际框架分布在连续谱上。
| 成品循环(商业) | |||
| 成品循环(开源) | |||
| 建材 | |||
| 可换零件的整机 | dsh | 原生支持 |
成品循环。Claude Agent SDK、Codex 给你优化好的 Agent 循环,开箱即用。你可以挂钩子、加 MCP server、写自定义 skill——配件可以随便加,但发动机(主循环)是厂商的,不能换。CrewAI、AutoGen 等开源成品循环框架占据中间地带:开源、可 fork 修改,但没有 Cordis 式的标准化插件生命周期机制来做运行时热替换。
建材:LangGraph。给你节点、边、checkpoint,你自己搭循环和状态机。最大灵活性,但循环驱动、消息组装、工具管道、沙箱集成、审批流、会话持久化都要自己处理。它是编排框架,不是运行时。
可换零件的整机:dsh。给你一个完整可用的 Agent——模型适配器、工具系统、沙箱、审批、会话日志、Web UI 全部就绪。从模型适配器到 Agent 循环到 UI 的每一个零件都可以拔下来换。架构上 Cordis 让运行时热替换成为可能——一台正在运转的机器,每个齿轮都可以在不停车的情况下拔下换上新的,而齿轮箱保证换下的旧齿轮不会留下一颗螺丝。但要诚实:在 v0.1 阶段,这种热替换(尤其是 Agent 循环本身)的实际稳定性还需要社区验证,目前更接近机制演示而非生产实践。
九、这一层真正值得研究的是什么
第一,运行时自修改的安全基座。传统软件里,运行时 monkey patch 几乎等于事故——你改了一个全局对象,不知道谁在依赖它,卸载时也无法保证干净撤销。Cordis 用 revertible effects 加路径无关性把这件事变安全:每个副作用都携带清理函数,卸载时按逆序撤销,最终状态与加载顺序无关。这意味着 Agent 不再受限于发布时的固定工具集,可以按需生长——加工具、换适配器、改策略——且每次生长都可审计、可回滚。v0.1 的边界要诚实:机制已就位,但 Agent 循环本身的热替换稳定性、复杂依赖图下的回滚正确性,还需社区验证。Floatboat 指出:能修改自身运行时的 Agent 只有在每次变更都可回滚时才值得信任。——编者注:dsh 是这一理念的产品落地。
第二,模型所见的对话消息即被记录。这个不变量把"可审计性"从事后追溯变成了运行时保证。Agent 看到了什么、调用了什么、返回了什么,全部在 append-only 日志里,字节匹配。在 Agent 越来越自主、越来越多操作生产环境的趋势下,这不是 nice-to-have,是必要条件。
第三,应用层无特权核心的插件树。Profile/Bundle/Patch 三层模型让每一个配置行都可覆盖,同时通过有序层叠保证组合的可预测性。dsh 有约 219 个 workspace 包、约 45.3 万行代码(以 TypeScript 为主),听起来很多,但核心机制——插件生命周期、依赖图、effect 追踪——集中在 Cordis 内核,其余大部分是插件实现。这种"每一行都可替换、且替换有机制保障"的开放程度,在开源 Agent 运行时中目前没有第二例。
这还打开了一个新的调试范式:Agent 在第 7 步出错,在固定循环框架里你只能重跑或手动检查;在 dsh 里,你可以 fork 第 7 步之前的会话,patch 掉出错的工具,然后重放——replay 不重新调用模型,不消耗额外 token。第三篇详细拆。
第一篇我们说,DeepSeek 用做市商的工程审美做 Agent 运行时。拆完架构,这句话有了具体含义:做市商不信任约定,他们信任机制——每笔交易可审计、每次操作可回滚、每个故障可复现。Cordis 把这种不信任变成插件生命周期的数学保证,dsh 把它变成 Agent 执行的运行时不变量。
约定会被违反。机制不会。
下一篇,我们拆 append-only 会话日志层:一个 Agent 在什么情况下会想 fork 自己?Turn 和 Step 怎么作为不可变事件写入日志?keyless replay 怎么做到不重新调用模型就能重放?Trajectory 视图为什么比传统 trace 更适合调试 Agent?这些才是 dsh 有别于其他框架最具体的工程差异。
本文基于 DeepSeek Harness 官方架构文档、Cordis 论文 draft(2026-08-13)、Floatboat 技术解析及 Developers Digest 代码审计。Cordis 论文尚未经同行评审,相关学术主张标注为论文 draft 或编者解读。Cordis v4.0.0-rc.7 自身为发布候选版,v4 核心 API 可能仍有调整。施一帆(shigma)的 GitHub 主页标注 DeepSeek,但其入职状态尚未经官方或本人确认。代码规模数据来自官方开发文档(约 219 个 workspace 包、约 45.3 万行代码,以 TypeScript 为主)。本文为系列第二篇,第一篇为总览,第三篇将拆解会话日志与 Trajectory 回放。
© 2026 AISTOC产品团队 · 转载请注明出处
夜雨聆风