Spring AI 入门第二课流式输出 运行时动态参数配置第一课跑通了最基础的同步调用这节课深入两个核心能力流式聊天打字机效果和运行时动态修改模型参数。把代码和原理都梳理一遍。一、先看整体代码结构Controller 里一共四个接口接口返回值特点/generateString同步阻塞调用第一课内容/generatestreamFluxChatResponse原始流式响应返回完整对象/generatestream2FluxString加工后的流式响应直接吐文本前端更好用/runtimeoptionsString运行时动态传 temperature覆盖配置文件下面重点拆解后三个。二、核心概念铺垫1. 为什么要用流式输出同步调用chatModel.call(message)的问题大模型生成完整回答需要几秒甚至几十秒这段时间前端一直转圈用户体验很差全部生成完才一次性返回数据量大时等待感更强流式输出Streaming的思路模型生成一个字/一个 token 就推一个前端边接收边渲染像 ChatGPT 一样的打字机效果底层用的是SSEServer-Sent Events技术2. Flux 是什么FluxT是Project Reactor响应式编程里的核心类表示包含 0~N 个元素的异步流。简单理解普通String→ 一次性返回一个完整字符串FluxString→ 源源不断地吐出多个字符串片段来一个发一个Spring WebFlux 原生支持 Flux 作为返回值自动帮你处理 SSE 格式。3. Prompt、Message、ChatResponse 体系第一课直接传 String第二课开始接触 Spring AI 的标准对象模型Prompt一次完整的对话请求 └── ListMessage消息列表 ├── UserMessage用户消息 ├── SystemMessage系统提示词 └── AssistantMessageAI 历史回复 └── ChatOptions本次请求的模型参数可选 ChatResponse一次完整的对话响应 └── ListGeneration 生成结果 └── getOutput() → AssistantMessage └── getText() → 实际文本内容三、流式接口一原始版本 generateStreamGetMapping(/generatestream)publicFluxChatResponsegenerateStream(RequestParam(valuemessage,defaultValue你是谁)Stringmessage){PromptpromptnewPrompt(newUserMessage(message));FluxChatResponsestreamchatModel.stream(prompt);returnstream;}代码解读构建 Prompt 对象把用户输入的字符串包装成UserMessage再封装成Prompt调用chatModel.stream(prompt)返回FluxChatResponse每个元素是一个 token 增量的响应直接返回 FluxSpring MVC / WebFlux 自动识别以流的形式写回 HTTP 响应特点返回的是完整的ChatResponse对象JSON 格式包含元数据前端需要自己从每个 chunk 里提取文本内容适合需要获取 token 用量、finish_reason 等完整信息的场景⚠️ 注意System.out.println(Deepseek回复stream)打印出来的只是 Flux 对象的引用不是实际内容。Flux 是懒执行的真正订阅也就是前端开始消费的时候才会触发数据流动。四、流式接口二实用版本 generateStream2GetMapping(/generatestream2)publicFluxStringgenerateStream2(RequestParam(valuemessage,defaultValue你是谁)Stringmessage,HttpServletResponseresponse){response.setContentType(text/event-stream);response.setCharacterEncoding(UTF-8);PromptpromptnewPrompt(newUserMessage(message));FluxChatResponsestreamchatModel.stream(prompt);FluxStringrespstream.map(newFunctionChatResponse,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()链式调用路径ChatResponse→Generationresult→AssistantMessageoutput→Stringtext3. 前端友好返回FluxString前端用EventSource或fetch ReadableStream直接拿文本拼接就行不用解析 JSON。可以用 Lambda 简化匿名内部类写法有点啰嗦可以改成FluxStringrespstream.map(chatResponse-chatResponse.getResult().getOutput().getText());五、运行时动态参数runtimeOptionsGetMapping(/runtimeoptions)publicStringruntimeOptions(RequestParam(valuemessage,defaultValue你是谁)Stringmessage,RequestParam(valuetemp,requiredfalse)Doubletemp){Promptprompt;if(temp!null){DeepSeekChatOptionsbuildDeepSeekChatOptions.builder().temperature(temp).build();promptnewPrompt(message,build);}else{promptnewPrompt(message);}ChatResponseresponsechatModel.call(prompt);returnresponse.getResult().getOutput().getText();}核心知识点1. 配置优先级运行时 全局配置application.properties里的temperature0.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逐块读取// 伪代码思路constresponseawaitfetch(/ai/generatestream2?messagemsg);constreaderresponse.body.getReader();constdecodernewTextDecoder();while(true){const{done,value}awaitreader.read();if(done)break;constchunkdecoder.decode(value);// 把 chunk 追加到 AI 消息气泡里实现打字机效果aiBubble.textContentchunk;}这样就能实现和 ChatGPT 官网一模一样的逐字输出效果。七、对比总结调用方式方法返回类型用户体验适用场景同步调用call()ChatResponse/String差全程等待简单测试、后端内部调用流式调用stream()FluxChatResponse好逐字输出需要完整元数据流式调用加工后stream() mapFluxString好逐字输出前端直接渲染推荐动态参数Prompt ChatOptions同同步/流式-需要按请求调整参数八、踩坑提醒Flux 是懒执行的不订阅就不会真正发请求直接打印 stream 对象看不到内容SSE 必须设置 Content-Typetext/event-stream否则浏览器当成普通响应一次性解析getResult() vs getResults()单条结果用getResult()拿第一个多条用getResults()别搞混流式输出中最后一条可能是空的模型输出结束时的 finish chunktext 可能为 null前端要做判空中文乱码问题记得设置characterEncodingUTF-8浏览器默认可能用 GBK总结这节课的两个重点——stream()流式调用解决了用户体验问题ChatOptions运行时参数解决了灵活性问题。掌握这两个基本就能做出一个体验不错的聊天应用了。下一课应该会进入多轮对话、对话记忆ChatMemory的部分继续深入