go语言实现调用大模型api的流式和非流式输出
Go 手写 HTTP 调用 DeepSeek API:从非流式到流式输出
无需 SDK,只用 Go 标准库,搞清楚 LLM API 通信的全部细节。
为什么要手写 HTTP?
现在 Go 生态里调用 LLM API 有很多现成的 SDK,比如 go-openai、字节的 Eino,一个比一个方便。
但 SDK 把底层细节全藏起来了——请求头怎么拼?流式数据 SSE 怎么解析?stream: true 和 stream: false 返回的 JSON 结构到底差在哪?
这篇文章就是要把这些都拆开看,用 Go 标准库从零写一个 DeepSeek API 的流式聊天客户端。
项目结构
1 | api_study/ |
三个文件,不到 120 行代码,拒绝过度设计。
第一步:定义数据结构(body.go)
DeepSeek API 是 OpenAI 兼容的,请求体格式长这样:
1 | { |
Go 结构体就是 JSON 的镜像:
1 | type Message struct { |
知识点:json:"stream" 这个字段名是 API 协议约定的,DeepSeek 服务端读到 stream: true 就会切到流式模式返回 SSE。你写的结构体字段名可以随便起,但 json tag 必须和 API 文档一致。
第二步:认证 & 端点(bearer.go)
1 | const apiEndpoint = "https://api.deepseek.com/v1/chat/completions" |
血的教训:API Key 永远用环境变量,绝对不要硬编码。我一开始图省事写在 const 里,结果代码截图里直接暴露了。发现后马上去 DeepSeek 后台吊销,重新生成了一个。
第三步:非流式调用(回顾)
先看非流式。请求发出去后,DeepSeek 返回一个完整的大 JSON:
1 | { |
Go 处理起来很简单——一次性解码:
1 | type ChatResponse struct { |
问题很明显:用户要等模型全部生成完才能看到结果。聊天场景里,体验很差——你以为程序卡死了,实际上只是模型还在生成。
第四步:改为流式
请求里加上 "stream": true,DeepSeek 返回的就完全不一样了,是一行一行的 SSE(Server-Sent Events):
1 | data: {"choices":[{"delta":{"content":"你"}}]} |
注意两个细节:
- 字段名变了:非流式是
message.content,流式是delta.content - 结束标志:
data: [DONE]表示流结束了
Go 用 bufio.Scanner 逐行读取,边读边打印:
1 | scanner := bufio.NewScanner(resp.Body) |
这样用户在终端里看到的就是一个字一个字往外蹦,跟网页版聊天一模一样。
第五步:主函数串联
1 | func main() { |
跑起来:
1 | $ DEEPSEEK_API_KEY="sk-xxx" go run . |
完整代码
完整代码见 GitHub Gist(链接),三个核心文件的完整版上面都贴了。
流式 vs 非流式:什么时候用哪个?
| 非流式 | 流式 | |
|---|---|---|
| 用户体验 | 干等,一次全出来 | 打字效果,互动感强 |
| 处理逻辑 | json.Decode 一行搞定 |
bufio.Scanner + 逐行 Unmarshal |
| 超时 | 生成时间长,易超时 | 持续有数据,不容易断 |
| 适合场景 | 后台摘要、分类、翻译(需要完整结果) | 聊天、写作、代码生成(希望即时反馈) |
实际项目里,聊天类产品几乎全是流式。非流式更多用在流水线里的一个环节——比如自动给文章打标签,不需要给人看过程。
今天踩过的坑
- 环境变量名不一致:代码里
os.Getenv("DEEPSEEK_API_KEY"),终端export API_KEY=xxx,死活读不到 - **chunk 结构体多套了一层
[]**:SSE 每行是单个对象,不是数组,json.Unmarshal静默失败,啥也打不出来 - 流式 JSON 字段名和非流式不同:
message.content→delta.content,一开始没注意文档 io包忘 import:用了io.ReadAll但没加 import,Go 编译器直接报错
总结
手写 HTTP 调用 LLM API 其实不复杂,核心就是:
- 按 API 文档拼好请求 JSON
stream: false→ 一次解码;stream: true→ 逐行解析 SSE- API Key 用环境变量,别作死硬编码
SDK 好用,但先把底层跑通,理解 HTTP → JSON → SSE 这条链路,后面换什么框架都是降维打击。
今日学习总结:Go 调用 LLM API — 非流式 vs 流式
项目结构
1 | api_study/ |
两种模式对比
| 维度 | 非流式 (Non-Stream) | 流式 (Stream) |
|---|---|---|
| 请求参数 | Stream: false |
Stream: true |
| 响应格式 | 一个完整 JSON 对象 | 多行 SSE (Server-Sent Events) |
| 响应示例 | {"choices":[{"message":{"content":"你好!"}}]} |
data: {"choices":[{"delta":{"content":"你"}}]} data: {"choices":[{"delta":{"content":"好"}}]} data: [DONE] |
| 读取方式 | json.Decoder 一次解码 |
bufio.Scanner 逐行扫描 |
| 用户体感 | 干等 → 一次性全出来 | 逐字打字效果 |
| 适合场景 | 后台批处理、需要完整结果再处理 | 聊天、实时交互 |
核心知识点
1. SSE 事件流格式
1 | data: <json payload>\n\n |
- 每行以
data:开头 - 结束标志是
data: [DONE] - 非流式用
message.content,流式用delta.content(字段名不同!)
2. 流式解析流程
1 | bufio.NewScanner(resp.Body) |
3. 安全实践
- API Key 绝不硬编码 → 用
os.Getenv()从环境变量读取 - 代码里有 Key 历史 → 立刻去后台吊销,生成新的
今日走过的大坑
| 坑 | 原因 | 解决 |
|---|---|---|
| Authentication Fails | 环境变量没设 / 变量名不一致 | export 前检查 os.Getenv 里的名字 |
****2297 is invalid |
Key 被吊销了 | 去 DeepSeek 后台重新生成 |
chunk 定义多了 [] |
外层多套了一个切片,Unmarshal 失败 | SSE 每行是单个对象,不是数组 |
undefined: io |
import 缺了 io |
加上 "io" |


