Chuyển đến nội dung chính

第 5 課:LLM 路由器 — 多提供者適配器模式

多 LLM 的設計適配器模式:OpenAI、Anthropic、Google、Groq、Ollama。 LLMRouter 類別具有後備鏈、任務複雜度路由、成本最佳化。

🧠 人工智慧與機器學習 — 第 4 課 第 5 課:LLM 路由器 — 多提供者適配器 圖案

從零開始搭建AI代理平台-與xClaw實戰

第 2 部分:LLM 引擎和代理核心

亞洲開發網

簡介

一個實用的人工智慧平台需要支援許多LLM提供者。您無法對 OpenAI 進行硬編碼 - 客戶可能希望在本地使用 Claude、Gemini 或 Ollama。 LLM Router 使用適配器模式解決了這個問題。


1.LLMAdapter接口

// packages/core/src/llm/types.ts
export interface LLMAdapter {
  readonly provider: string;

  chat(
    messages: LLMMessage[],
    tools?: ToolDefinition[],
    options?: LLMOptions,
  ): Promise<LLMResponse>;

  chatStream(
    messages: LLMMessage[],
    tools?: ToolDefinition[],
    options?: LLMOptions,
  ): AsyncGenerator<StreamEvent>;

  listModels(): Promise<ModelInfo[]>;
}

export interface LLMOptions {
  temperature?: number;
  maxTokens?: number;
  topP?: number;
  stop?: string[];
  jsonMode?: boolean;
}

export interface ModelInfo {
  id: string;
  name: string;
  provider: string;
  contextWindow: number;
  pricing?: { inputPer1k: number; outputPer1k: number };
}

每個 LLM 提供者都必須實作此介面 - 這就是 適配器模式。


2.OpenAI 適配器

// packages/core/src/llm/adapters/openai-adapter.ts
import OpenAI from 'openai';

export class OpenAIAdapter implements LLMAdapter {
  readonly provider = 'openai';
  private client: OpenAI;

  constructor(config: { apiKey: string; baseUrl?: string }) {
    this.client = new OpenAI({
      apiKey: config.apiKey,
      baseURL: config.baseUrl,
    });
  }

  async chat(messages: LLMMessage[], tools?: ToolDefinition[]): Promise<LLMResponse> {
    const response = await this.client.chat.completions.create({
      model: 'gpt-4o',
      messages: this.convertMessages(messages),
      tools: tools ? this.convertTools(tools) : undefined,
    });

    const choice = response.choices[0];
    return {
      content: choice.message.content ?? '',
      toolCalls: choice.message.tool_calls?.map(tc => ({
        id: tc.id,
        name: tc.function.name,
        arguments: JSON.parse(tc.function.arguments),
      })),
      usage: response.usage ? {
        promptTokens: response.usage.prompt_tokens,
        completionTokens: response.usage.completion_tokens,
        totalTokens: response.usage.total_tokens,
      } : undefined,
    };
  }

  async *chatStream(messages: LLMMessage[], tools?: ToolDefinition[]): AsyncGenerator<StreamEvent> {
    const stream = await this.client.chat.completions.create({
      model: 'gpt-4o',
      messages: this.convertMessages(messages),
      tools: tools ? this.convertTools(tools) : undefined,
      stream: true,
    });

    for await (const chunk of stream) {
      const delta = chunk.choices[0]?.delta;
      if (delta?.content) {
        yield { type: 'text-delta', delta: delta.content };
      }
      if (delta?.tool_calls) {
        for (const tc of delta.tool_calls) {
          if (tc.function?.name) {
            yield { type: 'tool-call-start', toolCallId: tc.id!, toolName: tc.function.name };
          }
          if (tc.function?.arguments) {
            yield { type: 'tool-call-args', toolCallId: tc.id!, args: tc.function.arguments };
          }
        }
      }
    }
    yield { type: 'finish' };
  }

  private convertMessages(messages: LLMMessage[]) {
    // Convert platform-agnostic format → OpenAI format
    return messages.map(m => ({
      role: m.role as 'system' | 'user' | 'assistant' | 'tool',
      content: m.content,
      tool_calls: m.toolCalls?.map(tc => ({
        id: tc.id,
        type: 'function' as const,
        function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
      })),
      tool_call_id: m.toolCallId,
    }));
  }

  private convertTools(tools: ToolDefinition[]) {
    return tools.map(t => ({
      type: 'function' as const,
      function: {
        name: t.name,
        description: t.description,
        parameters: t.parameters,
      },
    }));
  }
}

3.LLM 路由器 — 路由邏輯

// packages/core/src/llm/llm-router.ts
export type TaskComplexity = 'fast' | 'smart' | 'cheap';

export class LLMRouter {
  private adapters = new Map<string, LLMAdapter>();
  private fallbackChains: Record<TaskComplexity, string[]> = {
    fast: ['groq', 'openai', 'anthropic'],
    smart: ['anthropic', 'openai', 'google'],
    cheap: ['ollama', 'groq', 'openai'],
  };

  register(adapter: LLMAdapter) {
    this.adapters.set(adapter.provider, adapter);
  }

  // Resolve chain — tìm provider available theo priority
  resolveChain(complexity: TaskComplexity): LLMAdapter {
    const chain = this.fallbackChains[complexity];
    for (const provider of chain) {
      const adapter = this.adapters.get(provider);
      if (adapter) return adapter;
    }
    // Fallback: lấy bất kỳ adapter nào available
    const first = this.adapters.values().next().value;
    if (!first) throw new Error('No LLM providers configured');
    return first;
  }

  async chat(
    messages: LLMMessage[],
    tools?: ToolDefinition[],
    complexity: TaskComplexity = 'smart',
  ): Promise<LLMResponse> {
    const adapter = this.resolveChain(complexity);

    try {
      return await adapter.chat(messages, tools);
    } catch (error) {
      // Auto-fallback to next provider
      const chain = this.fallbackChains[complexity];
      const currentIndex = chain.indexOf(adapter.provider);

      for (let i = currentIndex + 1; i < chain.length; i++) {
        const fallback = this.adapters.get(chain[i]);
        if (fallback) {
          console.warn(`Falling back from ${adapter.provider} to ${chain[i]}`);
          return await fallback.chat(messages, tools);
        }
      }
      throw error;
    }
  }
}

4. Anthropic 與 Google 轉接器

// packages/core/src/llm/adapters/anthropic-adapter.ts
export class AnthropicAdapter implements LLMAdapter {
  readonly provider = 'anthropic';

  async chat(messages: LLMMessage[], tools?: ToolDefinition[]): Promise<LLMResponse> {
    // Anthropic API has a different format:
    // - system prompt is separate from messages
    // - tool_use blocks instead of tool_calls
    const systemMsg = messages.find(m => m.role === 'system');
    const otherMsgs = messages.filter(m => m.role !== 'system');

    const response = await this.client.messages.create({
      model: 'claude-sonnet-4-20250514',
      system: systemMsg?.content,
      messages: this.convertMessages(otherMsgs),
      tools: tools?.map(t => ({
        name: t.name,
        description: t.description,
        input_schema: t.parameters,
      })),
      max_tokens: 4096,
    });

    return this.parseResponse(response);
  }
}

每個適配器都有一個獨特的任務:在平台無關的介面和提供者的特定 API 之間轉換格式。


5. 總結

圖案角色
適配器規格 LLM 提供者之間的 API 差異
策略根據任務複雜度選擇提供者
責任鏈提供者失敗時的後備鏈

下一篇文章: 工具註冊表 — 為 AI Agent 註冊並執行工具。