You’ve already seen .stream() twice — a quick preview on a single model back in Module 4, and a slightly bigger one on an entire chain in Module 8. Both times, we deliberately kept it simple: loop over chunks, print them, move on. This module is where we finally slow down and understand streaming properly — what a chunk actually is, how real applications handle it, and where it genuinely doesn’t behave the way you might expect.
Why this matters more than it might seem
Here’s a question worth sitting with for a second: if .stream() and .invoke() produce the exact same final text, and take roughly the exact same total amount of time to finish, why does it matter which one you use?
The honest answer is that it doesn’t change how fast the model actually computes its answer. What it changes is when the user sees the first sign that anything is happening at all. With .invoke(), a user asking a genuinely long question stares at a blank screen for the entire duration — five seconds, ten seconds, sometimes longer — with zero feedback. With .stream(), the first few words can appear within a fraction of a second, and text keeps arriving steadily after that. The total time to a complete answer is nearly identical either way. The experience of waiting for it is genuinely, dramatically different. This is exactly why every major chat product you’ve ever used — the one you’re picturing right now — streams its responses by default.
Example 1: what a chunk actually is
Let’s look more closely at what .stream() actually hands you, piece by piece, instead of immediately printing and discarding it.
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4o-mini")
for chunk in model.stream("Name three planets."):
print(type(chunk), repr(chunk.content))
Run this, and you’ll notice each chunk isn’t a plain string — it’s an AIMessageChunk, a smaller, incremental cousin of the full AIMessage you’ve been working with since Module 4. Each one typically carries just a small piece of text in .content — sometimes a whole word, sometimes just part of one. This matters because it means chunks aren’t meant to be read individually as complete thoughts; they’re meant to be combined.
Example 2: reconstructing the full message from its chunks
Here’s something genuinely useful that a lot of people miss: AIMessageChunk objects support being added together with the + operator, which merges them into one larger, combined chunk.
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4o-mini")
full_message = None
for chunk in model.stream("Name three planets."):
print(chunk.content, end="", flush=True)
full_message = chunk if full_message is None else full_message + chunk
print("\n\nFull reconstructed message:", full_message.content)
print("Token usage (only available once fully reconstructed):", full_message.usage_metadata)
This is a genuinely practical pattern: print each chunk to the user immediately, as it arrives, while quietly accumulating the full message alongside it using +. By the time streaming finishes, full_message is a complete, real AIMessage-like object — including things like usage_metadata, which individual chunks don’t reliably carry on their own. You get the responsive, streamed experience and the complete final object to work with afterward, from the exact same loop.
Example 3: streaming an entire chain, and understanding why it’s able to work at all
Recall this from Module 8:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_template("Write a short poem about {topic}.")
model = init_chat_model("openai:gpt-4o-mini")
parser = StrOutputParser()
chain = prompt | model | parser
for chunk in chain.stream({"topic": "a rainy afternoon"}):
print(chunk, end="", flush=True)
It’s worth explicitly understanding why this works, now that you know what a chunk actually is. StrOutputParser, sitting at the end of this chain, is specifically written to handle a stream of incoming AIMessageChunk objects — pulling the plain text out of each one as it arrives, rather than waiting for a complete AIMessage to parse all at once. This is exactly why the chunks you see printed here are plain strings, not AIMessageChunk objects like in Example 1: StrOutputParser already did that extraction for you, chunk by chunk, as the stream flowed through it. Not every component you might place at the end of a chain is guaranteed to support this — but LangChain’s own built-in parsers, including this one, are specifically designed to.
Example 4: peeking inside a chain while it runs, with astream_events
Sometimes you don’t just want the final streamed text — you want visibility into which step of a multi-stage chain is currently running, useful for showing a user something like “searching…” followed by “writing answer…” rather than one undifferentiated stream of text.
import asyncio
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_template("Write a short poem about {topic}.")
model = init_chat_model("openai:gpt-4o-mini")
parser = StrOutputParser()
chain = prompt | model | parser
async def main():
async for event in chain.astream_events({"topic": "a quiet morning"}, version="v2"):
if event["event"] == "on_chat_model_start":
print("\n[Model started generating...]")
elif event["event"] == "on_chat_model_stream":
print(event["data"]["chunk"].content, end="", flush=True)
elif event["event"] == "on_chain_end" and event["name"] == "RunnableSequence":
print("\n[Chain finished.]")
asyncio.run(main())
astream_events is a genuinely different tool from .stream() — instead of only giving you the final output’s chunks, it gives you a full, real-time feed of every significant event happening anywhere inside the chain: when the model starts, each token as it streams, when the whole chain finishes, and more. This becomes especially valuable once you’re building agents later in this course, where a single request might involve several model calls and tool executions, and you genuinely want to show a user what stage of that process is currently happening, not just raw text arriving.
Common mistakes worth avoiding
Forgetting flush=True and wondering why nothing appears until the very end. Python’s print() normally buffers its output for efficiency, meaning text can sit in an internal buffer instead of actually appearing on screen immediately. flush=True forces each piece to display the moment it’s printed — without it, your carefully streamed output can look exactly like .invoke() did, defeating the entire point.
Treating an individual chunk as if it were a complete, parseable message. Recall Example 1 — a single AIMessageChunk might contain just half a word, or a fragment of a JSON structure if the model happens to be generating one. Trying to parse or validate an individual chunk on its own, rather than the fully accumulated result, will fail unpredictably, since a chunk is, by design, an incomplete piece of a larger whole.
Assuming structured output streams token by token, the way plain text does. This is a genuine, honest limitation worth knowing. Because a structured object generally needs to be complete and internally valid before it can be safely handed to you as a real Pydantic instance, .with_structured_output(...) typically does not stream incrementally the way plain text does — you’ll usually receive the complete, validated object only once generation has fully finished, even if you call .stream() on it.
What you should take away from this module
- Streaming doesn’t reduce total response time — it changes when the user sees the first sign of progress, which is a genuinely significant difference in real, felt experience.
- Each streamed chunk is an
AIMessageChunk, a small, incremental piece — not a complete message on its own. AIMessageChunkobjects can be combined with+, letting you both display output progressively and reconstruct the complete final message, including metadata like token usage.StrOutputParser, and other properly built LangChain components, know how to handle incoming chunks correctly, which is why streaming continues to work correctly even at the end of a multi-step chain.astream_eventsgives you visibility into everything happening inside a chain as it runs, not just the final output — genuinely valuable once your chains and agents grow more complex.- Structured output generally doesn’t stream incrementally, since the result needs to be complete and valid before it can be safely returned.
Where this goes next
The next module covers Batching and Async properly — you’ve used .batch() and .ainvoke() in passing since Modules 4 and 8, and now it’s time to understand exactly when and why you’d reach for each one in a real application handling more than one request at a time.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed