LLM 是逐 Token 生成文本的。流式响应让用户无需等待完整回复,即可逐字看到输出。本章覆盖低层 StreamingChatModel 回调体系、高层 AI Services 的 TokenStream 链式 API、Flux 集成、取消机制以及原始事件处理。
11.1 为什么需要流式响应?
非流式(Batch):
User: "讲个笑话" → [等待 3 秒...] → "有一天,小明..."
用户体验差:白屏等待,不知道是否卡死
流式(Streaming):
User: "讲个笑话" → "有" → "一天" → "," → "小" → "明" → ...
用户体验好:立即看到第一个字,可以边看边等待
两个核心接口:
// 非流式
public interface ChatModel {
ChatResponse chat(ChatRequest request);
}
// 流式
public interface StreamingChatModel {
void chat(ChatRequest request, StreamingChatResponseHandler handler);
void chat(String userMessage, StreamingChatResponseHandler handler);
}
11.2 低层 API:StreamingChatModel
StreamingChatResponseHandler 接口
public interface StreamingChatResponseHandler {
// ─── 文本流 ───
default void onPartialResponse(String partialResponse) {}
// 带取消能力
default void onPartialResponse(
PartialResponse partialResponse,
PartialResponseContext context
) {}
// ─── 思考过程(推理模型) ───
default void onPartialThinking(PartialThinking partialThinking) {}
default void onPartialThinking(
PartialThinking partialThinking,
PartialThinkingContext context
) {}
// ─── 工具调用流 ───
default void onPartialToolCall(PartialToolCall partialToolCall) {}
default void onPartialToolCall(
PartialToolCall partialToolCall,
PartialToolCallContext context
) {}
// 单个工具调用流结束
default void onCompleteToolCall(CompleteToolCall completeToolCall) {}
// ─── 原始事件(Provider 特定) ───
default void onUnmappedRawEvent(Object rawEvent) {}
// ─── 完成与错误(必须实现) ───
void onCompleteResponse(ChatResponse completeResponse);
void onError(Throwable error);
}
完整示例
StreamingChatModel model = OpenAiStreamingChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.build();
model.chat("Tell me a joke", new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
System.out.print(partialResponse); // 逐 Token 打印
}
@Override
public void onPartialThinking(PartialThinking partialThinking) {
System.out.print("[思考: " + partialThinking.text() + "]");
}
@Override
public void onPartialToolCall(PartialToolCall partialToolCall) {
System.out.println("Tool: " + partialToolCall.name() + " args: " + partialToolCall.partialArguments());
}
@Override
public void onCompleteToolCall(CompleteToolCall completeToolCall) {
// 1. 先拿到 ToolExecutionRequest 对象
ToolExecutionRequest request = completeToolCall.toolExecutionRequest();
// 2. 从 request 中获取函数名和参数
System.out.println("Tool complete: " + request.name() + " args: " + request.arguments());
}
@Override
public void onUnmappedRawEvent(Object rawEvent) {
// Provider 特定事件
if (rawEvent instanceof ServerSentEvent sse) {
System.out.println("SSE: " + sse.event() + " -> " + sse.data());
}
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
System.out.println("\n\nDone! Tokens: " +
completeResponse.metadata().tokenUsage().totalTokenCount());
}
@Override
public void onError(Throwable error) {
error.printStackTrace();
}
});
Lambda 简写
import static dev.langchain4j.model.LambdaStreamingResponseHandler.*;
// 只关心部分响应
model.chat("Tell me a joke", onPartialResponse(System.out::print));
// 关心部分响应 + 错误
model.chat("Tell me a joke", onPartialResponseAndError(System.out::print, Throwable::printStackTrace));
11.3 工具调用的流式过程
当 LLM 在流式模式下决定调用工具时,事件序列如下:
onPartialToolCall(index=0, id="call_abc", name="get_weather", partialArguments="{\"")
onPartialToolCall(index=0, id="call_abc", name="get_weather", partialArguments="city")
onPartialToolCall(index=0, id="call_abc", name="get_weather", partialArguments="\":\"")
onPartialToolCall(index=0, id="call_abc", name="get_weather", partialArguments="London")
onPartialToolCall(index=0, id="call_abc", name="get_weather", partialArguments="\"}")
onCompleteToolCall(index=0, id="call_abc", name="get_weather", arguments="{\"city\":\"London\"}")
// 多个工具时 index 递增:
onPartialToolCall(index=0, ...) // 第一个工具
onPartialToolCall(index=1, ...) // 第二个工具
⚠️ 不是所有 Provider 都流式传输部分工具调用。Bedrock、Google、Mistral、Ollama 只触发
onCompleteToolCall,不触发onPartialToolCall。
11.4 取消流式响应
在三个 *Context-bearing 回调中调用 cancel() 即可断开连接:
model.chat("Tell me a very long story", new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {
process(partialResponse.text());
if (shouldCancel(partialResponse.text())) {
context.streamingHandle().cancel();
// 之后不会再收到任何回调
}
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
// 如果取消了,这里不会被调用
}
@Override
public void onError(Throwable error) {
// 取消不会触发 onError
}
});
取消后:
onCompleteResponse不会被调用onError不会被调用- 无任何后续回调
11.5 原始事件(Unmapped Raw Events)
当 LLM Provider 返回框架未处理的特定事件时,通过 onUnmappedRawEvent 暴露:
model.chat(userMessage, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
System.out.print(partialResponse);
}
@Override
public void onUnmappedRawEvent(Object rawEvent) {
if (rawEvent instanceof ServerSentEvent sse) {
System.out.println("SSE事件: " + sse.event() + " -> " + sse.data());
}
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) { }
@Override
public void onError(Throwable error) { }
});
已处理的类型(部分响应、思考、工具调用)不会重复出现在原始事件中,所以可以放心同时消费两者。
各 Provider 的原始事件类型
| Provider | 原始事件类型 |
|---|---|
| OpenAI (社区), Anthropic, Gemini, Mistral, Ollama | dev.langchain4j.http.client.sse.ServerSentEvent |
| OpenAI (官方) Responses API | com.openai.models.responses.ResponseStreamEvent |
| OpenAI (官方) Chat Completions API | com.openai.models.chat.completions.ChatCompletionChunk |
| Amazon Bedrock | software.amazon.awssdk.services.bedrockruntime.model.ConverseStreamOutput |
| Google GenAI | com.google.genai.types.GenerateContentResponse |
11.6 高层 API:TokenStream
AI Services 中使用 TokenStream 返回类型,提供链式 API:
interface Assistant {
TokenStream chat(String message);
}
StreamingChatModel streamingModel = OpenAiStreamingChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.build();
Assistant assistant = AiServices.create(Assistant.class, streamingModel);
TokenStream tokenStream = assistant.chat("Tell me a joke");
tokenStream
// 文本流
.onPartialResponse((String partial) -> System.out.print(partial))
// 思考流
.onPartialThinking((PartialThinking thinking) -> System.out.print("[思考: " + thinking.text() + "]"))
// RAG 检索
.onRetrieved((List<Content> sources) -> System.out.println("检索到 " + sources.size() + " 篇文档"))
// 中间响应(工具调用循环中)
.onIntermediateResponse((ChatResponse intermediate) -> System.out.println("中间步骤: " + intermediate.aiMessage().text()))
// 工具调用流
.onPartialToolCall((PartialToolCall partialToolCall) -> System.out.println("Tool call: " + partialToolCall.name()))
.beforeToolExecution((BeforeToolExecution before) -> {
System.out.println("Executing: " + before.request().name());
System.out.println("Arguments: " + before.request().arguments());
})
.onToolExecuted((ToolExecution toolExecution) -> System.out.println("Tool done: " + toolExecution.result()))
// 原始事件
.onUnmappedRawEvent((Object rawEvent) -> System.out.println("Raw: " + rawEvent))
// 完成
.onCompleteResponse((ChatResponse completeResponse) -> System.out.println("\nDone! " + completeResponse.metadata().tokenUsage().totalTokenCount() + " tokens"))
// 错误
.onError((Throwable error) -> error.printStackTrace())
// 启动!
.start();
TokenStream 回调链的执行顺序
对于简单问答(无工具调用):
onRetrieved (如有 RAG)
→ onPartialResponse × N
→ onCompleteResponse
对于有工具调用的问答:
onRetrieved (如有 RAG)
→ onPartialToolCall × N (工具参数逐步到达)
→ beforeToolExecution
→ onToolExecuted
→ onIntermediateResponse (中间 LLM 响应)
→ ... (可能多轮工具调用)
→ onPartialResponse × N
→ onCompleteResponse
TokenStream 取消
tokenStream
.onPartialResponseWithContext((partial, context) -> {
System.out.print(partial.text());
if (shouldCancel()) {
context.streamingHandle().cancel();
}
})
.onCompleteResponse(complete -> {})
.onError(error -> {})
.start();
11.7 Flux(响应式流)
需要 langchain4j-reactor 模块支持:
interface Assistant {
Flux<String> chat(String message);
}
Assistant assistant = AiServices.create(Assistant.class, streamingModel);
Flux<String> flux = assistant.chat("Tell me a joke");
flux.subscribe(
token -> System.out.print(token),
error -> error.printStackTrace(),
() -> System.out.println("\nDone!")
);
Spring Boot + SSE
@RestController
class StreamingController {
@Autowired
Assistant assistant;
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> stream(@RequestParam String message) {
return assistant.chat(message)
.map(token -> ServerSentEvent.<String>builder()
.data(token)
.build());
}
}
前端 JavaScript 消费:
const eventSource = new EventSource('/stream?message=讲个笑话');
eventSource.onmessage = (event) => {
document.getElementById('output').innerText += event.data;
};
11.8 Guardrails 在流式场景中的行为
输出护栏在 TokenStream 中的工作方式:
1. LLM 开始流式输出
2. onPartialResponse 回调被缓冲(不立即发送)
3. 流结束后,执行 OutputGuardrail 链
4. 如果通过 → 缓冲的 partial 数据回放给 onPartialResponse
5. 如果 retry/reprompt → 整个流重新执行(同步等待)
6. 最终结果交给 onCompleteResponse
11.9 流式 vs 非流式:选择指南
| 场景 | 推荐 | 原因 |
|---|---|---|
| 聊天 UI | 流式 | 用户体验好,打字机效果 |
| 后台数据处理 | 非流式 | 无需实时展示 |
| 短回复(<50 tokens) | 非流式 | 流式开销大于收益 |
| 工具调用 | 视情况 | 流式能看到工具调用过程,但实现更复杂 |
| 结构化输出 | 非流式 | JSON Schema 模式暂不支持流式 |
| SSE 推送 | Flux | 完美契合 WebFlux 响应式模型 |