스트리밍
스트리밍 (Streaming · LangChain Python)
에이전트 실행에서 실시간 갱신을 스트리밍해요.
새 프로젝트에는 이벤트 스트리밍을 권장해요. LangChain v1.3부터 도입된 타입-프로젝션 API죠. 이벤트 스트리밍은 프로젝션(messages, values, tool calls, subgraphs)별로 별도의 이터레이터를 주기 때문에,
stream_mode덩어리를 분기하는 대신 각각을 독립적으로 소비할 수 있어요.
LangChain은 실시간 갱신을 표면화하는 스트리밍 시스템을 구현하고 있어요.
스트리밍은 LLM 기반 애플리케이션의 반응성을 높이는 데 결정적이에요. 완전한 응답이 준비되기 전에도 출력을 점진적으로 보여주면, 특히 LLM의 지연 시간을 다룰 때 사용자 경험(UX)이 크게 개선되죠.
개요
LangChain의 스트리밍 시스템으로 에이전트 실행의 라이브 피드백을 애플리케이션에 표면화할 수 있어요.
LangChain 스트리밍으로 가능한 것들:
- 에이전트 진행 스트리밍 — 에이전트 단계마다 상태 갱신을 받아요.
- LLM 토큰 스트리밍 — 언어 모델 토큰이 생성되는 대로 스트리밍해요.
- 사고/추론 토큰 스트리밍 — 모델의 추론 과정을 생성되는 대로 표면화해요.
- 커스텀 갱신 스트리밍 — 사용자 정의 신호(예:
"Fetched 10/100 records")를 보내요. - 여러 모드 스트리밍 —
updates(에이전트 진행),messages(LLM 토큰+메타데이터),custom(임의 사용자 데이터) 중에서 골라요.
아래 공통 패턴 섹션에 종단 간 예제가 더 있어요.
지원되는 스트림 모드
stream 또는 astream 메서드에 아래 스트림 모드 중 하나 이상을 리스트로 전달해요:
| 모드 | 설명 |
|---|---|
updates |
에이전트 단계마다 상태 갱신을 스트리밍해요. 같은 단계에서 여러 갱신이 일어나면(예: 여러 노드 실행) 각각 따로 스트리밍돼요. |
messages |
LLM이 호출된 그래프 노드에서 (token, metadata) 튜플을 스트리밍해요. |
custom |
스트림 라이터를 이용해 그래프 노드 안에서 커스텀 데이터를 스트리밍해요. |
에이전트 진행 스트리밍
에이전트 진행을 스트리밍하려면 stream_mode="updates"와 함께 stream 또는 astream 메서드를 써요. 이러면 에이전트 단계마다 이벤트가 하나씩 나와요.
예를 들어 도구를 한 번 호출하는 에이전트가 있다면, 이런 갱신이 보일 거예요:
- LLM 노드: 도구 호출 요청이 담긴
AIMessage - 도구 노드: 실행 결과가 담긴
ToolMessage - LLM 노드: 최종 AI 응답
config를 통해 thread_id를 전달하면 대화가 체크포인트에 기록되어, 이후 턴이 같은 대화 이력을 이어갈 수 있어요. thread_id는 stream_mode와 독립적이에요. context도 함께 넘길 수 있는데, 도구가 runtime.context에서 읽는 실행별 데이터예요.
def get_weather(city: str) -> str: """Get weather for a given city.""" return f"It's always sunny in {city}!"
agent = create_agent( model="google_genai:gemini-3.6-flash", tools=[get_weather], checkpointer=InMemorySaver() ) config = {"configurable": {"thread_id": str(uuid7())}} stream = agent.stream_events( # [!code highlight] {"messages": [{"role": "user", "content": "What is the weather in SF?"}]}, config=config, version="v3", # [!code highlight] ) for kind, item in stream.interleave("messages", "tool_calls"): # [!code highlight] if kind == "messages": for token in item.text: print(token, end="", flush=True) elif kind == "tool_calls": print(f"\nTool call: {item.tool_name}({item.input})") for delta in item.output_deltas: print(delta, end="", flush=True) print(f"\nTool result: {item.output}")
final_state = stream.output # [!code highlight]
```python OpenAI theme={...}
from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="openai:gpt-5.5",
tools=[get_weather],
checkpointer=InMemorySaver()
)
config = {"configurable": {"thread_id": str(uuid7())}}
stream = agent.stream_events( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
config=config,
version="v3", # [!code highlight]
)
for kind, item in stream.interleave("messages", "tool_calls"): # [!code highlight]
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
for delta in item.output_deltas:
print(delta, end="", flush=True)
print(f"\nTool result: {item.output}")
final_state = stream.output # [!code highlight]
from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="anthropic:claude-sonnet-4-6",
tools=[get_weather],
checkpointer=InMemorySaver()
)
config = {"configurable": {"thread_id": str(uuid7())}}
stream = agent.stream_events( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
config=config,
version="v3", # [!code highlight]
)
for kind, item in stream.interleave("messages", "tool_calls"): # [!code highlight]
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
for delta in item.output_deltas:
print(delta, end="", flush=True)
print(f"\nTool result: {item.output}")
final_state = stream.output # [!code highlight]
from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="openrouter:z-ai/glm-5.2",
tools=[get_weather],
checkpointer=InMemorySaver()
)
config = {"configurable": {"thread_id": str(uuid7())}}
stream = agent.stream_events( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
config=config,
version="v3", # [!code highlight]
)
for kind, item in stream.interleave("messages", "tool_calls"): # [!code highlight]
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
for delta in item.output_deltas:
print(delta, end="", flush=True)
print(f"\nTool result: {item.output}")
final_state = stream.output # [!code highlight]
from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="fireworks:accounts/fireworks/models/glm-5p2",
tools=[get_weather],
checkpointer=InMemorySaver()
)
config = {"configurable": {"thread_id": str(uuid7())}}
stream = agent.stream_events( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
config=config,
version="v3", # [!code highlight]
)
for kind, item in stream.interleave("messages", "tool_calls"): # [!code highlight]
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
for delta in item.output_deltas:
print(delta, end="", flush=True)
print(f"\nTool result: {item.output}")
final_state = stream.output # [!code highlight]
from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="baseten:zai-org/GLM-5.2",
tools=[get_weather],
checkpointer=InMemorySaver()
)
config = {"configurable": {"thread_id": str(uuid7())}}
stream = agent.stream_events( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
config=config,
version="v3", # [!code highlight]
)
for kind, item in stream.interleave("messages", "tool_calls"): # [!code highlight]
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
for delta in item.output_deltas:
print(delta, end="", flush=True)
print(f"\nTool result: {item.output}")
final_state = stream.output # [!code highlight]
from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="ollama:north-mini-code-1.0",
tools=[get_weather],
checkpointer=InMemorySaver()
)
config = {"configurable": {"thread_id": str(uuid7())}}
stream = agent.stream_events( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
config=config,
version="v3", # [!code highlight]
)
for kind, item in stream.interleave("messages", "tool_calls"): # [!code highlight]
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
for delta in item.output_deltas:
print(delta, end="", flush=True)
print(f"\nTool result: {item.output}")
final_state = stream.output # [!code highlight]
step: model
content: [{'type': 'tool_call', 'name': 'get_weather', 'args': {'city': 'San Francisco'}, 'id': 'call_9lBtsDbmmobzyA8xc4I4Ctne'}]
step: tools
content: [{'type': 'text', 'text': "It's always sunny in San Francisco!"}]
step: model
content: [{'type': 'text', 'text': "San Francisco weather: It's always sunny in San Francisco!\n\nIf you'd like the exact current conditions (temperature, humidity, wind) and a short forecast, I can fetch that next. Would you like me to pull live details for San Francisco?"}]
LLM 토큰 스트리밍
LLM이 만들어내는 토큰을 스트리밍하려면 stream_mode="messages"를 써요. 아래는 에이전트가 도구 호출과 최종 응답을 스트리밍하는 모습이에요.
from langchain.agents import create_agent
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="gpt-5-nano",
tools=[get_weather],
)
for chunk in agent.stream( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode="messages",
version="v2", # [!code highlight]
):
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
print(f"node: {metadata['langgraph_node']}")
print(f"content: {token.content_blocks}")
print("\n")
node: model
content: [{'type': 'tool_call_chunk', 'id': 'call_vbCyBcP8VuneUzyYlSBZZsVa', 'name': 'get_weather', 'args': '', 'index': 0}]
node: model
content: [{'type': 'tool_call_chunk', 'id': None, 'name': None, 'args': '{"', 'index': 0}]
node: model
content: [{'type': 'tool_call_chunk', 'id': None, 'name': None, 'args': 'city', 'index': 0}]
node: model
content: [{'type': 'tool_call_chunk', 'id': None, 'name': None, 'args': '":"', 'index': 0}]
node: model
content: [{'type': 'tool_call_chunk', 'id': None, 'name': None, 'args': 'San', 'index': 0}]
node: model
content: [{'type': 'tool_call_chunk', 'id': None, 'name': None, 'args': ' Francisco', 'index': 0}]
node: model
content: [{'type': 'tool_call_chunk', 'id': None, 'name': None, 'args': '"}', 'index': 0}]
node: model
content: []
node: tools
content: [{'type': 'text', 'text': "It's always sunny in San Francisco!"}]
node: model
content: []
node: model
content: [{'type': 'text', 'text': 'Here'}]
node: model
content: [{'type': 'text', 'text': "'s"}]
node: model
content: [{'type': 'text', 'text': ' what'}]
node: model
content: [{'type': 'text', 'text': ' I'}]
node: model
content: [{'type': 'text', 'text': ' got'}]
node: model
content: [{'type': 'text', 'text': ':'}]
node: model
content: [{'type': 'text', 'text': ' "'}]
node: model
content: [{'type': 'text', 'text': "It's"}]
node: model
content: [{'type': 'text', 'text': ' always'}]
node: model
content: [{'type': 'text', 'text': ' sunny'}]
node: model
content: [{'type': 'text', 'text': ' in'}]
node: model
content: [{'type': 'text', 'text': ' San'}]
node: model
content: [{'type': 'text', 'text': ' Francisco'}]
node: model
content: [{'type': 'text', 'text': '!"\n\n'}]
커스텀 갱신 스트리밍
도구가 실행되는 동안 도구의 갱신을 스트리밍하려면 get_stream_writer를 써요.
from langchain.agents import create_agent
from langgraph.config import get_stream_writer # [!code highlight]
def get_weather(city: str) -> str:
"""Get weather for a given city."""
writer = get_stream_writer() # [!code highlight]
# stream any arbitrary data
writer(f"Looking up data for city: {city}")
writer(f"Acquired data for city: {city}")
return f"It's always sunny in {city}!"
agent = create_agent(
model="claude-sonnet-4-6",
tools=[get_weather],
)
for chunk in agent.stream(
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode="custom", # [!code highlight]
version="v2", # [!code highlight]
):
if chunk["type"] == "custom": # [!code highlight]
print(chunk["data"]) # [!code highlight]
Looking up data for city: San Francisco
Acquired data for city: San Francisco
여러 모드 스트리밍
스트림 모드를 리스트로 넘기면 여러 모드를 지정할 수 있어요: stream_mode=["updates", "custom"].
각 스트리밍 덩어리는 type, ns, data 키를 가진 StreamPart 딕셔너리예요. chunk["type"]으로 스트림 모드를 판별하고, chunk["data"]로 페이로드에 접근해요.
from langchain.agents import create_agent
from langgraph.config import get_stream_writer
def get_weather(city: str) -> str:
"""Get weather for a given city."""
writer = get_stream_writer()
writer(f"Looking up data for city: {city}")
writer(f"Acquired data for city: {city}")
return f"It's always sunny in {city}!"
agent = create_agent(
model="gpt-5-nano",
tools=[get_weather],
)
for chunk in agent.stream( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode=["updates", "custom"],
version="v2", # [!code highlight]
):
print(f"stream_mode: {chunk['type']}") # [!code highlight]
print(f"content: {chunk['data']}") # [!code highlight]
print("\n")
stream_mode: updates
content: {'model': {'messages': [AIMessage(content='', response_metadata={'token_usage': {'completion_tokens': 280, 'prompt_tokens': 132, 'total_tokens': 412, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 256, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-5-nano-2025-08-07', 'system_fingerprint': None, 'id': 'chatcmpl-C9tlgBzGEbedGYxZ0rTCz5F7OXpL7', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--480c07cb-e405-4411-aa7f-0520fddeed66-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'San Francisco'}, 'id': 'call_KTNQIftMrl9vgNwEfAJMVu7r', 'type': 'tool_call'}], usage_metadata={'input_tokens': 132, 'output_tokens': 280, 'total_tokens': 412, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 256}})]}}
stream_mode: custom
content: Looking up data for city: San Francisco
stream_mode: custom
content: Acquired data for city: San Francisco
stream_mode: updates
content: {'tools': {'messages': [ToolMessage(content="It's always sunny in San Francisco!", name='get_weather', tool_call_id='call_KTNQIftMrl9vgNwEfAJMVu7r')]}}
stream_mode: updates
content: {'model': {'messages': [AIMessage(content='San Francisco weather: It's always sunny in San Francisco!\n\n', response_metadata={'token_usage': {'completion_tokens': 764, 'prompt_tokens': 168, 'total_tokens': 932, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 704, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-5-nano-2025-08-07', 'system_fingerprint': None, 'id': 'chatcmpl-C9tljDFVki1e1haCyikBptAuXuHYG', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--acbc740a-18fe-4a14-8619-da92a0d0ee90-0', usage_metadata={'input_tokens': 168, 'output_tokens': 764, 'total_tokens': 932, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 704}})]}}
공통 패턴
아래는 스트리밍의 일반적인 활용 사례를 보여 주는 예제들이에요.
사고/추론 토큰 스트리밍
일부 모델은 최종 답을 만들기 전에 내부 추론을 수행해요. 표준 콘텐츠 블록에서 type이 "reasoning"인 것만 필터링하면, 이 사고/추론 토큰이 만들어지는 대로 스트리밍할 수 있어요.
설정 방법은 reasoning 섹션과 프로바이더 통합 페이지를 참고하세요.
모델의 추론 지원을 빠르게 확인하려면 models.dev를 보세요.
에이전트에서 사고 토큰을 스트리밍하려면 stream_mode="messages"를 쓰고 reasoning 콘텐츠 블록으로 필터링해요:
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from langchain_core.runnables import Runnable
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
model = ChatAnthropic(
model_name="claude-sonnet-4-6",
timeout=None,
stop=None,
thinking={"type": "enabled", "budget_tokens": 5000},
)
agent: Runnable = create_agent(
model=model,
tools=[get_weather],
)
stream = agent.stream_events( # [!code highlight]
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
version="v3",
)
for message in stream.messages:
for token in message.reasoning:
print(f"[thinking] {token}", end="")
for token in message.text:
print(token, end="", flush=True)
[thinking] The user is asking about the weather in San Francisco. I have a tool
[thinking] available to get this information. Let me call the get_weather tool
[thinking] with "San Francisco" as the city parameter.
The weather in San Francisco is: It's always sunny in San Francisco!
이건 모델 프로바이더와 무관하게 똑같이 동작해요. LangChain은 프로바이더별 형식(Anthropic thinking 블록, OpenAI reasoning 요약 등)을 content_blocks 속성을 통해 표준 "reasoning" 콘텐츠 블록 타입으로 정규화해 줘요.
에이전트 없이 채팅 모델에서 바로 reasoning 토큰을 스트리밍하려면 채팅 모델 스트리밍을 참고하세요.
도구 호출 스트리밍
다음 두 가지를 모두 스트리밍하고 싶을 수 있어요:
- 도구 호출이 생성될 때 나오는 부분 JSON
- 실행되는 완성·파싱된 도구 호출
stream_mode="messages"를 지정하면 에이전트의 모든 LLM 호출이 만드는 메시지 덩어리를 증분 스트리밍해요. 파싱된 도구 호출이 담긴 완성 메시지에 접근하려면:
- 그 메시지가 상태에 추적되는 경우(
create_agent의 모델 노드처럼),stream_mode=["messages", "updates"]를 써서 상태 갱신으로 완성된 메시지에 접근해요. - 그 메시지가 상태에 추적되지 않는 경우, 커스텀 갱신을 쓰거나 스트리밍 루프 안에서 덩어리를 집계해요(다음 섹션).
from typing import Any
from langchain.agents import create_agent
from langchain.messages import AIMessage, AIMessageChunk, AnyMessage, ToolMessage
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent("openai:gpt-5.5", tools=[get_weather])
def _render_message_chunk(token: AIMessageChunk) -> None:
if token.text:
print(token.text, end="|")
if token.tool_call_chunks:
print(token.tool_call_chunks)
# N.B. all content is available through token.content_blocks
def _render_completed_message(message: AnyMessage) -> None:
if isinstance(message, AIMessage) and message.tool_calls:
print(f"Tool calls: {message.tool_calls}")
if isinstance(message, ToolMessage):
print(f"Tool response: {message.content_blocks}")
input_message = {"role": "user", "content": "What is the weather in Boston?"}
for chunk in agent.stream(
{"messages": [input_message]},
stream_mode=["messages", "updates"], # [!code highlight]
version="v2", # [!code highlight]
):
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
if isinstance(token, AIMessageChunk):
_render_message_chunk(token) # [!code highlight]
elif chunk["type"] == "updates": # [!code highlight]
for source, update in chunk["data"].items(): # [!code highlight]
if source in ("model", "tools"): # `source` captures node name
_render_completed_message(update["messages"][-1]) # [!code highlight]
[{'name': 'get_weather', 'args': '', 'id': 'call_D3Orjr89KgsLTZ9hTzYv7Hpf', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'city', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '":"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'Boston', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
Tool calls: [{'name': 'get_weather', 'args': {'city': 'Boston'}, 'id': 'call_D3Orjr89KgsLTZ9hTzYv7Hpf', 'type': 'tool_call'}]
Tool response: [{'type': 'text', 'text': "It's always sunny in Boston!"}]
The| weather| in| Boston| is| **|sun|ny|**|.|
완성된 메시지 접근하기
어떤 경우에는 완성된 메시지가 상태 갱신에 반영되지 않아요. 에이전트 내부에 접근할 수 있다면 커스텀 갱신으로 스트리밍 중에 이 메시지들에 접근할 수 있어요. 그게 아니면 스트리밍 루프 안에서 메시지 덩어리를 집계해요(아래 참고).
아래 예제를 보죠. 단순화한 가드레일 미들웨어에 스트림 라이터를 포함시켰어요. 이 미들웨어는 구조화된 "safe / unsafe" 평가를 만들기 위해 도구 호출을 시연해요(이 용도로 구조화된 출력을 쓸 수도 있어요):
from typing import Any, Literal
from langchain.agents.middleware import after_agent, AgentState
from langgraph.runtime import Runtime
from langchain.messages import AIMessage
from langchain.chat_models import init_chat_model
from langgraph.config import get_stream_writer # [!code highlight]
from pydantic import BaseModel
class ResponseSafety(BaseModel):
"""Evaluate a response as safe or unsafe."""
evaluation: Literal["safe", "unsafe"]
safety_model = init_chat_model("openai:gpt-5.5")
@after_agent(can_jump_to=["end"])
def safety_guardrail(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""Model-based guardrail: Use an LLM to evaluate response safety."""
stream_writer = get_stream_writer() # [!code highlight]
# Get the model response
if not state["messages"]:
return None
last_message = state["messages"][-1]
if not isinstance(last_message, AIMessage):
return None
# Use another model to evaluate safety
model_with_tools = safety_model.bind_tools([ResponseSafety], tool_choice="any")
result = model_with_tools.invoke(
[
{
"role": "system",
"content": "Evaluate this AI response as generally safe or unsafe."
},
{
"role": "user",
"content": f"AI response: {last_message.text}"
}
]
)
stream_writer(result) # [!code highlight]
tool_call = result.tool_calls[0]
if tool_call["args"]["evaluation"] == "unsafe":
last_message.content = "I cannot provide that response. Please rephrase your request."
return None
이 미들웨어를 에이전트에 포함시키고 커스텀 스트림 이벤트도 함께 받을 수 있어요:
from typing import Any
from langchain.agents import create_agent
from langchain.messages import AIMessageChunk, AIMessage, AnyMessage
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="openai:gpt-5.5",
tools=[get_weather],
middleware=[safety_guardrail], # [!code highlight]
)
def _render_message_chunk(token: AIMessageChunk) -> None:
if token.text:
print(token.text, end="|")
if token.tool_call_chunks:
print(token.tool_call_chunks)
def _render_completed_message(message: AnyMessage) -> None:
if isinstance(message, AIMessage) and message.tool_calls:
print(f"Tool calls: {message.tool_calls}")
if isinstance(message, ToolMessage):
print(f"Tool response: {message.content_blocks}")
input_message = {"role": "user", "content": "What is the weather in Boston?"}
for chunk in agent.stream(
{"messages": [input_message]},
stream_mode=["messages", "updates", "custom"], # [!code highlight]
version="v2", # [!code highlight]
):
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
if isinstance(token, AIMessageChunk):
_render_message_chunk(token)
elif chunk["type"] == "updates": # [!code highlight]
for source, update in chunk["data"].items(): # [!code highlight]
if source in ("model", "tools"):
_render_completed_message(update["messages"][-1])
elif chunk["type"] == "custom": # [!code highlight]
# access completed message in stream
print(f"Tool calls: {chunk['data'].tool_calls}") # [!code highlight]
[{'name': 'get_weather', 'args': '', 'id': 'call_je6LWgxYzuZ84mmoDalTYMJC', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'city', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '":"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'Boston', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
Tool calls: [{'name': 'get_weather', 'args': {'city': 'Boston'}, 'id': 'call_je6LWgxYzuZ84mmoDalTYMJC', 'type': 'tool_call'}]
Tool response: [{'type': 'text', 'text': "It's always sunny in Boston!"}]
The| weather| in| **|Boston|**| is| **|sun|ny|**|.|[{'name': 'ResponseSafety', 'args': '', 'id': 'call_O8VJIbOG4Q9nQF0T8ltVi58O', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'evaluation', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '":"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'safe', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
Tool calls: [{'name': 'ResponseSafety', 'args': {'evaluation': 'safe'}, 'id': 'call_O8VJIbOG4Q9nQF0T8ltVi58O', 'type': 'tool_call'}]
아니면 스트림에 커스텀 이벤트를 추가할 수 없다면, 스트리밍 루프 안에서 메시지 덩어리를 집계할 수도 있어요:
input_message = {"role": "user", "content": "What is the weather in Boston?"}
full_message = None # [!code highlight]
for chunk in agent.stream(
{"messages": [input_message]},
stream_mode=["messages", "updates"],
version="v2", # [!code highlight]
):
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
if isinstance(token, AIMessageChunk):
_render_message_chunk(token)
full_message = token if full_message is None else full_message + token # [!code highlight]
if token.chunk_position == "last": # [!code highlight]
if full_message.tool_calls: # [!code highlight]
print(f"Tool calls: {full_message.tool_calls}") # [!code highlight]
full_message = None # [!code highlight]
elif chunk["type"] == "updates": # [!code highlight]
for source, update in chunk["data"].items(): # [!code highlight]
if source == "tools":
_render_completed_message(update["messages"][-1])
사람-인-더-루프와 함께 스트리밍
사람-인-더-루프 인터럽트를 다루려면 위 예제를 발전시켜요:
- 사람-인-더-루프 미들웨어와 체크포인터로 에이전트를 구성해요
"updates"스트림 모드 중에 생성된 인터럽트를 수집해요- 그 인터럽트에 커맨드로 응답해요
from typing import Any
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.messages import AIMessage, AIMessageChunk, AnyMessage, ToolMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command, Interrupt
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
checkpointer = InMemorySaver()
agent = create_agent(
"openai:gpt-5.5",
tools=[get_weather],
middleware=[ # [!code highlight]
HumanInTheLoopMiddleware(interrupt_on={"get_weather": True}), # [!code highlight]
], # [!code highlight]
checkpointer=checkpointer, # [!code highlight]
)
def _render_message_chunk(token: AIMessageChunk) -> None:
if token.text:
print(token.text, end="|")
if token.tool_call_chunks:
print(token.tool_call_chunks)
def _render_completed_message(message: AnyMessage) -> None:
if isinstance(message, AIMessage) and message.tool_calls:
print(f"Tool calls: {message.tool_calls}")
if isinstance(message, ToolMessage):
print(f"Tool response: {message.content_blocks}")
def _render_interrupt(interrupt: Interrupt) -> None: # [!code highlight]
interrupts = interrupt.value # [!code highlight]
for request in interrupts["action_requests"]: # [!code highlight]
print(request["description"]) # [!code highlight]
input_message = {
"role": "user",
"content": (
"Can you look up the weather in Boston and San Francisco?"
),
}
config = {"configurable": {"thread_id": "some_id"}} # [!code highlight]
interrupts = [] # [!code highlight]
for chunk in agent.stream(
{"messages": [input_message]},
config=config, # [!code highlight]
stream_mode=["messages", "updates"],
version="v2", # [!code highlight]
):
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
if isinstance(token, AIMessageChunk):
_render_message_chunk(token)
elif chunk["type"] == "updates": # [!code highlight]
for source, update in chunk["data"].items(): # [!code highlight]
if source in ("model", "tools"):
_render_completed_message(update["messages"][-1])
if source == "__interrupt__": # [!code highlight]
interrupts.extend(update) # [!code highlight]
_render_interrupt(update[0]) # [!code highlight]
[{'name': 'get_weather', 'args': '', 'id': 'call_GOwNaQHeqMixay2qy80padfE', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"ci', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'ty": ', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"Bosto', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'n"}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': 'get_weather', 'args': '', 'id': 'call_Ndb4jvWm2uMA0JDQXu37wDH6', 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"ci', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'ty": ', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"San F', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'ranc', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'isco"', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '}', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
Tool calls: [{'name': 'get_weather', 'args': {'city': 'Boston'}, 'id': 'call_GOwNaQHeqMixay2qy80padfE', 'type': 'tool_call'}, {'name': 'get_weather', 'args': {'city': 'San Francisco'}, 'id': 'call_Ndb4jvWm2uMA0JDQXu37wDH6', 'type': 'tool_call'}]
Tool execution requires approval
Tool: get_weather
Args: {'city': 'Boston'}
Tool execution requires approval
Tool: get_weather
Args: {'city': 'San Francisco'}
다음으로 각 인터럽트에 대한 결정을 수집해요. 결정의 순서는 수집한 액션의 순서와 일치해야 해요는 점이 중요해요.
설명을 위해 하나의 도구 호출은 편집하고 다른 하나는 승인해 볼게요:
def _get_interrupt_decisions(interrupt: Interrupt) -> list[dict]:
return [
{
"type": "edit",
"edited_action": {
"name": "get_weather",
"args": {"city": "Boston, U.K."},
},
}
if "boston" in request["description"].lower()
else {"type": "approve"}
for request in interrupt.value["action_requests"]
]
decisions = {}
for interrupt in interrupts:
decisions[interrupt.id] = {
"decisions": _get_interrupt_decisions(interrupt)
}
decisions
{
'a96c40474e429d661b5b32a8d86f0f3e': {
'decisions': [
{
'type': 'edit',
'edited_action': {
'name': 'get_weather',
'args': {'city': 'Boston, U.K.'}
}
},
{'type': 'approve'},
]
}
}
그리고 나서 같은 스트리밍 루프에 커맨드를 전달해 재개할 수 있어요:
interrupts = []
for chunk in agent.stream(
Command(resume=decisions), # [!code highlight]
config=config,
stream_mode=["messages", "updates"],
version="v2", # [!code highlight]
):
# Streaming loop is unchanged
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
if isinstance(token, AIMessageChunk):
_render_message_chunk(token)
elif chunk["type"] == "updates": # [!code highlight]
for source, update in chunk["data"].items(): # [!code highlight]
if source in ("model", "tools"):
_render_completed_message(update["messages"][-1])
if source == "__interrupt__":
interrupts.extend(update)
_render_interrupt(update[0])
Tool response: [{'type': 'text', 'text': "It's always sunny in Boston, U.K.!"}]
Tool response: [{'type': 'text', 'text': "It's always sunny in San Francisco!"}]
-| **|Boston|**|:| It|'s| always| sunny| in| Boston|,| U|.K|.|
|-| **|San| Francisco|**|:| It|'s| always| sunny| in| San| Francisco|!|
서브에이전트에서 스트리밍
에이전트의 어느 지점에든 LLM이 여러 개 있으면, 메시지가 생성될 때 그 출처를 구분해야 하는 경우가 많아요.
이렇게 하려면 각 에이전트를 만들 때 name을 넘겨 주세요. 이 이름은 "messages" 모드로 스트리밍할 때 lc_agent_name 키로 메타데이터에서 쓸 수 있어요.
아래에서는 도구 호출 스트리밍 예제를 갱신해요:
- 도구를 내부에서 에이전트를 호출하는
call_weather_agent도구로 바꿔요 - 각 에이전트에
name을 추가해요 - 스트림을 만들 때
subgraphs=True를 지정해요 - 스트림 처리는 이전과 동일하지만,
create_agent의name파라미터로 어떤 에이전트가 활성인지 추적하는 로직을 추가해요
먼저 에이전트를 구성해요:
from typing import Any
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import AIMessage, AnyMessage
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
weather_model = init_chat_model("openai:gpt-5.5")
weather_agent = create_agent(
model=weather_model,
tools=[get_weather],
name="weather_agent", # [!code highlight]
)
def call_weather_agent(query: str) -> str:
"""Query the weather agent."""
result = weather_agent.invoke({
"messages": [{"role": "user", "content": query}]
})
return result["messages"][-1].text
supervisor_model = init_chat_model("openai:gpt-5.5")
agent = create_agent(
model=supervisor_model,
tools=[call_weather_agent],
name="supervisor", # [!code highlight]
)
다음으로, 어떤 에이전트가 토큰을 내보내는지 알려 주는 로직을 스트리밍 루프에 추가해요:
def _render_message_chunk(token: AIMessageChunk) -> None:
if token.text:
print(token.text, end="|")
if token.tool_call_chunks:
print(token.tool_call_chunks)
def _render_completed_message(message: AnyMessage) -> None:
if isinstance(message, AIMessage) and message.tool_calls:
print(f"Tool calls: {message.tool_calls}")
if isinstance(message, ToolMessage):
print(f"Tool response: {message.content_blocks}")
input_message = {"role": "user", "content": "What is the weather in Boston?"}
current_agent = None # [!code highlight]
for chunk in agent.stream(
{"messages": [input_message]},
stream_mode=["messages", "updates"],
subgraphs=True, # [!code highlight]
version="v2", # [!code highlight]
):
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
if agent_name := metadata.get("lc_agent_name"): # [!code highlight]
if agent_name != current_agent: # [!code highlight]
print(f"🤖 {agent_name}: ") # [!code highlight]
current_agent = agent_name # [!code highlight]
if isinstance(token, AIMessageChunk):
_render_message_chunk(token)
elif chunk["type"] == "updates": # [!code highlight]
for source, update in chunk["data"].items(): # [!code highlight]
if source in ("model", "tools"):
_render_completed_message(update["messages"][-1])
🤖 supervisor:
[{'name': 'call_weather_agent', 'args': '', 'id': 'call_asorzUf0mB6sb7MiKfgojp7I', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'query', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '":"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'Boston', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ' weather', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ' right', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ' now', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ' and', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': " today's", 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ' forecast', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
Tool calls: [{'name': 'call_weather_agent', 'args': {'query': "Boston weather right now and today's forecast"}, 'id': 'call_asorzUf0mB6sb7MiKfgojp7I', 'type': 'tool_call'}]
🤖 weather_agent:
[{'name': 'get_weather', 'args': '', 'id': 'call_LZ89lT8fW6w8vqck5pZeaDIx', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'city', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '":"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': 'Boston', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
Tool calls: [{'name': 'get_weather', 'args': {'city': 'Boston'}, 'id': 'call_LZ89lT8fW6w8vqck5pZeaDIx', 'type': 'tool_call'}]
Tool response: [{'type': 'text', 'text': "It's always sunny in Boston!"}]
Boston| weather| right| now|:| **|Sunny|**|.
|Today|'s| forecast| for| Boston|:| **|Sunny| all| day|**|.|Tool response: [{'type': 'text', 'text': 'Boston weather right now: **Sunny**.\n\nToday's forecast for Boston: **Sunny all day**.'}]
🤖 supervisor:
Boston| weather| right| now|:| **|Sunny|**|.
|Today|'s| forecast| for| Boston|:| **|Sunny| all| day|**|.|
스트리밍 비활성화
어떤 애플리케이션에서는 특정 모델에서 개별 토큰 스트리밍을 비활성화해야 할 수도 있어요. 이런 경우에 유용하죠:
- 멀티에이전트 시스템에서 어떤 에이전트가 출력을 스트리밍할지 제어할 때
- 스트리밍을 지원하는 모델과 지원하지 않는 모델을 섞을 때
- LangSmith에 배포하면서 특정 모델 출력이 클라이언트로 스트리밍되는 것을 막고 싶을 때
모델 초기화 시 streaming=False를 설정해요.
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="gpt-5.5",
streaming=False # [!code highlight]
)
자세한 내용은 LangGraph 스트리밍 가이드를 참고하세요.
v2 스트리밍 형식
stream() 또는 astream()에 version="v2"를 전달하면 통일된 출력 형식을 얻을 수 있어요. 모든 덩어리는 type, ns, data 키를 가진 StreamPart 딕셔너리예요 — 스트림 모드가 무엇이든, 몇 개든 같은 형태죠:
# Must unpack (mode, data) tuples
for mode, chunk in agent.stream(
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode=["updates", "custom"],
):
print(mode) # "updates" or "custom"
print(chunk) # payload
v2 형식은 invoke()도 개선해요 — 상태와 인터럽트 메타데이터를 깔끔하게 분리하는 .value와 .interrupts 속성을 가진 GraphOutput 객체를 반환하죠:
result = agent.invoke(
{"messages": [{"role": "user", "content": "Hello"}]},
version="v2",
)
print(result.value) # state (dict, Pydantic model, or dataclass)
print(result.interrupts) # tuple of Interrupt objects (empty if none)
v2 형식에 대한 더 자세한 내용(타입 좁히기, Pydantic/dataclass 강제 변환, 서브그래프 스트리밍 등)은 LangGraph 스트리밍 문서를 참고하세요.
관련 문서
- 프론트엔드 스트리밍 —
useStream으로 실시간 에이전트 상호작용을 위한 React UI 만들기 - 채팅 모델 스트리밍 — 에이전트나 그래프 없이 채팅 모델에서 바로 토큰 스트리밍하기
- 채팅 모델 추론 — 채팅 모델의 reasoning 출력 설정하고 접근하기
- 표준 콘텐츠 블록 — reasoning, text 등 다양한 콘텐츠 타입에 쓰이는 정규화된 콘텐츠 블록 형식 이해하기
- 사람-인-더-루프 스트리밍 — 인간 검토용 인터럽트를 다루면서 에이전트 진행 스트리밍하기
- LangGraph 스트리밍 —
values,debug모드, 서브그래프 스트리밍을 포함한 고급 스트리밍 옵션
출처: 공식문서 - Streaming