第一样东西(图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内置的异步编程流水线工具 。它的职责是提供一套标准操作符 (map、flatMap、timeout、retry)和灵活的线程调度能力 ,让你像写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层直接往外吐,全程无感。


