Cordis 插件系统上手,给 Harness 换模型适配器要改几行
从 llm 服务接口说起
DeepSeek Harness 的模型适配能力,核心在于 Cordis 插件系统对「llm 服务」的抽象。框架内部定义了一套通用的 LLM 服务接口,任何模型提供商只要实现这套接口,就能被 Harness 的 Agent 循环无感知地调用。
这意味着什么?假设你正在用 DeepSeek-V4-Pro 跑一个自动化编程任务,突然想切换到另一家云厂商的模型做对比测试。在传统架构里,你可能要改 Agent 逻辑、改请求封装、改错误处理——但在 Harness 里,真正需要动的地方往往只有插件配置那一层。
副作用追踪:ctx 上下文的安全网
Cordis 的 ctx 上下文对象是整个副作用追踪体系的入口。所有对系统状态的修改——无论是注册新服务、订阅事件,还是挂载中间件——都必须经由 ctx 完成。
// 插件典型的 activate 入口
export function apply(ctx: Context, config: Config) {
// 注册 llm 服务,框架自动追踪这条副作用
ctx.plugin('llm", {
async chat(messages, options) {
// 具体的模型调用实现
}
});
// 插件卸载时,上面注册的服务会自动撤销
}
ctx 上的每个操作内部都被封装为可追踪的副作用记录。当你卸载一个插件时,Cordis 会按注册时的逆序清理这些副作用,不会出现「插件没了,服务残留」的泄漏问题。更关键的是,这套机制是强制性的——不存在绕过 ctx 直接修改系统状态的合法路径,从工程上保证了追踪的完备性。
Proxy 代理与服务查找
插件之间的协作依赖服务查找机制。Cordis 在 ctx 上做了一层 Proxy 拦截,访问 ctx.llm、ctx.tools 这类属性时,框架会沿着插件层级向上回溯,找到最近一层的提供者。
// 上层插件访问 llm 服务时,无需关心具体由谁提供
const response = await ctx.llm.chat([
{ role: 'user', content: '分析这个函数的复杂度' }
]);
这种设计的妙处在于作用域隔离:子层插件可见父层服务,反之则不成立。多个模型适配器插件可以共存,Agent 循环根据配置决定加载哪一个,其余的自然被隔离在外。切换模型时,上层业务代码完全不用触碰。
生命周期状态机:防错的最后一道门
每个 Cordis 插件实例都内置了状态机。插件处于 active 状态时,注册副作用一切正常;一旦进入 disposed 状态,任何新的副作用尝试都会直接抛异常。
// 简化的生命周期示意
class PluginInstance {
private state: 'pending' | 'active' | 'disposing' | 'disposed';
registerEffect(effect: Effect) {
if (this.state === 'disposed') {
throw new Error('Cannot register effect on disposed plugin');
}
// 正常注册...
}
}
这个设计在异步场景下尤其重要。比如某个模型请求还在进行中,用户突然切换了配置导致旧插件被卸载——状态机确保旧插件上的未完成操作要么快速失败、要么被妥善取消,避免资源泄漏和状态错乱。
"时空可组合性"的工程落地
Cordis 论文里提到的「时空可组合性」,在 Harness 中有非常具体的工程对应:
| 维度 | 论文概念 | Harness 实现 |
|---|---|---|
| 时间 | 副作用可撤销 | ctx 追踪 + 卸载时逆序清理 |
| 空间 | 依赖可声明 | Proxy 层级查找 + 服务隔离 |
这套机制让「组合」变得真正安全。你可以把 DeepSeek 的模型适配器、第三方的工具插件、社区贡献的沙箱实现,像搭积木一样拼在一起,而不用担心隐式的耦合冲突。
最小可运行插件模板
下面是一个完整的第三方模型适配器插件,假设我们要接入某云厂商的 API:
// plugins/my-llm-adapter/src/index.ts
import { Context, Service } from 'cordis'
declare module 'cordis' {
interface Context {
llm: LLMService
}
}
interface LLMService {
chat(messages: Message[], options?: ChatOptions): Promise<ChatResponse>
}
export interface Config {
apiKey: string
baseURL: string
model: string
}
export function apply(ctx: Context, config: Config) {
const service: LLMService = {
async chat(messages, options) {
const response = await fetch(`${config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: config.model,
messages,
...options
})
});
return response.json();
}
};
// 注册到 ctx,自动纳入副作用追踪
ctx.set('llm', service);
// 可选:暴露配置变更时的热更新
ctx.on('dispose', () => {
// 清理连接池、取消 pending 请求等
});
}
配置层切换只需修改 Harness 的入口配置:
# harness.config.yaml
plugins:
# 注释掉 DeepSeek 官方适配器
# - deepseek-llm
# 启用自定义适配器
- path: ./plugins/my-llm-adapter
config:
apiKey: ${MY_API_KEY}
baseURL: https://api.example.com/v1
model: example-model-001
单元测试写法
插件的独立性和 ctx 的可追踪性,让单元测试变得直接。以下是用 vitest 的测试示例:
// plugins/my-llm-adapter/test/index.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { App } from 'cordis'
import { apply } from '../src'
describe('my-llm-adapter', () => {
it('should route chat to custom endpoint', async () => {
const fetchMock = vi.fn().mockResolvedValue({
json: () => Promise.resolve({ choices: [{ message: { content: 'ok' } }] })
});
global.fetch = fetchMock;
const app = new App();
app.plugin(apply, {
apiKey: 'test-key',
baseURL: 'https://test.example.com',
model: 'test-model'
});
await app.start();
const result = await app.llm.chat([{ role: 'user', content: 'hi' }]);
expect(result.choices[0].message.content).toBe('ok');
// 验证请求参数正确
expect(fetchMock).toHaveBeenCalledWith(
'https://test.example.com/chat/completions',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer test-key'
})
})
);
await app.stop();
});
it('should cleanup on dispose', async () => {
const app = new App();
app.plugin(apply, { apiKey: 'x', baseURL: 'http://x', model: 'x' });
await app.start();
// 模拟插件卸载
await app.stop();
// 验证后续访问抛出异常(状态机保护)
expect(() => app.llm.chat([])).rejects.toThrow();
});
});
测试里我们直接操作 App 实例,无需启动完整的 Harness 服务。app.start() 和 app.stop() 分别对应插件的激活与卸载,副作用的注册和清理在这两个边界上自动完成。
实际切换时的注意点
真正落地时,除了接口对齐,还有几个细节值得留意:
Token 计费与上下文窗口。不同厂商的计费单位和上下文长度限制差异很大,建议在适配器内部做一层统一的配额估算,避免上层 Agent 循环因预算误判而中断任务。
流式响应的处理。如果原模型支持 SSE 流式输出,而目标模型不支持,适配器层需要做好降级——要么在内部缓冲完整响应后再一次性返回,要么向上层暴露兼容的流式接口。Harness 的 llm 服务接口对此有预留设计,具体实现取决于你的场景需求。
错误码映射。各家的 HTTP 状态码和错误体格式不尽相同。建议在适配器里封装一层错误转换,把外部异常统一为 Harness 内部可识别的错误类型,方便 Trajectory 日志做归因分析。
这些都不需要碰 Harness 源码。模型适配作为纯插件存在,热插拔的边界非常清晰——这也是 Cordis「时空可组合性」在工程实践中最直接的收益。
更多推荐

所有评论(0)