跳转至

LLM 集成协议详解

状态: ✅ 集成协议完整已知

Client 架构

MonkeyCode 的 LLM Client 是一个 Go SDK(backend/pkg/llm/client.go),支持三种接口类型,根据模型配置自动选择。

// backend/pkg/llm/client.go — 核心 Client 结构体
type Client struct {
    provider    LLMProvider      // 提供商枚举
    apiKey      string           // API Key
    baseURL     string           // API 地址
    model       string           // 模型名称
    httpClient  *http.Client     // HTTP 客户端(可配置超时)
}

// 支持的提供商枚举
type LLMProvider string
const (
    ProviderOpenAI      LLMProvider = "openai"
    ProviderAnthropic   LLMProvider = "anthropic"
    ProviderDeepSeek    LLMProvider = "deepseek"
    ProviderSiliconFlow LLMProvider = "siliconflow"
    // ... 共 11 个
)

// 三种调用方法 — 根据接口类型自动选择
func (c *Client) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    // 用于 openai_chat 类型
    // 调用 openai SDK: github.com/sashabaranov/go-openai
}

func (c *Client) Responses(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    // 用于 openai_responses 类型
    // 原生 HTTP 调用 OpenAI Responses API
}

func (c *Client) Messages(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    // 用于 anthropic 类型
    // 调用 anthropic SDK: github.com/anthropics/anthropic-sdk-go
}

SDK 选择

// backend/pkg/llm/client.go — SDK 选择逻辑
import (
    openai "github.com/sashabaranov/go-openai"           // OpenAI SDK
    anthropic "github.com/anthropics/anthropic-sdk-go"   // Anthropic SDK
)

func NewClient(config *LLMConfig) *Client {
    switch config.InterfaceType {
    case InterfaceOpenAIChat:
        // 使用 go-openai SDK
        c := openai.NewClient(config.APIKey)
        if config.BaseURL != "" {
            c.BaseURL = config.BaseURL
        }
        return &Client{provider: ProviderOpenAI, ...}

    case InterfaceAnthropic:
        // 使用 anthropic-sdk-go
        c := anthropic.NewClient(config.APIKey)
        return &Client{provider: ProviderAnthropic, ...}
    }
}

接口类型自动检测

type InterfaceType string
const (
    InterfaceOpenAIChat      InterfaceType = "openai_chat"
    InterfaceOpenAIResponses InterfaceType = "openai_responses"
    InterfaceAnthropic       InterfaceType = "anthropic"
)

接口类型存储在模型配置的 interface_type 字段中,Agent 根据此字段选择对应的调用接口。

ChatRequest 统一格式

// 统一请求格式,适配三种接口类型
type ChatRequest struct {
    Messages      []Message     `json:"messages"`
    Model         string        `json:"model,omitempty"`
    MaxTokens     int           `json:"max_tokens,omitempty"`
    Temperature   float32       `json:"temperature,omitempty"`
    System        string        `json:"system,omitempty"`
    InterfaceType InterfaceType `json:"interface_type,omitempty"`
}

type Message struct {
    Role    string `json:"role"`    // "system" | "user" | "assistant"
    Content string `json:"content"` // 消息内容
}

type ChatResponse struct {
    Content string `json:"content"` // 响应文本
    Usage   Usage  `json:"usage"`   // Token 用量
}

实际调用链

Agent (VM 内)
  ├── @ai-sdk/openai-compatible.generateText()
  │     │
  │     └── HTTP POST → MonkeyCode 后端的 LLM Proxy
  │           │
  │           ├── backend/pkg/llm/client.go
  │           │     ├── Detect InterfaceType (从模型配置)
  │           │     ├── NewClient(config) → 选择 SDK
  │           │     └── client.ChatCompletion(ctx, req)
  │           │           │
  │           │           └── go-openai SDK → Provider API
  │           │                 ├── OpenAI: api.openai.com
  │           │                 ├── DeepSeek: api.deepseek.com
  │           │                 └── 等等
  │           │
  │           └── 流式响应返回 SSE → Agent
  └── ACP 事件 → TaskLive WS → Task Stream WS → 前端

错误处理

所有 LLM 错误被包装为中文描述:

// backend/pkg/llm/client.go — 错误包装
func ChatNoException(err error) string {
    return fmt.Sprintf("模型调用失败: %v", err)
}

// 实际的错误处理链
func (c *Client) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    resp, err := c.openaiClient.CreateChatCompletion(ctx, openaiReq)
    if err != nil {
        // 包装错误,返回友好消息
        return nil, fmt.Errorf("模型调用失败: %w", err)
    }
    return &ChatResponse{
        Content: resp.Choices[0].Message.Content,
        Usage:   Usage{
            PromptTokens:     resp.Usage.PromptTokens,
            CompletionTokens: resp.Usage.CompletionTokens,
            TotalTokens:      resp.Usage.TotalTokens,
        },
    }, nil
}

模拟模式

apiKey == "" 时返回模拟响应,用于开发/测试环境:

func (c *Client) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    // 模拟模式:没有 API Key 时返回 mock 数据
    if c.apiKey == "" {
        return &ChatResponse{
            Content: "这是模拟响应(没有配置 API Key)",
            Usage:   Usage{PromptTokens: 0, CompletionTokens: 0, TotalTokens: 0},
        }, nil
    }
    // 正常调用
}

附录:逆向分析代码示例

附录 A: Go SDK 调用测试 (Python 模拟)

# 模拟 MonkeyCode 后端的 LLM Client 调用逻辑
import httpx
from typing import Literal

InterfaceType = Literal["openai_chat", "openai_responses", "anthropic"]

class LLMClient:
    """模拟 chaitin/MonkeyCode 的 LLM Client"""

    def __init__(self, provider: str, api_key: str, base_url: str, model: str):
        self.provider = provider
        self.api_key = api_key
        self.base_url = base_url.rstrip('/')
        self.model = model

    def chat_completion(self, messages: list, interface_type: InterfaceType):
        """根据接口类型自动选择调用方式"""
        if interface_type == "openai_chat":
            return self._call_openai_chat(messages)
        elif interface_type == "openai_responses":
            return self._call_openai_responses(messages)
        elif interface_type == "anthropic":
            return self._call_anthropic(messages)

    def _call_openai_chat(self, messages: list):
        """调用 OpenAI Chat Completion API"""
        import openai
        client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
        resp = client.chat.completions.create(
            model=self.model,
            messages=messages,
        )
        return resp.choices[0].message.content

# 使用示例
client = LLMClient(
    provider="deepseek",
    api_key="sk-xxx",
    base_url="https://api.deepseek.com",
    model="deepseek-chat"
)
result = client.chat_completion(
    messages=[{"role": "user", "content": "Hello"}],
    interface_type="openai_chat"
)

附录 B: Go 源码 client.go 核心逻辑

// chaitin/MonkeyCode — backend/pkg/llm/client.go
// 完整 Client 调用链(基于源码重构)

func (c *Client) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
    openaiReq := openai.ChatCompletionRequest{
        Model: c.model,
        Messages: convertMessages(req.Messages),
        MaxTokens: req.MaxTokens,
        Temperature: req.Temperature,
    }

    if req.System != "" {
        openaiReq.Messages = append(
            []openai.ChatCompletionMessage{
                {Role: "system", Content: req.System},
            },
            openaiReq.Messages...,
        )
    }

    resp, err := c.openaiClient.CreateChatCompletionStreaming(ctx, openaiReq)
    if err != nil {
        return nil, ChatNoException(err)
    }
    defer resp.Close()

    // 累积流式响应
    var fullContent strings.Builder
    var usage Usage

    for {
        chunk, err := resp.Recv()
        if err != nil {
            break
        }
        fullContent.WriteString(chunk.Choices[0].Delta.Content)
        if chunk.Usage != nil {
            usage = Usage{
                PromptTokens:     chunk.Usage.PromptTokens,
                CompletionTokens: chunk.Usage.CompletionTokens,
                TotalTokens:      chunk.Usage.TotalTokens,
            }
        }
    }

    return &ChatResponse{
        Content: fullContent.String(),
        Usage:   usage,
    }, nil
}

相关章节