AI 流式输出总是半截 JSON:用 Go 增量缓冲器拼出完整工具参数
来源:17golang原创
时间:2026-07-26 15:02:20 497浏览 收藏
AI 接口改成流式返回后,最容易踩到的坑不是网络断开,而是把每个数据块误当成一个完整 JSON。工具参数可能被拆成 {"city":"、上海、"} 三段,逐块调用 json.Unmarshal 就会得到意外 EOF,重试逻辑还可能把同一次工具调用提交两遍。
流式块只代表传输片段,不代表语义边界。先按请求拼接并确认 JSON 完整,再把参数交给工具层,是最稳妥的处理顺序。
要点速览
- 每个流式块都可能从字符串中间切开,不能按块直接解析。
- 缓冲器需要同时记录请求标识、工具调用标识和原始片段。
- 只有完整 JSON 通过语法解析和字段校验后,才允许提交工具参数。
- 提交成功后要记住 tool_call_id,重连或重复片段不能再次触发。
问题现场:工具参数在半路变成意外 EOF
假设应用让模型选择天气工具,参数结构很小:
{"city":"上海","unit":"celsius"}
非流式响应通常一次拿到完整字符串;流式响应却可能按网络缓冲、服务端调度或上游事件格式拆分。下面这组片段完全合法,却没有一段单独构成完整对象:
{"city":"上
海","unit":"celsius"}
如果接收端在第一段就调用 json.Unmarshal,错误信息大概率是 unexpected end of JSON input。这个结果只能说明当前片段不完整,不能说明模型输出了坏 JSON。

先验证边界:分块位置和 JSON 边界是两回事
排查时先把原始事件打印成带序号的短日志,不要只打印拼接后的结果。实际项目里至少保留 request_id、tool_call_id、片段序号和字节长度:
type StreamPart struct {
RequestID string
ToolID string
Index int
Delta string
}
type ToolBuffer struct {
RequestID string
ToolID string
Raw strings.Builder
Committed bool
}
这里用 strings.Builder 是为了避免每次追加都复制整个字符串。更重要的是,缓冲器的生命周期要绑定到一次工具调用,而不是绑定到连接本身;同一条连接上可能连续返回多个工具调用。
不要用最后一个字符猜结束
看到 } 并不等于可以提交。字符串值里也可能出现大括号,转义字符还会让简单的计数器失真。可靠做法是把累计文本交给 JSON 解码器,并把“暂时不完整”和“确定损坏”分开处理。
最小修复:累计片段,完整后再校验字段
下面的缓冲器只负责一件事:接收增量文本,返回“暂时等待”“解析成功”或“内容损坏”。业务工具不会看到半截参数。
package streamjson
import (
"encoding/json"
"errors"
"strings"
)
var ErrWaiting = errors.New("json is not complete")
type WeatherArgs struct {
City string `json:"city"`
Unit string `json:"unit"`
}
type Buffer struct {
text strings.Builder
committed bool
}
func (b *Buffer) Push(delta string) (WeatherArgs, error) {
if b.committed {
return WeatherArgs{}, errors.New("tool call already committed")
}
b.text.WriteString(delta)
var args WeatherArgs
dec := json.NewDecoder(strings.NewReader(b.text.String()))
if err := dec.Decode(&args); err != nil {
if strings.Contains(err.Error(), "unexpected end") || strings.Contains(err.Error(), "unexpected EOF") {
return WeatherArgs{}, ErrWaiting
}
return WeatherArgs{}, err
}
if args.City == "" || (args.Unit != "celsius" && args.Unit != "fahrenheit") {
return WeatherArgs{}, errors.New("invalid weather arguments")
}
b.committed = true
return args, nil
}
生产代码还应检查解码器后面是否存在非空尾巴,避免把两个 JSON 对象黏在一起时误判成功。若上游协议能提供结束事件,也要把它当成必要条件之一:语法完整解决的是“能否解析”,结束事件解决的是“上游是否真的发完”。

定位根因:重连、重复片段和多工具调用
缓冲器能解决半截 JSON,但线上故障通常还藏着三类状态问题。
重连后不能无条件清空
如果断线后服务端从某个序号继续推送,清空缓冲区会丢掉前半段;如果服务端从头重播,则直接追加会把文本拼坏。接收端应依据事件序号或上游游标决定“续接”还是“重建”,并把选择写入日志。
同一个 tool_call_id 只能提交一次
提交工具前先查去重表,键可以是 request_id + tool_call_id。去重记录最好在工具任务入队前写入,并在失败时保存失败状态;不要用“调用成功后才记账”,否则超时重试会再次触发副作用操作。
多个调用要分开建缓冲器
不要把所有 delta 拼到一个全局字符串里。以 tool_call_id 建立 map,并限制单次参数的最大字节数,例如天气查询可以限制在 8 KB 内。超过上限立即停止接收并返回可观测错误,避免异常输出拖垮内存。
复查结果:用三组测试锁住边界
这个小组件不需要复杂压测,先用三组输入覆盖最容易回归的路径:
- 把 JSON 拆在普通字符之间,确认前几次返回
ErrWaiting,完整后只提交一次。 - 把分块切在中文 UTF-8 字节边界附近,确认上层按字符串事件传入,不自行按字节截断。
- 重复推送同一个
tool_call_id,确认第二次被去重;传入多余字段或非法枚举值时,确认工具层不会收到参数。
验收日志可以只保留这样的关键信息:tool_id=weather-17 parts=3 bytes=36 state=committed。原始参数是否需要落盘,要按数据敏感等级处理,城市字段可以记录,用户地址、账号和令牌不要混在调试日志里。
常见问题:流式 JSON 处理还要注意什么
为什么不直接等完整响应再解析?
如果业务不需要边生成边展示,等完整响应确实更简单;但工具调用、长文本和超时控制常常需要流式接收。即便选择等待,也应在服务端设置总字节上限和截止时间。
字符串被拆开时会不会产生乱码?
如果上层事件已经是 UTF-8 字符串,按字符串追加通常不会有问题。风险来自自行按字节切片后立刻转成字符串,所以应让事件解析层负责恢复编码边界。
收到完整 JSON 就能调用工具吗?
还不够。需要检查结束事件、工具名称、字段类型、枚举值、参数大小和去重状态。语法正确只代表文本能被解析。
缓冲区应该什么时候释放?
成功提交、确定失败、超时或取消时都要释放;同时记录释放原因。对长连接场景,可用定时器清理超过截止时间仍未完成的调用。
把流式处理收敛成一个清晰的边界
接收层只负责事件顺序和文本拼接,解析层负责 JSON 完整性,校验层负责字段约束,工具层负责副作用。四层边界清楚后,意外 EOF 不会再直接变成业务重试,重复片段也不会悄悄变成重复任务。
-
254 收藏
-
科技周边 · 人工智能 | 8小时前 | 人工智能 · sse · 流式输出 · 接口稳定性 · 重试 · SSE 断线重连 Responses API AI流式输出 sequence_number 重复片段217 收藏
-
188 收藏
-
科技周边 · 人工智能 | 2天前 | go · openai · AI接口 · Responses API · Go OpenAI Responses API background mode 异步轮询 大模型接口388 收藏
-
科技周边 · 人工智能 | 2天前 | go语言 · 异步任务 · 人工智能 · openai · API工程化 · Go 异步任务 轮询 数据保留 OpenAI Responses API background mode183 收藏
-
202 收藏
-
科技周边 · 人工智能 | 4天前 | API · go · 人工智能 · 工程实践 · 工具调用 · Go Anthropic Messages API tool_use tool_result Claude工具调用368 收藏
-
243 收藏
-
195 收藏
-
186 收藏
-
333 收藏
-
419 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习