DeepSeek Harness: LLM アダプタ

最終更新:2026-08-31

DSH の「モデル非依存」アプローチは単なるスローガンではなく、アーキテクチャ設計です。LLM アダプタは Cordis ケイパビリティシステムの Provider として、モデルの切り替えを電池の交換のように簡単にします。このレッスンでは LLM アダプタの登録、実装、マルチモデル管理を掘り下げます。

💡 ヒント:すべての LLM アダプタは同じ Definition — LLMCapability を実装します。Agent とツールはこのインターフェースにのみ依存し、下層のモデルが DeepSeek か GPT-4o かを気にしません。

📋 前提知識22-capability.md の完了、ケイパビリティ3役を理解していること

1. 学習内容

LLM アダプタアーキテクチャ


2. ctx.llm アダプタ登録

(1) LLM Capability インターフェース

DSH の LLM アダプタは LLMCapability インターフェースを実装:

TYPESCRIPT
interface LLMCapability {
  complete(request: LLMRequest): Promise<LLMResponse>
  stream(request: LLMRequest): AsyncIterable<StreamChunk>
  getModels(): ModelInfo[]
  getModelCapabilities(model: string): ModelCapabilities
}

▶ サンプル 2:

TYPESCRIPT
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:

YAML
# 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 の組み込みアダプタはこのプロトコルと互換:

TYPESCRIPT
interface OpenAIRequest {
  model: string
  messages: { role: string; content: string }[]
  temperature?: number
  max_tokens?: number
  stream?: boolean
  tools?: ToolDefinition[]
}

▶ サンプル 2:

YAML
# 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 を接続可能:

YAML
# ローカル 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) カスタムアダプタの作成

組み込みアダプタでは要件を満たせない場合、カスタムアダプタを作成:

TYPESCRIPT
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 アダプタを選択:

100%
graph TB
    REQ[LLM リクエスト] --> ROUTER{Model Router}
    ROUTER -->|コードタスク| CODE[DeepSeek Coder]
    ROUTER -->|推論| REASON[DeepSeek Reasoner]
    ROUTER -->|簡単なチャット| CHAT[GPT-4o-mini]

(2) ルーティングの設定

YAML
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) カスタムルーティングロジック

TYPESCRIPT
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 の統一ストリーミング出力プロトコル:

TYPESCRIPT
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 の消費

TYPESCRIPT
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 インターフェース

TYPESCRIPT
interface ModelCapabilities {
  chat: boolean
  toolUse: boolean
  vision: boolean
  maxTokens: number
  supportedModes: string[]
}

(2) モデルケイパビリティの宣言

TYPESCRIPT
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) ケイパビリティ宣言の目的

フレームワークはケイパビリティ宣言を使って決定:


7. マルチモデル負荷分散

(1) 設定

YAML
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) カスタム負荷分散

TYPESCRIPT
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
  }
}

❓ よくある質問

Q 複数の LLM アダプタを同時に登録できますか?
A デフォルトでは不可——後から登録されたものが前を上書きします。マルチモデル管理には Realm 分離またはルーティングアダプタを使用してください。
Q カスタムアダプタはどのメソッドを実装しなければなりませんか?
A completestreamgetModelsgetModelCapabilities の実装が必須。欠けていると TypeScript のコンパイルエラーになります。
Q カスタムアダプタのテスト方法は?
A InMemoryProvider パターンを使用——実際の HTTP リクエストを送信せず、事前設定されたレスポンスを返す。ユニットテストで llm サービスを置き換えます。
Q ストリーミング出力が中断された場合は?
A StreamChunk プロトコルには error 型が含まれています。コンシューマはエラーチャンクを処理し、ログに記録し、リトライするかどうかを判断すべきです。
Q モデルルーティングと負荷分散の違いは?
A ルーティングはリクエスト特性に基づいてモデルを選択(コード→コーダー)、負荷分散は同一モデルの複数エンドポイントにリクエストを分散します。
Q アダプタで API キーを安全に保存するには?
A 環境変数または .env ファイルを使用;設定にハードコードしない。Schemastery は .hidden() で機密フィールドをマークできます。

📖 まとめ


📝 練習問題

1. ⭐ 基礎:DSH を設定して OpenAI 互換エンドポイントで GPT-4o を使用し、API Key とモデル ID を設定し、Agent が会話を正常に完了することを確認してください。

2. ⭐⭐ 応用:ローカル Ollama サービス(http://localhost:11434/v1)にリクエストを転送するカスタム LLM アダプタを書いてください。complete と stream メソッドを実装し、ストリーミング出力をテスト。

3. ⭐⭐⭐ チャレンジ:Agent 実行モードに基づいてモデルを選択するルーティングアダプタを実装してください——PTC モードは reasoner、その他のモードは chat を使用。異なるモードの2つのセッションを作成し、異なるモデルが使用されることを確認。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%