【LangChain4j系列11】流式响应与 TokenStream

0 阅读5分钟

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, Ollamadev.langchain4j.http.client.sse.ServerSentEvent
OpenAI (官方) Responses APIcom.openai.models.responses.ResponseStreamEvent
OpenAI (官方) Chat Completions APIcom.openai.models.chat.completions.ChatCompletionChunk
Amazon Bedrocksoftware.amazon.awssdk.services.bedrockruntime.model.ConverseStreamOutput
Google GenAIcom.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 响应式模型