流式输出与springboot中的响应式编程

第一样东西(图1):底层原始驱动(回调式客户端)

它是干什么用的?

它是大模型厂商(如OpenAI、阿里云)官方Java SDK里提供的最底层网络通信类 。它的唯一职责是建立HTTP/SSE连接,收到一个Token就立刻回调你的方法。它不关心你的业务线程、不关心异常怎么传递,只管"推数据"。

举例(你直接写的痛苦代码):

java

复制代码
// 这是图1的真实模样:OpenAI官方提供的 StreamingCompletionHandler
OpenAiAsyncClient client = OpenAiClient.builder().build();

// 你要传一个匿名内部类给它
client.streamCompletion("讲个笑话", new StreamingCompletionHandler() {
    @Override
    public void onPartialResponse(String token) {
        // ⚠️ 注意:这个方法运行在 OkHttp 的 IO 工作线程中!
        // 你不能在这里做耗时操作,否则会阻塞网络读数据。
        System.out.print(token); 
    }

    @Override
    public void onComplete() {
        // 坑:主线程不知道你完成了,你得自己用 CountDownLatch 去等。
    }

    @Override
    public void onError(Throwable t) {
        // 坑:这个异常抛不到主线程,只能自己打日志。
    }
});
// 主线程执行到这里,方法就返回了,但数据还在后台慢慢推。

第二样东西(图3):响应式编程工具箱(Reactor库)

它是干什么用的?

它是SpringBoot 3内置的异步编程流水线工具 。它的职责是提供一套标准操作符mapflatMaptimeoutretry)和灵活的线程调度能力 ,让你像写Stream流一样处理异步数据,并自带背压(流量控制)

举例(你不涉及AI时单独用):

java

复制代码
// 这是图3的独舞:不涉及AI,只处理数字流
Flux.range(1, 10)                     // 生成1-10
    .map(i -> i * 2)                  // 转成偶数
    .delayElements(Duration.ofSeconds(1)) // 每秒发一个
    .doOnError(e -> log.error("出错啦"))
    .timeout(Duration.ofSeconds(5))   // 超过5秒没数据就报错
    .subscribe(System.out::println);  // 最终消费

它提供了声明式异常处理线程切换 (如.publishOn(Schedulers.boundedElastic())),让你彻底告别CountDownLatch和手写try-catch跨线程传递。


第三样东西(图2):粘合适配器(langchain4j-reactor模块)

它是干什么用的?

它是专门用来收拾图1烂摊子的封装工具 。它的唯一职责是:把图1那个丑陋的回调内部类,偷偷转换成一个漂亮的 Flux<String> 返回给调用方 。让你完全不用看到 onPartialResponse 这几个字。

举例(你引入依赖后优雅地调用):

java

复制代码
// 引入 dev.langchain4j:langchain4j-reactor 后
// 底层依旧用的是图1的 OpenAiAsyncClient,但被这个适配器包住了

@FunctionalInterface
interface Assistant {
    Flux<String> chat(String message); // 👈 直接返回 Flux,不再是 void
}

// 使用时,完全看不到回调
Assistant assistant = ...; 
Flux<String> tokenFlux = assistant.chat("讲个笑话");
// 拿到的 tokenFlux 就是图3的 Reactor 流,可以随意操作了!

🚀 三者一起应用的完整示例(SpringBoot WebFlux + AI)

假设我们做一个实时翻译助手 ,用户发来中文,AI流式返回英文翻译,且要求客户端断开连接时,AI立即停止生成(省算力)。

1. 依赖准备(图2进场)

xml

复制代码
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
    <version>1.0.1-beta6</version>
</dependency>
<!-- 同时引入大模型SDK(图1的底层)和 SpringBoot WebFlux(图3的容器) -->

2. 服务层代码(图2适配图1,产出图3的Flux)

java

复制代码
@Service
public class TranslationService {
    // 这是被 langchain4j 增强过的模型,内部已用 Flux.create() 包住了图1的回调
    private final StreamingChatLanguageModel model; 

    public Flux<String> translate(String chinese) {
        // 调用底层时,langchain4j-reactor 已经帮我们做了 dirty work:
        // 内部 new 了 StreamingChatResponseHandler,在 onPartialResponse 里调用了 emitter.next(token)
        Flux<String> rawFlux = model.stream("请把这句话翻译成英文:" + chinese);
        
        // 👇 这里开始全是图3(Reactor)的表演
        return rawFlux
            // 图3操作符:对每个token做后处理(加个括号)
            .map(token -> "[" + token + "]") 
            // 图3线程调度:把复杂的后处理(如存Redis)丢到自定义线程池,释放IO线程
            .publishOn(Schedulers.boundedElastic()) 
            // 图3背压配合:如果前端渲染慢,自动限流,不会把内存撑爆
            .doOnNext(token -> log.info("发出token: {}", token))
            // 图3异常处理:如果图1底层网络断了,直接返回友好提示
            .onErrorResume(e -> Flux.just("[翻译服务暂时不可用]"));
    }
}

3. 控制器层代码(图3对接WebFlux)

java

复制代码
@RestController
public class ChatController {
    @Autowired
    private TranslationService service;

    // 👇 SpringBoot WebFlux 响应式端点(MediaType 指明是流式SSE)
    @GetMapping(value = "/translate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> handleTranslate(@RequestParam String msg) {
        return service.translate(msg)
            // 图3最后操作:按SSE协议包装成 data:xxx\n\n 格式
            .map(content -> "data: " + content + "\n\n")
            // 图3生命周期:如果客户端浏览器关闭,触发取消信号
            .doOnCancel(() -> System.out.println("客户端走了,底层图1的HTTP连接将被中断"));
    }
}

4. 背后的血流成河(三者协作过程)

  • 请求进入 :WebFlux容器(Netty)收到请求,订阅了返回的Flux

  • 订阅触发(图3背压) :Netty向Flux请求数据(request(1)),这个请求一路传到适配器(图2)。

  • 适配器启动(图2) :适配器内部的Flux.create()收到了请求,开始调用图1的底层SDK发起HTTP流式请求。

  • 数据推送(图1 → 图2 → 图3)

    • AI返回第一个token "Hello",触发图1的onPartialResponse("Hello")

    • 图2的emitter.next("Hello")将数据推进Reactor管道。

    • 图3的.map(token -> "[" + token + "]")将其变成 "[Hello]"

    • 图3最终将data: [Hello]\n\n写回给浏览器。

  • 异常或取消 :若浏览器强行关闭,Netty调用subscription.cancel(),触发图3的doOnCancel,进而触发图2的emitter.cancel(),图2在内部监听取消事件,主动调用图1底层SDK的disconnect()方法,真正做到"用户不看,AI立即停止生成"。


总结(Java程序员版记忆口诀)

  • 图1(回调Handler) = JDBC的ResultSet老式回调,只负责拉数据,别的啥都不管,用起来巨麻烦。

  • 图3(Reactor) = Java 8 Stream的异步Pro Max版,自带线程池调度和超时控制,写起来很优雅。

  • 图2(适配器) = Spring的ResponseBodyEmitter封装 ,悄悄把图1的回调塞进图3的管道,让你在Service层直接返回Flux,Controller层直接往外吐,全程无感。

相关推荐
程序员老刘·1 小时前
Flutter版本选择指南:3.44密集修BUG,9月大限将至 | 2026年7月
flutter·ai编程·跨平台开发
咖啡星人k1 小时前
MonkeyCode 多模型调度与云端开发环境解析
ai编程·monkeycode
长不胖的路人甲1 小时前
斐波那契查找Java 实现 + 完整思路
java·开发语言
祐樹1 小时前
1987年5月25日下午15-17点出生性格、运势和命运
java
dkbnull1 小时前
Spring Boot依赖注入方式对比详解
spring boot·spring
怕浪猫1 小时前
第7章 多智能体协作:从单兵作战到群体智能
openai·agent·ai编程
神奇霸王龙1 小时前
Claude Code 三层架构Subagent并发优化实战
人工智能·ai·架构·agent·ai编程·并发·claude
正儿八经的少年1 小时前
Spring 事务保证数据一致性
java·数据库·spring
Lakers-242 小时前
1987年5月29日晚上19-21点出生性格、运势和命运
java