跳转至

Client 模块

统一且强大的 LLM 接入层


模块概述与核心组件

pkg/client 模块实现了 pkg/core 模块中定义的标准化大模型交互接口 core.Client。该模块旨在将不同大模型厂商千差万别的 HTTP 请求规范、工具调用结构以及 Server-Sent Events (SSE) 流式解析差异在底层抹平,为您提供极其一致的开发体验。

核心接口约定:

  • Chat(): 阻塞式请求,等待模型生成完毕后一次性返回 core.Response
  • ChatStream(): 流式请求,返回 core.Stream 迭代器,允许逐字渲染和思维链捕获。

内置开箱即用的提供商: openai, anthropic, deepseek, qwen, minimax, ollama, azureopenai


鉴权配置:API Key vs Auth Token

在使用各类厂商的 SDK 客户端时,正确的鉴权配置是第一步。base.Config 提供了两种不同的凭据字段应对不同场景:

1. 使用 API Key (标准后端服务)

大多数场景下,我们在开发者后台申请一串 sk-... 格式的密钥。使用 APIKey 字段进行配置: 提示:如果配置的字符串是环境变量名(如 OPENAI_API_KEY),GoChat 内部会自动尝试从环境变量中读取真正的值

config := openai.Config{
    Config: base.Config{
        APIKey: "sk-xxxxxxxxx", // 或直接写 "OPENAI_API_KEY"
        Model:  "gpt-4o",
    },
}
client, _ := openai.New(config)

2. 使用 Auth Token (OAuth2 或前台设备鉴权)

对于 Gemini、Qwen 等支持或强制要求 OAuth2 Device Flow 的情况,应用获得的是具有时效性的 Bearer Token(通常配合 pkg/provider 模块获取)。此时应使用 AuthToken

// 假设 token 来源于 authManager.GetToken()
config := qwen.Config{
    Config: base.Config{
        AuthToken: "ya29.a0AfB_bxxxxxx", // OAuth2 访问令牌
        Model:     "qwen-max",
    },
}
client, _ := qwen.New(config)
区别说明:当设置了 AuthToken 时,底层优先使用 Bearer <AuthToken> 构造 HTTP 认证头,跳过默认的 API Key 逻辑。


高级特性:开启深度思考 (Reasoning)

主流模型(如 DeepSeek-R1、OpenAI o1/o3、Claude 3.7)引入了基于强化学习的“深度思考(思维链)”能力。GoChat 在框架级别原生支持了对思考过程的捕获。

参数配置与使用条件

通过在 ChatChatStream 时传递选项 core.WithThinking(budget) 来开启思考模式:

  • budget:思考令牌的预算限制(部分模型如 Claude 强依赖此参数,最小通常为 1024)。
  • 底层会自动帮您将模型切换为对应的推理模型(如使用 Deepseek 时自动切换为 deepseek-reasoner,或向 OpenAI payload 注入 reasoning_effort 参数)。

获取思维流的完整代码示例

package main

import (
    "context"
    "fmt"
    "github.com/DotNetAge/gochat/pkg/client/base"
    "github.com/DotNetAge/gochat/pkg/client/deepseek"
    "github.com/DotNetAge/gochat/pkg/core"
)

func main() {
    client, _ := deepseek.New(deepseek.Config{
        Config: base.Config{APIKey: "DEEPSEEK_API_KEY"},
    })

    messages := []core.Message{core.NewUserMessage("为什么天空是蓝色的?请详细分析。")}

    // 开启深度思考模式,分配 2048 Tokens 预算
    stream, err := client.ChatStream(context.Background(), messages, core.WithThinking(2048))
    if err != nil {
        panic(err)
    }
    defer stream.Close()

    fmt.Println("=== 思考过程 ===")
    for stream.Next() {
        event := stream.Event()

        // 专门捕获思维链事件
        if event.Type == core.EventThinking {
            fmt.Printf("\033[90m%s\033[0m", event.Content) // 灰色打印思考内容
        } else if event.Type == core.EventContent {
            fmt.Print(event.Content) // 打印正式答复
        }
    }

    // 最终统计耗费
    fmt.Printf("\n\n消耗总 Token: %d\n", stream.Usage().TotalTokens)
}

部分厂商(如通义千问 Qwen、OpenAI 的特定接口)内置了强大的互联网搜索引擎插件。在 GoChat 中,仅需一个开关即可激活,使模型能够获取实时资讯。

启用方法与示例

使用 core.WithEnableSearch(true) 功能选项:

messages := []core.Message{core.NewUserMessage("昨天晚上的欧冠决赛比分是多少?")}

resp, err := client.Chat(context.Background(), messages, 
    core.WithEnableSearch(true), // 明确开启内置检索
)
if err != nil {
    panic(err)
}
fmt.Println(resp.Content) // 回复将基于最新搜索结果合成

多模态输入:文件与图片附件

多模态大语言模型(VLM)支持通过图像与文本进行联合推理。GoChat 提供了结构化的 core.ContentBlock 以及极其便利的 core.WithAttachments 选项来装载媒体。

方法 1:使用 Attachments 功能选项(推荐,最简洁)

package main

import (
    "context"
    "os"
    "github.com/DotNetAge/gochat/pkg/client/openai"
    "github.com/DotNetAge/gochat/pkg/core"
)

func main() {
    client, _ := openai.New(openai.Config{/* ... */})

    // 读取本地图片
    imgBytes, _ := os.ReadFile("dashboard.png")

    // 创建图片附件 (需指定准确的 MIME 类型)
    attachment := core.NewImageAttachment("dashboard.png", "image/png", imgBytes)

    messages := []core.Message{core.NewUserMessage("请帮我提取这张截图里的所有数据表指标。")}

    // 通过 Option 挂载附件
    resp, _ := client.Chat(context.Background(), messages, 
        core.WithAttachments(attachment),
    )
    println(resp.Content)
}

方法 2:底层 ContentBlock 手动构建

如果您需要精准控制图文交错的顺序:

messages := []core.Message{
    {
        Role: core.RoleUser,
        Content: []core.ContentBlock{
            {Type: core.ContentTypeText, Text: "这是第一张图:"},
            {Type: core.ContentTypeImage, MediaType: "image/jpeg", Data: base64Image1},
            {Type: core.ContentTypeText, Text: "这是第二张图,两者的区别是什么?"},
            {Type: core.ContentTypeImage, MediaType: "image/jpeg", Data: base64Image2},
        },
    },
}

扩展开发指南:如何接入全新的大模型厂商

得益于良好的组合式架构设计,如果您公司内部或者某个新兴小众厂商提供了私有的 API,将其封装为一个兼容 GoChat 体系的 Client 非常简单。

标准实现流程

  1. 组合基类:定义您的配置类并嵌套 base.Config,定义您的 Client 类并嵌套 *base.Client
  2. 实现接口:实现 ChatChatStream 两个方法。
  3. 格式转换:编写一个私有的请求构建函数,将 []core.Message 转换为目标 API 的 JSON 结构体;以及将返回的 JSON 响应转换为 core.Response / core.StreamEvent

代码骨架示例

package customllm

import (
    "context"
    "github.com/DotNetAge/gochat/pkg/client/base"
    "github.com/DotNetAge/gochat/pkg/core"
)

// 1. 定义配置与结构
type Config struct {
    base.Config
    CustomVersion string // 您的专有参数
}

type Client struct {
    base *base.Client
}

// 2. 构造函数
func New(config Config) (*Client, error) {
    return &Client{
        base: base.New(config.Config), // 复用底层带重试的 HTTP HTTPClient
    }, nil
}

// 3. 实现 Chat 方法
func (c *Client) Chat(ctx context.Context, messages []core.Message, opts ...core.Option) (*core.Response, error) {
    // a. 应用 Options
    options := core.ApplyOptions(opts...)
    messages = core.ProcessAttachments(messages, options.Attachments)

    var finalResponse *core.Response

    // b. 借助 base.Retry 实现自动指数退避重试
    err := c.base.Retry(ctx, func() error {
        // 在这里发起您的专有 HTTP Request...
        // resp, err := doHttpRequest(messages)
        // finalResponse = mapToCoreResponse(resp)
        return nil 
    })

    return finalResponse, err
}

// 4. 实现 ChatStream 方法
func (c *Client) ChatStream(ctx context.Context, messages []core.Message, opts ...core.Option) (*core.Stream, error) {
    // 发起 HTTP 请求获取流对象...
    // 启动 Goroutine 监听 body,把行数据映射为 core.StreamEvent 并塞入 channel

    ch := make(chan core.StreamEvent, 10)
    go func() {
        defer close(ch)
        // 伪代码解析 SSE
        for {
            // line := readLine()
            // 解析出文字后:
            ch <- core.StreamEvent{Type: core.EventContent, Content: "字"}
        }
    }()

    // 返回标准 Stream 迭代器
    return core.NewStream(ch, response.Body), nil
}

通过复用 base.Retrycore.NewStream,您只需寥寥百行代码即可实现一个完全符合企业级高可用标准的全新 LLM 客户端!