当前位置: 首页 > news >正文

Spring AI流式接口经过Nginx后不再逐字输出?缓冲、压缩与超时完整排查

文章摘要

Spring AI在本地通过Flux<String>和SSE可以正常逐段返回,但部署到Nginx、网关或CDN后,经常变成等待几十秒再一次性输出。根因通常不是模型没有流式返回,而是代理缓冲、响应压缩、错误的Content-Type、连接超时或业务代码中出现阻塞操作。本文给出从Spring WebFlux、SSE响应头、Nginx配置、网关链路到前端读取方式的完整排查流程。

一、先判断问题发生在哪一段

完整链路:

模型Provider → Spring AI ChatClient → Spring WebFlux → Nginx或网关 → 浏览器 → 前端渲染

任何一段都可能把流式响应变成批量响应。

建议按顺序测试:

1. 直接调用模型Provider 2. 本机调用Spring Boot接口 3. 绕过Nginx调用服务实例 4. 通过Nginx调用 5. 通过最终域名和CDN调用

如果第3步正常、第4步异常,问题基本在代理层。

二、Spring接口必须真正返回流

示例:

@RestController@RequestMapping("/api/ai")publicclassStreamingController{privatefinalChatClientchatClient;publicStreamingController(ChatClient.Builderbuilder){this.chatClient=builder.build();}@GetMapping(value="/stream",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<String>stream(@RequestParamStringmessage){returnchatClient.prompt().user(message).stream().content();}}

关键点:

返回Flux 使用stream() Content-Type为text/event-stream

错误示例:

publicStringstream(Stringmessage){returnchatClient.prompt().user(message).stream().content().collectList().block().toString();}

这里已经把整个流收集并阻塞,代理配置再正确也无法逐段输出。

三、不要在流中执行阻塞操作

错误:

returnchatClient.prompt().user(message).stream().content().map(chunk->{jdbcTemplate.update("insert into log(content) values (?)",chunk);returnchunk;});

JDBC调用会阻塞Netty事件线程。

推荐:

returnchatClient.prompt().user(message).stream().content().publishOn(Schedulers.boundedElastic()).doOnNext(this::writeAsyncLog);

更好的做法是把日志放入异步队列,不要每个Token写一次数据库。

建议按请求聚合:

流式发送Token → 内存累积完整回答 → 流结束后异步写入一次

四、Nginx默认缓冲会导致一次性返回

Nginx可能先缓存上游响应,达到一定大小后再发送给客户端。

SSE位置建议:

location /api/ai/stream { proxy_pass http://ai_backend; proxy_http_version 1.1; proxy_buffering off; proxy_cache off; proxy_read_timeout 600s; proxy_send_timeout 600s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; add_header X-Accel-Buffering no; }

最关键的是:

proxy_buffering off;

同时可以由应用返回:

X-Accel-Buffering: no

五、压缩也可能造成缓冲

gzip为了提高压缩率,可能等待更多数据后再输出。

如果只有SSE接口异常,可以针对该路径关闭:

gzip off;

或者确保:

text/event-stream

不被代理压缩。

排查时查看响应头:

curl-N-vhttps://example.com/api/ai/stream?message=hello

关注:

Content-Type Content-Encoding Transfer-Encoding X-Accel-Buffering Cache-Control

curl必须加:

-N

否则curl自身也可能缓冲输出。

六、推荐返回标准SSE事件

直接返回Flux<String>虽然简单,但生产系统更适合返回事件对象:

@GetMapping(value="/stream-events",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<ServerSentEvent<AiStreamEvent>>streamEvents(@RequestParamStringmessage){AtomicLongsequence=newAtomicLong();returnchatClient.prompt().user(message).stream().content().map(content->ServerSentEvent.<AiStreamEvent>builder().id(Long.toString(sequence.incrementAndGet())).event("delta").data(newAiStreamEvent("DELTA",content)).build()).concatWithValues(ServerSentEvent.<AiStreamEvent>builder().event("done").data(newAiStreamEvent("DONE","")).build());}

事件类型建议:

start delta tool_start tool_result error done

七、设置正确响应头

建议:

Content-Type: text/event-stream;charset=UTF-8 Cache-Control: no-cache, no-transform Connection: keep-alive X-Accel-Buffering: no

Spring示例:

@GetMapping(value="/stream",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicResponseEntity<Flux<String>>stream(...){Flux<String>body=...;returnResponseEntity.ok().header(HttpHeaders.CACHE_CONTROL,"no-cache, no-transform").header("X-Accel-Buffering","no").body(body);}

不要手工设置错误的:

Content-Length

流式响应长度在开始时通常未知。

八、网关也可能缓冲

链路可能是:

CDN → WAF → API Gateway → Nginx → Spring Boot

只修改最后一层Nginx不一定有效。

需要逐层检查:

  • 是否支持SSE;
  • 最大连接时长;
  • 空闲超时;
  • 响应缓冲;
  • 压缩;
  • 最大并发连接;
  • 是否改写Content-Type。

云网关常见限制:

30秒或60秒空闲超时 固定最大请求时长 不支持长连接

九、心跳防止空闲连接被关闭

模型在调用工具或深度推理时,可能一段时间没有Token。

可以定期发送心跳:

: ping

SSE中以冒号开头的是注释,浏览器不会当成业务事件。

Reactor示意:

Flux<ServerSentEvent<String>>heartbeat=Flux.interval(Duration.ofSeconds(15)).map(index->ServerSentEvent.<String>builder().comment("ping").build());

再与业务流合并。

但需要确保业务完成后心跳也被取消,避免连接无法结束。

十、前端读取方式是否正确

EventSource

适合GET和简单鉴权:

constsource=newEventSource(`/api/ai/stream?message=${encodeURIComponent(message)}`);source.addEventListener("delta",event=>{constdata=JSON.parse(event.data);appendText(data.content);});source.addEventListener("done",()=>{source.close();});

fetch流

适合POST、自定义Header和复杂请求:

constresponse=awaitfetch("/api/ai/stream",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message}),signal:abortController.signal});constreader=response.body.getReader();constdecoder=newTextDecoder();while(true){const{value,done}=awaitreader.read();if(done)break;consttext=decoder.decode(value,{stream:true});render(text);}

如果前端调用:

awaitresponse.text()

它会等待全部响应结束。

十一、浏览器看起来不流式,也可能是渲染策略

前端可能收到很多小Chunk,却为了性能进行批量渲染。

例如:

每100毫秒更新一次DOM

这是合理优化,但需要区分:

网络未流式

与:

前端主动批量渲染

在浏览器Network面板中查看响应到达时间,或直接使用curl -N验证。

十二、超时设置

至少检查:

模型客户端超时 Spring WebFlux超时 Reactor timeout Nginx proxy_read_timeout 网关空闲超时 浏览器请求取消

错误做法:

.timeout(Duration.ofSeconds(30))

复杂模型可能30秒仍未结束。

应区分:

首次Token超时 Token间隔超时 总任务超时

例如:

首次Token:15秒 空闲间隔:30秒 总时长:5分钟

十三、完整排查清单

□ ChatClient使用stream() □ Controller返回Flux □ produces为text/event-stream □ 没有collectList和block □ 没有阻塞数据库调用 □ curl -N直连服务可以逐段返回 □ Nginx proxy_buffering off □ SSE路径没有gzip缓冲 □ 没有错误Content-Length □ 网关支持长连接 □ 空闲超时足够长 □ 必要时发送心跳 □ 前端使用ReadableStream或EventSource □ 前端没有response.text()

总结

Spring AI流式接口经过Nginx后一次性输出,最常见根因是:

应用层收集了整个Flux 代理层开启缓冲或压缩 网关超时 前端等待完整Body

排查时应沿着模型、应用、代理和浏览器逐段验证,先确定数据在哪一层停止流动,再修改配置。

http://www.jsqmd.com/news/1277887/

相关文章:

  • C语言字符统计:从基础实现到高级应用全解析
  • 抖音小店新手入门必修课:开店前理清一件代发核心难题,提升创业成功率 - 抖掌柜
  • QMCDecode:3步解锁QQ音乐加密文件,让Mac用户真正拥有付费音乐
  • TileLang实战:用Python DSL自动生成高性能GPU计算内核
  • OPC UA工业数据采集:第一天盈利的轻量级解决方案
  • 怎么用AI写小说?从零开始,新手也能写出百万字长篇的完整步骤
  • 手机摄像头如何实现无网络文件传输?揭秘CFC项目的视觉编码黑科技
  • 2026抖音去水印是否侵权?合规在线方法及网站风险提醒 - 耶斯去水印
  • 2026年大数据分析工具排名:功能与性能解析 - 科技焦点
  • (2026最新)岳阳本地漏水检测维修公司靠谱推荐:正规防水补漏上门维修-墙面/屋顶/外墙/暗管漏水检测精准定位 - 即刻修防水
  • 基于Eclipse Milo的OPC UA服务器快速搭建与工业数据采集实践
  • MATLAB环形柱状图与核密度面积图的组合可视化方案
  • 基于Flink的实时数据血缘与作业状态监控实践
  • 学术写作全流程AI工具实战指南
  • 2026年数据指标平台推荐:管理能力与安全解析 - 科技焦点
  • OpCore Simplify:智能硬件适配引擎,自动化OpenCore EFI配置解决方案
  • 组织设计六大原则
  • 终极黑苹果配置指南:如何用OpCore Simplify工具快速构建OpenCore EFI
  • (2026最新)宜春本地人必选的靠谱漏水检测维修推荐:正规防水补漏防水-卫生间/厨房/屋顶/阳台/外墙渗漏水精准测漏,本地人的信赖之选 - 安佳防水
  • 基于MCP协议构建IDA Pro自动化分析服务器:原理、实现与恶意代码分析实战
  • 即席查询分析工具有哪些?2026年五大工具对比 - 科技焦点
  • 2026年智能问数平台排名:准确性与安全解析 - 科技焦点
  • SSE、WebSocket和WebRTC怎么选?AI聊天、语音Agent与工具进度推送架构指南
  • AI系统提示词精简优化:提升模型响应效果的关键策略
  • 多AP协同组网落地指南
  • SpringBoot+Vue高校教务管理系统开发实践与优化
  • AI代码助手自动补全如何成为软件供应链攻击新入口?
  • 亳州出发西藏,如何选对旅行社?我的西藏自驾游经验与高反保障干货(含靠谱地接社推荐)| 附:旅行社电话 - 西藏康泰旅行社
  • 民宿在哪里订比较便宜?手把手教你比价、领券、错峰,一站式省钱教程 - 工具软件使用方法推荐
  • TPS61185EVM-335评估板解析:多通道LED背光驱动设计实战指南