mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
Automated OpenWiki documentation update. OpenWiki result: success When the result is `failure`, this PR intentionally preserves only the pages completed before the failure. Merge it to make that progress the baseline for the next scheduled run. Co-authored-by: npentrel <5212232+npentrel@users.noreply.github.com>
594 lines
31 KiB
Markdown
594 lines
31 KiB
Markdown
---
|
|
type: "Concept"
|
|
title: "Streaming: Token-by-Token Output"
|
|
description: "How streaming works across LLM components and chains, token-by-token delivery via AIMessageChunk, callback integration, and memory/latency tradeoffs."
|
|
tags: [streaming, token-streaming, llm-output, chat-models, callbacks, astream, real-time-feedback]
|
|
verified:
|
|
- by: openwiki/0.5.0
|
|
at: 2026-09-28T08:35:20.640Z
|
|
sources:
|
|
- id: openwiki-source-c9313cf42f0120d86b20245f
|
|
resource: repo://libs/core/langchain_core/callbacks/base.py
|
|
- id: openwiki-source-c7a2c3ef4ec61c3e28011205
|
|
resource: repo://libs/core/langchain_core/callbacks/streaming_stdout.py
|
|
- id: openwiki-source-5f8bc32563177d89fbab9b2f
|
|
resource: repo://libs/core/langchain_core/language_models/chat_model_stream.py
|
|
- id: openwiki-source-c52037e7b642f7ac5a7642a8
|
|
resource: repo://libs/core/langchain_core/language_models/chat_models.py
|
|
- id: openwiki-source-77dc1fb726463969f9d53658
|
|
resource: repo://libs/core/langchain_core/messages/ai.py
|
|
- id: openwiki-source-b32b84365d17276620c41ebc
|
|
resource: repo://libs/core/langchain_core/messages/base.py
|
|
- id: openwiki-source-8bb392f5dbc1fe7faaf52430
|
|
resource: repo://libs/core/langchain_core/messages/human.py
|
|
- id: openwiki-source-dad8cfeb38a829e03e165986
|
|
resource: repo://libs/core/langchain_core/messages/system.py
|
|
- id: openwiki-source-a1981e868973f6fd7f71e12e
|
|
resource: repo://libs/core/langchain_core/runnables/base.py
|
|
- id: openwiki-source-ff9b926753e6cc6fe87acfe7
|
|
resource: repo://libs/langchain_v1/tests/unit_tests/agents/test_agent_streaming.py
|
|
generated: { by: "openwiki/0.5.0", at: "2026-09-28T08:35:20.640Z" }
|
|
---
|
|
|
|
## Overview
|
|
|
|
**Streaming** is the mechanism by which LangChain delivers model output incrementally, token by token, rather than waiting for the entire response. This enables real-time feedback in web UIs, console displays, and other user-facing contexts, and forms the foundation for building responsive applications that do not block on model latency.
|
|
|
|
Instead of blocking with `invoke()` until a full response is ready, applications call `stream()` or `astream()` and receive a sequence of partial outputs as they arrive from the model. Each chunk is an `AIMessageChunk` carrying delta content. Callbacks intercept these chunks via the `on_llm_new_token` event, making it possible to observe, log, or react to each token without collecting the entire response first.
|
|
|
|
Streaming flows through chains—prompts, models, output parsers, and other runnables—preserving incremental output delivery at each stage. By composition, a chain automatically supports streaming if all its components do. This page documents the mechanics of streaming across components, the trade-offs versus non-streaming invoke, and how to integrate streaming into applications.
|
|
|
|
## Synchronous Streaming: stream()
|
|
|
|
**Location**: `repo://libs/core/langchain_core/language_models/chat_models.py#L727-L856`
|
|
|
|
`BaseChatModel.stream()` is the primary synchronous streaming entry point. It yields `AIMessageChunk` objects as they are produced by the underlying model, with incremental content—a single token, a fragment of JSON, or a structured block update.
|
|
|
|
### Control Flow
|
|
|
|
1. **Check if streaming is implemented**: `_should_stream()` determines whether the model supports streaming. It checks:
|
|
- Whether `_stream()` is implemented on the model (not inherited from base)
|
|
- Whether streaming is explicitly disabled via `disable_streaming`, `stream=False`, or `streaming=False` on the model
|
|
- Whether an explicit `stream=True` kwarg is passed
|
|
- Whether a streaming callback handler is attached to the model
|
|
|
|
If streaming is not supported, `stream()` falls back to `invoke()` and yields one complete result cast to `AIMessageChunk`.
|
|
|
|
2. **Initialize callbacks**: A `CallbackManager` is configured from the provided `RunnableConfig`, binding callbacks, tags, and metadata for tracing and observability.
|
|
|
|
3. **Fire on_chat_model_start**: The callback lifecycle begins with `on_chat_model_start`, signaling that LLM invocation is beginning. This event is fired before the first token is yielded.
|
|
|
|
4. **Acquire rate limit**: If a rate limiter is attached to the model, `stream()` acquires a permit before beginning, blocking until the rate limit allows.
|
|
|
|
5. **Iterate model chunks**: For each `ChatGenerationChunk` from the underlying `_stream()` implementation:
|
|
- The chunk's message ID is set to a unique run ID (prefixed with `LC_ID_PREFIX`) if not already present, ensuring traceability.
|
|
- Response metadata (model provider, latency, token usage, etc.) is computed and attached via `_gen_info_and_msg_metadata()`.
|
|
- If the model's output version is "v1" (content-block structured format), content is transformed to content blocks and indexed.
|
|
- **on_llm_new_token is fired** with the chunk's content and the full chunk object, allowing callbacks to observe or buffer each token.
|
|
- The chunk message is cast to `AIMessageChunk` and yielded immediately to the caller.
|
|
- Chunks are accumulated in memory for later aggregation.
|
|
|
|
6. **Yield final "last" chunk**: After the model finishes, if no explicit `chunk_position="last"` was set, an empty chunk with `chunk_position="last"` is yielded. This signals to parsers and consumers that the stream is complete and that `tool_call_chunks` should be finalized into complete `tool_calls`.
|
|
|
|
7. **Callback lifecycle closes**: If successful, `on_llm_end` fires with a merged `ChatGeneration` containing all chunks. If an exception occurs, `on_llm_error` fires with partial accumulation, allowing callbacks to observe failures before the exception is re-raised.
|
|
|
|
### Fallback Behavior
|
|
|
|
If the model does not implement streaming (checked via `_should_stream(async_api=False)`), `stream()` delegates to `invoke()` and yields a single result cast to `AIMessageChunk`. This ensures all models provide a consistent streaming interface, even if only non-streaming invoke is available.
|
|
|
|
## Asynchronous Streaming: astream()
|
|
|
|
**Location**: `repo://libs/core/langchain_core/language_models/chat_models.py#L858-L991`
|
|
|
|
`BaseChatModel.astream()` is the async variant of `stream()`, mirroring the synchronous logic but using async/await and `AsyncCallbackManager`.
|
|
|
|
**Key differences**:
|
|
- Uses `await` for callback events (`await run_manager.on_llm_new_token(...)`, `await run_manager.on_llm_end(...)`)
|
|
- Iterates via `async for chunk in self._astream(...)`
|
|
- Acquires rate limit via `await self.rate_limiter.aacquire(blocking=True)`
|
|
|
|
The async streaming protocol is identical to sync: check `_should_stream(async_api=True)`, initialize callbacks, yield chunks immediately as they arrive, fire callbacks per token, finalize tool call chunks on the "last" signal.
|
|
|
|
### Async/Await Patterns
|
|
|
|
Applications using `astream()` should consume the async iterator in a loop:
|
|
|
|
```python
|
|
async for chunk in model.astream(messages):
|
|
# Process chunk immediately
|
|
print(chunk.content, end="", flush=True)
|
|
|
|
# OR collect chunks for later processing
|
|
chunks = []
|
|
async for chunk in model.astream(messages):
|
|
chunks.append(chunk)
|
|
final_message = sum(chunks) # Merge via + operator
|
|
```
|
|
|
|
## Message Chunks: Types and Composition
|
|
|
|
**Location**: `repo://libs/core/langchain_core/messages/ai.py#L418-L536`
|
|
|
|
During streaming, models emit **chunk variants** of standard message types. All chunk types inherit from `BaseMessageChunk` and are designed to be **mergeable** via the `+` operator, enabling efficient aggregation of streaming output.
|
|
|
|
### Message Chunk Types
|
|
|
|
Each core message type has a corresponding chunk variant:
|
|
|
|
- **`AIMessageChunk`**: Emitted by chat models during streaming. Contains:
|
|
- `content`: String or list of content blocks (delta for that step).
|
|
- `tool_call_chunks`: List of `ToolCallChunk` objects (incomplete tool calls being streamed).
|
|
- `chunk_position`: Optional `"last"` sentinel signaling stream completion.
|
|
- `response_metadata`: Model metadata (latency, provider, usage counts, etc.).
|
|
|
|
- **`HumanMessageChunk`**: Represents incremental user input. Used in streaming scenarios where user messages arrive in chunks (e.g., real-time voice transcription or chunked uploads).
|
|
|
|
- **`SystemMessageChunk`**: Represents incremental system prompt updates. Less common than AIMessageChunk but available for consistency.
|
|
|
|
- **`ToolMessageChunk`**: Represents incremental tool results. Supports streaming tool outputs that arrive in chunks (e.g., file downloads, streaming API responses).
|
|
|
|
All chunk types support the `+` operator for merging, making them composable at any level of a streaming pipeline.
|
|
|
|
### AIMessageChunk Structure
|
|
|
|
`AIMessageChunk` is the message type yielded during streaming. Unlike `AIMessage`, it represents a **partial, incremental update** to a conversation message and supports merging via the `+` operator.
|
|
|
|
**Fields**:
|
|
- **content**: String or list of content blocks (when `output_version="v1"`). During streaming, each chunk contains only the new token(s) or delta for that step. Content accumulates across chunks: text chunks concatenate, JSON chunks may append partial objects or arrays.
|
|
- **tool_call_chunks**: List of `ToolCallChunk` objects (incomplete tool calls being streamed). These are progressively updated as the model produces call ID, function name, and argument JSON. Arguments are accumulated and parsed incrementally via `parse_partial_json()`.
|
|
- **chunk_position**: Optional sentinel; when set to `"last"`, indicates the final chunk in the stream, triggering finalization of tool calls and reasoning blocks. When this chunk is aggregated, `tool_call_chunks` are parsed into complete `tool_calls` and `invalid_tool_calls` via the `init_tool_calls()` validator.
|
|
- **response_metadata**: Model-specific metadata (latency, model_provider, usage counters, finish reason, etc.) attached by the streaming handler. Metadata is merged across chunks, with usage counts summed.
|
|
|
|
### Merging Chunks via the + Operator
|
|
|
|
Streaming chunks accumulate via the `+` operator (implemented in `add_ai_message_chunks()`), which:
|
|
|
|
1. **Merges content**: Text content is concatenated; structured content blocks are merged according to block type.
|
|
2. **Concatenates tool_call arguments**: Arguments from tool_call_chunks are appended progressively, enabling incremental JSON parsing.
|
|
3. **Combines metadata**: Response metadata and usage counts are merged; for duplicated keys, later values override (except usage, which is summed).
|
|
4. **Preserves chunk_position**: If any chunk in the merge has `chunk_position="last"`, the result marks position as "last", triggering tool call finalization.
|
|
5. **Selects best ID**: The chunk ID is chosen by rank: provider-assigned (non-`LC_*` prefixed) > `LC_run_*` > `lc_*` auto IDs.
|
|
|
|
A complete `AIMessage` with finalized `tool_calls` (not chunks) is reconstructed when chunks are merged or when the "last" signal is received:
|
|
|
|
```python
|
|
# Accumulate chunks
|
|
chunks = []
|
|
async for chunk in model.astream(messages):
|
|
chunks.append(chunk)
|
|
|
|
# Merge all chunks into one message
|
|
final_message = chunks[0]
|
|
for chunk in chunks[1:]:
|
|
final_message = final_message + chunk
|
|
|
|
# tool_calls are now complete ToolCall objects, not ToolCallChunk
|
|
for tool_call in final_message.tool_calls:
|
|
print(tool_call["name"], tool_call["args"])
|
|
```
|
|
|
|
### Chunk Composition Flow
|
|
|
|
The following diagram shows how chunks flow through a streaming pipeline and merge into complete messages:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Model
|
|
participant StreamIterator as stream()<br/> Iterator
|
|
participant Callback as on_llm_new_token<br/> Callback
|
|
participant Aggregator as Chunk<br/> Accumulator
|
|
participant Merger as Chunk<br/> Merge (+ op)
|
|
participant Complete as Final<br/> AIMessage
|
|
|
|
Model->>StreamIterator: yield AIMessageChunk<br/> content="Hello"
|
|
StreamIterator->>Callback: fire on_llm_new_token<br/> token="Hello"
|
|
StreamIterator->>Aggregator: buffer chunk
|
|
StreamIterator-->>Model: pull next
|
|
|
|
Model->>StreamIterator: yield AIMessageChunk<br/> content=" world"
|
|
StreamIterator->>Callback: fire on_llm_new_token<br/> token=" world"
|
|
StreamIterator->>Aggregator: buffer chunk
|
|
StreamIterator-->>Model: pull next
|
|
|
|
Model->>StreamIterator: yield AIMessageChunk<br/> chunk_position="last"
|
|
StreamIterator->>Callback: fire on_llm_new_token<br/> token=""
|
|
StreamIterator->>Aggregator: buffer chunk
|
|
|
|
Note over Aggregator: All chunks buffered
|
|
Aggregator->>Merger: merge chunks:<br/> chunk1 + chunk2 + chunk_last
|
|
Merger->>Complete: single AIMessage<br/> with finalized tool_calls
|
|
Callback->>Callback: fire on_llm_end<br/> with merged result
|
|
```
|
|
|
|
The diagram shows how token-by-token chunks are yielded immediately, callbacks are fired for real-time observation, and final aggregation occurs only at the end.
|
|
|
|
## Token-Level Streaming in Action
|
|
|
|
Streaming operates at the **token level**, not the message level. Here's how tokens flow through a typical streaming sequence:
|
|
|
|
### Text Token Streaming
|
|
|
|
```python
|
|
# Model generates: "The capital of France is Paris"
|
|
|
|
from langchain_core.callbacks import StreamingStdOutCallbackHandler
|
|
|
|
messages = [HumanMessage("What is the capital of France?")]
|
|
callback = StreamingStdOutCallbackHandler()
|
|
|
|
for chunk in model.stream(messages, config=RunnableConfig(callbacks=[callback])):
|
|
# Each iteration receives one chunk
|
|
# Chunks might be: "The", " capital", " of", " France", " is", " Paris"
|
|
# callback.on_llm_new_token fires for each, writing to stdout immediately
|
|
|
|
# chunk.content is the token string
|
|
# chunk.tool_call_chunks is empty (no tools called)
|
|
# chunk.response_metadata has model latency, provider, etc.
|
|
pass
|
|
```
|
|
|
|
The `StreamingStdOutCallbackHandler` writes each token to stdout immediately, producing real-time text output without waiting for the full response.
|
|
|
|
### Tool Call Streaming
|
|
|
|
Tool call arguments stream across multiple chunks:
|
|
|
|
```python
|
|
# Model generates a tool call with JSON args:
|
|
# get_weather(city="Paris", unit="celsius")
|
|
|
|
chunks = []
|
|
async for chunk in model.astream(messages):
|
|
chunks.append(chunk)
|
|
|
|
# Each chunk might contain partial tool call data:
|
|
# Chunk 1: tool_call_chunks=[{"id": "tc1", "name": "get_weather", "args": ""}]
|
|
# Chunk 2: tool_call_chunks=[{"id": "tc1", "name": "get_weather", "args": "{\"ci"}]
|
|
# Chunk 3: tool_call_chunks=[{"id": "tc1", "name": "get_weather", "args": "...ty\":\"Paris\",\"unit\":"}]
|
|
# Chunk 4: tool_call_chunks=[{"id": "tc1", "name": "get_weather", "args": "...\"celsius\"}"]
|
|
# Chunk 5 (last): chunk_position="last" triggers finalization
|
|
|
|
if chunk.tool_call_chunks:
|
|
for tc in chunk.tool_call_chunks:
|
|
# parse_partial_json handles incomplete JSON gracefully
|
|
args = parse_partial_json(tc.get("args", ""))
|
|
print(f"Tool: {tc['name']}, partial args: {args}")
|
|
|
|
# Merge to get final message with complete tool_calls
|
|
final_message = sum(chunks[1:], chunks[0])
|
|
for tool_call in final_message.tool_calls:
|
|
print(f"Final: {tool_call['name']}({tool_call['args']})")
|
|
```
|
|
|
|
At each step, callbacks and custom handlers can observe partial tool call construction, enabling real-time visibility into tool invocation even before arguments are complete.
|
|
|
|
## Callback Integration: on_llm_new_token
|
|
|
|
**Location**: `repo://libs/core/langchain_core/callbacks/base.py#L65-L88`
|
|
|
|
The `on_llm_new_token` callback fires for each token or chunk during streaming, enabling real-time observation and logging.
|
|
|
|
### Callback Signature
|
|
|
|
```python
|
|
def on_llm_new_token(
|
|
self,
|
|
token: str | list[str | dict[str, Any]],
|
|
*,
|
|
chunk: GenerationChunk | ChatGenerationChunk | None = None,
|
|
run_id: UUID,
|
|
parent_run_id: UUID | None = None,
|
|
tags: list[str] | None = None,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
```
|
|
|
|
- **token**: The string token or list of content blocks (when output_version="v1"). For text streaming, this is a single word or subword; for structured output, this is a list of content block dicts with `type`, `text`, `reasoning`, `tool_call_chunk`, etc.
|
|
- **chunk**: The full `ChatGenerationChunk` carrying metadata, message ID, response metadata, and tool_call_chunks. This allows callbacks to inspect the complete chunk structure, not just the token.
|
|
- **run_id**: Unique identifier for this streaming run, used for tracing and correlation with parent operations.
|
|
- **parent_run_id**: ID of the parent run (chain or agent) that invoked this model.
|
|
- **tags**: Inheritable tags from the calling context, useful for filtering or routing callbacks.
|
|
|
|
### Built-in: StreamingStdOutCallbackHandler
|
|
|
|
```python
|
|
from langchain_core.callbacks import StreamingStdOutCallbackHandler
|
|
|
|
callback = StreamingStdOutCallbackHandler()
|
|
|
|
# Callbacks are passed via RunnableConfig
|
|
for chunk in model.stream(
|
|
messages,
|
|
config=RunnableConfig(callbacks=[callback])
|
|
):
|
|
pass # callback prints each token to stdout
|
|
```
|
|
|
|
The `StreamingStdOutCallbackHandler` implements `on_llm_new_token` to write tokens to `sys.stdout` immediately, making streaming output visible in real-time without buffering.
|
|
|
|
### Custom Streaming Callbacks
|
|
|
|
Create custom callbacks by subclassing `BaseCallbackHandler`:
|
|
|
|
```python
|
|
from langchain_core.callbacks import BaseCallbackHandler
|
|
from typing import Any
|
|
|
|
class MyStreamingCallback(BaseCallbackHandler):
|
|
def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
|
|
# Send token to WebSocket, log to database, etc.
|
|
websocket.send_json({"token": token})
|
|
|
|
def on_llm_end(self, response, **kwargs: Any) -> None:
|
|
# Final response complete
|
|
websocket.send_json({"status": "complete"})
|
|
|
|
def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
|
|
# Error occurred during streaming
|
|
websocket.send_json({"error": str(error)})
|
|
```
|
|
|
|
### LangSmith Integration
|
|
|
|
LangSmith automatically captures streaming via callbacks. When you use `stream()` with LangSmith callbacks attached:
|
|
|
|
1. **run_id** from `on_llm_new_token` is used to trace the streaming operation.
|
|
2. Each token fires `on_llm_new_token`, allowing LangSmith to record token-level granularity.
|
|
3. `on_llm_end` provides the final merged result, allowing LangSmith to record total latency and token counts.
|
|
4. If streaming fails, `on_llm_error` records the failure with partial results.
|
|
|
|
LangSmith projects these traces in the UI as token-by-token progress, enabling real-time observability of model streaming behavior.
|
|
|
|
## Streaming Through Chains
|
|
|
|
Streaming flows through chains composed of runnables (prompts, models, parsers). The streaming protocol is implemented at each stage via the `stream()` and `transform()` methods on `Runnable`.
|
|
|
|
### Default Behavior
|
|
|
|
**Location**: `repo://libs/core/langchain_core/runnables/base.py#L1194-L1235`
|
|
|
|
By default, `Runnable.stream()` yields one full output from `invoke()`. Subclasses that support streaming override `stream()` or `transform()` to yield chunks. The `transform()` method is the core streaming interface: it accepts an iterator of inputs and yields an iterator of outputs, enabling stateful transformations.
|
|
|
|
### Streaming through RunnableSequence
|
|
|
|
**Location**: `repo://libs/core/langchain_core/runnables/base.py#L3075-L3320`
|
|
|
|
`RunnableSequence` (a chain created with the `|` operator) automatically supports streaming if:
|
|
|
|
1. **All upstream components implement transform**: The `transform()` method maps streaming input to streaming output, enabling end-to-end streaming without buffering.
|
|
2. **The last component produces chunks**: Output parsers and models implement `transform()` to yield partial results.
|
|
|
|
If any component does not implement `transform()`, streaming begins only after that component completes (blocking point). Multiple blocking components create multiple buffering points, but the final output still streams from the last component if it supports streaming.
|
|
|
|
**Important**: `RunnableLambda` does not implement `transform()` by default, so it acts as a blocking component. For custom logic with streaming, subclass `Runnable` and override `transform()`.
|
|
|
|
### Streaming Example: Model → Parser
|
|
|
|
```python
|
|
from langchain_openai import ChatOpenAI
|
|
from langchain_core.output_parsers import StrOutputParser
|
|
|
|
model = ChatOpenAI()
|
|
parser = StrOutputParser()
|
|
chain = model | parser
|
|
|
|
# stream yields parser outputs incrementally as tokens arrive
|
|
for chunk in chain.stream("What is 2+2?"):
|
|
print(chunk, end="", flush=True)
|
|
```
|
|
|
|
When `model.stream()` yields chunks, the parser's `transform()` (inherited from `BaseTransformOutputParser`) consumes each chunk and yields its transformation. Text parsers (like `StrOutputParser`) extract text from `AIMessageChunk` and yield strings directly; JSON parsers yield partial JSON objects as they become parseable via `parse_partial_json()`.
|
|
|
|
### Streaming Mechanics: _transform_stream_with_config
|
|
|
|
**Location**: `repo://libs/core/langchain_core/runnables/base.py#L2502-L2599`
|
|
|
|
The `_transform_stream_with_config()` helper manages streaming with callbacks. It:
|
|
|
|
1. **Tees the input iterator** so the first element can be inspected for tracing without consuming it.
|
|
2. **Fires on_chain_start** before the transformer begins, signaling the start of a streaming chain operation.
|
|
3. **Invokes the transformer function** with the remaining input iterator and child callbacks.
|
|
4. **Yields chunks immediately** as the transformer produces them, enabling responsive streaming.
|
|
5. **Accumulates outputs** for the on_chain_end callback, optionally merging chunks via `+` if supported.
|
|
6. **Fires on_chain_end or on_chain_error** at completion, providing final merged output or exception context.
|
|
|
|
This mechanism ensures streaming callbacks fire for each chunk and that parent run managers know when a chain's streaming is complete.
|
|
|
|
## Streaming via stream_events: ChatModelStream
|
|
|
|
**Location**: `repo://libs/core/langchain_core/language_models/chat_model_stream.py`
|
|
|
|
For advanced use cases requiring detailed event granularity, `BaseChatModel.stream_events(version="v3")` returns a `ChatModelStream` object that exposes **typed projection properties** (`.text`, `.tool_calls`, `.usage`, `.reasoning`, `.output`) which accumulate protocol events as they arrive.
|
|
|
|
### Structured Event Streaming
|
|
|
|
Unlike token streaming (`stream()`), which yields tokens, `stream_events()` yields **protocol events**—structured objects representing model state changes:
|
|
|
|
- **text-delta**: Incremental text generation
|
|
- **reasoning-delta**: Incremental reasoning/thinking content (when supported)
|
|
- **tool_call_chunk**: Partial tool call with accumulated arguments
|
|
- **usage**: Token usage update (input, output, cached, etc.)
|
|
|
|
### Pull-Based Backpressure
|
|
|
|
The `ChatModelStream` and its projections (`.text`, `.tool_calls`, etc.) implement **pull-based backpressure** via the `SyncProjection` and `AsyncProjection` classes. When a consumer reads from a projection and catches up to the buffer:
|
|
|
|
1. The projection calls `_request_more()` to pull additional events from the producer (the model/graph).
|
|
2. The producer resumes and generates the next batch of events.
|
|
3. The projection buffers events and yields them to the consumer.
|
|
|
|
This backpressure mechanism prevents unbounded memory growth: the producer only generates events as the consumer requests them. Unlike callback-driven streaming (which delivers all tokens as fast as the model produces them), pull-based streaming allows the consumer to set the pace.
|
|
|
|
**Example: Consuming with backpressure**
|
|
|
|
```python
|
|
# Pull events on demand; producer waits if no consumer is pulling
|
|
for event in model.stream_events(messages, version="v3"):
|
|
if should_stop_early():
|
|
break # Producer stops; no buffered events accumulate
|
|
process_event(event)
|
|
|
|
# Or consume a specific projection with type safety
|
|
stream = model.stream_events(messages, version="v3")
|
|
for text_delta in stream.text: # Only text events
|
|
print(text_delta)
|
|
```
|
|
|
|
## Streaming in Agent Execution
|
|
|
|
**Location**: `repo://libs/langchain_v1/tests/unit_tests/agents/test_agent_streaming.py`
|
|
|
|
Agents created via `create_agent()` support streaming execution via `stream_events(version="v3")` on the returned agent graph. This enables real-time visibility into:
|
|
|
|
### Agent-Level Streaming
|
|
|
|
- **Tool calls**: Each tool call is tracked as it's generated, with its ID, name, and arguments accumulating incrementally.
|
|
- **Tool outputs**: When tools emit output deltas (via `runtime.emit_output_delta()`), these are visible in the stream as they arrive.
|
|
- **Messages**: The full conversation history updates as the agent runs, with each turn visible immediately.
|
|
- **Subgraphs**: When agent tools invoke nested agents, subgraph handles expose their own tool call projections, enabling hierarchical visibility.
|
|
|
|
### Tool Output Streaming
|
|
|
|
Tools can emit output deltas mid-execution:
|
|
|
|
```python
|
|
from langchain_core.tools import tool
|
|
from langchain.tools import ToolRuntime
|
|
|
|
@tool
|
|
def long_running_task(query: str, runtime: ToolRuntime) -> str:
|
|
"""A task that streams output as it runs."""
|
|
for chunk in process_incrementally(query):
|
|
# Emit partial output immediately
|
|
runtime.emit_output_delta(chunk)
|
|
|
|
# Return final result
|
|
return compute_final_result(query)
|
|
|
|
# When streaming the agent:
|
|
for item in agent.stream_events({"messages": [HumanMessage("go")]}, version="v3"):
|
|
if item.get("type") == "tool_output_delta":
|
|
print(f"Tool emitted: {item['content']}")
|
|
```
|
|
|
|
The agent stream captures these deltas in real-time, making them visible to observers before the tool completes.
|
|
|
|
### Agentic Loop Streaming
|
|
|
|
The agent execution loop itself can be streamed:
|
|
|
|
```python
|
|
from langchain.agents import create_agent
|
|
|
|
agent = create_agent(model, tools)
|
|
|
|
# Stream events from the agent graph
|
|
for event in agent.stream_events(
|
|
{"messages": [HumanMessage("Question")]},
|
|
version="v3"
|
|
):
|
|
# Event types: "on_llm_start", "on_llm_new_token", "on_tool_start",
|
|
# "on_tool_end", "on_chain_end", custom events, etc.
|
|
if event["type"] == "on_llm_new_token":
|
|
print(f"Token: {event['content']}")
|
|
elif event["type"] == "on_tool_start":
|
|
print(f"Tool: {event['tool']}")
|
|
elif event["type"] == "on_tool_end":
|
|
print(f"Tool result: {event['output']}")
|
|
```
|
|
|
|
### Tool Call Streaming with Projections
|
|
|
|
The most convenient way to stream agent execution is via projections:
|
|
|
|
```python
|
|
run = agent.stream_events({"messages": [HumanMessage("go")]}, version="v3")
|
|
|
|
# Stream tool calls as they're generated
|
|
for tool_call in run.tool_calls:
|
|
print(f"Tool call: {tool_call.tool_name}")
|
|
print(f"Args: {tool_call.args}")
|
|
|
|
# Stream tool output deltas if the tool emits them
|
|
for output_delta in tool_call.output_deltas:
|
|
print(f"Output: {output_delta}")
|
|
|
|
# Also available: run.messages, run.text, run.usage, run.output
|
|
```
|
|
|
|
### Hierarchical Agent Streaming
|
|
|
|
When agents invoke sub-agents via tools:
|
|
|
|
```python
|
|
# Outer agent calls a tool that delegates to an inner agent
|
|
for event in outer_agent.stream_events({...}, version="v3"):
|
|
if event_type == "subgraph":
|
|
# A sub-agent ran; access its projections
|
|
subgraph = event["subgraph"]
|
|
for inner_tool_call in subgraph.tool_calls:
|
|
print(f"Inner tool: {inner_tool_call.tool_name}")
|
|
```
|
|
|
|
Streaming hierarchical agents allows observing both the outer loop (which tools the outer agent calls) and inner loops (which tools nested agents call), providing full visibility into complex agent compositions.
|
|
|
|
## Memory and Latency Trade-offs: stream() vs invoke()
|
|
|
|
### invoke()
|
|
|
|
- **Latency**: Waits for the entire model response before returning. Introduces latency equal to the full model generation time.
|
|
- **Memory**: No intermediate storage required; only the final message is held in memory.
|
|
- **Responsiveness**: Blocks the calling thread/coroutine until complete. Users see no output until the response is fully generated.
|
|
- **Use case**: Batch processing, when a complete response is needed upfront before proceeding to the next step.
|
|
|
|
### stream()
|
|
|
|
- **Latency**: Yields the first token as soon as available; responsive to user. Time to first token (TTFT) is minimized.
|
|
- **Memory**: Requires buffering of accumulated chunks if the caller collects them. However, because chunks are yielded immediately, the caller can process and discard each chunk without holding the entire response.
|
|
- **Responsiveness**: Non-blocking; enables progressive display. Users see output appearing in real-time.
|
|
- **Use case**: Web UIs, console applications, user-facing interactions where real-time feedback improves UX.
|
|
|
|
### Streaming Does Not Add Latency
|
|
|
|
In practice, streaming does not add significant latency compared to invoke; the model produces tokens at the same rate. The difference is **when tokens are delivered to the caller**. Stream delivery is preferable for interactive applications because users see output appearing in real-time rather than a blank screen until the full response is ready.
|
|
|
|
### Backpressure and Memory Implications
|
|
|
|
When streaming with `stream()`:
|
|
|
|
- **Callback-driven delivery**: Tokens are yielded as fast as the model produces them. If the caller is slow to consume, tokens accumulate in the accumulator list within `stream()` until the loop ends or yields.
|
|
- **No unbounded growth**: The chunk accumulator is only used for the final `on_llm_end` callback; chunks are yielded immediately before accumulating. Thus, memory overhead is proportional to the response size, not model speed.
|
|
- **Consumer pacing**: Slow consumers (e.g., writing to disk) do not create backpressure; they simply process tokens as yielded.
|
|
|
|
When streaming with `stream_events()` (v3):
|
|
|
|
- **Pull-based backpressure**: The producer (model/graph) only generates events as the consumer requests them via the projection iterator. This naturally paces the producer to the consumer.
|
|
- **Bounded buffering**: The projection buffers events only until the consumer reads them. A slow consumer will naturally slow the producer, preventing unbounded memory growth.
|
|
- **Multiple independent consumers**: Multiple `for` loops over different projections (e.g., `.text` and `.tool_calls`) can replay all events from the buffer, supporting diverse consumption patterns without re-running the model.
|
|
|
|
## Best Practices for Streaming
|
|
|
|
1. **Flush output immediately**: When displaying streaming output in web or terminal, flush buffers after each chunk to ensure immediate visibility.
|
|
|
|
2. **Handle partial JSON carefully**: JSON parsers should use `parse_partial_json()` to extract complete structures from partial JSON as tokens arrive, rather than waiting for the full response.
|
|
|
|
3. **Merge chunks for final use**: If you need the complete response, collect chunks and merge them via `+`:
|
|
```python
|
|
chunks = [chunk for chunk in model.stream(messages)]
|
|
final = chunks[0]
|
|
for chunk in chunks[1:]:
|
|
final = final + chunk
|
|
```
|
|
|
|
4. **Use callbacks for side effects**: Implement `on_llm_new_token` for logging, metrics, and webhooks rather than processing each yielded chunk in the loop. Callbacks decouple application logic from streaming concerns.
|
|
|
|
5. **Respect backpressure**: When using `stream_events()`, let the consumer pace the producer. Don't artificially speed up event generation.
|
|
|
|
6. **Disable streaming selectively**: For long-running operations or when you need predictable latency, use `invoke()` instead of `stream()`, or pass `stream=False` to override the default.
|
|
|
|
7. **Test both sync and async paths**: Streaming behavior may differ between `stream()` and `astream()` depending on model implementation and callback executors. Test both for your use case.
|
|
|
|
8. **Observe tool calls incrementally**: When tools are involved, use `on_llm_new_token` with full `chunk` inspection to see tool call arguments accumulating, enabling early detection of tool invocation intent before arguments are complete.
|
|
|
|
9. **Use `stream_events()` for complex workflows**: For agents, hierarchical tools, and workflows with multiple streaming sources, prefer `stream_events(version="v3")` for precise event-level control and pull-based backpressure.
|