03 - LLM 接口:连接大脑 github 源码(欢迎star)

目标

理解如何连接 OpenAI、DeepSeek 等大模型 API,实现统一的 LLM 接口。


1. 为什么需要统一接口?

问题:不同 LLM API 格式不同

OpenAI 格式

POST https://api.openai.com/v1/chat/completions
{
  "model": "gpt-4",
  "messages": [{"role": "user", "content": "你好"}]
}

DeepSeek 格式(兼容 OpenAI):

POST https://api.deepseek.com/v1/chat/completions
{
  "model": "deepseek-chat",
  "messages": [{"role": "user", "content": "你好"}]
}

Anthropic 格式(不同):

POST https://api.anthropic.com/v1/messages
{
  "model": "claude-3",
  "messages": [{"role": "user", "content": "你好"}]
}

解决方案:适配器模式

┌─────────────────────────────────────────────────────────┐
│                     Agent                                │
│                   (使用统一接口)                          │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│              LLMProvider (接口)                          │
│         complete(request) -> Promise<response>          │
└─────────────────────────┬───────────────────────────────┘
                          │
          ┌───────────────┼───────────────┐
          │               │               │
          ▼               ▼               ▼
   ┌────────────┐  ┌────────────┐  ┌────────────┐
   │OpenAIProvider│ │DeepSeek    │ │Anthropic   │
   │             │  │Provider    │  │Provider   │
   └────────────┘  └────────────┘  └────────────┘
          │               │               │
          ▼               ▼               ▼
     api.openai     api.deepseek    api.anthropic

好处:Agent 不需要关心底层是哪个模型,统一调用。


2. 完整代码实现

创建文件 src/core/llm.ts

/**
 * LLM Provider 实现 - 多模型统一接口
 */

import { LLMProvider, LLMRequest, LLMResponse } from './types.js';

// ============================================================================
// 第一部分:OpenAI 格式 Provider
// ============================================================================

/**
 * OpenAI 格式 Provider
 * 
 * 支持:
 * - OpenAI (GPT-3.5, GPT-4)
 * - DeepSeek (兼容 OpenAI 格式)
 * - 其他兼容 OpenAI 格式的国产模型
 */
export class OpenAIProvider implements LLMProvider {
  readonly name = 'openai';
  
  /**
   * @param apiKey API 密钥
   * @param baseUrl API 基础地址,默认 OpenAI 官方
   */
  constructor(
    private apiKey: string,
    private baseUrl = 'https://api.openai.com/v1'
  ) {}

  /**
   * 调用 LLM 完成请求
   * 
   * @param request LLMRequest 对象
   * @returns LLMResponse 对象
   */
  async complete(request: LLMRequest): Promise<LLMResponse> {
    // 调用 API
    const response = await fetch(`${this.baseUrl}/chat/completions`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: request.model,
        messages: request.messages,
        tools: request.tools,
        max_tokens: request.maxTokens,
        temperature: request.temperature,
      }),
    });

    // 处理错误
    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`LLM API 错误: ${response.status} - ${errorText}`);
    }

    // 解析响应
    const data = await response.json();
    
    // OpenAI 响应格式:
    // {
    //   "choices": [{
    //     "message": {
    //       "content": "...",
    //       "tool_calls": [...]
    //     }
    //   }],
    //   "usage": {...}
    // }
    const choice = data.choices?.[0];

    return {
      // 文本内容
      content: choice?.message?.content,
      
      // 工具调用
      toolCalls: choice?.message?.tool_calls?.map((tc: any) => ({
        id: tc.id,
        type: 'function',
        function: {
          name: tc.function.name,
          arguments: tc.function.arguments,
        },
      })),
      
      // Token 使用量
      usage: {
        inputTokens: data.usage?.prompt_tokens || 0,
        outputTokens: data.usage?.completion_tokens || 0,
      },
    };
  }
}

// ============================================================================
// 第二部分:Anthropic 格式 Provider
// ============================================================================

/**
 * Anthropic (Claude) Provider
 * 
 * Anthropic API 格式与 OpenAI 略有不同,需要转换
 */
export class AnthropicProvider implements LLMProvider {
  readonly name = 'anthropic';

  constructor(
    private apiKey: string,
    private baseUrl = 'https://api.anthropic.com/v1'
  ) {}

  async complete(request: LLMRequest): Promise<LLMResponse> {
    // Anthropic 使用 "messages" 端点,不是 "chat/completions"
    const response = await fetch(`${this.baseUrl}/messages`, {
      method: 'POST',
      headers: {
        'x-api-key': this.apiKey,  // 注意:不是 Authorization
        'Content-Type': 'application/json',
        'anthropic-version': '2023-06-01',  // 需要版本头
      },
      body: JSON.stringify({
        model: request.model,
        messages: request.messages.map(m => ({
          // Anthropic 不支持 system 角色,转为 user
          role: m.role === 'system' ? 'user' : m.role,
          content: m.content,
        })),
        tools: request.tools?.map(t => ({
          name: t.function.name,
          description: t.function.description,
          input_schema: t.function.parameters,  // 字段名不同
        })),
        max_tokens: request.maxTokens || 4000,  // Anthropic 要求必须
        temperature: request.temperature,
      }),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`LLM API 错误: ${response.status} - ${errorText}`);
    }

    const data = await response.json();

    // Anthropic 响应格式不同
    // content 是数组,包含 text 和 tool_use
    const textContent = data.content
      ?.filter((c: any) => c.type === 'text')
      .map((c: any) => c.text)
      .join('');

    const toolCalls = data.content
      ?.filter((c: any) => c.type === 'tool_use')
      .map((c: any) => ({
        id: c.id,
        type: 'function',
        function: {
          name: c.name,
          arguments: JSON.stringify(c.input),  // input 是对象,需要 stringify
        },
      }));

    return {
      content: textContent,
      toolCalls,
      usage: {
        inputTokens: data.usage?.input_tokens || 0,
        outputTokens: data.usage?.output_tokens || 0,
      },
    };
  }
}

// ============================================================================
// 第三部分:Provider 工厂
// ============================================================================

/**
 * 创建 Provider 的工厂函数
 * 
 * 根据模型名称自动选择合适的 Provider
 * 
 * 模型名称格式:provider/model
 * 例如:
 * - openai/gpt-4
 * - anthropic/claude-3-sonnet
 * - deepseek-chat(默认使用 OpenAI 格式)
 */
export function createProvider(
  model: string,
  apiKey: string,
  baseUrl?: string
): LLMProvider {
  // 解析 provider/model 格式
  const [provider] = model.split('/');

  switch (provider) {
    case 'openai':
      return new OpenAIProvider(apiKey, baseUrl);
    
    case 'anthropic':
    case 'claude':
      return new AnthropicProvider(apiKey, baseUrl);
    
    default:
      // 默认使用 OpenAI 格式(兼容多数国产模型)
      return new OpenAIProvider(apiKey, baseUrl);
  }
}

3. 代码详解

3.1 fetch API 使用

const response = await fetch(url, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(data),
});

关键点

  • Authorization: Bearer {token} - 标准的 API 认证方式
  • Content-Type: application/json - 告诉服务器发送的是 JSON
  • JSON.stringify() - 将 JS 对象转为 JSON 字符串

3.2 错误处理

if (!response.ok) {
  const errorText = await response.text();
  throw new Error(`LLM API 错误: ${response.status} - ${errorText}`);
}

HTTP 状态码

  • 200 OK - 成功
  • 401 Unauthorized - API Key 错误
  • 429 Too Many Requests - 请求太频繁
  • 500 Internal Server Error - 服务器错误

3.3 响应解析

OpenAI 响应结构

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "你好!",
      "tool_calls": [{
        "id": "call_123",
        "type": "function",
        "function": {
          "name": "write_file",
          "arguments": "{\"path\":\"test.txt\"}"
        }
      }]
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21
  }
}

提取数据

const choice = data.choices?.[0];  // 取第一个选择
const content = choice?.message?.content;  // 文本内容
const toolCalls = choice?.message?.tool_calls;  // 工具调用

3.4 格式转换

Anthropic 的特殊处理

// OpenAI: tool_calls[].function.arguments 是字符串
// Anthropic: content[].input 是对象

// 需要转换
function: {
  name: c.name,
  arguments: JSON.stringify(c.input),  // 对象 -> 字符串
}

4. 使用示例

4.1 基本调用

const provider = createProvider(
  'deepseek-chat',
  'sk-your-api-key',
  'https://api.deepseek.com/v1'
);

const response = await provider.complete({
  model: 'deepseek-chat',
  messages: [
    { role: 'system', content: 'You are helpful.' },
    { role: 'user', content: '你好' }
  ],
});

console.log(response.content);  // "你好!有什么我可以帮助你的吗?"

4.2 带工具调用

const response = await provider.complete({
  model: 'deepseek-chat',
  messages: [{ role: 'user', content: '创建一个文件' }],
  tools: [{
    type: 'function',
    function: {
      name: 'write_file',
      description: '写入文件',
      parameters: {
        type: 'object',
        properties: {
          path: { type: 'string' },
          content: { type: 'string' }
        },
        required: ['path', 'content']
      }
    }
  }],
});

if (response.toolCalls) {
  // LLM 要求调用工具
  for (const call of response.toolCalls) {
    console.log(`调用: ${call.function.name}`);
    console.log(`参数: ${call.function.arguments}`);
  }
}

4.3 不同模型对比

// OpenAI
const openai = createProvider(
  'openai/gpt-4',
  'sk-openai-key'
);

// DeepSeek
const deepseek = createProvider(
  'deepseek-chat',
  'sk-deepseek-key',
  'https://api.deepseek.com/v1'
);

// Anthropic
const anthropic = createProvider(
  'anthropic/claude-3-sonnet',
  'sk-ant-key'
);

// 统一接口调用
for (const provider of [openai, deepseek, anthropic]) {
  const response = await provider.complete({
    model: '...',
    messages: [{ role: 'user', content: '你好' }],
  });
  console.log(response.content);
}

5. 高级话题

5.1 流式响应

流式响应可以实时显示 LLM 的输出(打字机效果):

async function* streamComplete(request: LLMRequest) {
  const response = await fetch(url, {
    ...options,
    headers: {
      ...headers,
      'Accept': 'text/event-stream',  // 关键头
    },
  });

  const reader = response.body?.getReader();
  
  while (true) {
    const { done, value } = await reader!.read();
    if (done) break;
    
    // 解析 SSE 数据
    const text = new TextDecoder().decode(value);
    yield text;
  }
}

// 使用
for await (const chunk of streamComplete(request)) {
  process.stdout.write(chunk);  // 实时输出
}

5.2 重试机制

网络可能失败,需要重试:

async function completeWithRetry(
  provider: LLMProvider,
  request: LLMRequest,
  maxRetries = 3
): Promise<LLMResponse> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await provider.complete(request);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      
      // 指数退避
      const delay = Math.pow(2, i) * 1000;
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

5.3 Token 计算

// 简单估算(不准确,仅参考)
function estimateTokens(text: string): number {
  // 英文:1 token ≈ 4 字符
  // 中文:1 token ≈ 1 字符
  const englishChars = text.replace(/[^\x00-\x7F]/g, '').length;
  const chineseChars = text.length - englishChars;
  return Math.ceil(englishChars / 4) + chineseChars;
}

6. 练习

练习 1:添加重试

complete 方法添加重试机制。

练习 2:支持更多模型

实现一个 Google Gemini 的 Provider。

练习 3:流式输出

实现流式响应,实时显示 LLM 输出。


Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐