哈喽各位后端小伙伴👋
现在几乎所有AI对话、智能报告单分析、AI客服场景,都不再使用一次性全量返回接口了。
大家日常使用ChatGPT、通义千问、阿里百炼时看到的**逐字输出、打字机效果**,底层本质就是大模型流式返回(Stream流式输出)。
很多同学对接大模型时只会写同步接口一次性等待全部结果,不仅接口超时风险极高 ,前端白屏等待时间过长,用户体验极差。
-
✅ 什么是大模型流式响应?和普通同步接口区别在哪
-
✅ Spring WebFlux + Flux实现流式返回底层原理
-
✅ 百炼Agent流式接口完整业务代码逐行剖析
-
✅ 流式输出文本清洗、异常兜底、全量结果归集实战
-
✅ 线上开发踩坑避坑总结
一、先搞懂:为什么大模型一定要用流式返回?
1. 同步一次性返回(传统接口)
后端调用大模型,等待AI生成完所有内容后,一次性返回完整文本。
-
AI生成长文本耗时可达5s-20s,接口极易超时
-
前端长时间空白,用户以为服务卡死
-
服务端一直阻塞等待,吞吐量极低
2. Flux流式分段返回(SSE协议)
后端接收大模型每一段生成的碎片文字,收到一段、向前端推送一段,前端实时拼接,实现打字机效果。
-
无接口超时压力,长文本生成毫无压力
-
前端实时展示内容,交互体验拉满
-
基于WebFlux非阻塞响应式编程,服务端并发性能更强
3. 技术选型:为什么用Flux而不是List/Iterator?
Spring WebFlux提供两种响应式返回值:
-
Mono:0/1个元素,适用于单次结果返回
-
Flux:0/N个元素,持续推送多个数据流,完美适配大模型流式分片输出
二、业务场景说明
本次实战业务:后端对接阿里百炼Agent智能应用,上传体检报告单图片+文字提问,AI逐段返回报告单解读结果,前端实现实时打字机展示。
核心能力:支持图文多模态输入、流式分片输出、自动清洗AI返回的Markdown代码块、异常统一兜底、全量回答内容归集入库。
三、完整可运行业务源码
先贴出完整源码,下文逐行拆解每一处关键逻辑:
java
import com.alibaba.dashscope.app.Application;
import com.alibaba.dashscope.app.ApplicationParam;
import com.alibaba.dashscope.app.ApplicationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import io.reactivex.rxjava3.core.Flowable;
import java.util.Arrays;
import java.util.List;
/**
* 调用百炼Agent大模型,流式返回AI识别结果
* @param apiKey 百炼密钥
* @param appId 百炼自建应用ID
* @param question 用户提问
* @param images 图片地址列表(多模态图文识别)
* @return Flux<String> 流式分片文本
*/
private Flux<String> intentionRecognize(String apiKey, String appId, String question, List<String> images) {
log.info("ReportOcrServiceImpl-intentionRecognize-开始调用百炼Agent分析报告单");
try {
// 用于归集完整的AI返回内容,最终入库保存全量回答
StringBuilder sb = new StringBuilder();
// 1. 封装用户对话消息
Message message = Message.builder()
.role(Role.USER.getValue())
.content(question)
.build();
// 2. 组装百炼Agent调用请求参数,开启流式输出
ApplicationParam param = ApplicationParam.builder()
.apiKey(apiKey)
.appId(appId)
.messages(Arrays.asList(message))
.incrementalOutput(true) // 核心配置:开启增量流式输出
.images(images) // 传入图片,支持图文多模态识别
.build();
// 3. 创建百炼应用调用实例,发起流式调用
Application application = new Application();
Flowable<ApplicationResult> result = application.streamCall(param);
// 4. 适配器转换:RxJava Flowable 转 Spring WebFlux Flux
return Flux.create(sink -> {
Flux.from(result)
.subscribe(item -> {
// 单次分片返回的文本内容
String output = item.getOutput().getText();
log.info("百炼流式分片原始数据:{}", JSON.toJSONString(item));
// 非空判断,过滤空分片数据
if (!StringUtils.isBlank(output)) {
// 关键:清洗AI自带的换行符、```json代码块标记
output = output.replace("\n", "").replaceAll("```json\\s*|\\s*```", "");
// 拼接全量结果,用于后续数据库存储
sb.append(output);
}
// 向前端推送当前分片内容,实现实时流式输出
sink.next(output);
}, error -> {
// 异常回调:调用大模型出现报错,关闭流
log.error("百炼Agent流式调用发生异常", error);
sink.complete();
}, () -> {
// 流正常结束回调:所有分片推送完毕,保存完整回答
log.info("ReportOcrServiceImpl-intentionRecognize-调用完成-完整回答:{}", JSON.toJSONString(sb.toString()));
sink.complete();
});
});
} catch (NoApiKeyException e) {
log.error("百炼API密钥错误", JSON.toJSONString(e.getStackTrace()));
throw new RuntimeException("大模型密钥配置异常", e);
} catch (InputRequiredException e) {
log.error("百炼请求入参缺失", JSON.toJSONString(e.getStackTrace()));
throw new RuntimeException("大模型请求参数缺失", e);
}
}
四、逐行核心代码深度拆解(必看)
1. 流式开关核心配置
java
.incrementalOutput(true)
这是对接百炼流式接口最关键的一行代码:
-
true:开启增量流式输出,大模型生成一段文字就实时返回一段
-
false:关闭流式,接口阻塞等待全部内容生成完毕再一次性返回
想要打字机效果,这个参数必须开启。
2. 多模态能力:图文一起传入大模型
java
.images(images)
本次业务是体检报告单AI分析,需要同时传入图片+文字问题,百炼原生支持多模态入参,无需额外处理图片Base64,直接传入图片URL即可,适配图文结合的AI分析场景。
3. Flowable 适配 Flux:响应式流格式转换
java
Flowable<ApplicationResult> result = application.streamCall(param);
return Flux.create(sink -> {
Flux.from(result)
.subscribe(...);
});
这里很多人会疑惑为什么要做转换:
-
百炼SDK底层流式返回采用的是RxJava Flowable
-
我们Spring WebFlux项目标准返回是Reactor Flux
-
通过Flux.create桥接,sink作为发射器,把第三方SDK的数据流,统一适配为后端标准的WebFlux流式流
4. sink发射器:流式数据推送核心
-
sink.next(output):每收到一段AI分片,立刻推送给前端,实时展示
-
sink.complete():流正常结束/异常结束,关闭连接,释放服务端资源
5. AI返回内容清洗(业务刚需)
java
output = output.replace("\n", "").replaceAll("```json\\s*|\\s*```", "");
实际开发中,大模型经常自动携带冗余格式:换行符、markdown的json代码块标记,直接返回前端会造成排版错乱。
这行代码统一清洗格式,保证前端拿到干净纯文本,不用前端二次处理。
6. 全量内容归集:流式展示+入库两不误
java
StringBuilder sb = new StringBuilder();
sb.append(output);
流式输出是分片推送,前端只能实时看片段,无法拿到完整回答。
服务端通过StringBuilder全程归集所有分片,流结束后可以将完整AI回答存入数据库,满足溯源、历史记录查询业务需求。
7. 分层异常捕获
-
NoApiKeyException:百炼密钥为空/密钥错误
-
InputRequiredException:请求参数缺失、appId错误、消息体为空
-
订阅onError:大模型服务内部报错、网络波动、限流熔断
全覆盖异常捕获,避免流式接口报错直接崩掉服务。
五、补充Controller层接口(直接对接前端)
Service层写完流式方法,Controller层需要指定SSE媒体类型,前端才能正常接收流式分片,完整接口代码如下:
java
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/ai/report")
public class AiReportController {
@Autowired
private ReportOcrServiceImpl reportOcrService;
/**
* 报告单AI分析流式接口
* 必须指定 produces = MediaType.TEXT_EVENT_STREAM_VALUE
* 开启SSE服务端推送协议,前端EventSource直接对接
*/
@PostMapping(value = "/stream/analyze", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamAnalyzeReport(@RequestBody ReportQueryDTO dto) {
return reportOcrService.intentionRecognize(
dto.getApiKey(),
dto.getAppId(),
dto.getQuestion(),
dto.getImages()
);
}
}
重点提醒 :接口必须增加 produces = MediaType.TEXT_EVENT_STREAM_VALUE,声明为SSE服务端推送流,否则前端无法接收分片数据,流式直接失效。
六、线上开发7个避坑总结(实战踩坑)
-
必须开启incrementalOutput=true,忘记配置直接变成同步阻塞接口,流式完全失效
-
一定要做文本清洗,大模型自带markdown格式符,不清洗前端排版直接乱掉
-
流必须主动complete,异常和正常结束都要关闭sink,否则会造成服务端连接泄露、连接堆积
-
区分分片内容和完整内容:sink.next推分片给前端,StringBuilder归集完整内容用于入库,各司其职
-
禁止在Flux内部做阻塞操作,流式接口是非阻塞响应式,不要加Thread.sleep、同步http调用
-
异常不能直接返回Mono.error,流式接口报错需要通过sink异常回调关闭流,而非抛出普通异常
-
前端必须使用EventSource对接,普通axios post无法接收流式分片,需要前端适配SSE协议
七、同步VS流式接口完整对比
| 对比维度 | 同步一次性返回(Mono) | 流式分片返回(Flux) |
|---|---|---|
| 接口响应时机 | 全部生成完毕才响应 | 生成一段,响应一段 |
| 接口超时风险 | 极高,长文本极易超时 | 无超时压力,长文本友好 |
| 前端体验 | 长时间空白,体验差 | 打字机实时输出,体验好 |
| 服务端性能 | 阻塞线程,并发差 | 非阻塞响应式,并发高 |
| 适用场景 | 短文本问答、简单内容生成 | 智能客服、文档分析、长文案生成 |
八、博主总结
现在后端对接大模型,流式输出已经是业务标配,不再是可选功能。
本次基于阿里百炼Agent的真实业务代码,核心关键点就3个:
-
开启SDK原生流式增量输出开关
-
RxJava流适配WebFlux标准Flux流
-
分片实时推送前端 + 全量内容归集入库,兼顾体验和数据溯源
这套代码可以直接复用在通义千问、百炼、OpenAI、本地大模型流式对接场景,逻辑通用,只需要替换对应SDK即可。