当前位置: 首页 > 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.cnnetsun.cn/news/3696774.html

相关文章:

  • 波段舞者系统 同花顺期货通指标
  • AI研究自动化:从数据清洗视角构建可复现实验流水线
  • 4大核心技术深度解析:ESP-Drone如何实现低成本无人机自主飞行
  • NLTK实现统计机器翻译原理与实战指南
  • ImageJ插件开发入门:用Java扩展开源图像处理软件的核心功能
  • .NET Core企业级快速开发框架设计与实践
  • fofr事件监测系统:技术架构、部署实践与实时预警应用
  • TileLang与TVM:Python DSL实现GPU高性能计算与Tensor Core优化
  • C语言字符统计:从基础实现到高级应用全解析
  • 【CTF-编程-NC】最长公共前缀
  • 3分钟快速上手:SillyTavern AI聊天前端完整安装指南
  • Python项目代码骨架构建与目标函数设计实践
  • 杭州GEO服务商测评:向朴科技的技术优势与实践案例
  • 计算机图书热销榜TOP1的运作机制与内容策略
  • 专科生论文写作利器:10大AI工具实测推荐
  • 技术博客写作规范:从事件驱动架构到可复现工程实践
  • DeepSeek DSpark投机解码技术:大模型推理加速85%的工程实践
  • AI工具如何提升论文写作效率:9款实用工具测评
  • 从聊天到项目:解锁Codex AI编程副驾驶的实战全链路指南
  • 企业AI知识库技术架构深度解析:从异构存储到RAG管线的全链路设计
  • Bibata Cursor:开源光标主题的终极指南,让你的桌面焕然一新
  • 拓竹A1C 3D打印机:工科生高速打印入门指南与项目实践
  • Navicat重置试用期终极解决方案:3种专业策略告别14天限制
  • Drive-JEPA:自动驾驶中的视频联合嵌入预测与轨迹蒸馏技术
  • AI绘画工作流优化:infinite-canvas本地部署与批量出图实战
  • 终极oh-my-opencode配置指南:从新手到专家的AI代理优化秘籍
  • Java技术栈面试实战:Spring Boot与微服务架构深度解析
  • TPIC7710EVM评估板深度解析:从硬件设计到软件驱动的电机控制实战
  • Python与CNN实战:鱼类图像识别系统开发指南
  • 洛雪音乐音源实践手册:三步解锁全网无损音乐的完整方案