go调用openai兼容api必须显式设置stream=true并声明accept: text/event-stream,否则退化为同步响应;需用bufio.scanner解析sse流,避免io.readall或json.decode整响应体,注意超时控制与假活连接处理。

Go 调用 OpenAI 兼容 API 时必须设 stream=true
不加这个参数,哪怕后端支持流式,API 也会退化为同步响应——你收不到任何 data: 块,只等到最后一次性返回完整 JSON。OpenAI、Azure OpenAI、Ollama、AnythingLLM 等主流兼容服务都遵循此规则。
常见错误现象:{"error":{"message":"Invalid request: stream must be a boolean","type":"invalid_request_error"}} 或更隐蔽的:HTTP 200 成功,但响应体是普通 {"choices":[{"message":{"content":"..."}}]},完全不是 SSE 格式。
实操建议:
- 请求体中显式传
stream: true(JSON bool 类型,不是字符串) - 用
http.Header.Set("Accept", "text/event-stream")显式声明接受 SSE - 禁用 HTTP 重定向(
Client.CheckRedirect = func(...){ return http.ErrUseLastResponse }),避免重定向后丢失流式头 - 不要用
http.Post简写,它默认不带Content-Type,某些代理会拒绝无类型的流请求
用 net/http + bufio.Scanner 解析 SSE 数据块
OpenAI 流式响应是标准 Server-Sent Events(SSE),每块以 data: {...} 开头,可能夹杂 event:、id:、retry: 行。Go 标准库没内置 SSE 解析器,但 bufio.Scanner 配合自定义 SplitFunc 足够可靠。
容易踩的坑:
- 直接
io.ReadAll(resp.Body)—— 会阻塞到连接关闭,失去“逐块处理”意义 - 用
json.NewDecoder直接解码整个响应体 —— SSE 不是合法 JSON 数组,会报invalid character 'd' looking for beginning of value - 忽略空行或注释行(
: ping),导致 scanner 错位
关键代码片段:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
scanner := bufio.NewScanner(resp.Body)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
return i + 1, data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
})
<p>for scanner.Scan() {
line := bytes.TrimSpace(scanner.Bytes())
if len(line) == 0 || bytes.HasPrefix(line, []byte(":")) {
continue // skip empty lines and comments
}
if bytes.HasPrefix(line, []byte("data: ")) {
payload := bytes.TrimPrefix(line, []byte("data: "))
if string(payload) == "[DONE]" {
break
}
var chunk map[string]interface{}
if err := json.Unmarshal(payload, &chunk); err != nil {
continue // ignore malformed chunk
}
if delta, ok := chunk["choices"].([]interface{})[0].(map[string]interface{})["delta"].(map[string]interface{}); ok {
if content, ok := delta["content"].(string); ok && content != "" {
fmt.Print(content) // or send to channel
}
}
}
}</p>
别让 bytes.Buffer 在流式场景里自动扩容
如果你把所有流式数据先攒进 bytes.Buffer 再统一处理(比如想做完整 JSON 校验),在长响应下极易触发频繁 grow——实测 5MB 响应可导致数百次内存分配,CPU 时间 70% 耗在 bytes.(*Buffer).grow 上。
正确做法是边读边解析,不落地缓存整块响应。若真需缓冲(如做反向代理),预估最大单块长度(通常 content 字段很少超 4KB)并初始化:
-
buf := bytes.NewBuffer(make([]byte, 0, 4096))—— 预分配 4KB 底层数组 - 对每个
data:行,先buf.Reset()再buf.Write(),避免累积 - 绝对不要在循环里反复
new(bytes.Buffer),对象创建本身也有开销
超时与连接中断必须手动控制
流式连接是长连接,http.Client.Timeout 对整个请求生效,但首字节延迟(TTFB)和块间间隔(inter-chunk delay)需分开管理。OpenAI 默认块间隔约 100–500ms,但模型卡住或网络抖动时可能长达数秒。
建议设置:
-
Client.Timeout = 60 * time.Second(防整体挂死) - 用
time.AfterFunc启动独立计时器,在连续 5 秒没收到新块时主动resp.Body.Close() - 检查
scanner.Err()是否为io.EOF或net.ErrClosed,而非静默退出 - 重试逻辑要区分:连接失败可重试;已收到部分
data:但中途断开,需带cursor或last_id续传(部分服务支持)
真正难处理的是“假活连接”:TCP 连接未关闭,但服务端停止发包。这种状态只能靠心跳或块间隔超时探测,没有银弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










