mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
4.2 KiB
4.2 KiB
LLM 适配器
本文介绍如何为 Harness 接入一个新的 LLM 提供方。
概述
LLM 适配器是一个继承 LlmAdapter 的类,实现 stream() 方法,将 Harness 的统一请求格式转换为具体 API 的调用。
最小实现
import type { Context } from 'cordis'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
class MyAdapter extends LlmAdapter {
private apiKey: string
constructor(apiKey: string) {
super()
this.apiKey = apiKey
}
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// 1. 将 options.messages 转换为你的 API 格式
// 2. 调用 API(流式)
// 3. 将 API 响应转换为 StreamChunk 序列
}
}
export interface Config {
apiKey: string
models: string[]
}
export const name = 'my-llm-adapter'
export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
const adapter = new MyAdapter(config.apiKey)
ctx.llm.registerAdapter(config.models, adapter)
}
StreamChunk 协议
stream() 必须按以下协议 yield chunk:
// 1. 每个内容块以 block-start 开始
yield { type: 'block-start', index: 0, blockType: 'text' }
// 2. 文本块使用 text-delta
yield { type: 'text-delta', index: 0, text: 'Hello' }
yield { type: 'text-delta', index: 0, text: ' world' }
// 3. 每个内容块以 block-end 结束(携带完整 block)
yield {
type: 'block-end',
index: 0,
block: { type: 'text', text: 'Hello world' },
}
// 4. Tool call 块
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
yield {
type: 'tool-call-delta',
index: 1,
id: CallId('call-123'),
name: 'bash',
argumentsDelta: '{"command":"ls"}',
}
yield {
type: 'block-end',
index: 1,
block: {
type: 'tool-call',
id: CallId('call-123'),
name: 'bash',
arguments: '{"command":"ls"}',
},
}
// 5. Token 用量
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
// 6. 结束原因
yield { type: 'finish', reason: { kind: 'stop' } }
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
关键规则
- 每个
block-start必须有对应的block-end index从 0 递增,标识内容块顺序tool-call-delta的argumentsDelta是 JSON 字符串的增量(可以一次 yield 全部,也可以分多次)finish必须是最后一个 chunkusage在finish之前 yield
GenerateOptions
stream() 接收的请求包含:
interface GenerateOptions {
/** 模型名 */
model: string
/** 对话历史 */
messages: Message[]
/** 可用的 tool 列表 */
tools?: ToolSpec[]
/** 系统提示词 */
system?: string
/** 最大输出 token */
maxTokens?: number
/** 温度 */
temperature?: number
}
你的适配器需要将这些映射到具体 API 的参数。
注册适配器
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
第一个参数是该适配器支持的模型名列表。当用户在 cordis.yml 中配置 model: model-name-1 时,框架会路由到这个适配器。
在 cordis.yml 中使用
- id: my-llm
name: './src/my-llm-adapter.ts'
config:
apiKey: !!js process.env.MY_API_KEY
models:
- my-model-v1
- my-model-v2
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
config:
model: my-model-v1 # 引用上面注册的模型名
实战参考
仓库中有两个完整实现可供参考:
packages/llm/llm-deepseek/— DeepSeek API 适配器(OpenAI 兼容格式)packages/llm/llm-pi-ai/— Pi AI 适配器(不同的 API 格式)examples/echo-agent/src/mock-llm.ts— 最简 mock 适配器(教学用)
mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地逻辑演示了完整的 chunk 序列。
错误处理
适配器中的异常会被 agent-loop 捕获并转化为 LlmError,告知上层。不需要在 stream() 内部做错误恢复——让异常冒泡即可。
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const response = await fetch(this.endpoint, { /* ... */ })
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
// ... 正常流式处理
}