第 10 章:流式输出与 Streaming
文章目录
-
- [第 10 章:流式输出与 Streaming](#第 10 章:流式输出与 Streaming)
-
- [10.1 本章目标](#10.1 本章目标)
- [10.2 核心概念](#10.2 核心概念)
-
- [stream_mode 七种模式对比](#stream_mode 七种模式对比)
- [v2 vs v3 流式 API](#v2 vs v3 流式 API)
- [10.3 实战](#10.3 实战)
-
- [实战 1:Token 级打字机效果(messages 模式)](#实战 1:Token 级打字机效果(messages 模式))
- [实战 2:多模式组合流式(v2 统一格式)](#实战 2:多模式组合流式(v2 统一格式))
- [实战 3:自定义流式事件(custom 模式)](#实战 3:自定义流式事件(custom 模式))
- [实战 4:v3 事件流式 API(推荐)](#实战 4:v3 事件流式 API(推荐))
- [10.4 API 速查](#10.4 API 速查)
- [10.5 错误与避坑指南](#10.5 错误与避坑指南)
-
- [坑 1:同步 stream() 在 async 函数中调用](#坑 1:同步 stream() 在 async 函数中调用)
- [坑 2:messages 模式未过滤不需要的模型输出](#坑 2:messages 模式未过滤不需要的模型输出)
- [坑 3:custom 模式在节点外调用 get_stream_writer()](#坑 3:custom 模式在节点外调用 get_stream_writer())
- [坑 4:v2 格式未判断 chunk 类型](#坑 4:v2 格式未判断 chunk 类型)
- [10.6 最佳实践总结](#10.6 最佳实践总结)
10.1 本章目标
学完本章你将能够:
- 理解
stream() 和 astream() 的区别和使用场景
- 掌握所有
stream_mode 模式(values/updates/debug/messages/custom/checkpoints/tasks)
- 学会 v2 统一格式和 v3 事件流式 API
- 实现 Token 级打字机效果和自定义流式事件
10.2 核心概念
stream_mode 七种模式对比
| 模式 |
输出内容 |
典型用途 |
values |
每步后完整 State 快照 |
查看完整状态变化 |
updates |
每步增量更新(含节点名) |
了解哪个节点改了什么 |
messages |
LLM Token 流式输出 (token, metadata) |
打字机效果 |
custom |
通过 get_stream_writer() 的自定义数据 |
进度条、状态通知 |
debug |
尽可能多的调试信息 |
排查问题 |
checkpoints |
checkpoint 创建事件 |
监控持久化 |
tasks |
任务启动/完成事件 |
监控并行任务 |
v2 vs v3 流式 API
| 特性 |
v2 |
v3(推荐) |
| API |
stream(mode=[...], version="v2") |
stream_events(version="v3") |
| 输出格式 |
统一 StreamPart 格式 |
类型化投影 |
| 中断检测 |
需手动处理 |
stream.interrupts 属性 |
| 最终状态 |
需手动收集 |
stream.output 属性 |
10.3 实战
实战 1:Token 级打字机效果(messages 模式)
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END, add_messages
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
llm = ChatOpenAI(model="gpt-4o", temperature=0.7, streaming=True)
def chat_node(state: State) -> dict:
response = llm.invoke(
[SystemMessage(content="你是一个友好的助手")] + list(state["messages"])
)
return {"messages": [response]}
builder = StateGraph(State)
builder.add_node("chat", chat_node)
builder.add_edge(START, "chat")
builder.add_edge("chat", END)
graph = builder.compile()
# ============================================
# 流式输出:Token 级打字机效果
# ============================================
print("AI: ", end="", flush=True)
for chunk in graph.stream(
{"messages": [HumanMessage(content="写一首关于编程的短诗")]},
stream_mode="messages",
):
if isinstance(chunk, tuple):
msg_chunk, metadata = chunk
if hasattr(msg_chunk, "content") and msg_chunk.content:
print(msg_chunk.content, end="", flush=True)
实战 2:多模式组合流式(v2 统一格式)
# 同时使用多种 stream_mode
for chunk in graph.stream(
{"messages": [HumanMessage(content="写一首诗")]},
stream_mode=["values", "updates", "messages"],
version="v2",
):
chunk_type = chunk.get("type")
if chunk_type == "values":
print(f"[values] 消息数: {len(chunk['data'].get('messages', []))}")
elif chunk_type == "updates":
for node_name, update in chunk["data"].items():
print(f"[updates] 节点 '{node_name}' 更新了 State")
elif chunk_type == "messages":
msg_chunk, metadata = chunk["data"]
if hasattr(msg_chunk, "content") and msg_chunk.content:
print(msg_chunk.content, end="", flush=True)
实战 3:自定义流式事件(custom 模式)
from langgraph.config import get_stream_writer
import time
def long_running_node(state: State) -> dict:
"""模拟长时间运行的节点,通过 custom 模式发送进度通知。"""
writer = get_stream_writer() # 获取流式写入器
writer({"progress": 0, "status": "开始处理..."})
time.sleep(0.5)
writer({"progress": 30, "status": "正在分析数据..."})
time.sleep(0.5)
writer({"progress": 60, "status": "正在生成结果..."})
time.sleep(0.5)
writer({"progress": 100, "status": "处理完成!"})
return {"messages": [AIMessage(content="处理完成!")]}
# 接收自定义事件
for chunk in graph.stream(
{"messages": [HumanMessage(content="开始处理")]},
stream_mode=["custom", "messages"],
version="v2",
):
if chunk["type"] == "custom":
event = chunk["data"]
print(f"\n [{event['progress']}%] {event['status']}", end="")
elif chunk["type"] == "messages":
msg_chunk, _ = chunk["data"]
if hasattr(msg_chunk, "content") and msg_chunk.content:
print(f"\nAI: {msg_chunk.content}", end="")
实战 4:v3 事件流式 API(推荐)
stream = graph.stream_events(
{"messages": [HumanMessage(content="写一首诗")]},
version="v3",
)
# 类型化投影:直接遍历 LLM 消息 token
for msg in stream.messages:
for token in msg.text:
print(token, end="", flush=True)
# 获取最终状态
final = stream.output
print(f"\n\n最终消息数: {len(final['messages'])}")
# 检查中断
if stream.interrupted:
print(f"图被中断: {stream.interrupts}")
10.4 API 速查
| API |
完整签名 |
入参说明 |
返回值 |
说明 |
.stream(input, stream_mode) |
stream(input, stream_mode, version) |
input: 初始 State; stream_mode: 模式或模式列表; version: "v2" |
Iterator[dict] |
同步流式 |
.astream(input, stream_mode) |
astream(input, stream_mode, version) |
同 stream |
AsyncIterator[dict] |
异步流式 |
stream_mode="values" |
模式名 |
无 |
完整 State 快照 |
每步后完整状态 |
stream_mode="updates" |
模式名 |
无 |
增量更新 |
每个节点的变更 |
stream_mode="messages" |
模式名 |
无 |
(token, metadata) |
LLM Token 流 |
stream_mode="custom" |
模式名 |
无 |
自定义数据 |
通过 get_stream_writer |
stream_mode="debug" |
模式名 |
无 |
调试信息 |
节点执行详情 |
get_stream_writer() |
get_stream_writer() |
无 |
Writer 函数 |
在节点中发送自定义事件 |
.stream_events(input, v3) |
stream_events(input, version="v3") |
input: 初始 State; version: "v3" |
EventsStream |
v3 事件流式 API(推荐) |
10.5 错误与避坑指南
坑 1:同步 stream() 在 async 函数中调用
# ❌ 错误写法
async def bad_handler():
for chunk in graph.stream(...): # 同步 stream 在 async 中阻塞事件循环
print(chunk)
# ✅ 正确写法
async def good_handler():
async for chunk in graph.astream(...): # 使用 astream
print(chunk)
坑 2:messages 模式未过滤不需要的模型输出
# ❌ 错误:所有 LLM 输出都混在一起
for chunk in graph.stream(..., stream_mode="messages"):
print(chunk[0].content) # 可能包含内部模型的输出
# ✅ 正确:使用 tags 过滤
model = ChatOpenAI(model="gpt-4o").with_config({"tags": ["user_facing"]})
for chunk in graph.stream(..., stream_mode="messages"):
msg_chunk, metadata = chunk
if "user_facing" in metadata.get("tags", []):
print(msg_chunk.content)
坑 3:custom 模式在节点外调用 get_stream_writer()
# ❌ 错误写法
writer = get_stream_writer() # 在节点外调用 → 报错
# ✅ 正确写法
def my_node(state: State) -> dict:
writer = get_stream_writer() # 在节点内调用
writer({"status": "ok"})
坑 4:v2 格式未判断 chunk 类型
# ❌ 错误:假设所有 chunk 都是 messages
for chunk in graph.stream(..., stream_mode=["values", "messages"], version="v2"):
print(chunk["data"][0].content) # values 类型的 chunk 没有 .content!
# ✅ 正确:先判断类型
for chunk in graph.stream(..., stream_mode=["values", "messages"], version="v2"):
if chunk["type"] == "messages":
msg_chunk, _ = chunk["data"]
print(msg_chunk.content)
10.6 最佳实践总结
- 生产环境使用
astream() 异步流式:不阻塞事件循环,支持更多并发
- Token 流式用
messages 模式,调试用 debug 模式:各取所需
- 使用 v2 统一格式处理多模式组合 :
version="v2" 统一返回 StreamPart
- 新项目优先使用 v3 事件流式 API:类型化投影更安全、更易用
- 自定义进度通知用
custom 模式 :配合 get_stream_writer() 实现实时进度