DeepSeek Harness: LLM アダプタ
最終更新:2026-08-31
DSH の「モデル非依存」アプローチは単なるスローガンではなく、アーキテクチャ設計です。LLM アダプタは Cordis ケイパビリティシステムの Provider として、モデルの切り替えを電池の交換のように簡単にします。このレッスンでは LLM アダプタの登録、実装、マルチモデル管理を掘り下げます。
LLMCapability を実装します。Agent とツールはこのインターフェースにのみ依存し、下層のモデルが DeepSeek か GPT-4o かを気にしません。
📋 前提知識:22-capability.md の完了、ケイパビリティ3役を理解していること
1. 学習内容
- ctx.llm アダプタ登録
- OpenAI 互換エンドポイントアダプタ
- カスタムモデルルーティング
- StreamChunk プロトコル
- モデルケイパビリティ宣言
- マルチモデル負荷分散
2. ctx.llm アダプタ登録
(1) LLM Capability インターフェース
DSH の LLM アダプタは LLMCapability インターフェースを実装:
interface LLMCapability {
complete(request: LLMRequest): Promise<LLMResponse>
stream(request: LLMRequest): AsyncIterable<StreamChunk>
getModels(): ModelInfo[]
getModelCapabilities(model: string): ModelCapabilities
}
▶ サンプル 2:
import { Service, Context } from '@deepseek-ai/cordis'
export default class MyLLMAdapter extends Service {
constructor(ctx: Context) {
super(ctx, 'llm')
}
}
super(ctx, 'llm') はアダプタを llm サービスとして登録します。
(3) 組み込みアダプタ
DSH には2つの LLM アダプタが付属:
| アダプタ | サービス名 | サポートモデル |
|---|---|---|
| DeepSeek | llm |
deepseek-chat, deepseek-reasoner |
| OpenAI Compatible | llm |
任意の OpenAI 互換 API |
▶ サンプル 4:
# cordis.yml
plugins:
llm:
$replace:./adapters/my-custom-llm
config:
apiKey:sk-xxx
endpoint:https://api.example.com/v1
3. OpenAI 互換エンドポイントアダプタ
(1) プロトコル概要
OpenAI Chat Completions API は事実上の業界標準です。DSH の組み込みアダプタはこのプロトコルと互換:
interface OpenAIRequest {
model: string
messages: { role: string; content: string }[]
temperature?: number
max_tokens?: number
stream?: boolean
tools?: ToolDefinition[]
}
▶ サンプル 2:
# cordis.yml
plugins:
llm:
config:
provider:openai-compatible
apiKey:sk-xxx
endpoint:https://api.openai.com/v1
models:
- id:gpt-4o
capabilities:[chat, tool-use, vision]
- id:gpt-4o-mini
capabilities:[chat, tool-use]
(3) カスタムエンドポイント
任意の OpenAI 互換 API を接続可能:
# ローカル Ollama に接続
plugins:
llm:
config:
provider:openai-compatible
endpoint:http://localhost:11434/v1
apiKey:ollama
models:
- id:llama3
capabilities:[chat]
# Azure OpenAI に接続
plugins:
llm:
config:
provider:openai-compatible
endpoint:https://my-resource.openai.azure.com/openai/deployments/my-deployment
apiKey:xxx
headers:
api-key:xxx
(4) カスタムアダプタの作成
組み込みアダプタでは要件を満たせない場合、カスタムアダプタを作成:
import { Service, Context } from '@deepseek-ai/cordis'
import { LLMCapability, LLMRequest, LLMResponse, StreamChunk } from '@deepseek-ai/dsh'
export const Config = Schema.object({
apiKey: Schema.string().required().description('API key'),
endpoint: Schema.string().required().description('API endpoint'),
defaultModel: Schema.string().default('custom-model').description('Default model ID')
})
export default class CustomLLMAdapter extends Service implements LLMCapability {
static inject = []
private apiKey: string
private endpoint: string
private defaultModel: string
constructor(ctx: Context) {
super(ctx, 'llm')
this.apiKey = ctx.config.apiKey
this.endpoint = ctx.config.endpoint
this.defaultModel = ctx.config.defaultModel
}
async complete(request: LLMRequest): Promise<LLMResponse> {
const response = await fetch(`${this.endpoint}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: request.model || this.defaultModel,
messages: request.messages,
temperature: request.temperature,
max_tokens: request.maxTokens,
stream: false
})
})
const data = await response.json()
return {
content: data.choices[0].message.content,
model: data.model,
usage: data.usage
}
}
async *stream(request: LLMRequest): AsyncIterable<StreamChunk> {
const response = await fetch(`${this.endpoint}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: request.model || this.defaultModel,
messages: request.messages,
temperature: request.temperature,
max_tokens: request.maxTokens,
stream: true
})
})
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
if (data === '[DONE]') return
try {
const parsed = JSON.parse(data)
const delta = parsed.choices[0].delta
if (delta.content) {
yield { type: 'text', content: delta.content }
}
} catch {}
}
}
}
}
getModels() {
return [{ id: this.defaultModel, capabilities: ['chat', 'tool-use'] }]
}
getModelCapabilities(model: string) {
return { chat: true, toolUse: true, vision: false }
}
}
4. カスタムモデルルーティング
(1) ルーティング概念
モデルルーティングはリクエストの特性に基づいて異なる LLM アダプタを選択:
graph TB
REQ[LLM リクエスト] --> ROUTER{Model Router}
ROUTER -->|コードタスク| CODE[DeepSeek Coder]
ROUTER -->|推論| REASON[DeepSeek Reasoner]
ROUTER -->|簡単なチャット| CHAT[GPT-4o-mini]
(2) ルーティングの設定
plugins:
llm:
config:
routing:
default:deepseek-chat
rules:
- match:
mode:ptc
model:deepseek-reasoner
- match:
mode:creative
model:deepseek-chat
- match:
tools:[shell]
model:deepseek-coder
(3) カスタムルーティングロジック
export default class RoutingLLMAdapter extends Service {
private adapters: Map<string, LLMCapability> = new Map()
constructor(ctx: Context) {
super(ctx, 'llm')
}
async complete(request: LLMRequest): Promise<LLMResponse> {
const model = this._selectModel(request)
const adapter = this._getAdapter(model)
return adapter.complete({ ...request, model })
}
private _selectModel(request: LLMRequest): string {
if (request.tools?.some(t => t.name === 'shell')) {
return 'deepseek-coder'
}
if (request.metadata?.mode === 'ptc') {
return 'deepseek-reasoner'
}
return 'deepseek-chat'
}
private _getAdapter(model: string): LLMCapability {
const prefix = model.split('-')[0]
return this.adapters.get(prefix) || this.adapters.get('default')!
}
}
5. StreamChunk プロトコル
(1) プロトコル定義
StreamChunk は DSH の統一ストリーミング出力プロトコル:
type StreamChunk =
| { type: 'text'; content: string }
| { type: 'tool_call'; id: string; name: string; arguments: string }
| { type: 'tool_result'; id: string; result: any }
| { type: 'error'; error: Error }
| { type: 'done'; reason: 'stop' | 'tool_use' | 'length' }
(2) チャンクタイプの説明
| 型 | 説明 | 発生源 |
|---|---|---|
text |
テキストコンテンツ断片 | LLM ストリーミング出力 |
tool_call |
ツール呼び出しリクエスト | LLM がツール呼び出しを決定 |
tool_result |
ツール実行結果 | ツール実行後 |
error |
エラー情報 | いずれかの段階のエラー |
done |
ストリーム終了 | LLM 出力完了 |
(3) StreamChunk の消費
for await (const chunk of ctx.llm.stream(request)) {
switch (chunk.type) {
case 'text':
process.stdout.write(chunk.content)
break
case 'tool_call':
console.log(`\nTool call: ${chunk.name}`)
break
case 'tool_result':
console.log(`Tool result:`, chunk.result)
break
case 'error':
console.error(`Error:`, chunk.error)
break
case 'done':
console.log(`\nDone (${chunk.reason})`)
break
}
}
6. モデルケイパビリティ宣言
(1) ModelCapabilities インターフェース
interface ModelCapabilities {
chat: boolean
toolUse: boolean
vision: boolean
maxTokens: number
supportedModes: string[]
}
(2) モデルケイパビリティの宣言
getModels(): ModelInfo[] {
return [
{
id: 'deepseek-chat',
capabilities: {
chat: true,
toolUse: true,
vision: false,
maxTokens: 65536,
supportedModes: ['standard', 'ptc', 'minimal', 'creative']
}
},
{
id: 'deepseek-reasoner',
capabilities: {
chat: true,
toolUse: true,
vision: false,
maxTokens: 65536,
supportedModes: ['ptc']
}
}
]
}
(3) ケイパビリティ宣言の目的
フレームワークはケイパビリティ宣言を使って決定:
- このモデルでツール使用を許可するか(toolUse)
- 画像送信を許可するか(vision)
- 最大トークン制限(maxTokens)
- サポートする実行モード(supportedModes)
7. マルチモデル負荷分散
(1) 設定
plugins:
llm:
config:
loadBalancing:
strategy:round-robin
endpoints:
- model:deepseek-chat
endpoint:https://api.deepseek.com/v1
apiKey:sk-key1
weight:3
- model:gpt-4o
endpoint:https://api.openai.com/v1
apiKey:sk-key2
weight:1
(2) 負荷分散戦略
| 戦略 | 説明 |
|---|---|
round-robin |
ラウンドロビン |
random |
ランダム選択 |
weighted |
重み付け分散 |
least-latency |
最小レイテンシ選択 |
(3) カスタム負荷分散
export default class LoadBalancedLLM extends Service {
private endpoints: Endpoint[]
private currentIndex = 0
async complete(request: LLMRequest): Promise<LLMResponse> {
const endpoint = this._nextEndpoint()
return endpoint.complete(request)
}
private _nextEndpoint(): Endpoint {
const endpoint = this.endpoints[this.currentIndex]
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length
return endpoint
}
}
❓ よくある質問
complete、stream、getModels、getModelCapabilities の実装が必須。欠けていると TypeScript のコンパイルエラーになります。llm サービスを置き換えます。error 型が含まれています。コンシューマはエラーチャンクを処理し、ログに記録し、リトライするかどうかを判断すべきです。.env ファイルを使用;設定にハードコードしない。Schemastery は .hidden() で機密フィールドをマークできます。📖 まとめ
- LLM アダプタは Cordis ケイパビリティシステムの Provider で、
LLMCapabilityインターフェースを実装 - 組み込みの OpenAI 互換アダプタは任意の互換 API(DeepSeek、GPT-4o、Ollama 等)をサポート
- カスタムアダプタは4つのメソッドを実装:complete/stream/getModels/getModelCapabilities
- モデルルーティングはリクエスト特性に基づいて異なるモデルを選択;負荷分散は複数エンドポイントに分散
- StreamChunk プロトコルはストリーミング出力を統一:text/tool_call/tool_result/error/done
- モデルケイパビリティ宣言はフレームワークにモデルの能力を伝え、フレームワークは機能を制限
📝 練習問題
1. ⭐ 基礎:DSH を設定して OpenAI 互換エンドポイントで GPT-4o を使用し、API Key とモデル ID を設定し、Agent が会話を正常に完了することを確認してください。
2. ⭐⭐ 応用:ローカル Ollama サービス(http://localhost:11434/v1)にリクエストを転送するカスタム LLM アダプタを書いてください。complete と stream メソッドを実装し、ストリーミング出力をテスト。
3. ⭐⭐⭐ チャレンジ:Agent 実行モードに基づいてモデルを選択するルーティングアダプタを実装してください——PTC モードは reasoner、その他のモードは chat を使用。異なるモードの2つのセッションを作成し、異なるモデルが使用されることを確認。