LLM 适配器
编写自定义模型适配器插件,支持 DeepSeek-V3、R1 思维链拆分、本地私有化 Ollama/vLLM 接入与流式解析。
在企业级 AI 智能体落地的实际场景中,大语言模型往往部署在多样的异构基础设施上——从 DeepSeek 官方公有云 API、企业私有云部署的 vLLM / Ollama 集群,到具有特定安全鉴权网关的代理服务。
DeepSeek Harness 通过 @deepseek-ai/dsh-llm 提供了高度解耦的 LlmAdapter 基类 与 6 阶段标准流式协议(StreamChunk Protocol)。无论底层推理引擎是何种接口格式,只需实现一个适配器插件,即可无缝融入 Harness 的智能体规划循环中。
本文将带领你从零编写一个 “企业级私有化 vLLM / Ollama 推理适配器(Self-Hosted vLLM Adapter)”。
架构透视:6 阶段 StreamChunk 标准流式协议
Harness 的调度器与 Trajectory 日志追踪器依赖统一的事件切片协议与模型进行流式交互:
┌─────────────────────────────────────────────────────────────┐
│ 6 阶段 StreamChunk 标准流式协议 │
├─────────────────────┬───────────────────────────────────────┤
│ 1. block-start │ 开启一个文本内容块或工具调用块 │
├─────────────────────┼───────────────────────────────────────┤
│ 2. text-delta │ 流式推送思考过程与文本增量 Token │
├─────────────────────┼───────────────────────────────────────┤
│ 3. tool-call-delta │ 流式拼接 Tool Call 参数的 JSON 片段 │
├─────────────────────┼───────────────────────────────────────┤
│ 4. block-end │ 声明当前内容块或工具调用块结束 │
├─────────────────────┼───────────────────────────────────────┤
│ 5. usage │ 上报 Token 统计及 DeepSeek 命中缓存量 │
├─────────────────────┼───────────────────────────────────────┤
│ 6. finish │ 声明本次推理流完成,携带 finishReason │
└─────────────────────┴───────────────────────────────────────┘
步骤一:继承 LlmAdapter 实现适配器
创建 custom-plugins/vllm-adapter/src/vllm-adapter.ts 文件:
import {
LlmAdapter,
type GenerateOptions,
type StreamChunk,
LlmError,
LlmErrorCode
} from '@deepseek-ai/dsh-llm';
export interface VllmConfig {
endpoint: string;
modelName: string;
apiKey?: string;
}
export class VllmCustomAdapter extends LlmAdapter {
constructor(private config: VllmConfig) {
super();
}
// 1. 生成带有 DeepSeek 优化标识的 HTTP 请求头
public override attributionHeaders(): Record<string, string> {
return {
'X-Client-Agent': 'DeepSeek-Harness-vLLM-Adapter/1.0',
'X-Enable-KV-Cache-Reuse': 'true'
};
}
// 2. 核心异步生成器:将底层 SSE 数据流转换为 StreamChunk 标准格式
public override async *generate(options: GenerateOptions): AsyncGenerator<StreamChunk> {
const url = `${this.config.endpoint}/v1/chat/completions`;
let response: Response;
try {
response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.apiKey ?? 'none'}`,
...this.attributionHeaders()
},
body: JSON.stringify({
model: this.config.modelName,
messages: options.messages,
stream: true,
temperature: options.temperature ?? 0.0
})
});
} catch (err: any) {
throw new LlmError(LlmErrorCode.NETWORK_ERROR, `无法连接到私有模型网关: ${err.message}`);
}
if (!response.ok) {
const errorText = await response.text();
throw new LlmError(
response.status === 401 ? LlmErrorCode.AUTHENTICATION_FAILED : LlmErrorCode.INFERENCE_FAILED,
`私有模型网关返回错误 (HTTP ${response.status}): ${errorText}`
);
}
// 3. 模拟 SSE 解析与 StreamChunk 流式转换
yield { type: 'block-start', blockType: 'text', index: 0 };
yield { type: 'text-delta', delta: '正在通过私有化 vLLM 集群处理您的请求...', index: 0 };
yield { type: 'block-end', index: 0 };
// 4. 上报 Token 消耗与 DeepSeek 专属 KV Cache 命中统计
yield {
type: 'usage',
usage: {
promptTokens: 120,
completionTokens: 25,
totalTokens: 145,
cacheReadTokens: 90 // 命中 KV Cache 的前缀 Token
}
};
// 5. 声明流完成
yield { type: 'finish', finishReason: 'stop' };
}
}
步骤二:将适配器封装为 Cordis 插件
创建 custom-plugins/vllm-adapter/src/index.ts,在微内核中注册适配器:
import type { Context } from '@deepseek-ai/cordis';
import { VllmCustomAdapter, type VllmConfig } from './vllm-adapter';
export const name = 'vllm-adapter-plugin';
export const inject = ['llm'];
export function apply(ctx: Context, config: VllmConfig) {
const adapter = new VllmCustomAdapter(config);
// 将适配器注册到全局 llm 服务的提供商列表
ctx.llm.registerAdapter('vllm-private', adapter);
console.log(`[vLLM] 私有模型适配器已成功挂载 (模型: ${config.modelName})`);
}
步骤三:在 cordis.yml 中声明使用私有适配器
mode: standard
model:
# 指定使用刚刚注册的 vllm-private 适配器
provider: vllm-private
name: deepseek-ai/DeepSeek-V3
endpoint: http://192.168.1.100:8000
apiKey: env(PRIVATE_CLUSTER_KEY)
plugins:
- name: "./custom-plugins/vllm-adapter"
config:
endpoint: "http://192.168.1.100:8000"
modelName: "deepseek-ai/DeepSeek-V3"
- name: "@deepseek-ai/dsh-plugin-bash"
常见问题解答 (FAQ)
Q1: 为什么必须按照 6 阶段协议推送 StreamChunk?
解答:Harness 的上层 UI 前端(如 Web 控制台中的思考流折叠栏)以及 Trajectory 审计追踪器,依赖 block-start 和 block-end 边界来安全地隔离文本生成与 JSON 工具调用。若缺少生命周期分片,前端将无法正确渲染工具执行进度。
Q2: 遇到 API 限流或鉴权失败时,为什么要抛出 LlmError 而不是常规的 Error?
解答:Harness 调度器内置了针对 LlmError 的智能重试与自动降级策略。例如遇到 RATE_LIMIT_EXCEEDED 时会自动执行指数退避重试,而常规的 Error 会被视为不可恢复的致命异常直接终止会话。
Q3: cacheReadTokens 字段有什么用?
解答:这是针对 DeepSeek 模型架构的重要优化指标。DeepSeek 对命中 KV Cache 的前缀输入仅收取常规费用的 10%~20%。精准记录该字段能够让企业清晰评估 Agent 调用的成本收益。
Q4: 如何在适配器中支持大模型流式思考过程(Reasoning / Thought)?
解答:在解析 SSE 流时,如果检测到 DeepSeek-R1 的思考标签,可 yield { type: 'block-start', blockType: 'thinking', index: 0 },并通过 text-delta 推送思考内容,最后通过 block-end 闭合。