【从零开始 DeepSeek Harness 插件开发教程】二、一切皆插件:简单理解一下 Cordis 内核与 DSH 心智模型
目录
- 前言
- 一、先理解:为什么需要 Harness?
- 1. 普通聊天机器人
- 2. 能使用工具的 AI
- 3. 可以自由扩展的 AI 系统
- 二、理解 Cordis:一切能力如何组织起来?
- 三、插件到底长什么样?
- 1. 声明依赖,并在挂载时执行代码
- 2. ctx 到底是什么?
- 3. 插件不只要启动,还要负责清理
- 四、Cordis 的四个核心概念
- 1. 服务和事件有什么区别?
- 2. DSH 的核心服务从哪里来?
- 五、Agent Loop:AI 是怎样一步步完成任务的?
- Agent Loop 在 DSH 中有什么特别之处?
- Step 和 Turn 又是什么?
- 六、Profile、Patch 与 Seam:怎样组合和替换能力?
- 1. Profile:选择一套基础运行组合
- 2. Patch:在基础配置上进行调整
- 3. Seam:让底层实现可以被替换
- 七、建立 DSH 的完整心智模型
- 1. 系统由什么组成?
- 2. 插件怎样协作?
- 3. AI 怎样完成任务?
- 4. 怎样观察和干预执行?
- 5. 怎样增加或替换能力?
- 八、真正开发插件时,应该怎样思考?
- 总结
- 下一篇预告
前言
上一篇文章:一、DeepSeek Harness 初体验:从零跑通开发者预览
理解 DeepSeek Harness(DSH):从零建立 Cordis 插件框架的心智模型
前面我们已经把 Harness 跑起来了,但它对我们来说仍然是一个黑盒。
我们只知道执行 pnpm dsh web,就能启动一个可以聊天的网页。至于消息是怎么变成工具调用的、插件之间如何协作、系统又是如何判断任务完成的,这些问题依然让人摸不着头脑。
而想要真正开始写插件,就必须先看懂这个黑盒。
好消息是,DSH 的设计哲学相对简洁:系统的各项能力都可以通过插件组织和扩展。
我不打算一上来就钻进复杂的源码,而是先从基础概念入手,再通过代码示例理解插件的工作方式,最后建立一套能够指导后续开发的心智模型。
OK,我们开始吧!
一、先理解:为什么需要 Harness?
在了解 Cordis 之前,我们先想一个问题:一个 AI 模型,怎样才能从「会回答问题」变成「能帮我们做事」?
1. 普通聊天机器人
假设你问 AI:
帮我看看这个文件里有什么问题。
如果 AI 只有文本输入和输出能力,没有读取文件的工具,那么它就只能让你把文件内容复制给它,再根据你提供的内容进行分析。
它可以思考,但无法直接访问你的文件。
2. 能使用工具的 AI
现在,我们给 AI 接入一个读取文件的工具。
当你再次提出同样的问题时,AI 就可以请求工具读取文件,获得文件内容,再根据结果进行分析。
它不再只是回答问题,还能通过外部工具获取信息、执行操作。
3. 可以自由扩展的 AI 系统
如果我们还想让 AI 操作浏览器、执行代码、记录用户操作,甚至检查某些操作是否安全,该怎么办?
一种做法是不断修改系统核心代码,把所有功能都写进去。
但这样做很快就会遇到问题:功能越来越多,代码越来越复杂,维护和升级也会变得困难。
另一种思路是,把不同能力拆成独立模块,需要什么能力,就接入对应的模块。
这就是插件化架构的价值。
在 DSH 中,可以先这样理解几个基础概念:
| 概念 | 通俗理解 | 主要职责 |
|---|---|---|
| DeepSeek 等 AI 模型 | 大脑 | 理解输入、生成回答、决定下一步行动 |
| Harness | 工作环境和办事流程 | 为模型提供工具、组织任务执行、管理运行过程 |
| Cordis | 模块组织与协作框架 | 管理插件、服务依赖、事件和生命周期 |
| Plugin(插件) | 独立的功能模块 | 为系统增加或替换能力 |
| Tool(工具) | 可以被调用的具体操作 | 读取文件、查询数据、执行某项操作 |
需要注意:Harness 不是模型本身,Cordis 也不是模型。 它们解决的是模型如何在一个软件系统中运行、使用工具和完成任务的问题。
接下来,我们就来看看 DSH 是如何通过 Cordis 把这些模块组织起来的。
二、理解 Cordis:一切能力如何组织起来?
DSH 底层使用 Cordis 作为插件框架。
官方架构文档对它的描述可以概括为:
Cordis 允许插件向共享上下文贡献服务、类型化事件和可逆副作用。模型适配器、工具注册表、会话日志以及 Agent Loop 等系统能力,也可以由插件提供和组合。
翻译成人话就是:
插件不仅可以增加新功能,也可以参与组织系统原本就有的功能。
例如,我们准备编写一个操作录制插件 action-recorder。
它可以向系统注册一组工具,让模型能够请求这些工具执行操作。只要系统的工具机制允许,这个插件就可以像其他工具插件一样参与运行。
这意味着,我们不必为了每增加一个功能,就去修改整个系统的核心代码。
不过,这并不意味着任何能力都能无条件替换。能否扩展或替换某项能力,仍然取决于它是否提供了相应的接口,以及插件是否满足相关约定。
理解这一点后,我们再来看看一个插件究竟长什么样。
三、插件到底长什么样?
先看两个精简的 TypeScript 示例,理解插件最基本的工作方式。
1. 声明依赖,并在挂载时执行代码
export const name = 'observe-ingress'
export const inject = ['webServer', 'webhookRuntime']
export function apply(ctx: Context) {
console.log('web server port =', ctx.webServer.port)
}
逐行看这段代码在说什么。
第一步:给插件起个名字。
export const name = 'observe-ingress'
这里的 name 用于标识插件。
第二步:声明自己需要哪些服务。
export const inject = ['webServer', 'webhookRuntime']
inject 可以理解为一份依赖清单。
在这个示例中,插件需要:
webServer:与 Web 服务器相关的服务。webhookRuntime:与 Webhook 运行环境相关的服务。
框架可以根据这些依赖关系安排插件的初始化时机,使插件在执行相关逻辑时能够获得所需的服务。
第三步:在 apply(ctx) 中实现插件逻辑。
export function apply(ctx: Context) {
console.log('web server port =', ctx.webServer.port)
}
这里有两个重要概念:
apply:插件被应用时执行的函数。ctx:共享上下文,可以暂时理解为一个能够访问系统服务的工具箱。
因此,这段代码的意思就是:插件启动时,通过共享上下文访问 Web 服务器服务,读取端口并打印出来。
2. ctx 到底是什么?
我们前面提到了共享上下文,它也是理解 Cordis 的关键。
你可以把 ctx 想象成一张公共服务台。插件不需要自己实现所有能力,而是可以通过它访问系统中已经提供的服务。
在 DSH 的相关模块中,你可能会看到这些入口:
| 服务入口 | 可以先这样理解 |
|---|---|
ctx.tools | 工具的注册与执行机制 |
ctx.llm | 与模型通信相关的接口 |
ctx.sessions | 会话事件日志相关能力 |
ctx.agents | Agent 相关管理能力 |
具体有哪些服务,以及它们提供哪些方法,需要以当前版本的实际实现为准。
对于初学者来说,先记住一句话就够了:
插件通过 ctx 使用其他模块提供的能力,而不必把所有功能都写在自己内部。
3. 插件不只要启动,还要负责清理
再看第二个例子:
export function apply(ctx: Context) {
const timer = setInterval(() => {}, 60_000)
ctx.effect(() => () => {
clearInterval(timer)
console.log('清理定时器')
})
}
这段代码引入了一个新的概念:ctx.effect()。
我们先看它解决了什么问题。
假设插件启动时创建了一个定时器。如果插件后来被卸载,但定时器没有停止,那么它仍然可能继续运行。
类似的问题还可能出现在:
- 没有移除的事件监听器;
- 没有结束的子进程;
- 没有注销的工具;
- 没有释放的其他资源。
因此,一个设计良好的插件不仅应该知道如何启动,也应该知道如何清理自己创建的资源。
在这个示例中:
ctx.effect(() => () => {
clearInterval(timer)
})
内部的函数负责清理定时器,而 ctx.effect() 用于登记相应的清理逻辑。按照该接口的生命周期约定,在插件被卸载等需要清理的时机,框架可以调用登记的清理函数。
为什么这叫「可逆副作用」?
因为启动定时器改变了系统状态,而清理函数提供了撤销这项影响的方式。
当然,ctx.effect() 不是魔法。只有正确登记清理逻辑,并且清理代码本身写得正确,资源才能被妥善释放。
另外,apply 还可以接收配置参数,例如 apply(ctx, config)。这里暂时先聚焦插件的基本结构。
四、Cordis 的四个核心概念
现在,我们已经接触了插件、共享上下文和清理机制。接下来,把 Cordis 的基本概念归纳为四类。
| 概念 | 通俗理解 | 主要作用 |
|---|---|---|
| Plugin(插件) | 一个功能模块 | 将某项能力接入系统 |
| Service(服务) | 一项可供其他模块使用的能力 | 让插件访问或调用其他模块的功能 |
| Event(事件) | 通知或流程钩子 | 在特定时机通知其他模块,或允许它们介入流程 |
| Effect(副作用) | 需要善后处理的操作 | 在适当的生命周期阶段清理插件造成的影响 |
我们再用一家餐厅来理解它们之间的关系。
假设你经营一家餐厅:
- 插件:负责接单的员工、负责做菜的员工。
- 服务:点餐系统、厨房系统。
- 事件:顾客下单、菜品完成、订单取消时发出的通知。
- 副作用:为了某个订单启动的临时任务,订单取消后需要进行清理。
这四个概念不是互相竞争的东西,而是从不同角度描述同一个系统。
一个插件可以使用其他服务、监听事件,并创建需要清理的资源。
1. 服务和事件有什么区别?
这是一个很容易混淆的地方。
可以这样区分:
服务回答的是:我能使用什么能力?
例如,工具服务负责管理工具的注册和执行。插件可以通过对应的服务接入工具。
事件回答的是:某件事发生时,我能在哪里观察或介入?
例如,在某些工具执行前的事件钩子上,插件可以检查操作是否符合安全策略。
在 DSH 的相关架构中,可以进一步关注以下几类事件:
- 会话事件:记录消息、步骤等运行信息,帮助系统追踪执行过程。
- Agent 事件:观察模型请求、步骤开始等智能体执行过程。
- 工具或文件系统相关事件:在框架提供相应钩子的情况下,观察操作或执行额外检查。
需要注意,事件是否支持拦截、修改或取消操作,取决于具体事件的设计。并不是所有事件都能阻止后续行为。
2. DSH 的核心服务从哪里来?
如果进一步阅读 DSH 的代码,你可能会看到这些模块:
| 模块 | 主要职责 | 对应的服务入口 |
|---|---|---|
core/session | 会话事件日志 | ctx.sessions |
core/system-prompt | 提示词片段与工具 Schema 组装 | ctx.systemPrompt |
core/tools | 工具注册与执行机制 | ctx.tools |
core/agent | Agent 接口与相关管理 | ctx.agents |
core/agent-loop | 默认的智能体循环实现 | ctx.agentLoop |
llm/llm | 模型通信相关接口与适配器 | ctx.llm |
这张表的目的不是让你马上记住所有模块,而是让你知道:当你想寻找某项能力时,可以先从对应的服务入口和模块入手。
其中最值得优先理解的是:
ctx.tools:管理 AI 可以使用的工具。ctx.llm:提供与模型通信相关的接口。
接下来,我们就把这两者串起来,看看 AI 是如何真正完成任务的。
五、Agent Loop:AI 是怎样一步步完成任务的?
假设你给 AI 一个任务:
帮我检查这个项目中的代码问题。
模型第一次收到请求时,可能会决定先读取某个文件。但读取文件需要调用外部工具,因此系统还需要负责执行工具、收集结果,再把结果交回模型。
之后,模型可能发现还需要读取另一个文件,于是再次请求工具。
整个过程可以简化为:
- 模型接收任务,决定下一步行动。
- 模型请求调用某个工具。
- 系统执行工具,并获得结果。
- 系统把工具结果交还给模型。
- 模型根据新结果继续执行,或者给出最终回答。
这个不断推进任务的过程,就是 Agent Loop(智能体循环) 所要解决的问题。
如果系统只能请求模型一次,就很难自然地组织这样的多步骤任务。
Agent Loop 在 DSH 中有什么特别之处?
在 DSH 的插件化设计中,Agent Loop 本身也可以由插件提供。
这意味着,系统不仅可以扩展工具,还可能在符合框架约定的情况下扩展或替换驱动任务执行的循环机制。
当然,能否替换以及替换需要满足什么接口条件,仍然要看具体实现。
Step 和 Turn 又是什么?
阅读 Agent Loop 相关代码时,还可能遇到 Step 和 Turn。
可以先用一个简单的层级关系理解它们:
- Step:任务执行中的一个步骤,通常围绕一次模型请求及其相关的工具调用展开。
- Turn:更大范围的一轮处理过程,可以包含多个 Step。
例如,用户提出一个问题,模型先读取文件,再查询数据,最后总结回答。这可能涉及多个执行步骤,而不是一次模型请求就能全部完成。
具体的 Step 和 Turn 边界,需要以 DSH 当前版本的定义为准。
到这里,我们已经知道模型如何借助工具完成任务。但如果我们想更换某种能力,或者为系统增加一个插件,又该从哪里入手?
这就要理解另外三个概念:Profile、Patch 和 Seam。
六、Profile、Patch 与 Seam:怎样组合和替换能力?
1. Profile:选择一套基础运行组合
Profile 可以理解为一份「启动套餐」。
一个 AI 系统可能需要提供不同的运行方式,例如网页交互、无图形界面运行,或者供其他程序调用的开发接口。
不同运行方式可能需要不同的插件组合。
例如,网页交互模式可能需要 Web 服务器和聊天界面;另一种运行模式则可能只需要模型服务和工具执行能力。
通过 Profile,系统可以选择一套适合当前场景的基础配置。
这里提到的具体配置名称和包含的插件,应以当前版本的实际配置为准。
2. Patch:在基础配置上进行调整
假设你想测试自己编写的插件,但不想直接修改官方默认配置。
这时,可以通过额外的配置覆盖,在基础组合上增加插件、调整选项,或者在框架允许的情况下替换某个配置条目。
这就是 Patch 的基本思路。
如果把 Profile 比作一份基础套餐,那么 Patch 就像是根据自己的需求调整套餐内容。
例如:
dsh --profile web --dump-config
可以用于查看指定 Profile 下的配置结果。
需要注意:配置覆盖不等于运行中的系统一定支持动态热更新。能否在运行时热挂载或卸载插件,需要查看具体实现。
3. Seam:让底层实现可以被替换
最后是一个比较抽象的概念:Seam。
Seam 可以翻译为「可替换边界」。
想象一下,你有一台机器,其中一个零件需要更换。
理想情况下,你只需要更换这个零件,而不必把整台机器拆掉重造。
软件中的 Seam 也是类似的思想:在合适的接口边界上,让底层实现能够被替换,而不必大幅修改使用它的上层代码。
我们以文件读取能力为例。
假设上层代码只依赖一个统一接口:
interface FileSystem {
readFile(path: string): Promise<string>
}
那么,底层就可以有不同的实现:
- 本地文件系统:读取本机文件。
- 远程文件系统:通过远程服务获取文件内容。
只要两种实现都遵守相同的接口约定,上层代码就可以在不大幅修改自身逻辑的情况下,切换底层实现。
这个例子是为了说明 Seam 的一般设计思想,并不代表 DSH 已经存在完全相同的接口。
要理解 Seam,可以记住三个角色:
- 服务定义或接口约定:规定能力应该如何使用。
- 服务实现:真正完成工作的代码。
- 消费方:调用这项能力的其他模块。
三者之间的边界越清晰,替换实现时通常就越容易控制影响范围。
不过,Seam 不等于插件本身。
插件关注的是功能模块如何组织、接入和管理;Seam 关注的是某项能力的实现能否被替换。两者可以配合使用,但解决的是不同的问题。
七、建立 DSH 的完整心智模型
前面我们分别讲了插件、服务、事件、副作用、Agent Loop、Profile、Patch 和 Seam。
现在,把它们重新串起来。
flowchart TD
A["用户提出任务"] --> B["运行配置:Profile"]
B --> C["配置调整:Patch"]
C --> D["Cordis:组织插件与依赖"]
D --> E["共享上下文 ctx 与服务"]
E --> F["Agent Loop:驱动任务执行"]
F --> G["模型请求与工具调用"]
G --> H["工具执行并返回结果"]
H --> F
D --> I["事件:观察或介入流程"]
D --> J["Plugin / Seam:扩展或替换能力"]
J --> D
D --> K["Effect:清理插件创建的资源"]
这张图可以帮助我们从整体上理解 DSH。
1. 系统由什么组成?
系统由多个可以组合的插件和服务构成。
Profile:选择基础运行组合。Patch:在基础组合上进行配置调整。Plugin:提供或扩展具体能力。
2. 插件怎样协作?
插件通过依赖关系、共享上下文和服务接口协作。
Cordis:组织插件及其依赖。ctx:提供访问服务的入口。Service:让其他模块能够使用某项能力。
3. AI 怎样完成任务?
模型负责理解任务、生成回答和决定下一步行动;工具负责执行具体操作;Agent Loop 负责组织两者之间的反复交互。
模型请求工具、工具返回结果、模型继续判断,这样任务才能逐步推进。
4. 怎样观察和干预执行?
通过事件扩展点,插件可以在框架允许的阶段观察流程,或者进行检查和干预。
例如,记录工具调用信息,或者在特定操作执行前进行权限检查。
5. 怎样增加或替换能力?
- 通过插件增加新的功能模块。
- 通过 Patch 调整插件组合或配置。
- 通过 Seam 所代表的清晰接口边界,替换符合约定的底层实现。
- 通过 Effect 登记资源清理逻辑,避免插件卸载后留下不必要的影响。
如果你能把这五个问题串起来,DSH 对你来说就不再只是一个能够启动网页的黑盒了。
八、真正开发插件时,应该怎样思考?
建立心智模型的意义,不是记住所有英文名词,而是在面对新需求时,知道应该从哪里入手。
以后你遇到一个新需求,不要马上问:
我要修改哪个文件?
而是先问自己以下五个问题。
1. 我要增加什么能力?
先明确需求,再寻找对应的服务或工具注册机制。
例如,想让模型记录用户操作,就需要考虑操作录制逻辑,以及如何把相应能力接入系统。
2. 它应该在哪个阶段工作?
如果只是提供一个新工具,可能需要接入工具系统。
如果需要观察某个操作何时发生,则要寻找合适的事件或生命周期扩展点。
3. 它依赖什么?
确认插件运行时需要哪些服务,并按照框架约定声明依赖。
4. 它需要替换已有能力吗?
如果只是增加新功能,可能不需要替换任何底层实现。
如果需要更换某项现有能力,就要先找到明确的服务接口或可替换边界。
5. 插件卸载后,需要清理什么?
如果插件创建了定时器、监听器、子进程或其他资源,就要考虑如何正确释放它们,并按照框架约定登记清理逻辑。
这五个问题,能够帮助我们从「想到哪里改到哪里」,转向「先理解系统结构,再选择正确的扩展点」。
总结
如果只用几句话总结本文:
- Harness:为模型提供工具、任务组织和运行管理能力。
- Cordis:组织插件、服务依赖、事件和生命周期。
- Plugin:独立提供或扩展系统能力的模块。
- Service 与 Event:分别解决能力访问,以及特定时机的通知和介入问题。
- Agent Loop:组织模型请求、工具执行和结果反馈,让任务能够分步骤推进。
- Profile 与 Patch:分别用于选择基础运行组合,以及调整配置。
- Seam 与 Effect:分别关注实现能否被替换,以及插件创建的资源如何清理。
我认为,理解 DSH 最重要的不是背诵这些名词,而是形成一种思考方式:
先找到能力,再理解依赖;先找到扩展点,再考虑如何实现;最后别忘了资源的生命周期。
下一篇预告
心智模型到位了,下一篇就真刀真枪:徒手写第一个 DSH 插件。
我们从最简单的插件开始,逐步完成插件定义、服务接入、工具注册,再看看它是怎样进入 DSH 的运行流程的。