掌握如何在LangChain4J中处理流式输出,实现实时响应和长文本处理
时间:30分钟 | 难度:⭐⭐⭐ | Week 1 Day 6
官方Example信息
- GitHub链接:StreamingExample.java
- 相关Example:ChatStreamingExample、StreamingResponseExample
- 所在路径:src/main/java/dev/langchain4j/examples/
- 代码行数:约50-80行
- 难度:中级 ⭐⭐⭐
学习目标
- 理解流式输出的核心概念 ✅ 2026-03-07
- 掌握StreamingChatLanguageModel的使用
- 学会处理流式响应的各个阶段
- 实现完整的流式输出服务
- 在生产系统中应用流式输出
- 处理流式输出的错误和异常
🚀 快速入门:什么是流式输出?
流式输出的本质
普通输出(等待完整回复):
用户: 写一篇1000字的文章
LLM: [等待30秒] → 返回完整1000字
流式输出(实时显示):
用户: 写一篇1000字的文章
LLM: 文 → 章 → 标 → 题 → ... → (实时显示)
为什么需要流式输出?
问题:
1. 长文本生成时间长,用户等待时间长
2. 用户体验差(感觉系统没反应)
3. 无法知道进度(什么时候结束?)
4. 不适合实时聊天场景
解决方案:流式输出
1. 边生成边返回
2. 用户可以立即看到内容
3. 大大改善用户体验
4. 适合聊天、长文本生成等场景
流式输出的工作流程
┌─────────────────────────────────────────────┐
│ 用户请求 │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ LLM开始生成Token │
└────────────────┬────────────────────────────┘
│
┌────────┴────────┐
│ │
▼ ▼
生成Token1 等待更多Token
│ │
│ ▼
│ 生成Token2,3,...
│ │
│ ▼
└─────────► 流式返回给客户端
│
▼
客户端实时显示
深度讲解
1️⃣ StreamingChatLanguageModel 基础
@Service
public class StreamingChatService {
// StreamingChatLanguageModel 支持流式输出
private final StreamingChatLanguageModel model;
public StreamingChatService() {
this.model = OpenAiStreamingChatModel.builder()
.apiKey("sk-proj-xxx")
.modelName("gpt-4o-mini")
.build();
}
/**
* 基础流式聊天
*/
public void streamChat(String userMessage) {
model.stream(userMessage, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
// 每次收到新的Token时调用
System.out.print(token);
}
@Override
public void onComplete(Response<AiMessage> response) {
// 流式输出完成
System.out.println("\n输出完成");
}
@Override
public void onError(Throwable error) {
// 错误处理
System.err.println("错误: " + error.getMessage());
}
});
}
}
2️⃣ 完整的流式服务实现
@Service
public class FullStreamingService {
private final StreamingChatLanguageModel model;
public FullStreamingService() {
this.model = OpenAiStreamingChatModel.builder()
.apiKey("sk-proj-xxx")
.modelName("gpt-4o-mini")
.temperature(0.7)
.build();
}
/**
* 流式输出带缓存
*/
public String streamWithBuffer(String userMessage) {
StringBuilder fullResponse = new StringBuilder();
model.stream(userMessage, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
// 边输出边缓存
fullResponse.append(token);
System.out.print(token); // 实时显示
}
@Override
public void onComplete(Response<AiMessage> response) {
System.out.println("\n输出完成");
System.out.println("总字数: " + fullResponse.length());
}
@Override
public void onError(Throwable error) {
System.err.println("流式输出失败: " + error.getMessage());
}
});
return fullResponse.toString();
}
/**
* 流式输出到文件
*/
public void streamToFile(String userMessage, String filePath) {
try (FileWriter writer = new FileWriter(filePath)) {
model.stream(userMessage, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
try {
writer.write(token);
writer.flush(); // 实时刷盘
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void onComplete(Response<AiMessage> response) {
System.out.println("文件输出完成");
}
@Override
public void onError(Throwable error) {
System.err.println("文件输出失败: " + error.getMessage());
}
});
} catch (IOException e) {
throw new RuntimeException("打开文件失败", e);
}
}
}
3️⃣ 流式输出的多种处理方式
方式1:简单的实时输出
public void simpleStream(String question) {
model.stream(question, token -> {
System.out.print(token); // 实时打印
});
}
方式2:带统计信息的流式输出
public void streamWithStatistics(String question) {
long startTime = System.currentTimeMillis();
int tokenCount = 0;
model.stream(question, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
System.out.print(token);
tokenCount++;
}
@Override
public void onComplete(Response<AiMessage> response) {
long duration = System.currentTimeMillis() - startTime;
System.out.println("\n");
System.out.println("统计信息:");
System.out.println("- Token数: " + tokenCount);
System.out.println("- 耗时: " + duration + "ms");
System.out.println("- 速度: " + (tokenCount * 1000.0 / duration) + " tokens/s");
}
@Override
public void onError(Throwable error) {
System.err.println("输出失败: " + error.getMessage());
}
});
}
方式3:流式输出到WebSocket(前后端实时通信)
@RestController
@RequestMapping("/api/chat")
public class StreamingChatController {
@Autowired
private StreamingChatLanguageModel model;
@PostMapping("/stream")
public SseEmitter streamChat(@RequestParam String question) {
SseEmitter emitter = new SseEmitter();
// 异步处理流式输出
new Thread(() -> {
try {
model.stream(question, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
try {
// 发送给前端
emitter.send(SseEmitter.event()
.name("message")
.data(token));
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onComplete(Response<AiMessage> response) {
try {
emitter.send(SseEmitter.event()
.name("done")
.data("complete"));
emitter.complete();
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onError(Throwable error) {
emitter.completeWithError(error);
}
});
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
}
4️⃣ 流式输出错误处理
@Service
public class RobustStreamingService {
private final StreamingChatLanguageModel model;
private static final Logger log = LoggerFactory.getLogger(RobustStreamingService.class);
public void streamWithErrorHandling(String question) {
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
model.stream(question, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
System.out.print(token);
}
@Override
public void onComplete(Response<AiMessage> response) {
log.info("流式输出成功");
}
@Override
public void onError(Throwable error) {
log.error("流式输出失败: ", error);
if (error instanceof RateLimitException) {
log.warn("触发频率限制,等待重试");
try {
Thread.sleep(5000); // 等待5秒后重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
});
return; // 成功则退出
} catch (Exception e) {
retryCount++;
log.warn("第{}次重试失败", retryCount);
if (retryCount >= maxRetries) {
throw new RuntimeException("流式输出失败,已重试" + maxRetries + "次", e);
}
}
}
}
}
💻 实战:完整的流式聊天应用
@Service
public class StreamingChatApplication {
@Autowired
private StreamingChatLanguageModel model;
private static final Logger log = LoggerFactory.getLogger(StreamingChatApplication.class);
/**
* 生产级别的流式聊天实现
*/
public void chat(String userMessage, StreamingResponseCallback callback) {
long startTime = System.currentTimeMillis();
int[] tokenCount = {0};
try {
// 验证输入
if (userMessage == null || userMessage.trim().isEmpty()) {
callback.onError(new IllegalArgumentException("消息不能为空"));
return;
}
model.stream(userMessage, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
try {
tokenCount[0]++;
callback.onToken(token);
} catch (Exception e) {
log.error("处理Token失败", e);
onError(e);
}
}
@Override
public void onComplete(Response<AiMessage> response) {
long duration = System.currentTimeMillis() - startTime;
StreamingMetrics metrics = StreamingMetrics.builder()
.tokenCount(tokenCount[0])
.duration(duration)
.tokensPerSecond(tokenCount[0] * 1000.0 / duration)
.build();
callback.onComplete(metrics);
log.info("流式聊天完成: {}tokens in {}ms", tokenCount[0], duration);
}
@Override
public void onError(Throwable error) {
log.error("流式聊天错误", error);
callback.onError(error);
}
});
} catch (Exception e) {
log.error("启动流式聊天失败", e);
callback.onError(e);
}
}
}
// 回调接口
public interface StreamingResponseCallback {
void onToken(String token);
void onComplete(StreamingMetrics metrics);
void onError(Throwable error);
}
@Data
@Builder
class StreamingMetrics {
private int tokenCount;
private long duration;
private double tokensPerSecond;
}
🔧 流式输出最佳实践
✅ 好的做法
// 1. 合理的超时控制
model.stream(question, handler); // 设置合理超时
// 2. 完整的错误处理
public void onError(Throwable error) {
if (error instanceof TimeoutException) {
// 处理超时
} else if (error instanceof RateLimitException) {
// 处理频率限制
}
}
// 3. 资源释放
try (SomeResource resource = new SomeResource()) {
model.stream(question, handler);
}
// 4. 性能监控
long startTime = System.currentTimeMillis();
model.stream(question, handler);
long duration = System.currentTimeMillis() - startTime;
❌ 坏的做法
// ❌ 没有错误处理
model.stream(question, token -> System.out.print(token));
// ❌ 阻塞主线程
// 应该异步处理,不要阻塞UI线程
model.stream(question, handler); // 在UI线程中调用
// ❌ 没有超时控制
// 可能导致连接一直挂起
学习成果检查:
- 能解释什么是流式输出及为什么需要
- 能使用StreamingChatLanguageModel
- 能处理流式响应的各个阶段
- 能实现流式输出到文件
- 能实现前后端实时通信的流式输出
- 能处理流式输出的错误和超时
下一步:完成Week 1的学习总结和周发布,准备开始Week 2的核心篇学习。