从 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.llmctx.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「时空可组合性」在工程实践中最直接的收益。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐