一切都是插件:DeepSeek Harness背后的Cordis设计理念
DeepSeek Harness 如何利用可逆效应、反应式依赖关系以及可替换的智能体循环等 Cordis 的理念,通过插件构建智能体运行时环境。
来源: DeepSeek Quietly Built a Different Kind of AI Agent Framework
常规方案及其缺陷
大多数智能体框架都基于相同的要素:一个语言模型、几款工具、存储的对话记录,以及围绕这些要素构成的循环结构。下图展示了其整体架构。
User asks something
↓
LLM thinks
↓
LLM calls a tool
↓
Tool returns data
↓
LLM continues
↓
Final answer
按定义,那已经算作一个代理了。问题出现在系统规模扩大之后——需求越来越多:多个模型提供方、浏览器工具、shell执行环境、MCP服务器、审批流程、内存管理、遥测功能、身份验证机制、不同的用户界面、后台代理、自定义提示词、权限控制以及热重载功能。很快,各个部分就会相互依赖。更换模型提供方会影响代理循环,移除插件会留下事件监听器,而重新加载工具则可能中断当时正在运行的代理。此外,插件还依赖于那些可能在运行过程中出现或消失的服务。
DeepSeek Harness的解决方式截然不同。它的核心理念听起来简单得近乎不可思议:一切都是插件,就连大多数框架视为核心的部分也不例外。
在这一切之下是一个名为 Cordis 的框架,它源自一篇题为《时空可组合性的编程范式》的研究论文。这篇论文极具价值,因为该项目并非仅仅另一种用于组织 JavaScript 模块的方案,而是致力于解决一个更深层次的工程问题:在运行中的程序中,组件如何能够相互连接、退出、替换以及改变依赖关系,同时避免运行时陷入混乱?
什么是 harness
DeepSeek Harness,简称 dsh,是由 DeepSeek 发布的开源智能体 harness。Harness 是围绕模型存在的基础设施。无论该模型是 DeepSeek、Claude、GPT、Gemini 还是本地的模型,harness 都会处理与其相关的一切事务。
┌───────────────┐
│ LLM │
└───────┬───────┘
│
┌──────────▼──────────┐
│ Agent Harness │
│ │
│ Prompt construction │
│ Tool execution │
│ Session history │
│ Permissions │
│ Streaming │
│ Agent lifecycle │
│ Model adapters │
└─────────────────────┘
这一差异至关重要。人们通常关注模型的智能程度,但像 Claude Code、Codex、Cursor 或长期运行的自主代理这样的产品表明,其运行环境同样重要。DeepSeek Harness 将运行环境视为由动态组合的插件构成的整体,而非单一的整体应用,这一设计理念直接继承自 Cordis。
依赖关系难题
想象一下用三个插件来构建一个编程代理。
GitPlugin
TerminalPlugin
CodingAgent
编码工具需要 Git 和终端。启动时最简单的方法是按顺序加载它们。
load Git
load Terminal
load CodingAgent
这很简单,大多数依赖注入框架都能很好地处理。更棘手的问题是:如果在程序运行时 Git 插件消失了会怎样?它可能已被升级,配置可能发生了变化,用户可能已将其禁用,或者可能有其他实现替代了它。此时运行时必须停止所有依赖该服务的操作。
CodingAgent
↓ -------->depends on
GitPlugin
如果 Git 消失了,编码代理就不应再假装它存在。而如果有其他 Git 提供商出现,或许该代理就应该重新开始工作。这里隐藏着两个独立的问题,Cordis 的论文将它们分别称为时间可组合性与空间可组合性。
时间可组合性:干净地退出
假设某个插件被加载并注册了一个事件监听器。
events.on("message", handler)
之后插件会被卸载,问题在于监听器会怎样。如果没有任何机制将其移除,虽然插件本身已不存在,但其部分功能仍会继续运行。此外,插件可能还启动了定时器、建立了数据库连接、注册了命令、修改了共享状态、添加了工具、绑定了事件处理程序并启动了工作进程。卸载插件意味着要撤销所有这些操作。
这就是临时组合性:加入系统的组件应当让系统能够完全撤销该组件的所有操作。Cordis将此类变化视为可逆的效果。
用通俗语言解释效果
所谓效果,指的就是代码执行的那些会改变外部世界或运行时状态的操作。以工具注册为例。
toolRegistry.add("search", searchTool)
在调用之前,注册表的状态如下。
tools = {}
之后它会持有新工具。
tools = {
search: searchTool
}
前向操作即加法运算。
add(searchTool)
不过,Cordis期望的还不止于此。它希望该操作能拥有自身的逆操作。从概念上讲,一种效果会将某个动作与其撤销操作配对在一起。
Effect:
do: add search tool
undo: remove search tool
如果没有数学符号,其含义如下。
state --effect--> new state
new state --inverse--> original state
那个逆操作正是关键所在,Cordis将整个组合称为可撤销效应。
每一次变更都需要相应的清理
Cordis通过类似以下的模式来实现这一点:一个函数负责执行变更,同时返回用于撤销该变更的函数。
ctx.effect(() => {
registerSomething()
return () => {
unregisterSomething()
}
})
运行时会应用该效果,同时存储清理函数,待插件被移除时再调用它。
apply effect
↓
store cleanup function
↓
plugin runs
↓
plugin unloads
↓
run cleanup
当一个插件包含多个效果时,它们会按固定顺序依次应用。
effect A
effect B
effect C
清理过程会以相反的顺序进行,就像栈的弹出方式一样。
undo C
undo B
undo A
为什么要反向操作?试想一个这样启动的插件。
1. open database
2. start service using database
3. start agent using service
按相同顺序关闭会很危险,因为当其他程序仍在使用数据库时它就已经被关闭了。
close database
stop service
stop agent
安全的排序则恰恰相反。
stop agent
stop service
close database
那是后进先出原则。
Last In
First Out
Cordis将这种机制整合到了组件生命周期管理中。
仅靠反向清理是不够的
这里的内容就变得有趣了。假设插件A修改了一个共享值,随后插件B又修改了这个相同的值,之后A试图恢复它最初看到的状态,这样就可能会抹掉B所做的修改。
Initial: x = 10
Plugin A: x = 20
Plugin B: x = 30
Plugin A unloads: restore x = 10
B仍然处于活跃状态,但其状态已消失。因此,可逆效应只有在特定的独立性假设下才是安全的,本文将这种假设形式化为效应独立性。通俗来说,当施加或撤销某一效应不会使另一效应的含义失效时,这两种效应就是独立的。由于不同的工具作用于不同的位置,通常满足这一条件,因此可以视为独立。
Plugin A registers tool "search"
Plugin B registers tool "calculator"
更换相同的配置可能无法解决问题。
Plugin A replaces configuration X
Plugin B also replaces configuration X
当 Cordis 的上下文系统被提及时,这一区别会再次出现。
空间可组合性:了解插件需要什么
时间可组合性关注的是某个组件是否可以安全地被移除。而空间可组合性则关注系统是否明白该组件依赖于什么。想象一下这个插件。
CodingAgent
它需要三样东西。
LLM
ToolRegistry
SessionStore
传统代码可能会使用全局变量。
global.llm
global.tools
global.sessions
在这种情况下,架构本身并不知道这些依赖关系。Cordis会将它们明确体现出来。从概念上讲,插件会声明它要注入的内容。
inject: [
"llm",
"tools",
"sessions"
]
现在运行时可以将插件的需求以树形结构显示出来。
CodingAgent
├── requires LLM
├── requires ToolRegistry
└── requires SessionStore
这样就能在这些服务发生变化时做出响应。
反应式依赖关系
这或许是 Cordis 中最出色的设计思路。假设某个组件需要一个大型语言模型。
LLM
起初并不存在。
LLM unavailable
因此该组件无法被激活。稍后会有一个提供者插件加载并提供一个。
OpenAI Plugin loads
↓
provides LLM
↓
dependency becomes satisfied
↓
component activates
如果移除该提供者,其对应功能也会随之消失。
OpenAI Plugin unloads
↓
LLM disappears
↓
dependency becomes unsatisfied
↓
dependent component unloads
当加载其他提供程序时,该功能会恢复。
DeepSeek Plugin
↓
provides LLM
↓
component becomes active again
因此,依赖项并非在启动时一次性检查。它们是反应式的,这也是该论文使用“反应式共效应”这一术语的原因。
效应与共效应
这些术语听起来很复杂,但区分起来其实很简单。效应描述的是某个组件所改变的内容,从插件传递到环境中。
Plugin → Environment
示例包括这些操作。
register tool
open database
subscribe to event
start timer
共效应描述的是组件从其环境中所需要的东西,其作用方向与前者相反。
Environment → Plugin
典型的例子就是这些需求。
need an LLM
need a logger
need session storage
need a tool registry
将两者结合,需求就会按单一方向流转。
requirements
Environment ───────────────────> Plugin
而效果则不断回流。
Environment <─────────────────── Plugin
effects
这就是 Cordis 的基本理念。一个组件会说明它需要什么、能提供什么,以及它能实现哪些可逆的效果。
I NEED these services.
I PROVIDE these services.
I PERFORM these reversible effects.
运行时随后会协调组件的生命周期。
“时空”一词的由来
现在这篇论文的标题就说得通了。空间可组合性关注的是组件之间的关系。
A depends on B
B provides X
C depends on X
时间组合性关注的是组件如何随时间变化而移动。
load
activate
replace
deactivate
unload
reload
它们共同构成了完整的哲学体系。
Spatiotemporal composability = dependency-aware composition + reversible lifecycle changes
上下文对象
DeepSeek Harness不会手动通过构造函数传递数十个服务。插件之间通过共享的上下文进行交互,该上下文的命名通常为此类形式。
ctx
从概念上讲,它展示了主要服务。
ctx
├── llm
├── tools
├── sessions
├── agents
├── agentLoop
├── logger
└── ...
ctx并不仅仅是一个装满全局变量的 JavaScript 对象。通过它,插件能够找到所需的依赖项、提供服务、注册效果、监听事件、隔离资源、拦截对服务的访问以及协调生命周期,正因如此,Cordis 将该上下文视为一种统一的抽象概念。
从理论到实际项目
DeepSeek Harness 通过这一共享的上下文来暴露其各个主要子系统。
ctx.sessions
ctx.systemPrompt
ctx.tools
ctx.agents
ctx.agentLoop
ctx.llm
注意一个不同寻常的现象:就连代理循环也仅仅是一项服务而已。
ctx.agentLoop
因此,该循环可以被替换,并非不可更改的框架代码。这便引出了设计的核心原则:不存在所有功能都必须修改的特殊核心部分。
为何将循环作为插件很重要
大多数智能体框架都包含类似这样的核心循环。
while not finished:
response = model(history)
if response.tool_call:
result = execute_tool(response.tool_call)
history.append(result)
else:
return response
随后,各种高级功能开始对其进行调整。运行命令前需要审批?那就修改循环结构。需要追踪功能?同样要修改循环结构。需要重试机制、多智能体协作或不同的推理后端?还是得修改循环结构。最终它就变成了一个中心节点,各种工具、模型和内存都依附于它之上。
Agent Loop
/ | \
/ | \
tools models memory
/ | \
approvals retries tracing
该循环会变成应用程序的“上帝对象”。DeepSeek Harness通过让该循环与其他所有组件共享相同的插件架构,避免了这一问题。
一个智能体轮次的流程
在底层,一次交互大致会经历这样的几个阶段。
turn/start
↓
claim incoming user input
↓
assemble prompt + available tools
↓
agent/pre-step
↓
step/start
↓
record user message
↓
reconstruct conversation history
↓
send model request
↓
stream assistant response
↓
tool call?
│
├── yes → execute tool
│ record result
│ continue another step
│
└── no → finish
↓
step/end
↓
turn/end
重要的是,每个阶段都会暴露事件和服务接口,这样插件就可以在不重写整个运行时的情况下参与其中。
作为代理中间件的瀑布流事件
Cordis 支持多种处理事件的模式。其中一种便捷的模式——瀑布流模式,其行为类似于中间件。可以将其想象成请求在管道中依次传递的过程。
request
↓
Plugin A
↓
Plugin B
↓
Plugin C
↓
LLM
插件B可以用一个能够接收下一步指令的处理程序来包裹执行流程。
async function handler(next) {
console.log("before")
const result = await next()
console.log("after")
return result
}
执行过程是嵌套的,每个插件会在内部调用之前和之后执行操作。
Plugin A before
Plugin B before
Plugin C before
LLM
Plugin C after
Plugin B after
Plugin A after
这些功能包括权限控制、请求重写、遥测、缓存、模型路由、安全检查以及上下文注入,它们均无需硬编码到模型适配器中。
模型位于能力接口之后
该框架还对大语言模型进行了抽象,因此代理无需了解每个提供方的SDK,只需与单一服务进行交互即可。
Agent
↓
ctx.llm
↓
Model Adapter
↓
Provider API
可能有某个提供商为 DeepSeek API 实现它。
DeepSeek API
另一篇则涉及 OpenAI。
OpenAI
第三部分涉及Anthropic。
Anthropic
代理架构的其余部分也用于实现相同的大语言模型功能。Harness文档有时将此称为“功能接口”,它由服务定义和服务提供者组成。
Service Definition
↓
Service Provider
↓
Consumer
对于大型语言模型而言,该链条从接口延伸至特定的服务提供商。
LLM interface
↓
OpenAI provider
↓
Agent loop
因此,添加模型提供者并不一定意味着要重写运行时代码。你只需为相同功能提供另一种实现方式即可。
基于事件的会话
该框架不会将对话保存为一个作为唯一真实数据的大型可变数组。相反,会话是以仅能追加事件的流形式存储的。
turn/start
user/message
assistant/chunk
assistant/chunk
assistant/message
tool/call
tool/result
assistant/message
turn/end
模型历史可以基于这些事件重新构建,因此会话日志才是真实的数据来源。
Session log = source of truth
这取代了常见的替代方案。
Mutable chat history = source of truth
为何代理程序能从事件源中受益
由于代理程序的复杂性日益增加,仅存储简单的消息列表已往往不够用。
[
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
]
调试时需要了解哪些工具被启用、哪些被调用,哪个模型生成了响应、应用了何种系统提示、请求从何处开始、推理步骤在何处结束、当前生效的配置是什么、是否重新尝试了调用以及返回了何种流式输出。该框架会记录结构化的会话事件,从而能够重建模型所看到的所有内容。其设计原则之一是:模型能看到的任何内容都应能从日志中恢复出来。这样就不必猜测智能体为何会如此行事,而是可以直接还原其背后的请求。
Fiber与生命周期状态
Cordis用Fiber来表示正在运行的组件,这与操作系统的线程无关。Cordis Fiber是一个运行时组件实例,它会经历诸如这样的状态变化。
PENDING
↓
LOADING
↓
ACTIVE
↓
UNLOADING
↓
DISPOSED
也存在故障状态。更重要的是,光纤会记录关于自身的多项信息。
what dependencies I need
what providers currently satisfy them
what effects I have created
what cleanup functions belong to me
当依赖项发生变化时,fiber 可以刷新其生命周期。
为什么提供者身份很重要
假设某个组件依赖于数据库服务。
database
目前它由一家供应商提供。
DatabaseProvider A
在热重载过程中,它会被另一个替换掉。
DatabaseProvider B
即使两者返回的 JavaScript 对象相同,Cordis 也能区分提供者的身份。原因在于依赖关系并不仅仅意味着某个值存在。
"value exists"
它也可能意味着该值来自另一个组件实例。
"this value came from a different component instance"
因此,更换提供者可以促使依赖项正确重新加载,从而避免大量因引用过时而产生的错误。
按依赖顺序卸载
设想一个由某个插件负责提供数据库的链式结构。
Plugin A provides database
第二个插件依赖于数据库,而第三个插件则依赖于第二个插件。
Plugin B depends on databasePlugin C depends on Plugin B
依赖关系链如下所示。
A
↓
B
↓
C
现在第一个已被移除。简单的运行时系统可能会立即关闭数据库,即便另外两个仍在执行中。而Cordis则会协调依赖关系图,随后开始卸载操作。
A begins unloading
然后,在原始对象完成清理之前,其依赖对象会依次被释放。
↓ mark A unavailableB notices dependency disappeared↓ B unloadsC notices B disappeared↓ C unloadsthenA completes cleanup
顺序至关重要,因为它能防止依赖方使用已经严重损坏的资源。
隔离:一个名称,不同的世界
单一的全球服务命名空间可能会带来限制。试想有两个代理需要不同的数据库。
Agent A → Database A
Agent B → Database B
两者仍然要求同样的东西。
database
Cordis支持隔离机制,因此服务解析是针对一对节点来进行的。
service name + isolation realm
而不仅仅是一个名字而已。
service name
从概念上讲,每个领域都会将同一个名称解析为各自的实例。
Realm A: database → DB_A
Realm B: database → DB_B
因此,相同的逻辑依赖关系会根据上下文的不同而产生不同的解决方式,这有助于实现多租户系统、多个代理、测试环境、沙箱插件以及独立的工作空间。
拦截
Cordis 还能通过元数据来控制对服务的访问。以工具注册表为例,管理员代理就可以查看这些工具。
shell
browser
filesystem
deploy
而受限代理只能看到更小的数据集。
browser
filesystem-readonly
无需构建独立的注册表,即可根据上下文拦截访问请求。
ctx + metadata
↓
service resolution
↓
modified view of capability
这对政策系统而言是个很强的模式。
更安全的热模块替换
热重载听起来很简单。
file changed
↓
reload module
在插件运行时,情况会变得复杂。如果新模块存在错误,原有的正常版本该何去何从?Cordis加载器支持类似事务处理的重新加载功能。
working plugin
↓
code changes
↓
attempt new import
↓
success?
┌───────┴───────┐
yes no
↓ ↓
replace rollback
plugin old module
这与可逆效果的理念十分契合。那些具有明确生命周期、依赖关系及清理机制的插件,在更换时带来的风险要小得多。
现实案例研究
Cordis并非仅为DeepSeek Harness而创建。相关论文研究了基于它的开源聊天机器人框架Koishi,其社区生态系统拥有数千个插件。如此完善的生态系统使得动态组合方式比十个手工编写的模块更具优势。不过论文也持谨慎态度,并未声称控制性测试已显示出显著的性能或效率提升,仍需进一步评估。
并非所有操作都能撤销
理论在此处遇到了现实限制。有些运行时操作确实是可逆的,比如文件的打开与关闭。
open file
→ close file
其他组合也同样配得很好。
allocate memory
→ free memorystart worker
→ terminate workerregister event listener
→ unregister listener
还有发送电子邮件这一步。
send email
它的倒数会是什么?
unsend email?
通常那是不可能的,其他一系列操作也是如此。
send HTTP request
transfer money
publish message
delete external data
当某事物影响到外部世界后,可能并不存在真正的逆转。最佳的解决办法就是补偿。
payment → refund
message → follow-up correction
database mutation → compensating transaction
补偿与数学上的逆运算并非相同概念。该论文明确指出了这一区别,而 Cordis 在处理那些运行时确实能够控制并恢复的效应时表现最佳。
Cordis 委托你处理清理工作
假设某个插件在某个效应内部启动了一个服务器,并返回一个清理函数。
ctx.effect(() => {
startServer()
return () => {
// supposedly cleanup
}
}))
Cordis 无法证明清理操作确实能够逆转启动状态。正式模型认为逆操作必须将环境恢复到之前的状态,但具体实现则取决于程序员能否编写出有效的清理函数。因此,将某种效应与清理操作配对其实是一种需要遵循的规范。
effect + cleanup
这并非自动形式化验证,这是一个重要的区别。
为何这适合人工智能代理
传统应用程序在启动后通常保持相对稳定,而代理系统则恰恰相反。未来的代理系统可能会持续执行此类操作。
install tool
remove tool
change model
spawn agent
kill agent
load skill
reload code
switch permissions
connect service
disconnect service
modify environment
想象这样一个代理:它判断自己需要 GitHub 支持,于是加载相应的 GitHub 功能;之后当该功能不再有用时便将其卸载,或替换掉出问题的实现方案。这样的系统需要比传统依赖注入器更强大的运行时组合语义。若没有这些机制,长期运行的代理就可能会积累各种残留物。
orphan listeners
stale tools
dangling processes
broken dependencies
zombie services
duplicated handlers
Cordis致力于将运行时组合提升为一流软件概念。
为何这种设计如此优雅
由于Cordis负责处理组合逻辑,DeepSeek Harness几乎可以以相同方式对所有内容进行建模。
Model provider → plugin
Tool → plugin
Agent loop → plugin
Session store → plugin
UI → plugin
Telemetry → plugin
Approval system → plugin
Sandbox → plugin
Credential store → plugin
新功能并不会成为框架内的特例,而是会成为某个组合系统中的更多参与者。
配置文件与插件包
如果所有内容都是插件,没人愿意每次都手动组装五十个插件。因此该框架提供了更高级别的组合方式。配置文件用于描述带名称的系统设置,而插件包则负责分发成组的插件及其相关配置。一个基础插件包可能长这样。
Base Bundle
├── model adapters
├── tools
├── sessions
├── sandbox
├── approvals
├── credentials
├── settings
└── telemetry
另一个软件包可以添加一个网络应用程序。
Web Application
另一种方法可以生成无头代理。
Headless Agent
该架构保持模块化设计,不会给日常使用带来不便。
值得牢记的五个要点
即便其他一切都被忽略,这五点仍是必须记住的。
首先考虑可逆效果:组件所做的任何更改都应具备撤销方式。
change + inverse
第二种是反应式共效应。组件声明其所需资源,当依赖关系出现、消失或变化时,运行时会作出响应。
dependency available → activate
dependency gone → deactivate
第三是上下文。各种效果、依赖关系以及服务访问都存在于同一个统一的运行时上下文中。
ctx
第四种是动态组件演算,它通过组件的需求、提供的功能以及变化情况来描述该组件,从而为运行时提供足够的结构以处理组件的加载与卸载问题。
Dependencies + Provisions + Effects
第五点是时空可组合性,它将空间与时间层面结合起来。
spatial:
who depends on whom?
+
temporal:
how can components safely enter and leave?
简而言之,就是可逆变化加上反应式依赖。
为何其意义超越单个项目
目前大多数智能体框架仍假设应用程序是相对静态的:启动程序、加载工具、加载模型、运行智能体、停止。而自主智能体则趋向于在持续运行的同时能够自我修改的运行环境。试想一种能够按照这样的流程行事的智能体。
discover a capability
install it
use it
upgrade it
replace it
remove it
continue running
在那个阶段,插件生命周期管理不再只是一个便利的架构特性,而变成了基础基础设施。这正是让该项目有趣的地方。其模型并非最创新的部分,真正有创意的是围绕它的运行时机制。
更深层的启示
软件架构通常关注某一时刻的组件组合方式。你将各个部分组装起来并运行程序。
A + B + C
Cordis提出了更尖锐的问题。
What if A disappears?
What if B is replaced?
What if C suddenly gains a dependency?
What if another implementation of A appears?
What if all of this happens while the system is running?
这完全改变了问题的性质。各个组件不再只是静态的模块,而是像有生命的参与者一样:它们出现、建立关联、改变周围环境、依赖其他组件,最终离开。
核心要点
DeepSeek Harness看起来可能只是另一个智能体框架,它的吸引力并不在于能够调用大语言模型或运行工具——很多框架都能做到这一点。其真正的创新之处在于,组合系统本身就构成了架构:每种功能都是一个组件,每一种依赖关系都清晰明确,每个可控制的效果都知道如何自行清理,当依赖关系发生变化时组件会作出响应,甚至智能体循环也可以被替换,它并非不可修改的框架代码。其指导原则是让每种功能都成为组件,让每一种依赖关系都清晰可见,让每个效果都能实现自我撤销。
在普通应用中,这是一种简洁的插件架构。但在需要长时间运行且能自我修改的AI智能体中,这一点可能更为重要。如果这些智能体最终要改变自身的运行环境,软件就需要一种合理的方法来实现变更,同时避免逐渐破坏其运行的环境。这正是DeepSeek Harness及其背后的Cordis研究旨在解决的问题。
相关阅读
- DS-STAR详解:谷歌的数据科学智能体及其成功之道 —— 本文介绍了谷歌的DS-STAR数据科学智能体的结构:它的七个模块、DS-STAR+报告循环、已发布的提示词、消融实验结果以及存在的局限。