06-流式输出学习

4 阅读5分钟

掌握如何在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的核心篇学习。