最近我给自己的 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
这些方向逐步补进去。
一起成为勇猛精进的人类。