用 Go 打开大模型的正确姿势:OpenAI、字节、百度、阿里、MiniMax 全系 HTTPS 调用与流式实践

“模型越来越多,接口五花八门,工程上到底该怎么统一调用、稳定落地?”
这是一篇从工程实战出发的长文:用一套 Go 与 HTTPS 的通用骨架,串起纯文本大模型、文生图、图生问(图像理解问答)、TTS 语音合成与多模态输入的主流调用姿势,覆盖 OpenAI、字节(火山方舟/豆包)、百度(文心千帆/ERNIE)、阿里(通义千问/DashScope)、MiniMax 等服务商。文末还有常见坑与优化建议,欢迎留言交流经验。


目录

  • 为什么用 Go + HTTPS 打通全家桶
  • 核心通用件(SSE 流式读取、非流式 POST、Base64 工具)
  • OpenAI:文本、流式、文生图、图生问、TTS 全示例
  • 字节(火山方舟/豆包):OpenAI 风格的 Chat 与图片理解
  • 阿里(通义千问/DashScope):兼容模式快速接入与流式
  • 百度(文心千帆/ERNIE):获取 Access Token、非流式与流式
  • MiniMax:Chat 基本调用与流式
  • 多模态输入模式要点(图生问的“data URL”安全做法)
  • 稳定性与工程化:超时、重试、限流、灰度与观测
  • 常见坑与排错清单
  • 结语与互动

为什么用 Go + HTTPS 打通全家桶

  • Go 自带高性能网络库,SSE/长连接实现轻量、稳定、便携。
  • 各家平台虽参数命名不同,但基础都是 HTTPS + JSON(流式多为 SSE 或行分隔 JSON),完全可以抽象出一套“通用骨架 + 少量适配”的模式。
  • 使用 OpenAI 兼容模式的服务(如部分厂商提供的 compatible-mode/OpenAI-style 接口),可直接复用一套调用逻辑,显著降低接入成本。

实战经验表明:

  • 非流式(一次性返回):最稳且易于重试,但对长回答/进度反馈不友好。
  • 流式(SSE):用户体验最好,可边收边显,但需要注意超时、断线重试与增量拼接。

核心通用件(SSE、HTTP、Base64)

以下是后文所有示例都会用到的 Go 辅助。默认依赖标准库,便于理解与迁移。

package llmcommon

import (
	"bufio"
	"context"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// 创建一个适合流式的 HTTP Client:连接复用、TLS、合理超时
func NewHTTPClient() *http.Client {
	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   10 * time.Second,
			KeepAlive: 60 * time.Second,
		}).DialContext,
		ForceAttemptHTTP2:     true,
		MaxIdleConns:          200,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		TLSClientConfig:       &tls.Config{MinVersion: tls.VersionTLS12},
	}
	return &http.Client{
		Transport: transport,
		// 流式不要设置固定 Timeout,交给 context 控制
		Timeout: 0,
	}
}

// 通用:非流式 POST JSON,返回原始响应体字节
func PostJSON(ctx context.Context, client *http.Client, url string, headers map[string]string, payload any) ([]byte, *http.Response, error) {
	b, err := json.Marshal(payload)
	if err != nil {
		return nil, nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(b)))
	if err != nil {
		return nil, nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, resp, err
	}
	defer resp.Body.Close()
	if resp.StatusCode/100 != 2 {
		body, _ := io.ReadAll(resp.Body)
		return nil, resp, errors.New(resp.Status + ": " + string(body))
	}
	body, err := io.ReadAll(resp.Body)
	return body, resp, err
}

// 通用:SSE/按行流式读取,兼容 "data: xxx" 与纯 JSON 行
func ReadSSE(ctx context.Context, resp *http.Response, onEvent func(jsonLine []byte) error) error {
	defer resp.Body.Close()
	reader := bufio.NewReader(resp.Body)
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		line, err := reader.ReadBytes('\n')
		if err != nil {
			if err == io.EOF {
				return nil
			}
			return err
		}
		s := strings.TrimSpace(string(line))
		if s == "" {
			continue
		}
		// 兼容标准 SSE 与纯 JSON 行
		if strings.HasPrefix(s, "data:") {
			s = strings.TrimSpace(strings.TrimPrefix(s, "data:"))
		}
		if s == "[DONE]" {
			return nil
		}
		if err := onEvent([]byte(s)); err != nil {
			return err
		}
	}
}

// 工具:读取本地文件并转成 data URL(用于图生问、避免外链)
func FileToDataURL(path string, mime string) (string, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	ext := strings.ToLower(filepath.Ext(path))
	if mime == "" {
		// 简单猜测 MIME(必要时手动传入更稳妥)
		switch ext {
		case ".png":
			mime = "image/png"
		case ".jpg", ".jpeg":
			mime = "image/jpeg"
		case ".webp":
			mime = "image/webp"
		default:
			mime = "application/octet-stream"
		}
	}
	return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(b), nil
}

OpenAI:文本、流式、文生图、图生问、TTS

OpenAI 是事实标准,很多厂商提供兼容风格接口。以下用 Chat Completions 与经典 Images/TTS 端点,示例以环境变量 OPENAI_API_KEY 取密钥。

  • 基础 URL:https://api.openai.com/v1
  • 认证:Authorization: Bearer ${OPENAI_API_KEY}

1) 纯文本大模型:非流式与流式

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"

	"your/module/llmcommon"
)

type OAChatMessageText struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}
type OAChatRequest struct {
	Model       string              `json:"model"`
	Messages    []OAChatMessageText `json:"messages"`
	Temperature float32             `json:"temperature,omitempty"`
	Stream      bool                `json:"stream,omitempty"`
}

type OAChatResponse struct {
	ID      string `json:"id"`
	Object  string `json:"object"`
	Choices []struct {
		Index        int `json:"index"`
		FinishReason string `json:"finish_reason"`
		Message      struct {
			Role    string `json:"role"`
			Content string `json:"content"`
		} `json:"message,omitempty"`
		// 流式增量
		Delta struct {
			Role    string `json:"role,omitempty"`
			Content string `json:"content,omitempty"`
		} `json:"delta,omitempty"`
	} `json:"choices"`
}

func openaiNonStream() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	url := "https://api.openai.com/v1/chat/completions"
	headers := map[string]string{
		"Authorization": "Bearer " + os.Getenv("OPENAI_API_KEY"),
	}
	req := OAChatRequest{
		Model: "gpt-4o-mini", // 文本与多模态通吃;仅文本问题也可用
		Messages: []OAChatMessageText{
			{Role: "system", Content: "You are a concise assistant."},
			{Role: "user", Content: "用三句话解释什么是幂等性。"},
		},
		Temperature: 0.7,
	}
	body, _, err := llmcommon.PostJSON(ctx, client, url, headers, req)
	if err != nil {
		return err
	}
	var resp OAChatResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return err
	}
	if len(resp.Choices) > 0 {
		fmt.Println("非流式回答:", resp.Choices[0].Message.Content)
	}
	return nil
}

func openaiStream() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

	url := "https://api.openai.com/v1/chat/completions"
	headers := map[string]string{
		"Authorization": "Bearer " + os.Getenv("OPENAI_API_KEY"),
		"Content-Type":  "application/json",
	}

	req := OAChatRequest{
		Model: "gpt-4o-mini",
		Messages: []OAChatMessageText{
			{Role: "user", Content: "写一首四行现代诗,每行不超过8个字。"},
		},
		Stream: true,
	}
	// 手工发起请求(为了拿到流式响应体)
	b, _ := json.Marshal(req)
	httpReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(b)))
	for k, v := range headers {
		httpReq.Header.Set(k, v)
	}
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	if resp.StatusCode/100 != 2 {
		defer resp.Body.Close()
		data, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("status=%s, body=%s", resp.Status, string(data))
	}
	fmt.Print("流式回答:")
	return llmcommon.ReadSSE(ctx, resp, func(line []byte) error {
		var chunk OAChatResponse
		if err := json.Unmarshal(line, &chunk); err != nil {
			return nil // 有些实现会混入心跳或非 JSON 行,忽略
		}
		if len(chunk.Choices) > 0 {
			fmt.Print(chunk.Choices[0].Delta.Content)
		}
		return nil
	})
}

func main() {
	_ = openaiNonStream()
	_ = openaiStream()
}

2) 图生问(图像理解问答)

安全起见,避免外网图片 URL,建议使用 data URL。OpenAI 的 chat/completions 支持 content 为数组,混合文本与图像。

// 省略 import 与客户端创建
type ContentText struct {
	Type string `json:"type"`
	Text string `json:"text"`
}
type ContentImageURL struct {
	Type     string `json:"type"`
	ImageURL struct {
		URL string `json:"url"`
	} `json:"image_url"`
}
type OAMessageVision struct {
	Role    string        `json:"role"`
	Content []interface{} `json:"content"`
}
type OAChatVisionReq struct {
	Model    string            `json:"model"`
	Messages []OAMessageVision `json:"messages"`
	Stream   bool              `json:"stream,omitempty"`
}

func openaiVisionFromFile() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	dataURL, err := llmcommon.FileToDataURL("local-image.png", "image/png")
	if err != nil {
		return err
	}
	msg := OAMessageVision{
		Role: "user",
		Content: []interface{}{
			ContentText{Type: "text", Text: "这张图主要包含什么元素?简要回答。"},
			ContentImageURL{Type: "image_url", ImageURL: struct{ URL string `json:"url"` }{URL: dataURL}},
		},
	}
	req := OAChatVisionReq{
		Model:    "gpt-4o-mini",
		Messages: []OAMessageVision{msg},
	}
	url := "https://api.openai.com/v1/chat/completions"
	headers := map[string]string{"Authorization": "Bearer " + os.Getenv("OPENAI_API_KEY")}
	body, _, err := llmcommon.PostJSON(ctx, client, url, headers, req)
	if err != nil {
		return err
	}
	var resp OAChatResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return err
	}
	fmt.Println("图生问回答:", resp.Choices[0].Message.Content)
	return nil
}

3) 文生图(图像生成)

type OAImageGenReq struct {
	Model           string `json:"model"` // 建议 gpt-image-1
	Prompt          string `json:"prompt"`
	Size            string `json:"size,omitempty"`            // 例如 "1024x1024"
	ResponseFormat  string `json:"response_format,omitempty"` // "b64_json"
}
type OAImageGenResp struct {
	Created int64 `json:"created"`
	Data    []struct {
		B64JSON string `json:"b64_json"`
	} `json:"data"`
}

func openaiImageGen() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer cancel()
	url := "https://api.openai.com/v1/images/generations"
	headers := map[string]string{
		"Authorization": "Bearer " + os.Getenv("OPENAI_API_KEY"),
	}
	req := OAImageGenReq{
		Model:          "gpt-image-1",
		Prompt:         "一只在月球上喝茶的猫,赛博朋克,霓虹色调",
		Size:           "1024x1024",
		ResponseFormat: "b64_json",
	}
	body, _, err := llmcommon.PostJSON(ctx, client, url, headers, req)
	if err != nil {
		return err
	}
	var resp OAImageGenResp
	if err := json.Unmarshal(body, &resp); err != nil {
		return err
	}
	if len(resp.Data) > 0 {
		imgBytes, _ := base64.StdEncoding.DecodeString(resp.Data[0].B64JSON)
		return os.WriteFile("gen.png", imgBytes, 0644)
	}
	return nil
}

4) TTS(文本转语音)

type OATTSReq struct {
	Model  string `json:"model"`  // 如 gpt-4o-mini-tts
	Input  string `json:"input"`
	Voice  string `json:"voice"`  // 如 "alloy"
	Format string `json:"format"` // "mp3" | "wav"
}

func openaiTTS() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()
	url := "https://api.openai.com/v1/audio/speech"
	headers := map[string]string{
		"Authorization": "Bearer " + os.Getenv("OPENAI_API_KEY"),
		"Content-Type":  "application/json",
	}
	req := OATTSReq{
		Model:  "gpt-4o-mini-tts",
		Input:  "欢迎来到 Go 与大模型的世界。",
		Voice:  "alloy",
		Format: "mp3",
	}
	b, _ := json.Marshal(req)
	httpReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(b)))
	for k, v := range headers {
		httpReq.Header.Set(k, v)
	}
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode/100 != 2 {
		data, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("status=%s, body=%s", resp.Status, string(data))
	}
	audio, _ := io.ReadAll(resp.Body)
	return os.WriteFile("speech.mp3", audio, 0644)
}

字节(火山方舟/豆包):OpenAI 风格 Chat 与图片理解

火山方舟(Ark)的大模型服务接口在风格上与 OpenAI 高度一致(路径与字段名基本相同),上手门槛较低。以下示例以 ARK_API_KEY 为密钥环境变量。

  • 基础 URL(Chat):https://ark.cn-beijing.volces.com/api/v3/chat/completions
  • 认证:Authorization: Bearer ${ARK_API_KEY}
  • 模型名:控制台里可见(如开通的 Doubao Chat 模型或自定义 Endpoint ID)

1) 纯文本 Chat:非流式与流式

// 复用上文 OAChatRequest/OAChatResponse 结构
func arkChatNonStream() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	url := "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
	headers := map[string]string{
		"Authorization": "Bearer " + os.Getenv("ARK_API_KEY"),
	}
	req := OAChatRequest{
		Model: "ep-xxxxxxxxxxxxxxxx", // 方舟“Endpoint ID”或官方模型名
		Messages: []OAChatMessageText{
			{Role: "user", Content: "豆包你好,用一句话介绍你自己。"},
		},
	}
	body, _, err := llmcommon.PostJSON(ctx, client, url, headers, req)
	if err != nil {
		return err
	}
	var resp OAChatResponse
	_ = json.Unmarshal(body, &resp)
	fmt.Println("Ark 非流式:", resp.Choices[0].Message.Content)
	return nil
}

func arkChatStream() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	url := "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
	req := OAChatRequest{
		Model: "ep-xxxxxxxxxxxxxxxx",
		Messages: []OAChatMessageText{
			{Role: "user", Content: "流式输出 3 条旅行建议。"},
		},
		Stream: true,
	}
	b, _ := json.Marshal(req)
	httpReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(b)))
	httpReq.Header.Set("Authorization", "Bearer "+os.Getenv("ARK_API_KEY"))
	httpReq.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	return llmcommon.ReadSSE(ctx, resp, func(line []byte) error {
		var chunk OAChatResponse
		if json.Unmarshal(line, &chunk) == nil && len(chunk.Choices) > 0 {
			fmt.Print(chunk.Choices[0].Delta.Content)
		}
		return nil
	})
}

2) 图生问(图片理解)

Ark 的多模态 Chat 支持 OpenAI 风格的 content 数组,直接复用上文的 vision 模式。把 model 换成 Ark 的模型或 Endpoint 即可:

func arkVisionFromFile() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()
	url := "https://ark.cn-beijing.volces.com/api/v3/chat/completions"

	dataURL, _ := llmcommon.FileToDataURL("image.jpg", "image/jpeg")
	msg := OAMessageVision{
		Role: "user",
		Content: []interface{}{
			ContentText{Type: "text", Text: "找出图中品牌 logo,并简述场景。"},
			ContentImageURL{Type: "image_url", ImageURL: struct{ URL string `json:"url"` }{URL: dataURL}},
		},
	}
	req := OAChatVisionReq{
		Model:    "ep-xxxxxxxxxxxxxxxx",
		Messages: []OAMessageVision{msg},
	}
	headers := map[string]string{"Authorization": "Bearer " + os.Getenv("ARK_API_KEY")}
	body, _, err := llmcommon.PostJSON(ctx, client, url, headers, req)
	if err != nil {
		return err
	}
	var resp OAChatResponse
	_ = json.Unmarshal(body, &resp)
	fmt.Println(resp.Choices[0].Message.Content)
	return nil
}

阿里(通义千问/DashScope):兼容模式快速接入与流式

DashScope 提供“OpenAI 兼容模式”入口,能直接用 chat/completions 语义;这对统一接入非常友好。以下以 DASHSCOPE_API_KEY 为密钥。

  • 基础 URL(兼容):https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
  • 认证:Authorization: Bearer ${DASHSCOPE_API_KEY}
  • 模型名:如 qwen-plusqwen2.5-72b-instruct 等,以控制台为准
func dashscopeCompatChatStream() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	url := "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
	req := OAChatRequest{
		Model: "qwen-plus",
		Messages: []OAChatMessageText{
			{Role: "system", Content: "回答请简洁。"},
			{Role: "user", Content: "用要点列出 Rust 的三大优势。"},
		},
		Stream: true,
	}
	b, _ := json.Marshal(req)
	httpReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(b)))
	httpReq.Header.Set("Authorization", "Bearer "+os.Getenv("DASHSCOPE_API_KEY"))
	httpReq.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	return llmcommon.ReadSSE(ctx, resp, func(line []byte) error {
		var chunk OAChatResponse
		if json.Unmarshal(line, &chunk) == nil && len(chunk.Choices) > 0 {
			fmt.Print(chunk.Choices[0].Delta.Content)
		}
		return nil
	})
}

说明:

  • 文生图与 TTS 在 DashScope 有原生接口(如“通义万相”等),路径与参数不同;接入时按官方文档确定 endpoint 与字段。工程上可沿用本文的“非流式/流式骨架”,只需替换 URL 与请求结构。

百度(文心千帆/ERNIE):获取 Access Token、非流式与流式

与其他家不同,百度多数接口采用先获取 access_token 再调用 API 的模式。

  • 获取 Token:https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${API_KEY}&client_secret=${SECRET_KEY}
  • Chat(非流式/流式参数):https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=...
  • 认证:在 URL 上携带 access_token
type BaiduTokenResp struct {
	AccessToken string `json:"access_token"`
	ExpiresIn   int64  `json:"expires_in"`
}

func baiduGetToken(ctx context.Context, client *http.Client, ak, sk string) (string, error) {
	url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s", ak, sk)
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	var tr BaiduTokenResp
	if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
		return "", err
	}
	return tr.AccessToken, nil
}

type BaiduMsg struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}
type BaiduChatReq struct {
	Messages []BaiduMsg `json:"messages"`
	Stream   bool       `json:"stream,omitempty"`
}

type BaiduChatResp struct {
	Result string `json:"result"`
	// 流式时,多次返回,含 is_end 等字段
	ID     string `json:"id,omitempty"`
	IsEnd  bool   `json:"is_end,omitempty"`
}

func baiduChatNonStream() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	token, err := baiduGetToken(ctx, client, os.Getenv("BAIDU_AK"), os.Getenv("BAIDU_SK"))
	if err != nil {
		return err
	}
	url := "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=" + token
	req := BaiduChatReq{
		Messages: []BaiduMsg{{Role: "user", Content: "请用 3 条要点介绍边缘计算。"}},
		Stream:   false,
	}
	body, _, err := llmcommon.PostJSON(ctx, client, url, nil, req)
	if err != nil {
		return err
	}
	var resp BaiduChatResp
	_ = json.Unmarshal(body, &resp)
	fmt.Println("百度非流式:", resp.Result)
	return nil
}

func baiduChatStream() error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	token, _ := baiduGetToken(ctx, client, os.Getenv("BAIDU_AK"), os.Getenv("BAIDU_SK"))
	url := "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=" + token

	req := BaiduChatReq{
		Messages: []BaiduMsg{{Role: "user", Content: "分步骤讲讲如何做需求澄清。"}},
		Stream:   true,
	}
	b, _ := json.Marshal(req)
	httpReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(b)))
	httpReq.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	// 百度流式常见为按行 JSON(不一定带 data: 前缀),用兼容读取
	return llmcommon.ReadSSE(ctx, resp, func(line []byte) error {
		var chunk BaiduChatResp
		if json.Unmarshal(line, &chunk) == nil {
			fmt.Print(chunk.Result)
		}
		return nil
	})
}

说明:

  • 百度的多模态与图像生成有单独产品与字段(常见为传 image 的 base64 或 URL)。工程上仍沿用“非流式/流式骨架”,按文档调整字段名即可。
  • 在“千帆”控制台也可找到 OpenAI 兼容入口(如果已开通),那就可以用统一的 OpenAI 风格调用。

MiniMax:Chat 基本调用与流式

MiniMax 的 Chat 接口与 OpenAI 风格接近,另有 GroupId 或组织信息要求(按控制台为准)。以下示例展示典型用法。密钥环境变量 MINIMAX_API_KEY,假设使用 v1/v2 Chat 端点之一。

  • 典型基础 URL:https://api.minimax.chat/v1/text/chatcompletion_v2(或以官方文档为准)
  • 认证:Authorization: Bearer ${MINIMAX_API_KEY}
  • 组织:可能需要在查询参数或头部携带 GroupId(务必以控制台信息为准)
  • 模型:如 abab6.5-chat
type MiniMaxChatReq struct {
	Model     string              `json:"model"`
	Messages  []OAChatMessageText `json:"messages"` // 多数场景兼容 role/content
	Stream    bool                `json:"stream,omitempty"`
	// 可能还需额外字段,按官方文档补充
}
type MiniMaxChatResp struct {
	Choices []struct {
		Message struct {
			Role    string `json:"role"`
			Content string `json:"content"`
		} `json:"message,omitempty"`
		Delta struct {
			Content string `json:"content,omitempty"`
		} `json:"delta,omitempty"`
	} `json:"choices"`
}

func minimaxChat(groupID string) error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	base := "https://api.minimax.chat/v1/text/chatcompletion_v2"
	url := base + "?GroupId=" + groupID

	req := MiniMaxChatReq{
		Model: "abab6.5-chat",
		Messages: []OAChatMessageText{
			{Role: "user", Content: "给我三条效率提升建议。"},
		},
		Stream: false,
	}
	headers := map[string]string{
		"Authorization": "Bearer " + os.Getenv("MINIMAX_API_KEY"),
	}
	body, _, err := llmcommon.PostJSON(ctx, client, url, headers, req)
	if err != nil {
		return err
	}
	var resp MiniMaxChatResp
	_ = json.Unmarshal(body, &resp)
	fmt.Println("MiniMax 非流式:", resp.Choices[0].Message.Content)
	return nil
}

func minimaxChatStream(groupID string) error {
	client := llmcommon.NewHTTPClient()
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	url := "https://api.minimax.chat/v1/text/chatcompletion_v2?GroupId=" + groupID
	req := MiniMaxChatReq{
		Model: "abab6.5-chat",
		Messages: []OAChatMessageText{
			{Role: "user", Content: "流式输出 5 个面试小技巧。"},
		},
		Stream: true,
	}
	b, _ := json.Marshal(req)
	httpReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(b)))
	httpReq.Header.Set("Authorization", "Bearer "+os.Getenv("MINIMAX_API_KEY"))
	httpReq.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	return llmcommon.ReadSSE(ctx, resp, func(line []byte) error {
		var chunk MiniMaxChatResp
		if json.Unmarshal(line, &chunk) == nil && len(chunk.Choices) > 0 {
			fmt.Print(chunk.Choices[0].Delta.Content)
		}
		return nil
	})
}

说明:

  • MiniMax 的字段细节、是否需要 use_standard_sse 等参数,以最新文档为准;但工程上 SSE/非流式骨架完全相同。
  • TTS、图像生成等也有服务端点,调用方式同理(替换 URL/请求结构、二进制/JSON 返回处理)。

多模态输入文本模型要点(图生问)

  • 内容数组:大多采用 OpenAI 风格的 messages[].content = [ text_part, image_part, ... ]
  • 图片推荐用 data URL:避免公网可访问 URL 的权限与可用性问题。用上文 FileToDataURL 转换本地文件最稳妥。
  • 尺寸/页数控制:有的服务支持传 image 额外参数(如缩放、采样策略)。图片过大可先本地缩放再发送,降低时延与成本。
  • 注意计费维度:多模态通常按 tokens + 图片分辨率/面积计费。

稳定性与工程化:超时、重试、限流、观测

  • 超时策略:流式由 context 控制总体超时;非流式可在 http.Client 上设定请求超时。
  • 幂等与重试:429/5xx 时指数退避,流式中断可在上层做“上下文回放 + 断点续推”(视模型与业务而定)。
  • 速率限制:读取 Retry-After 或平台自定义限流头,按租户/模型维度做滑动窗口限速。
  • 观测:打点记录“请求耗时、tokens 消耗、错误率、断线率、SSE 事件数”,便于容量规划与回归定位。
  • 安全:密钥走 KMS/密管,运行时注入到环境变量或凭据服务,严禁入库或写死在代码里。

一个简单的 429/5xx 重试示例(可封装在 PostJSON 外层):

func WithRetry(do func() error) error {
	backoff := []time.Duration{300 * time.Millisecond, 800 * time.Millisecond, 1500 * time.Millisecond}
	var last error
	for i := 0; i < len(backoff); i++ {
		if err := do(); err != nil {
			last = err
			time.Sleep(backoff[i])
			continue
		}
		return nil
	}
	return last
}

常见坑与排错清单

  • 流式读不到数据:检查是否真的返回 text/event-stream;有些平台需要专用头(如启用 SSE)或 stream=true
  • 非标准行:SSE 中可能混入心跳或注释行,解析前先 TrimSpace 并过滤非 JSON。
  • 超时过短:图片/语音处理较慢,务必放宽服务器端等待与客户端超时。
  • 证书/代理:公司网络下的 TLS 代理或自签证书需显式信任或跳过(不建议跳过,建议导入根证书)。
  • Base64 太大:多图或大图时,建议压缩/裁剪;或传文件地址给同云侧对象存储(按官方支持)。
  • 模型名不匹配/无权限:不同租户可见模型不同,先在控制台确认配额、模型名、地域。
  • Content-Type 错:几乎都是 application/json;上传文件的接口再改为 multipart/form-data。

结语与互动

通过一套“通用 HTTPS + Go 骨架”,可以较为平滑地接入 OpenAI、字节(火山方舟/豆包)、百度(文心千帆/ERNIE)、阿里(通义千问/DashScope)、MiniMax 等多家大模型服务,覆盖纯文本、流式、文生图、图生问、TTS 与多模态输入的主流场景。工程上关键在于抽象:把“非流式 POST JSON”“SSE 流式读取”“data URL 安全传图”“Access Token 管理”做成可复用组件,其余只是“换 URL 与字段”。

你已经在生产用哪几家?流式体验、稳定性与性价比如何?欢迎在评论区分享踩坑与最佳实践,也可以补充更多厂商的兼容/差异点,一起完善这份调用指南。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐