我把 OpenAI 协议塞进了 Go 工具库:go-commons 开始支持 AI 了

最近我给自己的 Go 工具库 go-commons 加了一块新的能力:

AI。

项目地址:

text 复制代码
https://github.com/Rodert/go-commons

go-commons 原本是一个偏传统的 Go 通用工具库,里面有字符串、时间、文件、JSON、配置、并发、系统信息等工具。

现在我给它增加了:

text 复制代码
ai/
├── types.go
└── openai/
    ├── client.go
    └── client_test.go

也就是说,现在 go-commons 不只是:

text 复制代码
stringutils
timeutils
fileutils
jsonutils
configutils
concurrentutils

还开始承担一部分:

text 复制代码
AI Infrastructure

目前已经实现了统一 AI 类型、OpenAI Chat Completions 协议、OpenAI Compatible API、流式输出、Token Usage 和 Tool Calling 等基础能力。项目 README 目前也已经把定位调整为:

A standard-library-first Go infrastructure library for backend and modern AI applications.

换句话说,它开始从传统 Go Utils,慢慢向:

text 复制代码
Go Backend Commons
        +
AI Infrastructure

这个方向发展。

这篇文章就完整讲一下我是怎么设计的,以及怎么使用。


一、为什么我要自己封装一层 AI API?

很多人可能会问:

OpenAI 已经有 SDK 了。

各种第三方 SDK 也很多。

为什么还要自己封装?

原因其实很简单:

现在 AI 项目真正麻烦的不是调用一个 OpenAI API,而是同时接很多不同模型。

例如我们今天可能调用:

text 复制代码
OpenAI

明天换成:

text 复制代码
DeepSeek

或者:

text 复制代码
SiliconFlow

甚至自己部署:

text 复制代码
Ollama
vLLM

它们中的很多接口都是 OpenAI Compatible。

如果业务代码直接和某个官方 SDK 强绑定:

go 复制代码
openaiClient.Chat.Completions.New(...)

以后每换一个模型,业务代码可能都要改。

我更希望最终业务代码长这样:

go 复制代码
resp, err := client.Chat(ctx, &ai.Request{
	Model: "gpt-5",
	Messages: []ai.Message{
		{
			Role:    ai.RoleUser,
			Content: "你好",
		},
	},
})

至于:

text 复制代码
client

后面到底连的是:

text 复制代码
OpenAI
DeepSeek
SiliconFlow
Ollama
vLLM
ChongPlus

业务层最好根本不用关心。

这就是这一层封装存在的意义。


二、现在 go-commons 的 AI 结构

目前仓库里的结构非常简单:

text 复制代码
go-commons
│
├── ai
│   ├── types.go
│   │
│   └── openai
│       ├── client.go
│       └── client_test.go
│
├── concurrentutils
├── configutils
├── convertutils
├── cryptutils
├── errorutils
├── fileutils
├── jsonutils
├── netutils
├── sliceutils
├── stringutils
├── systemutils
├── timeutils
└── validationutils

这里我没有直接把所有东西全部塞到:

text 复制代码
openai

里面。

而是额外做了一层:

text 复制代码
ai

这里定义的是:

Provider Neutral,也就是供应商无关的数据结构。

现在 ai/types.go 中主要包含这些对象:

text 复制代码
Role
Message
Tool
ToolCall
Request
Usage
Response
Chunk
Client

这一层其实是整个设计里最重要的东西。


三、先设计统一 Client

核心接口目前非常简单:

go 复制代码
type Client interface {
	Chat(
		ctx context.Context,
		request *Request,
	) (*Response, error)

	Stream(
		ctx context.Context,
		request *Request,
	) (<-chan Chunk, error)
}

完整一点看:

go 复制代码
package ai

import "context"

type Client interface {
	Chat(
		ctx context.Context,
		request *Request,
	) (*Response, error)

	Stream(
		ctx context.Context,
		request *Request,
	) (<-chan Chunk, error)
}

这意味着:

只要一个 Provider 实现:

go 复制代码
Chat()
Stream()

就可以接入整个体系。

未来完全可以出现:

go 复制代码
var client ai.Client

然后根据配置选择不同 Provider:

go 复制代码
switch provider {
case "openai":
	client = openai.NewClient(...)
case "claude":
	client = claude.NewClient(...)
case "gemini":
	client = gemini.NewClient(...)
}

后面的业务逻辑就不用改:

go 复制代码
response, err := client.Chat(ctx, req)

这是我希望最终达到的效果。


四、统一 Role

不同厂商对角色定义并不完全相同。

所以我先定义了统一 Role:

go 复制代码
type Role string

const (
	RoleSystem    Role = "system"
	RoleDeveloper Role = "developer"
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleTool      Role = "tool"
)

于是业务代码不用到处写:

go 复制代码
"user"
"assistant"
"system"

可以写成:

go 复制代码
ai.RoleUser
ai.RoleAssistant
ai.RoleSystem

例如:

go 复制代码
messages := []ai.Message{
	{
		Role:    ai.RoleSystem,
		Content: "你是一名高级 Go 工程师。",
	},
	{
		Role:    ai.RoleUser,
		Content: "帮我解释一下 context.Context。",
	},
}

五、统一 Message

Message 当前定义:

go 复制代码
type Message struct {
	Role       Role   `json:"role"`
	Content    string `json:"content,omitempty"`
	Name       string `json:"name,omitempty"`
	ToolCallID string `json:"tool_call_id,omitempty"`
}

这里除了最普通的:

text 复制代码
role
content

还预留了:

text 复制代码
name
tool_call_id

因此以后 Tool Calling 不需要再重新设计消息模型。

例如普通对话:

go 复制代码
message := ai.Message{
	Role:    ai.RoleUser,
	Content: "Go 的 goroutine 是什么?",
}

Assistant:

go 复制代码
message := ai.Message{
	Role:    ai.RoleAssistant,
	Content: "goroutine 是 Go 运行时管理的轻量级并发执行单元。",
}

Tool 返回:

go 复制代码
message := ai.Message{
	Role:       ai.RoleTool,
	ToolCallID: "call_123456",
	Content:    `{"weather":"sunny","temperature":31}`,
}

同一个 Message 类型就可以覆盖整个 Chat 生命周期。


六、统一 AI 请求 Request

现在定义是:

go 复制代码
type Request struct {
	Model       string    `json:"model"`
	Messages    []Message `json:"messages"`
	Temperature *float64  `json:"temperature,omitempty"`
	MaxTokens   int       `json:"max_tokens,omitempty"`
	Tools       []Tool    `json:"tools,omitempty"`
}

所以一条完整请求可以这样写:

go 复制代码
temperature := 0.7

request := &ai.Request{
	Model: "gpt-5",
	Messages: []ai.Message{
		{
			Role:    ai.RoleSystem,
			Content: "你是一名 Go 后端专家。",
		},
		{
			Role:    ai.RoleUser,
			Content: "Gin 和 net/http 有什么区别?",
		},
	},
	Temperature: &temperature,
	MaxTokens:   2000,
}

注意我这里把:

go 复制代码
Temperature

定义成了:

go 复制代码
*float64

而不是:

go 复制代码
float64

这是一个看起来很小但很重要的区别。

因为:

go 复制代码
0

可能是用户真正想设置的值。

如果是普通 float64

go 复制代码
Temperature float64 `json:"temperature,omitempty"`

那么 0 会被 omitempty 忽略。

使用:

go 复制代码
*float64

就可以区分:

text 复制代码
nil

和:

text 复制代码
0


七、统一 Token Usage

现在 AI 应用还有一个绕不开的问题:

Token。

尤其做 API 平台、企业应用、Agent 或计费系统的时候,Token Usage 基本是必须记录的。

所以这里也做了统一:

go 复制代码
type Usage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
}

例如:

go 复制代码
fmt.Println("Prompt:", resp.Usage.PromptTokens)
fmt.Println("Completion:", resp.Usage.CompletionTokens)
fmt.Println("Total:", resp.Usage.TotalTokens)

以后就可以非常容易加入:

text 复制代码
费用计算
用户账单
模型调用统计
Prometheus
日志
限额控制

例如:

go 复制代码
func calculateCost(
	usage ai.Usage,
	inputPrice float64,
	outputPrice float64,
) float64 {

	inputCost := float64(usage.PromptTokens) / 1_000_000 * inputPrice
	outputCost := float64(usage.CompletionTokens) / 1_000_000 * outputPrice

	return inputCost + outputCost
}

调用:

go 复制代码
cost := calculateCost(
	resp.Usage,
	1.25,
	10,
)

fmt.Printf("本次请求费用:$%.6f\n", cost)

八、统一 Response

返回结果:

go 复制代码
type Response struct {
	ID           string     `json:"id"`
	Model        string     `json:"model"`
	Content      string     `json:"content"`
	FinishReason string     `json:"finish_reason,omitempty"`
	ToolCalls    []ToolCall `json:"tool_calls,omitempty"`
	Usage        Usage      `json:"usage"`
}

所以普通业务代码根本不用关心 OpenAI 原始 JSON:

go 复制代码
response, err := client.Chat(ctx, request)

if err != nil {
	panic(err)
}

fmt.Println(response.Content)

想拿模型:

go 复制代码
fmt.Println(response.Model)

想看结束原因:

go 复制代码
fmt.Println(response.FinishReason)

想看 Token:

go 复制代码
fmt.Println(response.Usage.TotalTokens)

想判断有没有调用工具:

go 复制代码
if len(response.ToolCalls) > 0 {
	for _, call := range response.ToolCalls {
		fmt.Println(call.Name)
		fmt.Println(call.Arguments)
	}
}

这也是统一协议层带来的好处。


九、安装 go-commons

直接:

bash 复制代码
go get github.com/Rodert/go-commons

然后导入:

go 复制代码
import (
	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

十、第一个完整 OpenAI Demo

下面直接来完整代码。

新建:

text 复制代码
main.go

代码:

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

func main() {
	apiKey := os.Getenv("OPENAI_API_KEY")

	if apiKey == "" {
		log.Fatal("请设置 OPENAI_API_KEY 环境变量")
	}

	client := openai.NewClient(openai.Config{
		APIKey: apiKey,
	})

	ctx, cancel := context.WithTimeout(
		context.Background(),
		60*time.Second,
	)
	defer cancel()

	temperature := 0.7

	request := &ai.Request{
		Model: "gpt-5",
		Messages: []ai.Message{
			{
				Role:    ai.RoleSystem,
				Content: "你是一名资深 Go 工程师,请使用中文回答。",
			},
			{
				Role:    ai.RoleUser,
				Content: "用简单的话解释 goroutine 和 channel。",
			},
		},
		Temperature: &temperature,
		MaxTokens:   2000,
	}

	response, err := client.Chat(ctx, request)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("ID:")
	fmt.Println(response.ID)

	fmt.Println()

	fmt.Println("Model:")
	fmt.Println(response.Model)

	fmt.Println()

	fmt.Println("Content:")
	fmt.Println(response.Content)

	fmt.Println()

	fmt.Println("Finish Reason:")
	fmt.Println(response.FinishReason)

	fmt.Println()

	fmt.Println("Usage:")
	fmt.Printf(
		"Prompt=%d Completion=%d Total=%d\n",
		response.Usage.PromptTokens,
		response.Usage.CompletionTokens,
		response.Usage.TotalTokens,
	)
}

运行:

bash 复制代码
export OPENAI_API_KEY="sk-xxxx"

go run main.go

核心其实只有:

go 复制代码
client := openai.NewClient(openai.Config{
	APIKey: os.Getenv("OPENAI_API_KEY"),
})

然后:

go 复制代码
response, err := client.Chat(
	ctx,
	&ai.Request{
		Model: "gpt-5",
		Messages: []ai.Message{
			{
				Role:    ai.RoleUser,
				Content: "你好",
			},
		},
	},
)

十一、它不只能调用 OpenAI

这其实是我觉得目前实现里非常有价值的一点。

openai.Client 并没有把 BaseURL 写死。

Config 定义是:

go 复制代码
type Config struct {
	APIKey    string
	BaseURL   string
	HTTPClient *http.Client
	Headers   http.Header
}

如果不传 BaseURL,默认:

text 复制代码
https://api.openai.com/v1

如果传:

go 复制代码
BaseURL: "https://example.com/v1"

它就会请求:

text 复制代码
https://example.com/v1/chat/completions

因此它其实天然就是一个:

text 复制代码
OpenAI Compatible Client

十二、调用 OpenAI Compatible 平台

完整代码:

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

func main() {
	apiKey := os.Getenv("AI_API_KEY")

	if apiKey == "" {
		log.Fatal("AI_API_KEY 未设置")
	}

	client := openai.NewClient(openai.Config{
		APIKey: apiKey,
		BaseURL: "https://api.example.com/v1",
	})

	ctx, cancel := context.WithTimeout(
		context.Background(),
		120*time.Second,
	)
	defer cancel()

	response, err := client.Chat(
		ctx,
		&ai.Request{
			Model: "your-model-name",
			Messages: []ai.Message{
				{
					Role:    ai.RoleSystem,
					Content: "You are a helpful assistant.",
				},
				{
					Role:    ai.RoleUser,
					Content: "介绍一下 Golang。",
				},
			},
		},
	)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(response.Content)
}

实际上 DeepSeek、SiliconFlow、Ollama、vLLM 等提供 OpenAI-compatible endpoint 的服务,也都可以沿用这套调用思路;当前项目 README 也明确把这类兼容 Provider 列入了 AI 模块定位。


十三、用 go-commons 调自己的 API 中转站

如果你自己有 OpenAI Compatible API,比如:

text 复制代码
https://api.example.com/v1

代码仍然完全一样:

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

func main() {
	client := openai.NewClient(openai.Config{
		APIKey:  os.Getenv("AI_API_KEY"),
		BaseURL: "https://api.example.com/v1",
	})

	resp, err := client.Chat(
		context.Background(),
		&ai.Request{
			Model: "gpt-5",
			Messages: []ai.Message{
				{
					Role:    ai.RoleUser,
					Content: "写一个 Go 快速排序。",
				},
			},
		},
	)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resp.Content)
}

这样你的代码完全不会绑定某一家平台。


十四、完整流式输出 Demo

AI 对话应用里:

text 复制代码
Streaming

基本是刚需。

目前实现里已经支持:

go 复制代码
Stream(
	ctx context.Context,
	request *ai.Request,
) (<-chan ai.Chunk, error)

完整代码:

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

func main() {
	client := openai.NewClient(openai.Config{
		APIKey: os.Getenv("OPENAI_API_KEY"),
	})

	ctx, cancel := context.WithTimeout(
		context.Background(),
		2*time.Minute,
	)
	defer cancel()

	stream, err := client.Stream(
		ctx,
		&ai.Request{
			Model: "gpt-5",
			Messages: []ai.Message{
				{
					Role: ai.RoleSystem,
					Content: `
你是一名高级 Go 工程师。
回答尽量专业,但是不要过度复杂。
`,
				},
				{
					Role:    ai.RoleUser,
					Content: "详细解释一下 Go Scheduler。",
				},
			},
		},
	)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("AI:")

	for chunk := range stream {

		if chunk.Err != nil {
			log.Fatal(chunk.Err)
		}

		if chunk.Content != "" {
			fmt.Print(chunk.Content)
		}

		if chunk.FinishReason != "" {
			fmt.Printf(
				"\n\nFinishReason: %s\n",
				chunk.FinishReason,
			)
		}

		if chunk.Usage != nil {
			fmt.Printf(
				"\nTokens: %d\n",
				chunk.Usage.TotalTokens,
			)
		}
	}
}

十五、底层 SSE 是怎么实现的?

目前 Stream() 最终会发送:

text 复制代码
POST /chat/completions

并:

json 复制代码
{
  "stream": true
}

然后通过:

go 复制代码
bufio.Scanner

逐行读取 SSE。

逻辑大致如下:

go 复制代码
scanner := bufio.NewScanner(body)

for scanner.Scan() {

	line := scanner.Text()

	if !strings.HasPrefix(line, "data:") {
		continue
	}

	data := strings.TrimSpace(
		strings.TrimPrefix(line, "data:"),
	)

	if data == "[DONE]" {
		return
	}

	var response wireResponse

	err := json.Unmarshal(
		[]byte(data),
		&response,
	)

	if err != nil {
		return
	}

	chunk := response.toChunk()

	chunks <- chunk
}

最终使用者完全不用处理:

text 复制代码
data:

也不用处理:

text 复制代码
[DONE]

只需要:

go 复制代码
for chunk := range stream {
	fmt.Print(chunk.Content)
}

即可。


十六、Chunk 为什么单独设计?

流式返回定义:

go 复制代码
type Chunk struct {
	ID           string     `json:"id,omitempty"`
	Model        string     `json:"model,omitempty"`
	Content      string     `json:"content,omitempty"`
	FinishReason string     `json:"finish_reason,omitempty"`
	ToolCalls    []ToolCall `json:"tool_calls,omitempty"`
	Usage        *Usage     `json:"usage,omitempty"`
	Err          error      `json:"-"`
}

这意味着一个流式 Chunk 不只是:

text 复制代码
文字

还可能带:

text 复制代码
Tool Call
Finish Reason
Usage
Error

所以以后做 Agent:

text 复制代码
模型输出
   ↓
Tool Call
   ↓
执行函数
   ↓
返回模型

这一套结构不用重写。


十七、完整 Tool Calling 示例

现在 Tool 的定义是:

go 复制代码
type Tool struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Parameters  any    `json:"parameters,omitempty"`
}

OpenAI Provider 会自动转换成:

json 复制代码
{
  "type": "function",
  "function": {}
}

例如做一个天气工具:

go 复制代码
weatherTool := ai.Tool{
	Name:        "get_weather",
	Description: "查询一个城市的天气",
	Parameters: map[string]any{
		"type": "object",
		"properties": map[string]any{
			"city": map[string]any{
				"type":        "string",
				"description": "城市名称",
			},
		},
		"required": []string{
			"city",
		},
	},
}

完整请求:

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

func main() {

	client := openai.NewClient(
		openai.Config{
			APIKey: os.Getenv("OPENAI_API_KEY"),
		},
	)

	weatherTool := ai.Tool{
		Name:        "get_weather",
		Description: "查询指定城市当前天气",
		Parameters: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"city": map[string]any{
					"type":        "string",
					"description": "城市名称,例如 Beijing",
				},
			},
			"required": []string{
				"city",
			},
		},
	}

	response, err := client.Chat(
		context.Background(),
		&ai.Request{
			Model: "gpt-5",
			Messages: []ai.Message{
				{
					Role:    ai.RoleUser,
					Content: "北京现在天气怎么样?",
				},
			},
			Tools: []ai.Tool{
				weatherTool,
			},
		},
	)

	if err != nil {
		log.Fatal(err)
	}

	if len(response.ToolCalls) == 0 {
		fmt.Println(response.Content)
		return
	}

	for _, call := range response.ToolCalls {

		fmt.Println("Tool ID:")
		fmt.Println(call.ID)

		fmt.Println("Tool Name:")
		fmt.Println(call.Name)

		fmt.Println("Arguments:")
		fmt.Println(call.Arguments)
	}
}

模型可能返回:

text 复制代码
Tool Name:
get_weather

Arguments:
{"city":"北京"}

十八、真正执行 Tool

我们再把例子做完整一些。

先定义参数:

go 复制代码
type WeatherArgs struct {
	City string `json:"city"`
}

模拟天气接口:

go 复制代码
func getWeather(city string) string {

	return fmt.Sprintf(
		`{"city":"%s","temperature":31,"weather":"晴"}`,
		city,
	)
}

完整 Tool 执行逻辑:

go 复制代码
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

type WeatherArgs struct {
	City string `json:"city"`
}

func getWeather(city string) string {
	return fmt.Sprintf(
		`{"city":"%s","temperature":31,"weather":"晴"}`,
		city,
	)
}

func main() {

	ctx := context.Background()

	client := openai.NewClient(
		openai.Config{
			APIKey: os.Getenv("OPENAI_API_KEY"),
		},
	)

	tool := ai.Tool{
		Name:        "get_weather",
		Description: "查询城市天气",
		Parameters: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"city": map[string]any{
					"type": "string",
				},
			},
			"required": []string{"city"},
		},
	}

	messages := []ai.Message{
		{
			Role:    ai.RoleUser,
			Content: "北京今天适合穿短袖吗?",
		},
	}

	first, err := client.Chat(
		ctx,
		&ai.Request{
			Model:    "gpt-5",
			Messages: messages,
			Tools: []ai.Tool{
				tool,
			},
		},
	)

	if err != nil {
		log.Fatal(err)
	}

	if len(first.ToolCalls) == 0 {

		fmt.Println(first.Content)

		return
	}

	call := first.ToolCalls[0]

	var args WeatherArgs

	err = json.Unmarshal(
		[]byte(call.Arguments),
		&args,
	)

	if err != nil {
		log.Fatal(err)
	}

	result := getWeather(args.City)

	fmt.Println(
		"工具执行结果:",
		result,
	)

	messages = append(
		messages,
		ai.Message{
			Role:    ai.RoleAssistant,
			Content: first.Content,
		},
	)

	messages = append(
		messages,
		ai.Message{
			Role:       ai.RoleTool,
			ToolCallID: call.ID,
			Content:    result,
		},
	)

	second, err := client.Chat(
		ctx,
		&ai.Request{
			Model:    "gpt-5",
			Messages: messages,
			Tools: []ai.Tool{
				tool,
			},
		},
	)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println()
	fmt.Println("AI:")
	fmt.Println(second.Content)
}

这个结构其实已经开始有一点:

text 复制代码
Agent

的味道了。


十九、可以自己做一个 Tool Registry

如果以后工具越来越多,就不能不停写:

go 复制代码
if name == "xxx"

可以封装:

go 复制代码
package tools

import (
	"context"
	"fmt"
)

type Handler func(
	ctx context.Context,
	args string,
) (string, error)

type Registry struct {
	handlers map[string]Handler
}

func NewRegistry() *Registry {
	return &Registry{
		handlers: make(
			map[string]Handler,
		),
	}
}

func (r *Registry) Register(
	name string,
	handler Handler,
) {
	r.handlers[name] = handler
}

func (r *Registry) Execute(
	ctx context.Context,
	name string,
	args string,
) (string, error) {

	handler, ok := r.handlers[name]

	if !ok {
		return "",
			fmt.Errorf(
				"tool not found: %s",
				name,
			)
	}

	return handler(ctx, args)
}

注册:

go 复制代码
registry := tools.NewRegistry()

registry.Register(
	"get_weather",
	func(
		ctx context.Context,
		args string,
	) (string, error) {

		var input WeatherArgs

		if err := json.Unmarshal(
			[]byte(args),
			&input,
		); err != nil {

			return "", err
		}

		return getWeather(
			input.City,
		), nil
	},
)

执行:

go 复制代码
result, err := registry.Execute(
	ctx,
	call.Name,
	call.Arguments,
)

下一步基本就能继续发展成:

text 复制代码
Agent Runtime

二十、支持自定义 HTTP Client

现在 Config 还允许:

go 复制代码
HTTPClient *http.Client

这是一个我比较喜欢的设计。

因为生产环境里几乎不会永远使用:

go 复制代码
http.DefaultClient

例如可以自己设置 Timeout:

go 复制代码
httpClient := &http.Client{
	Timeout: 120 * time.Second,
}

完整:

go 复制代码
client := openai.NewClient(
	openai.Config{

		APIKey: os.Getenv(
			"AI_API_KEY",
		),

		BaseURL: "https://api.example.com/v1",

		HTTPClient: &http.Client{
			Timeout: 120 * time.Second,
		},
	},
)

以后甚至可以:

text 复制代码
代理
Transport
连接池
TLS
Tracing
Metrics

全部放进去。


二十一、自定义 Header

Config 还支持:

go 复制代码
Headers http.Header

例如:

go 复制代码
headers := make(http.Header)

headers.Set(
	"X-Request-Source",
	"go-commons",
)

headers.Set(
	"X-App-ID",
	"demo-app",
)

client := openai.NewClient(
	openai.Config{

		APIKey: os.Getenv(
			"AI_API_KEY",
		),

		BaseURL:
			"https://api.example.com/v1",

		Headers:
			headers,
	},
)

底层会自动加入:

http 复制代码
Authorization: Bearer xxx
Content-Type: application/json
Accept: application/json, text/event-stream

并合并自定义 Header。

这对企业网关和 API 中转平台尤其有用。


二十二、封装一个业务层 AI Service

在真实项目里,我一般不会直接到处:

go 复制代码
client.Chat(...)

而是再封装一个:

text 复制代码
AIService

例如:

go 复制代码
package service

import (
	"context"

	"github.com/Rodert/go-commons/ai"
)

type AIService struct {
	client ai.Client
	model  string
}

func NewAIService(
	client ai.Client,
	model string,
) *AIService {

	return &AIService{
		client: client,
		model:  model,
	}
}

func (s *AIService) Ask(
	ctx context.Context,
	question string,
) (string, error) {

	resp, err := s.client.Chat(
		ctx,
		&ai.Request{
			Model: s.model,
			Messages: []ai.Message{
				{
					Role: ai.RoleSystem,
					Content:
						"你是一名专业的 AI 助手。",
				},
				{
					Role: ai.RoleUser,
					Content:
						question,
				},
			},
		},
	)

	if err != nil {
		return "", err
	}

	return resp.Content, nil
}

main:

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/Rodert/go-commons/ai/openai"
	"your-project/service"
)

func main() {

	client := openai.NewClient(
		openai.Config{
			APIKey: os.Getenv(
				"AI_API_KEY",
			),
		},
	)

	aiService :=
		service.NewAIService(
			client,
			"gpt-5",
		)

	answer, err :=
		aiService.Ask(
			context.Background(),
			"什么是 Redis 缓存击穿?",
		)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(answer)
}

这里有一个很重要的点:

go 复制代码
AIService

依赖的是:

go 复制代码
ai.Client

而不是:

go 复制代码
*openai.Client

因此未来完全可以:

go 复制代码
client := claude.NewClient(...)

而:

go 复制代码
AIService

一行都不用修改。


二十三、这其实就是依赖倒置

架构就变成:

text 复制代码
              Business

                  |
                  v

              ai.Client

                  |
       ---------------------
       |         |         |
       v         v         v

    OpenAI    Claude    Gemini

业务依赖:

text 复制代码
Interface

而不是依赖:

text 复制代码
OpenAI

这个设计带来的最大价值不是代码少几行。

而是:

你的业务不再和某一家模型厂商绑定。


二十四、以后加入 Claude 应该怎么设计?

目前仓库公开的 AI 目录中,我看到的是:

text 复制代码
ai/
├── types.go
└── openai/

也就是说现在主要完成的是 OpenAI / OpenAI Compatible 这一层,Claude 还可以继续作为下一层 Provider 扩展。

我建议未来直接:

text 复制代码
ai/
├── types.go
│
├── openai/
│   └── client.go
│
├── claude/
│   └── client.go
│
└── gemini/
    └── client.go

Claude 实现:

go 复制代码
type Client struct {
	apiKey string
	baseURL string
	httpClient *http.Client
}

然后:

go 复制代码
func (c *Client) Chat(
	ctx context.Context,
	request *ai.Request,
) (*ai.Response, error) {
	// Request 转 Claude 协议
	// 请求 /v1/messages
	// Claude Response 转 ai.Response
}

再实现:

go 复制代码
func (c *Client) Stream(
	ctx context.Context,
	request *ai.Request,
) (<-chan ai.Chunk, error) {
	// Claude SSE
}

于是:

go 复制代码
var _ ai.Client = (*Client)(nil)

就可以保证:

go 复制代码
claude.Client

满足统一接口。


二十五、为什么 Claude 不应该直接复用 OpenAI 格式?

因为 Claude 原生 Messages API 和 OpenAI Chat Completions 并不是一个协议。

例如统一层:

go 复制代码
ai.Request

应该只是内部标准。

真正发送给 Claude 时,再转换:

text 复制代码
ai.Request
      ↓
Claude Wire Request
      ↓
Anthropic API
      ↓
Claude Wire Response
      ↓
ai.Response

OpenAI 同样:

text 复制代码
ai.Request
      ↓
OpenAI Wire Request
      ↓
OpenAI API
      ↓
OpenAI Wire Response
      ↓
ai.Response

现在 openai/client.go 本身已经采用了这个思路:

go 复制代码
type wireRequest struct {
	Model       string
	Messages    []ai.Message
	Temperature *float64
	MaxTokens   int
	Tools       []wireTool
	Stream      bool
}

再通过:

go 复制代码
toWireRequest()

将统一 Request 转成 OpenAI 请求。

这个设计是对的。

以后 Claude 也应该拥有自己的:

go 复制代码
wireRequest
wireResponse


二十六、建议下一步加入 Middleware

现在已经有:

text 复制代码
Client
Request
Response
Stream
Tool
Usage

下一步非常适合加入:

text 复制代码
Middleware

例如:

go 复制代码
type Middleware func(
	next Client,
) Client

以后可以实现:

text 复制代码
Logging
Retry
Metrics
Tracing
RateLimit
Fallback
Billing

例如:

go 复制代码
client :=
	ai.Use(
		openaiClient,
		ai.WithRetry(3),
		ai.WithLogging(),
		ai.WithMetrics(),
	)

最终:

text 复制代码
Business

   ↓

Logging

   ↓

Metrics

   ↓

Retry

   ↓

OpenAI

二十七、Retry 可以怎么写?

例如定义:

go 复制代码
type RetryClient struct {
	next       ai.Client
	maxRetries int
	delay      time.Duration
}

实现:

go 复制代码
func (r *RetryClient) Chat(
	ctx context.Context,
	req *ai.Request,
) (*ai.Response, error) {

	var lastErr error

	for i := 0; i <= r.maxRetries; i++ {

		resp, err :=
			r.next.Chat(
				ctx,
				req,
			)

		if err == nil {
			return resp, nil
		}

		lastErr = err

		if i == r.maxRetries {
			break
		}

		select {

		case <-ctx.Done():

			return nil,
				ctx.Err()

		case <-time.After(r.delay):

		}
	}

	return nil, lastErr
}

这样:

text 复制代码
429
502
503
网络抖动

都可以进一步统一处理。


二十八、以后甚至可以做 Provider Fallback

这是我觉得非常值得发展的地方。

例如:

text 复制代码
GPT
 |
失败
 |
 v
Claude
 |
失败
 |
 v
DeepSeek

定义:

go 复制代码
type FallbackClient struct {
	clients []ai.Client
}

代码:

go 复制代码
func (f *FallbackClient) Chat(
	ctx context.Context,
	req *ai.Request,
) (*ai.Response, error) {

	var lastErr error

	for _, client :=
		range f.clients {

		resp, err :=
			client.Chat(
				ctx,
				req,
			)

		if err == nil {
			return resp, nil
		}

		lastErr = err
	}

	return nil, lastErr
}

初始化:

go 复制代码
fallback := &FallbackClient{
	clients: []ai.Client{
		openaiClient,
		claudeClient,
		deepseekClient,
	},
}

业务:

go 复制代码
response, err :=
	fallback.Chat(
		ctx,
		request,
	)

这样一个小工具库就已经开始具备:

text 复制代码
AI Gateway SDK

的能力了。


二十九、甚至可以继续加入负载均衡

例如:

go 复制代码
type RoundRobinClient struct {
	clients []ai.Client
	index   atomic.Uint64
}

调用:

go 复制代码
func (r *RoundRobinClient) Chat(
	ctx context.Context,
	req *ai.Request,
) (*ai.Response, error) {

	index :=
		r.index.Add(1)

	client :=
		r.clients[
			int(index)%len(r.clients)
		]

	return client.Chat(
		ctx,
		req,
	)
}

于是:

text 复制代码
Client A
Client B
Client C

可以轮询。

进一步可以做:

text 复制代码
权重
优先级
健康检查
自动摘除
熔断
Fallback

这基本就是 AI Gateway 的核心底层逻辑。


三十、做一个统一 Provider Factory

以后 Provider 多了,可以写:

go 复制代码
package aiclient

import (
	"fmt"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

type Config struct {
	Provider string
	APIKey   string
	BaseURL  string
}

func New(
	config Config,
) (ai.Client, error) {

	switch config.Provider {

	case "openai":

		return openai.NewClient(
			openai.Config{
				APIKey: config.APIKey,
				BaseURL: config.BaseURL,
			},
		), nil

	default:

		return nil,
			fmt.Errorf(
				"unsupported provider: %s",
				config.Provider,
			)
	}
}

配置:

yaml 复制代码
ai:
  provider: openai
  base_url: https://api.example.com/v1
  model: gpt-5

业务层甚至不需要知道 Provider。


三十一、结合 configutils

这也是 go-commons 把 AI 放在同一个仓库里的一个优势。

你本来已经有:

text 复制代码
configutils

以后可以直接组合。

例如环境变量:

bash 复制代码
AI_PROVIDER=openai
AI_API_KEY=sk-xxxx
AI_BASE_URL=https://api.example.com/v1
AI_MODEL=gpt-5

代码:

go 复制代码
config :=
	configutils.LoadConfigFromEnv(
		"AI_",
	)

然后:

go 复制代码
client := openai.NewClient(
	openai.Config{

		APIKey:
			config.GetString(
				"api_key",
				"",
			),

		BaseURL:
			config.GetString(
				"base_url",
				"",
			),
	},
)

于是:

text 复制代码
configutils
+
ai
+
errorutils
+
concurrentutils

就能直接组合成 AI 应用底层框架。


三十二、完整 CLI AI Demo

再给一个更完整的例子:

做一个终端 AI。

go 复制代码
package main

import (
	"bufio"
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/Rodert/go-commons/ai"
	"github.com/Rodert/go-commons/ai/openai"
)

func main() {

	apiKey :=
		os.Getenv(
			"AI_API_KEY",
		)

	if apiKey == "" {
		log.Fatal(
			"AI_API_KEY 未设置",
		)
	}

	client :=
		openai.NewClient(
			openai.Config{

				APIKey:
					apiKey,

				BaseURL:
					"https://api.example.com/v1",
			},
		)

	model :=
		os.Getenv(
			"AI_MODEL",
		)

	if model == "" {
		model = "gpt-5"
	}

	reader :=
		bufio.NewReader(
			os.Stdin,
		)

	messages :=
		[]ai.Message{
			{
				Role:
					ai.RoleSystem,

				Content:
					"你是一名专业的 AI 编程助手。",
			},
		}

	fmt.Println(
		"AI CLI 已启动",
	)

	fmt.Println(
		"输入 exit 退出",
	)

	for {

		fmt.Print(
			"\nYou > ",
		)

		text, err :=
			reader.ReadString(
				'\n',
			)

		if err != nil {
			log.Fatal(err)
		}

		text =
			strings.TrimSpace(
				text,
			)

		if text == "" {
			continue
		}

		if text == "exit" {
			break
		}

		messages =
			append(
				messages,
				ai.Message{
					Role:
						ai.RoleUser,
					Content:
						text,
				},
			)

		stream, err :=
			client.Stream(
				context.Background(),
				&ai.Request{
					Model:
						model,
					Messages:
						messages,
				},
			)

		if err != nil {

			fmt.Println(
				"请求失败:",
				err,
			)

			continue
		}

		fmt.Print(
			"\nAI > ",
		)

		var answer strings.Builder

		for chunk :=
			range stream {

			if chunk.Err != nil {

				fmt.Println(
					"\nstream error:",
					chunk.Err,
				)

				break
			}

			fmt.Print(
				chunk.Content,
			)

			answer.WriteString(
				chunk.Content,
			)
		}

		fmt.Println()

		messages =
			append(
				messages,
				ai.Message{
					Role:
						ai.RoleAssistant,
					Content:
						answer.String(),
				},
			)
	}
}

这已经可以直接运行成一个最简单的:

text 复制代码
AI CLI


三十三、还能做一个 HTTP AI 服务

比如 Gin 项目:

go 复制代码
type ChatRequest struct {
	Message string `json:"message"`
}

type ChatResponse struct {
	Content string `json:"content"`
}

Handler:

go 复制代码
func ChatHandler(
	client ai.Client,
	model string,
) gin.HandlerFunc {

	return func(c *gin.Context) {

		var req ChatRequest

		if err :=
			c.ShouldBindJSON(
				&req,
			); err != nil {

			c.JSON(
				http.StatusBadRequest,
				gin.H{
					"error":
						err.Error(),
				},
			)

			return
		}

		response, err :=
			client.Chat(
				c.Request.Context(),
				&ai.Request{
					Model:
						model,
					Messages:
						[]ai.Message{
							{
								Role:
									ai.RoleUser,
								Content:
									req.Message,
							},
						},
				},
			)

		if err != nil {

			c.JSON(
				http.StatusInternalServerError,
				gin.H{
					"error":
						err.Error(),
				},
			)

			return
		}

		c.JSON(
			http.StatusOK,
			ChatResponse{
				Content:
					response.Content,
			},
		)
	}
}

main:

go 复制代码
package main

import (
	"os"

	"github.com/gin-gonic/gin"

	"github.com/Rodert/go-commons/ai/openai"
)

func main() {

	client :=
		openai.NewClient(
			openai.Config{

				APIKey:
					os.Getenv(
						"AI_API_KEY",
					),

				BaseURL:
					os.Getenv(
						"AI_BASE_URL",
					),
			},
		)

	router :=
		gin.Default()

	router.POST(
		"/api/chat",
		ChatHandler(
			client,
			"gpt-5",
		),
	)

	router.Run(
		":8080",
	)
}

这样你已经可以快速搭一个自己的:

text 复制代码
AI Backend


三十四、项目现在已经不是简单的 Utils 了

我觉得这次变化真正有意义的地方,并不是:

text 复制代码
多了一个 openai/client.go

而是项目定位开始变化。

以前:

text 复制代码
go-commons

String
Time
JSON
File
Config
Concurrent
System

现在:

text 复制代码
go-commons

Backend Commons
        +
AI Commons

README 里也已经加入:

text 复制代码
AI infrastructure
Provider-neutral messages
Token usage
Tool calls
Streaming chunks
OpenAI-compatible providers

这些能力。


三十五、我后面准备继续做什么?

我目前更倾向于继续沿着:

text 复制代码
基础设施

这个方向走。

而不是直接在 go-commons 里造一个特别重的 Agent Framework。

我认为后续比较合理的路线是:

text 复制代码
V1
OpenAI Compatible
Chat
Stream
Usage
Tool Calling

↓

V2
Claude
Gemini

↓

V3
Retry
Middleware
Logging
Metrics

↓

V4
Fallback
Load Balance
Provider Router

↓

V5
MCP Client
Tool Registry

↓

V6
Lightweight Agent Runtime

三十六、最终想做成什么样?

我希望以后写 Go AI 项目,可以直接:

go 复制代码
client := aiclient.New(...)

然后:

go 复制代码
resp, err :=
	client.Chat(
		ctx,
		request,
	)

今天后面是:

text 复制代码
OpenAI

明天可以换:

text 复制代码
Claude

后天换:

text 复制代码
Gemini

甚至:

text 复制代码
DeepSeek
Ollama
vLLM
Local Model

业务代码完全不用变化。

更进一步:

text 复制代码
Application

     ↓

go-commons/ai

     ↓

Provider Router

     ↓

---------------------------------

|        |        |        |

OpenAI Claude  Gemini DeepSeek

     ↓

Tool / MCP / Agent

如果最终能做到这里,那么 go-commons 就不再只是一个:

text 复制代码
Go 工具箱

而会成为:

面向现代 Go 后端和 AI 应用开发的一套轻量级基础设施库。

这也是我现在更想做的方向。

项目:

text 复制代码
https://github.com/Rodert/go-commons

如果你也在使用 Go 开发 AI 应用、Agent、API 网关或者模型中转服务,可以关注一下这个项目。

后面我也会继续把:

text 复制代码
Claude
Gemini
Tool Calling
MCP
Agent

这些方向逐步补进去。

一起成为勇猛精进的人类。

相关推荐
caimouse44 分钟前
ReactOS 图形系统分析(23):引擎内存管理 — mem.c
c语言·开发语言
caimouse44 分钟前
ReactOS 图形系统分析(15):窗口对象 — EWNDOBJ(engwindow.c)
c语言·开发语言
HZZD_HZZD1 小时前
非侵入式负荷监测选`Seq2Point`还是`LSTM`?合众致达实测:洗衣机分解F1达0.87、`NDE`误差降27%,附PyTorch完整实现
人工智能·pytorch·lstm
青 春 记 忆1 小时前
零基础入门python07:让程序记住数据——JSON文件和异常处理
开发语言·windows·python·json·python3.11
嘟哩DuliDuli1 小时前
AI 账单变高的技术原因:重复上下文和用量归属
android·人工智能·安全·ai·软件工程
Tom·Ge1 小时前
AI创业者通识日报 | 2026年8月13日
人工智能·大模型·ai创业·ai创业者
tech讯息1 小时前
企业 AI 办公平台如何标准化落地?哪些云方案适配企业统一部署?—— 优先评估统一工作台、权限管控与系统集成能力
人工智能
IT_陈寒1 小时前
搞不定JavaScript的数组去重?你可能漏了这两个坑
前端·人工智能·后端
豌豆学姐1 小时前
likeadmin-api 全驱动数字人参数避坑:file_url、ref_file_url 和 mode 怎么传
人工智能·aigc·api·数字人·全驱动数字人