
字节笔记本
2026年10月6日 · 约 7 分钟读完
DeepSeek Harness 接新模型:七条适配器协议
DeepSeek 开源的 agent 框架 DeepSeek Harness(dsh,MIT 协议)把「一切皆插件」贯彻到了模型层:要接入一个新的模型提供方,写的东西也是一个标准插件。官方在仓库的 Cookbook 里专门放了一篇「添加 LLM 适配器」的实操文档,并给出两个参考实现:llm-deepseek 直接用 HTTP 对接官方接口,SSE 流由 eventsource-parser 分帧;llm-pi-ai 则选择封装现成的 LLM 库。两条路线共用同一套契约,核心定义在 packages/llm/llm/src/types.ts 的 StreamChunk 文档里,官方要求动手前先读它。

适配器长什么样
一个最小可用的适配器由两部分组成:继承 LlmAdapter 的类和它的异步生成器方法 stream(),再加上一段标准样式的插件注册代码:
class MyAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> { … }
}
export const name = 'llm-myprovider'
export const inject = ['llm']
export const Config: z<Config> = z.object({ apiKey: z.string(), … })
export function apply(ctx: Context, config: Config) {
ctx.llm.registerAdapter(['my-provider'], new MyAdapter(…))
}注册机制有几个值得注意的设计。它是基于副作用的,因此天然兼容 HMR(热模块替换);每个提供方路由只允许绑定一个适配器,重复注册会直接抛出异常,多条路由一起注册时要么全部成功、要么全部失败。请求侧用 options.provider 选择适配器,options.model 直接使用提供方自己的模型 ID,所以带动态模型目录的适配器不用重新走生命周期配置就能上新模型。
密钥管理走 Cordis 原生路线:用 schemastery 声明 Config 并配环境变量回退,在 cordis.yml 里通过 !!js process.env.MY_KEY 注入。文档特别强调,不要在代码里自作主张去读某个自己约定的密钥文件。
七条流式协议约定
文档把两个参考实现共同验证过的约定列成了硬性义务,共七条:
- usage 必须在 finish 之前发出,finish 之后不再发出任何内容。 稳健做法是先缓冲 finish 与 usage,等提供方的流结束标记出现再统一 flush,这样能兼容某些提供方在末尾补发仅含 usage 分片的行为。
- 工具调用的
arguments全程保持原始 JSON 字符串。 流式片段用argumentsDelta发送;如果提供方直接返回解析好的对象,要在block-end时重新 stringify。 - 块的
index按首次出现的流顺序分配,同一块的每次 delta 复用这个 index。 - 错误有且只有两条合法路径: 从
stream()直接抛出,用于传输与协议故障,使用带稳定 code 的LlmError;或者用finish {kind: 'error' | 'aborted'}结束流,用于提供方带内故障。消费方两种都会处理,适配器要按故障类别选好路径并写进文档。 - 遵守
options.signal,把它传给 fetch 或你使用的 SDK。 - 不支持的参数要显式报错。 提供方做不到
GenerateOptions里的某个字段时,比如不支持 stop sequences 却收到了stop列表,就抛LlmError(..., 'UNSUPPORTED'),而不是静默丢弃。 - 跨调用需要的原生状态放进
finish.replayState。 提供方在后续调用中需要响应 ID、签名等原生元数据时,把最小无损 JSON 投影作为finish.replayState发出,重建历史时先验证它。LlmRuntime只在历史路由和目标路由当前属于同一个适配器实例时才传递该状态,同模型、跨模型或跨提供方的恢复是否合法由适配器自己决定;状态缺失时,绝不许只凭提供方或模型名称推断原生回放。

思考模式与模型元数据
提供方特有的思考模式开关仍放在适配器自己的 Config 里。确切的模型元数据则统一走一个提供方无关的能力接缝:实现 resolveModel(),返回提供方与模型身份,可选携带 context 和 reasoning 字段;只有配置里确实指定了默认值,才声明 defaultEffort;解析模型时传入的可选 AbortSignal 也要遵守。
推理强度的设计比较克制:它是一组有序的不透明 ID,由适配器负责映射成提供方请求。适配器给出的可选列表是权威的,包括适配器在支持时定义的 off;对外不暴露最终协议值的具体拼写,也不把不支持的值自动修正成其他档位。ID 本身不必与协议表示一致。
分层实现与验证
结构上,文档要求把职责拆开:wire format 类型、请求序列化、传输解析、分片转换和适配器类各自独立承担单一职责,llm-deepseek 就是官方给出的参考布局。验证环节则遵循仓库的测试策略,适配器覆盖率、真实提供方检查和已发布入口要求都由该策略统一规定。
需要提醒的是,dsh 目前处于开发者预览阶段,官方明确说明后续会有破坏兼容性的变更。建议先用 npx @deepseek-ai/dsh web 在本地把 Web UI 跑起来感受整体形态,再照着契约文档动手。本文整理自官方 Cookbook 的添加 LLM 适配器一章,参考实现源码见 llm-deepseek。



