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

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

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

一、先看整体代码结构

Controller 里一共四个接口:

接口 返回值 特点
/generate String 同步阻塞调用,第一课内容
/generatestream Flux<ChatResponse> 原始流式响应,返回完整对象
/generatestream2 Flux<String> 加工后的流式响应,直接吐文本,前端更好用
/runtimeoptions String 运行时动态传 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

java 复制代码
@GetMapping("/generatestream")
public Flux<ChatResponse> generateStream(
        @RequestParam(value="message", defaultValue="你是谁") String message) {
    Prompt prompt = new Prompt(new UserMessage(message));
    Flux<ChatResponse> stream = chatModel.stream(prompt);
    return stream;
}

代码解读

  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

java 复制代码
@GetMapping("/generatestream2")
public Flux<String> generateStream2(
        @RequestParam(value="message", defaultValue="你是谁") String message,
        HttpServletResponse response) {
    response.setContentType("text/event-stream");
    response.setCharacterEncoding("UTF-8");
    
    Prompt prompt = new Prompt(new UserMessage(message));
    Flux<ChatResponse> stream = chatModel.stream(prompt);
    
    Flux<String> resp = stream.map(new Function<ChatResponse, String>() {
        @Override
        public String apply(ChatResponse chatResponse) {
            return chatResponse.getResult().getOutput().getText();
        }
    });
    return resp;
}

相比版本一的改进

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

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

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

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

java 复制代码
chatResponse.getResult().getOutput().getText()

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

3. 前端友好

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

可以用 Lambda 简化

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

java 复制代码
Flux<String> resp = stream.map(chatResponse -> 
    chatResponse.getResult().getOutput().getText()
);

五、运行时动态参数:runtimeOptions

java 复制代码
@GetMapping("/runtimeoptions")
public String runtimeOptions(
        @RequestParam(value="message", defaultValue="你是谁") String message,
        @RequestParam(value="temp", required=false) Double temp) {
    
    Prompt prompt;
    if (temp != null) {
        DeepSeekChatOptions build = DeepSeekChatOptions.builder()
                .temperature(temp)
                .build();
        prompt = new Prompt(message, build);
    } else {
        prompt = new Prompt(message);
    }
    
    ChatResponse response = chatModel.call(prompt);
    return response.getResult().getOutput().getText();
}

核心知识点

1. 配置优先级:运行时 > 全局配置
  • application.properties 里的 temperature=0.8全局默认值
  • 通过 DeepSeekChatOptions 构建的参数是单次请求级别的,优先级更高
  • 每次调用都可以传不同的 temperature,互不影响
2. Builder 模式构建选项
java 复制代码
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 逐块读取:

javascript 复制代码
// 伪代码思路
const response = await fetch('/ai/generatestream2?message=' + msg);
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value);
    // 把 chunk 追加到 AI 消息气泡里,实现打字机效果
    aiBubble.textContent += chunk;
}

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

七、对比总结

调用方式 方法 返回类型 用户体验 适用场景
同步调用 call() ChatResponse / String 差,全程等待 简单测试、后端内部调用
流式调用 stream() Flux<ChatResponse> 好,逐字输出 需要完整元数据
流式调用(加工后) stream() + map Flux<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)的部分,继续深入~

相关推荐
公众号:fuwuqiBMC6 小时前
(转自“服务器BMC”)服务器BMC功能——DDR故障预测
运维·服务器·人工智能
会助力智能会务6 小时前
会务系统供应商怎么选?会助力智能会务系统,一站式数字化会务服务商
大数据·运维·人工智能
额恩666 小时前
阶段五:HttpOnly Cookie 登录持久化
java·spring
中微极客6 小时前
2026年AI视频生成模型选型与工程实践指南
人工智能·音视频
阿童木写作6 小时前
跨马翻译:一站式图片视频智能处理,提升跨境电商工作效率
人工智能·python·音视频
出门吃三碗饭6 小时前
Google 提示词零基础教学:从“会提问”到“让 AI 真正解决问题
人工智能
意图共鸣6 小时前
技术解读:《智能体三角模型白皮书》中的三问校验法——意图共鸣科技的商用AI评估框架
人工智能·科技