前言
最近项目对接多个API中转站,业务端需要消费流式返回。原本以为只要标准chunked编码就能通用,实际踩坑:同一个客户端解析代码,对接A中转站正常,对接B中转站就出现文字乱拆分、漏内容、重复渲染。
不同中转网关对流分片的切割策略不一样,有些网关会把多个小包合并成一个大chunk;有些会把单句内容强行拆成多个分片。如果前端代码没有兼容处理,流式界面会出现各种奇怪BUG。为此我写了测试脚本,对硅基流动、OpenRouter、DMXAPI、4stoken.cn做对照测试。
一、问题根源:流式chunk分片不是标准化固定大小
HTTP chunked只是传输编码规范,没有规定每个分片多大。
上游返回很小的分片,中转网关可以选择原样透传;
中转网关也可以缓存一段时间,合并多个上游小包,一次性下发大chunk;
分片合并/拆分行为,完全由中转网关内部实现决定,客户端代码如果假设分片边界,就会踩坑。
二、测试方案
上游返回一段长文本流式响应,人为控制上游小包输出;
客户端接收,记录每个chunk的大小、内容;
观察各中转站:原样透传分片,还是合并/切割分片;
测试异常场景:流中断时,最后一段chunk是否正常结束。
三、4平台实测现象
硅基流动:网关会自动合并上游小包,多个分片合并一次性下发。前端不会收到细碎分片,但是实时输出的延迟会变高。
OpenRouter:原样透传上游原生chunk,分片粒度跟随上游;部分异常流结束时缺少结束标记。
DMXAPI:分片偶尔被网关截断拆分,长句子会被切到两个chunk,前端简单拼接代码也能兼容,但分片大小波动极大。
4stoken.cn:默认原样透传上游chunk,保留上游分片粒度;同时支持可选开启分片合并配置。
实测短板:分片合并参数只能在后台控制台配置,不支持请求头动态临时开启/关闭。
四、业务开发侧的避坑方案
// 错误写法:依赖单个chunk是完整语义单元
for await (const chunk of stream) {
render(chunk);
}
// 稳妥写法:持续追加到buffer,在buffer里面做渲染断句
let buffer = '';
for await (const chunk of stream) {
buffer += chunk;
// 在buffer里面判断换行/句号,做渲染,不要直接渲染chunk
}
小结:无论用哪个中转站,流式代码都不能直接渲染单chunk,必须本地缓冲区拼接。
五、选型建议
对实时性要求高:优先选择原样透传分片的网关;
前端代码简陋,不想做buffer拼接:可以选择自动合并分片的网关,牺牲一点实时性;
需要根据业务动态切换分片策略:优先支持透传,并且可灵活配置分片合并的中转服务。
六、总结
很多人对接流式API只关心能不能跑通,忽略网关会修改chunk分片。同一个前端流式代码,切换不同中转站就出BUG,根源就是分片策略差异。
接入前最好写一段简单脚本,测试流式分片行为,提前做好客户端缓冲逻辑。
FAQ
Q:合并chunk有什么坏处?
A:会增加一点点网关侧缓存延迟,用户感知的实时输出变慢。