第2讲:LLM API 统一网关——把 OpenAI/Claude/文心一言 管起来

一、为什么需要统一网关

想象一下你的业务接了三个大模型:

复制代码
业务代码中直接调用:
┌──────────────────────────────────────────────┐
│  func askAI(question string) string {         │
│      if model == "openai" {                   │
│          // openai 的请求格式                  │
│      } else if model == "claude" {            │
│          // claude 的请求格式                  │
│      } else if model == "wenxin" {            │
│          // 文心一言的请求格式                  │
│      }                                        │
│  }                                            │
└──────────────────────────────────────────────┘

痛点:
1. 每个模型 SDK 不同、请求格式不同、错误码不同
2. 切换模型要改业务代码
3. 无法统一做限流、鉴权、降级
4. 新接一个模型要改所有调用方

统一网关的目标:对外暴露一套标准接口,对内屏蔽各厂商差异。


二、架构设计

复制代码
┌──────────┐     ┌──────────┐     ┌──────────┐
│ 业务服务A │     │ 业务服务B │     │ 业务服务C │
└─────┬────┘     └─────┬────┘     └─────┬────┘
      │                │                │
      └────────────────┼────────────────┘
                       │
              ┌────────▼────────┐
              │   LLM API 网关   │
              │                  │
              │  统一请求/响应格式 │
              │  鉴权 / 限流     │
              │  模型路由 / 降级  │
              │  Token 统计      │
              └───┬────┬────┬───┘
                  │    │    │
         ┌────────┘    │    └────────┐
         ▼             ▼             ▼
   ┌──────────┐ ┌──────────┐ ┌──────────┐
   │ OpenAI   │ │ Claude   │ │ 文心一言  │
   └──────────┘ └──────────┘ └──────────┘

三、统一请求/响应模型

复制代码
// llm/gateway/model.go
package gateway

import (
    "time"
)

// ChatRequest 统一请求格式
type ChatRequest struct {
    Model       string          `json:"model"`        // 模型标识:gpt-4/claude-3/wenxin-4
    Messages    []ChatMessage   `json:"messages"`
    Temperature float64         `json:"temperature,omitempty"`
    MaxTokens   int             `json:"max_tokens,omitempty"`
    Stream      bool            `json:"stream,omitempty"`
    Tools       []Tool          `json:"tools,omitempty"`
}

// ChatMessage 统一消息格式
type ChatMessage struct {
    Role    string `json:"role"`    // system/user/assistant/tool
    Content string `json:"content"`
    Name    string `json:"name,omitempty"`
}

// ChatResponse 统一响应格式
type ChatResponse struct {
    ID        string         `json:"id"`
    Model     string         `json:"model"`
    Choices   []Choice       `json:"choices"`
    Usage     TokenUsage     `json:"usage"`
    CreatedAt time.Time      `json:"created_at"`
}

// Choice 选项
type Choice struct {
    Index        int          `json:"index"`
    Message      ChatMessage  `json:"message"`
    FinishReason string       `json:"finish_reason"`
}

// TokenUsage Token 用量
type TokenUsage struct {
    PromptTokens     int `json:"prompt_tokens"`
    CompletionTokens int `json:"completion_tokens"`
    TotalTokens      int `json:"total_tokens"`
}

// Tool 工具定义
type Tool struct {
    Type     string       `json:"type"`     // function
    Function ToolFunction `json:"function"`
}

type ToolFunction struct {
    Name        string      `json:"name"`
    Description string      `json:"description"`
    Parameters  interface{} `json:"parameters"`
}

四、适配器模式实现

复制代码
// llm/gateway/adapter.go
package gateway

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
    "time"
)

// LLMAdapter 模型适配器接口
type LLMAdapter interface {
    Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
    ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error)
    Name() string
}

// ─── OpenAI 适配器 ───

type OpenAIAdapter struct {
    apiKey  string
    baseURL string
    client  *http.Client
}

func NewOpenAIAdapter(apiKey string) *OpenAIAdapter {
    return &OpenAIAdapter{
        apiKey:  apiKey,
        baseURL: "https://api.openai.com/v1",
        client:  &http.Client{Timeout: 60 * time.Second},
    }
}

func (a *OpenAIAdapter) Name() string {
    return "openai"
}

func (a *OpenAIAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    // 转换请求格式
    openAIReq := a.toOpenAIRequest(req)

    data, _ := json.Marshal(openAIReq)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST",
        a.baseURL+"/chat/completions",
        strings.NewReader(string(data)))
    httpReq.Header.Set("Authorization", "Bearer "+a.apiKey)
    httpReq.Header.Set("Content-Type", "application/json")

    resp, err := a.client.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("openai request: %w", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)

    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("openai error %d: %s", resp.StatusCode, string(body))
    }

    // 解析 OpenAI 响应
    var openAIResp openAIChatResponse
    if err := json.Unmarshal(body, &openAIResp); err != nil {
        return nil, fmt.Errorf("parse openai response: %w", err)
    }

    return a.toUnifiedResponse(&openAIResp), nil
}

func (a *OpenAIAdapter) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
    // Streaming 实现(下讲详细展开)
    ch := make(chan ChatResponse)
    go func() {
        defer close(ch)
        // SSE 解析逻辑
    }()
    return ch, nil
}

// OpenAI 原始请求结构
type openAIChatRequest struct {
    Model       string           `json:"model"`
    Messages    []openAIMessage  `json:"messages"`
    Temperature float64          `json:"temperature,omitempty"`
    MaxTokens   int              `json:"max_tokens,omitempty"`
    Stream      bool             `json:"stream,omitempty"`
    Tools       []openAITool     `json:"tools,omitempty"`
}

type openAIMessage struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type openAITool struct {
    Type     string           `json:"type"`
    Function openAIToolFunction `json:"function"`
}

type openAIToolFunction struct {
    Name        string      `json:"name"`
    Description string      `json:"description"`
    Parameters  interface{} `json:"parameters"`
}

type openAIChatResponse struct {
    ID      string          `json:"id"`
    Object  string          `json:"object"`
    Model   string          `json:"model"`
    Choices []openAIChoice  `json:"choices"`
    Usage   openAIUsage     `json:"usage"`
}

type openAIChoice struct {
    Index        int             `json:"index"`
    Message      openAIMessage   `json:"message"`
    FinishReason string          `json:"finish_reason"`
}

type openAIUsage struct {
    PromptTokens     int `json:"prompt_tokens"`
    CompletionTokens int `json:"completion_tokens"`
    TotalTokens      int `json:"total_tokens"`
}

func (a *OpenAIAdapter) toOpenAIRequest(req *ChatRequest) *openAIChatRequest {
    messages := make([]openAIMessage, len(req.Messages))
    for i, m := range req.Messages {
        messages[i] = openAIMessage{Role: m.Role, Content: m.Content}
    }

    tools := make([]openAITool, len(req.Tools))
    for i, t := range req.Tools {
        tools[i] = openAITool{
            Type: t.Type,
            Function: openAIToolFunction{
                Name:        t.Function.Name,
                Description: t.Function.Description,
                Parameters:  t.Function.Parameters,
            },
        }
    }

    return &openAIChatRequest{
        Model:       req.Model,
        Messages:    messages,
        Temperature: req.Temperature,
        MaxTokens:   req.MaxTokens,
        Stream:      req.Stream,
        Tools:       tools,
    }
}

func (a *OpenAIAdapter) toUnifiedResponse(openAIResp *openAIChatResponse) *ChatResponse {
    choices := make([]Choice, len(openAIResp.Choices))
    for i, c := range openAIResp.Choices {
        choices[i] = Choice{
            Index: c.Index,
            Message: ChatMessage{
                Role:    c.Message.Role,
                Content: c.Message.Content,
            },
            FinishReason: c.FinishReason,
        }
    }

    return &ChatResponse{
        ID:      openAIResp.ID,
        Model:   openAIResp.Model,
        Choices: choices,
        Usage: TokenUsage{
            PromptTokens:     openAIResp.Usage.PromptTokens,
            CompletionTokens: openAIResp.Usage.CompletionTokens,
            TotalTokens:      openAIResp.Usage.TotalTokens,
        },
        CreatedAt: time.Now(),
    }
}

// ─── Claude 适配器 ───

type ClaudeAdapter struct {
    apiKey  string
    baseURL string
    client  *http.Client
}

func NewClaudeAdapter(apiKey string) *ClaudeAdapter {
    return &ClaudeAdapter{
        apiKey:  apiKey,
        baseURL: "https://api.anthropic.com/v1",
        client:  &http.Client{Timeout: 60 * time.Second},
    }
}

func (a *ClaudeAdapter) Name() string {
    return "claude"
}

func (a *ClaudeAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    // Claude 的消息格式不同:system 单独传,messages 只有 user/assistant
    claudeReq := a.toClaudeRequest(req)

    data, _ := json.Marshal(claudeReq)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST",
        a.baseURL+"/messages",
        strings.NewReader(string(data)))
    httpReq.Header.Set("x-api-key", a.apiKey)
    httpReq.Header.Set("anthropic-version", "2023-06-01")
    httpReq.Header.Set("Content-Type", "application/json")

    resp, err := a.client.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("claude request: %w", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)

    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("claude error %d: %s", resp.StatusCode, string(body))
    }

    var claudeResp claudeResponse
    if err := json.Unmarshal(body, &claudeResp); err != nil {
        return nil, fmt.Errorf("parse claude response: %w", err)
    }

    return a.toUnifiedResponse(&claudeResp), nil
}

func (a *ClaudeAdapter) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
    ch := make(chan ChatResponse)
    go func() {
        defer close(ch)
    }()
    return ch, nil
}

// Claude 请求/响应结构
type claudeRequest struct {
    Model       string           `json:"model"`
    Messages    []claudeMessage  `json:"messages"`
    System      string           `json:"system,omitempty"`
    MaxTokens   int              `json:"max_tokens"`
    Temperature float64          `json:"temperature,omitempty"`
}

type claudeMessage struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type claudeResponse struct {
    ID      string          `json:"id"`
    Type    string          `json:"type"`
    Role    string          `json:"role"`
    Content []claudeContent `json:"content"`
    Model   string          `json:"model"`
    Usage   claudeUsage     `json:"usage"`
}

type claudeContent struct {
    Type string `json:"type"`
    Text string `json:"text"`
}

type claudeUsage struct {
    InputTokens  int `json:"input_tokens"`
    OutputTokens int `json:"output_tokens"`
}

func (a *ClaudeAdapter) toClaudeRequest(req *ChatRequest) *claudeRequest {
    var system string
    messages := make([]claudeMessage, 0)

    for _, m := range req.Messages {
        if m.Role == "system" {
            system = m.Content
        } else {
            messages = append(messages, claudeMessage{Role: m.Role, Content: m.Content})
        }
    }

    maxTokens := req.MaxTokens
    if maxTokens == 0 {
        maxTokens = 1024
    }

    return &claudeRequest{
        Model:       req.Model,
        Messages:    messages,
        System:      system,
        MaxTokens:   maxTokens,
        Temperature: req.Temperature,
    }
}

func (a *ClaudeAdapter) toUnifiedResponse(claudeResp *claudeResponse) *ChatResponse {
    content := ""
    if len(claudeResp.Content) > 0 {
        content = claudeResp.Content[0].Text
    }

    return &ChatResponse{
        ID:    claudeResp.ID,
        Model: claudeResp.Model,
        Choices: []Choice{
            {
                Index:        0,
                Message:      ChatMessage{Role: "assistant", Content: content},
                FinishReason: "stop",
            },
        },
        Usage: TokenUsage{
            PromptTokens:     claudeResp.Usage.InputTokens,
            CompletionTokens: claudeResp.Usage.OutputTokens,
            TotalTokens:      claudeResp.Usage.InputTokens + claudeResp.Usage.OutputTokens,
        },
        CreatedAt: time.Now(),
    }
}

// ─── 文心一言适配器 ───

type WenxinAdapter struct {
    apiKey    string
    secretKey string
    client    *http.Client
    token     string
}

func NewWenxinAdapter(apiKey, secretKey string) *WenxinAdapter {
    return &WenxinAdapter{
        apiKey:    apiKey,
        secretKey: secretKey,
        client:    &http.Client{Timeout: 60 * time.Second},
    }
}

func (a *WenxinAdapter) Name() string {
    return "wenxin"
}

func (a *WenxinAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    // 先获取 access_token
    if err := a.refreshToken(ctx); err != nil {
        return nil, err
    }

    wenxinReq := a.toWenxinRequest(req)
    data, _ := json.Marshal(wenxinReq)

    url := fmt.Sprintf("https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=%s", a.token)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(data)))
    httpReq.Header.Set("Content-Type", "application/json")

    resp, err := a.client.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("wenxin request: %w", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)

    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("wenxin error %d: %s", resp.StatusCode, string(body))
    }

    var wenxinResp wenxinResponse
    if err := json.Unmarshal(body, &wenxinResp); err != nil {
        return nil, fmt.Errorf("parse wenxin response: %w", err)
    }

    return a.toUnifiedResponse(&wenxinResp), nil
}

func (a *WenxinAdapter) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
    ch := make(chan ChatResponse)
    go func() {
        defer close(ch)
    }()
    return ch, nil
}

// 文心一言请求/响应结构
type wenxinRequest struct {
    Messages    []wenxinMessage `json:"messages"`
    Temperature float64         `json:"temperature,omitempty"`
    TopP        float64         `json:"top_p,omitempty"`
}

type wenxinMessage struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type wenxinResponse struct {
    ID               string       `json:"id"`
    Object           string       `json:"object"`
    Created          int64        `json:"created"`
    Result           string       `json:"result"`
    IsTruncated      bool         `json:"is_truncated"`
    NeedClearHistory bool         `json:"need_clear_history"`
    Usage            wenxinUsage  `json:"usage"`
}

type wenxinUsage struct {
    PromptTokens     int `json:"prompt_tokens"`
    CompletionTokens int `json:"completion_tokens"`
    TotalTokens      int `json:"total_tokens"`
}

func (a *WenxinAdapter) refreshToken(ctx context.Context) error {
    url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
        a.apiKey, a.secretKey)

    resp, err := a.client.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    var result struct {
        AccessToken string `json:"access_token"`
    }
    json.NewDecoder(resp.Body).Decode(&result)

    a.token = result.AccessToken
    return nil
}

func (a *WenxinAdapter) toWenxinRequest(req *ChatRequest) *wenxinRequest {
    messages := make([]wenxinMessage, len(req.Messages))
    for i, m := range req.Messages {
        messages[i] = wenxinMessage{Role: m.Role, Content: m.Content}
    }

    return &wenxinRequest{
        Messages:    messages,
        Temperature: req.Temperature,
    }
}

func (a *WenxinAdapter) toUnifiedResponse(wenxinResp *wenxinResponse) *ChatResponse {
    return &ChatResponse{
        ID:    wenxinResp.ID,
        Model: "wenxin",
        Choices: []Choice{
            {
                Index:        0,
                Message:      ChatMessage{Role: "assistant", Content: wenxinResp.Result},
                FinishReason: "stop",
            },
        },
        Usage: TokenUsage{
            PromptTokens:     wenxinResp.Usage.PromptTokens,
            CompletionTokens: wenxinResp.Usage.CompletionTokens,
            TotalTokens:      wenxinResp.Usage.TotalTokens,
        },
        CreatedAt: time.Now(),
    }
}

五、网关核心实现

复制代码
// llm/gateway/gateway.go
package gateway

import (
    "context"
    "fmt"
    "log"
    "sync"
    "time"

    "golang.org/x/time/rate"
)

// LLMGateway LLM API 网关
type LLMGateway struct {
    adapters    map[string]LLMAdapter
    limiter     *rate.Limiter
    stats       *GatewayStats
    mu          sync.RWMutex
}

// GatewayStats 网关统计
type GatewayStats struct {
    TotalRequests   int64            `json:"total_requests"`
    TotalTokens     int64            `json:"total_tokens"`
    Errors          map[string]int64 `json:"errors"`
    AvgLatencyMs    float64          `json:"avg_latency_ms"`
    lastReset       time.Time
    mu              sync.Mutex
}

// GatewayOption 网关配置
type GatewayOption struct {
    QPSLimit    float64
    BurstLimit  int
}

// NewLLMGateway 创建网关
func NewLLMGateway(opts GatewayOption) *LLMGateway {
    limiter := rate.NewLimiter(rate.Limit(opts.QPSLimit), opts.BurstLimit)

    return &LLMGateway{
        adapters: make(map[string]LLMAdapter),
        limiter:  limiter,
        stats: &GatewayStats{
            Errors:    make(map[string]int64),
            lastReset: time.Now(),
        },
    }
}

// RegisterAdapter 注册模型适配器
func (g *LLMGateway) RegisterAdapter(adapter LLMAdapter) {
    g.mu.Lock()
    defer g.mu.Unlock()
    g.adapters[adapter.Name()] = adapter
    log.Printf("[Gateway] Registered adapter: %s", adapter.Name())
}

// Chat 统一聊天接口
func (g *LLMGateway) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    start := time.Now()

    // 1. 限流检查
    if err := g.limiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limit: %w", err)
    }

    // 2. 查找适配器
    adapter, err := g.getAdapter(req.Model)
    if err != nil {
        return nil, err
    }

    // 3. 调用模型
    resp, err := adapter.Chat(ctx, req)

    // 4. 记录统计
    latency := time.Since(start)
    g.recordStats(adapter.Name(), resp, err, latency)

    return resp, err
}

// ChatStream 统一流式接口
func (g *LLMGateway) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
    adapter, err := g.getAdapter(req.Model)
    if err != nil {
        return nil, err
    }

    req.Stream = true
    return adapter.ChatStream(ctx, req)
}

// GetStats 获取统计信息
func (g *LLMGateway) GetStats() *GatewayStats {
    g.stats.mu.Lock()
    defer g.stats.mu.Unlock()
    return g.stats
}

// getAdapter 获取对应模型的适配器
func (g *LLMGateway) getAdapter(model string) (LLMAdapter, error) {
    g.mu.RLock()
    defer g.mu.RUnlock()

    // 根据模型名推断厂商
    provider := detectProvider(model)
    adapter, ok := g.adapters[provider]
    if !ok {
        return nil, fmt.Errorf("unsupported model: %s (provider: %s)", model, provider)
    }

    return adapter, nil
}

// detectProvider 根据模型名推断厂商
func detectProvider(model string) string {
    if model == "" {
        return "openai"
    }

    switch {
    case containsAny(model, "gpt", "o1", "o3"):
        return "openai"
    case containsAny(model, "claude"):
        return "claude"
    case containsAny(model, "ernie", "wenxin"):
        return "wenxin"
    default:
        return "openai" // 默认走 OpenAI 格式
    }
}

func containsAny(s string, substrs ...string) bool {
    for _, sub := range substrs {
        if len(sub) <= len(s) {
            for i := 0; i <= len(s)-len(sub); i++ {
                if s[i:i+len(sub)] == sub {
                    return true
                }
            }
        }
    }
    return false
}

// recordStats 记录调用统计
func (g *LLMGateway) recordStats(provider string, resp *ChatResponse, err error, latency time.Duration) {
    g.stats.mu.Lock()
    defer g.stats.mu.Unlock()

    g.stats.TotalRequests++

    if err != nil {
        g.stats.Errors[provider]++
        return
    }

    if resp != nil {
        g.stats.TotalTokens += int64(resp.Usage.TotalTokens)
    }

    // 滑动平均延迟
    avg := g.stats.AvgLatencyMs
    count := float64(g.stats.TotalRequests)
    g.stats.AvgLatencyMs = avg + (float64(latency.Milliseconds())-avg)/count
}

六、HTTP 服务与使用示例

复制代码
// llm/gateway/server.go
package gateway

import (
    "encoding/json"
    "log"
    "net/http"
)

// HTTPServer 网关 HTTP 服务
type HTTPServer struct {
    gateway *LLMGateway
}

func NewHTTPServer(gateway *LLMGateway) *HTTPServer {
    return &HTTPServer{gateway: gateway}
}

// HandleChat 聊天接口
func (s *HTTPServer) HandleChat(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var req ChatRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    resp, err := s.gateway.Chat(r.Context(), &req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

// HandleStats 统计接口
func (s *HTTPServer) HandleStats(w http.ResponseWriter, r *http.Request) {
    stats := s.gateway.GetStats()
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(stats)
}

// Run 启动 HTTP 服务
func (s *HTTPServer) Run(addr string) error {
    mux := http.NewServeMux()
    mux.HandleFunc("/v1/chat/completions", s.HandleChat)
    mux.HandleFunc("/stats", s.HandleStats)

    log.Printf("[Gateway] Server starting on %s", addr)
    return http.ListenAndServe(addr, mux)
}

// ─── 使用示例 ───

func Example() {
    // 1. 创建网关
    gateway := NewLLMGateway(GatewayOption{
        QPSLimit:   100,
        BurstLimit: 20,
    })

    // 2. 注册适配器
    gateway.RegisterAdapter(NewOpenAIAdapter("sk-your-openai-key"))
    gateway.RegisterAdapter(NewClaudeAdapter("sk-ant-your-claude-key"))
    gateway.RegisterAdapter(NewWenxinAdapter("your-api-key", "your-secret-key"))

    // 3. 启动 HTTP 服务
    server := NewHTTPServer(gateway)
    go server.Run(":8080")

    // 4. 业务方调用(只需知道一个地址)
    // curl -X POST http://localhost:8080/v1/chat/completions \
    //   -H 'Content-Type: application/json' \
    //   -d '{
    //     "model": "gpt-4",
    //     "messages": [{"role": "user", "content": "你好"}]
    //   }'

    // 5. 切换模型只需改 model 字段
    // curl -X POST http://localhost:8080/v1/chat/completions \
    //   -d '{"model": "claude-3-opus", "messages": [...]}'
}

七、生产坑位

# 坑位 现象 解决方案
1 API Key 泄露​ Key 被窃取导致巨额账单 网关统一管理 Key,业务方无感知
2 模型限流​ 429 Too Many Requests 网关层排队 + 退避重试
3 超时未处理​ 请求卡死导致 goroutine 泄漏 统一超时控制(context deadline)
4 Token 统计不准​ 账单对不上 网关统一计数 + 定期对账
5 模型降级缺失​ 主力模型挂了,业务全挂 配置 fallback 模型列表
6 流式中断​ 客户端断连后服务器还在推 检测 ctx.Done() 及时清理

八、本讲小结

复制代码
┌─────────────────────────────────────────────────────────────┐
│                     第2讲 核心收获                            │
│                                                              │
│  ✅ 理解了 LLM API 网关的必要性                              │
│  ✅ 掌握了适配器模式屏蔽多厂商差异                           │
│  ✅ 实现了 OpenAI / Claude / 文心一言 的适配器               │
│  ✅ 实现了统一限流、统计、路由                               │
│  ✅ 提供了统一的 HTTP 接口供业务方调用                      │
│                                                              │
│  核心设计模式:适配器模式                                    │
│  核心原则:业务代码不改,换模型只改 model 字段              │
└─────────────────────────────────────────────────────────────┘

九、配套工具推荐

🧰 开发之余的小工具推荐

在调试 LLM API 网关时,经常需要对比不同模型的返回结果、清洗 JSON 响应、生成测试用的 RequestID:

  • JSON 格式化:zz365.top 的 JSON 工具可以把大模型返回的嵌套 JSON 一键展开,快速定位字段路径
  • UUID 生成器:给每次 API 调用分配唯一 ID,方便链路追踪和排障
  • Base64 编解码:处理某些模型返回的 Base64 编码内容
  • 文本差异对比:对比 GPT-4 和 Claude 对同一 Prompt 的输出差异

同类可备站点:

  • tool.lu(综合在线工具箱)
  • 360站长工具(zz360.cc,偏 SEO/建站方向)

提醒:所有涉及 API Key 和业务数据的操作,务必先在本地脱敏再使用在线工具。


下一讲预告:《Prompt 工程与管理------像管理代码一样管理提示词》

你会学到:Prompt 模板化、版本管理、A/B 测试、变量注入、Prompt 效果评估。

相关推荐
Kobebryant-Manba1 小时前
IEKT向量矩阵流程&评估
人工智能·算法·矩阵
奈落241 小时前
工程界讲角色边界,控制论讲博弈与稳定性:从 Swarms 的实践清单到《自动化学报》的协同综述,多智能体‘协同‘这件事有两套语言
人工智能·深度学习·自动化·ai 编程
小王R语言学习1 小时前
PaddleNLP:基于飞桨的大模型开发套件
人工智能·其他·paddlepaddle
椒颜皮皮虾྅1 小时前
让 C# 读懂整张图:DeploySharp 驱动 PaddleOCR v4/v5/v6,打通 OCR 全流程
开发语言·人工智能·深度学习·学习·c#·ocr·cuda
roman_日积跬步-终至千里1 小时前
【数据建模入门】从一家小店看懂数仓与机器学习:先理业务,再串技术
数据仓库·人工智能·机器学习·数据分析·数据建模
BJ_Bonree1 小时前
聊点技术 | Bonree ONE 业务链路观测实战 支付成功率下降,问题究竟出在哪一环?
大数据·人工智能
Bode_20021 小时前
TRIZ 理论知识体系与架构全景图
人工智能
richard_first1 小时前
第21章 训练一个简单的 GPT
人工智能·gpt·深度学习
Highcharts.js1 小时前
Highcharts 集成到 React 或 Vue 项目中时,如何高效地使用 TypeScript技巧?
vue.js·人工智能·react.js·typescript·前端框架·highcharts