应定义通用LLM接口并按厂商分包实现,用函数式选项配置模型特有参数,分层处理HTTP请求、适配与业务逻辑,为不同延迟模型单独配置http.Client连接池。

用接口抽象统一模型调用入口
直接为每个模型写一套 client 结构体,很快就会失控。真正可维护的做法是先定义一个通用 LLM 接口:
type LLM interface {
Generate(ctx context.Context, prompt string) (string, error)
GenerateWithMessages(ctx context.Context, messages []map[string]string) (string, error)
}这个接口只暴露业务真正需要的能力,不绑定具体实现。OpenAI、Gemini、Ollama、DeepSeek-R1 等各自实现该接口,业务层调用时完全不用关心背后是谁。
容易踩的坑:
- 不要把
temperature、max_tokens等参数塞进接口方法签名——它们因模型而异,应在初始化 client 时配置 - 别让接口返回
map[string]interface{}或原始bytes,否则上层还得做类型断言和错误解析 - 所有方法必须带
ctx context.Context参数,否则无法控制超时或取消
按模型厂商分包管理初始化逻辑
不同模型的初始化方式差异很大,硬塞在一个工厂函数里会导致耦合和难以测试。推荐按厂商建子包:llm/openai、llm/gemini、llm/ollama、llm/deepseek。
立即学习“go语言免费学习笔记(深入)”;
每个子包暴露一个标准构造函数,例如:
func New(opts ...Option) (LLM, error)
其中 Option 是函数式选项模式,比如 openai.WithBaseURL("https://api.deepseek.com/v2")、ollama.WithEndpoint("http://192.168.1.100:11434")。
关键点:
-
openai.New()默认连官方 API,但 DeepSeek-R1 必须显式传WithBaseURL,且路径含/v2(不是/v1) -
gemini.New()当前版本不支持流式响应,Call()返回完整字符串,别误以为能边收边吐 -
ollama.New()若启用了 Basic Auth,得手动构造http.Client并传入ollama.WithHTTPClient
避免在 HTTP client 层混入业务逻辑
很多封装把重试、日志、指标埋点全堆在 http.Client 上,结果一换模型就得重写一堆胶水代码。正确做法是分层:
- 底层:纯 HTTP 请求,只负责发包收包,不做任何业务解释
- 中间层:适配器(Adapter),把各模型的 JSON 响应结构统一转成你定义的
Response结构体 - 顶层:业务 client,组合 adapter + 重试策略 + context 超时 + 错误分类(如
ErrRateLimited、ErrAuthFailed)
示例中常见错误:
- 在
http.Transport里加日志——这会污染所有请求,包括健康检查、metrics 接口 - 把
max_retries写死在 client 初始化里——不同模型限流策略不同,Ollama 本地服务通常不需要重试,而公网 API 可能需 2~3 次 - 用
strings.Contains(err.Error(), "timeout")判断超时——应该用errors.Is(err, context.DeadlineExceeded)或检查net.OpError
并发安全与连接池配置必须显式控制
Go 的 http.Client 本身是并发安全的,但默认配置对高并发大模型调用很危险:
- 默认
MaxIdleConnsPerHost = 2,意味着每 host 最多 2 个空闲连接——压测时立刻卡住 - 不设
Timeout的 client 在 DNS 解析失败或后端无响应时会永久阻塞 - 未设置
IdleConnTimeout和KeepAlive,连接复用率低,TCP 握手开销陡增
Langchaingo 或自研 client 都应强制要求传入定制 *http.Client,例如:
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
},
}真正难处理的是:不同模型服务的响应延迟差异极大(Ollama 本地可能 200ms,Gemini 可能 5s),一套连接池参数很难兼顾。最稳妥的方式是为每类模型单独配 client 实例,而不是全局复用一个。


















