Spring AI 第二课-流式输出 运行时动态参数配置
2026/7/19 3:46:28 网站建设 项目流程

Spring AI 入门第二课:流式输出 & 运行时动态参数配置

第一课跑通了最基础的同步调用,这节课深入两个核心能力:流式聊天(打字机效果)运行时动态修改模型参数。把代码和原理都梳理一遍。

一、先看整体代码结构

Controller 里一共四个接口:

接口返回值特点
/generateString同步阻塞调用,第一课内容
/generatestreamFlux<ChatResponse>原始流式响应,返回完整对象
/generatestream2Flux<String>加工后的流式响应,直接吐文本,前端更好用
/runtimeoptionsString运行时动态传 temperature,覆盖配置文件

下面重点拆解后三个。

二、核心概念铺垫

1. 为什么要用流式输出?

同步调用chatModel.call(message)的问题:

  • 大模型生成完整回答需要几秒甚至几十秒
  • 这段时间前端一直转圈,用户体验很差
  • 全部生成完才一次性返回,数据量大时等待感更强

流式输出(Streaming)的思路:

  • 模型生成一个字/一个 token 就推一个
  • 前端边接收边渲染,像 ChatGPT 一样的打字机效果
  • 底层用的是SSE(Server-Sent Events)技术

2. Flux 是什么?

Flux<T>Project Reactor响应式编程里的核心类,表示包含 0~N 个元素的异步流

简单理解:

  • 普通String→ 一次性返回一个完整字符串
  • Flux<String>→ 源源不断地吐出多个字符串片段,来一个发一个

Spring WebFlux 原生支持 Flux 作为返回值,自动帮你处理 SSE 格式。

3. Prompt、Message、ChatResponse 体系

第一课直接传 String,第二课开始接触 Spring AI 的标准对象模型:

Prompt(一次完整的对话请求) └── List<Message>(消息列表) ├── UserMessage(用户消息) ├── SystemMessage(系统提示词) └── AssistantMessage(AI 历史回复) └── ChatOptions(本次请求的模型参数,可选) ChatResponse(一次完整的对话响应) └── List<Generation> 生成结果 └── getOutput() → AssistantMessage └── getText() → 实际文本内容

三、流式接口一:原始版本 generateStream

@GetMapping("/generatestream")publicFlux<ChatResponse>generateStream(@RequestParam(value="message",defaultValue="你是谁")Stringmessage){Promptprompt=newPrompt(newUserMessage(message));Flux<ChatResponse>stream=chatModel.stream(prompt);returnstream;}

代码解读

  1. 构建 Prompt 对象:把用户输入的字符串包装成UserMessage,再封装成Prompt
  2. 调用chatModel.stream(prompt):返回Flux<ChatResponse>,每个元素是一个 token 增量的响应
  3. 直接返回 Flux:Spring MVC / WebFlux 自动识别,以流的形式写回 HTTP 响应

特点

  • 返回的是完整的ChatResponse对象(JSON 格式),包含元数据
  • 前端需要自己从每个 chunk 里提取文本内容
  • 适合需要获取 token 用量、finish_reason 等完整信息的场景

⚠️ 注意:System.out.println("Deepseek回复:"+stream)打印出来的只是 Flux 对象的引用,不是实际内容。Flux 是懒执行的,真正订阅(也就是前端开始消费)的时候才会触发数据流动。

四、流式接口二:实用版本 generateStream2

@GetMapping("/generatestream2")publicFlux<String>generateStream2(@RequestParam(value="message",defaultValue="你是谁")Stringmessage,HttpServletResponseresponse){response.setContentType("text/event-stream");response.setCharacterEncoding("UTF-8");Promptprompt=newPrompt(newUserMessage(message));Flux<ChatResponse>stream=chatModel.stream(prompt);Flux<String>resp=stream.map(newFunction<ChatResponse,String>(){@OverridepublicStringapply(ChatResponsechatResponse){returnchatResponse.getResult().getOutput().getText();}});returnresp;}

相比版本一的改进

1. 手动设置 SSE 响应头
response.setContentType("text/event-stream");response.setCharacterEncoding("UTF-8");

明确告诉浏览器:这是 SSE 流式响应,请一条一条接收处理。

2. map 转换:从 ChatResponse 提取纯文本

stream.map()把每个ChatResponse对象转换成纯文本字符串:

chatResponse.getResult().getOutput().getText()

链式调用路径:ChatResponseGeneration(result)→AssistantMessage(output)→String(text)

3. 前端友好

返回Flux<String>,前端用EventSourcefetch + ReadableStream直接拿文本拼接就行,不用解析 JSON。

可以用 Lambda 简化

匿名内部类写法有点啰嗦,可以改成:

Flux<String>resp=stream.map(chatResponse->chatResponse.getResult().getOutput().getText());

五、运行时动态参数:runtimeOptions

@GetMapping("/runtimeoptions")publicStringruntimeOptions(@RequestParam(value="message",defaultValue="你是谁")Stringmessage,@RequestParam(value="temp",required=false)Doubletemp){Promptprompt;if(temp!=null){DeepSeekChatOptionsbuild=DeepSeekChatOptions.builder().temperature(temp).build();prompt=newPrompt(message,build);}else{prompt=newPrompt(message);}ChatResponseresponse=chatModel.call(prompt);returnresponse.getResult().getOutput().getText();}

核心知识点

1. 配置优先级:运行时 > 全局配置
  • application.properties里的temperature=0.8全局默认值
  • 通过DeepSeekChatOptions构建的参数是单次请求级别的,优先级更高
  • 每次调用都可以传不同的 temperature,互不影响
2. Builder 模式构建选项
DeepSeekChatOptions.builder().temperature(temp).model("deepseek-chat")// 也可以动态切换模型.topP(0.9)// 其他参数也能设.build();

除了 temperature,还能动态设置 model、topP、maxTokens 等几乎所有模型参数。

3. Prompt 的双参数构造

new Prompt(message, build)把消息内容和本次的参数选项打包在一起,传给chatModel.call()

应用场景

  • 前端让用户滑动调节"创意度"(temperature)
  • 同一个应用里不同功能用不同参数:代码生成调低温度,写文案调高温度
  • A/B 测试不同参数的效果

六、前端对接流式输出的思路(补充)

前端页面要对接/generatestream2,不能再用普通fetch一次性拿结果,要用ReadableStream逐块读取:

// 伪代码思路constresponse=awaitfetch('/ai/generatestream2?message='+msg);constreader=response.body.getReader();constdecoder=newTextDecoder();while(true){const{done,value}=awaitreader.read();if(done)break;constchunk=decoder.decode(value);// 把 chunk 追加到 AI 消息气泡里,实现打字机效果aiBubble.textContent+=chunk;}

这样就能实现和 ChatGPT 官网一模一样的逐字输出效果。

七、对比总结

调用方式方法返回类型用户体验适用场景
同步调用call()ChatResponse/String差,全程等待简单测试、后端内部调用
流式调用stream()Flux<ChatResponse>好,逐字输出需要完整元数据
流式调用(加工后)stream() + mapFlux<String>好,逐字输出前端直接渲染,推荐
动态参数Prompt + ChatOptions同同步/流式-需要按请求调整参数

八、踩坑提醒

  1. Flux 是懒执行的:不订阅就不会真正发请求,直接打印 stream 对象看不到内容
  2. SSE 必须设置 Content-Typetext/event-stream,否则浏览器当成普通响应一次性解析
  3. getResult() vs getResults():单条结果用getResult()(拿第一个),多条用getResults(),别搞混
  4. 流式输出中最后一条可能是空的:模型输出结束时的 finish chunk,text 可能为 null,前端要做判空
  5. 中文乱码问题:记得设置characterEncoding=UTF-8,浏览器默认可能用 GBK

总结:这节课的两个重点——stream()流式调用解决了用户体验问题,ChatOptions运行时参数解决了灵活性问题。掌握这两个,基本就能做出一个体验不错的聊天应用了。下一课应该会进入多轮对话、对话记忆(ChatMemory)的部分,继续深入~

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询