스트리밍

스트리밍은 LLM 기반 애플리케이션의 응답성을 향상시키는 데 매우 중요합니다. 완전한 응답이 준비되기 전에 출력을 점진적으로 표시함으로써, LLM의 지연 시간을 다룰 때 사용자 경험(UX)을 크게 개선할 수 있습니다.

개요

스트리밍 시스템을 사용하면 에이전트 실행으로부터 실시간 피드백을 애플리케이션에 제공할 수 있습니다.

스트리밍으로 가능한 것들:

  • 에이전트 진행 상황 스트리밍: 각 에이전트 단계 후 상태 업데이트를 받습니다.
  • LLM 토큰 스트리밍: 언어 모델 토큰이 생성되는 대로 스트리밍합니다.
  • 커스텀 업데이트 스트리밍: 사용자 정의 신호를 방출합니다 (예: "100개 중 10개의 레코드를 가져왔습니다").
  • 다중 모드 스트리밍: updates(에이전트 진행 상황), messages(LLM 토큰 + 메타데이터), 또는 custom(임의의 사용자 데이터) 중에서 선택합니다.

Agent progress

에이전트 진행 상황을 스트리밍하려면 stream() 또는 astream() 메서드를 stream_mode="updates"와 함께 사용하세요. 이는 모든 에이전트 단계 후에 이벤트를 방출합니다.

예를 들어, 도구를 한 번 호출하는 에이전트가 있다면 다음과 같은 업데이트를 볼 수 있습니다:

  • LLM 노드: 도구 호출 요청이 포함된 AIMessage
  • 도구 노드: 실행 결과가 포함된 ToolMessage
  • LLM 노드: 최종 AI 응답
from langchain.agents import create_agent
 
 
def get_weather(city: str) -> str:
    """지정된 도시의 날씨를 가져옵니다."""
 
    return f"{city}에는 항상 햇살이 가득해요!"
 
 
agent = create_agent(
    model="openai:gpt-5-nano",
    tools=[get_weather],
)
 
for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "샌프란시스코 날씨는 어때요?"}]},
    stream_mode="updates",
):
    for step, data in chunk.items():
        print(f"step: {step}")
        print(f"content: {data['messages'][-1].content_blocks}")
step: model
content: [{'type': 'tool_call', 'name': 'get_weather', 'args': {'city': 'San Francisco'}, 'id': 'call_teanAF831fU4ZyRRKkDaceRZ'}]
step: tools
content: [{'type': 'text', 'text': 'San Francisco에는 항상 햇살이 가득해요!'}]
step: model
content: [{'type': 'text', 'text': '샌프란시스코의 현재 날씨는 맑고 햇빛이 강한 편이에요. 원하시면 구체적인 기온, 바람 속도, 습도 같은 자세한 정보를 알려드릴게요. 다른 도시의 날씨도 궁금하신가요?'}]

LLM tokens

LLM이 생성하는 토큰을 스트리밍하려면 stream_mode="messages"를 사용하세요. 아래에서 도구 호출과 최종 응답을 스트리밍하는 에이전트의 출력을 볼 수 있습니다.

for token, metadata in agent.stream(
    {"messages": [{"role": "user", "content": "샌프란시스코 날씨는 어때요?"}]},
    stream_mode="messages",  # <- messages를 사용
):
    print(f"node: {metadata['langgraph_node']}")
    print(f"content: {token.content_blocks}")
    print("\n")
node: model
content: [{'type': 'tool_call_chunk', 'id': 'call_UhqORKbJRptURTgeWRp6o4fR', '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: model
content: []


node: model
content: []


node: tools
content: [{'type': 'text', 'text': 'San Francisco에는 항상 햇살이 가득해요!'}]


node: model
content: [{'type': 'tool_call_chunk', 'id': 'call_eLOMO9oFLrrLxfwTS0lBnJMo', '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: model
content: []


node: model
content: []


node: tools
content: [{'type': 'text', 'text': 'San Francisco에는 항상 햇살이 가득해요!'}]


node: model
content: []


node: model
content: [{'type': 'text', 'text': '샌'}]


node: model
content: [{'type': 'text', 'text': '프'}]


node: model
content: [{'type': 'text', 'text': '란'}]


node: model
content: [{'type': 'text', 'text': '시'}]


node: model
content: [{'type': 'text', 'text': '스'}]


node: model
content: [{'type': 'text', 'text': '코'}]


node: model
content: [{'type': 'text', 'text': '의'}]


node: model
content: [{'type': 'text', 'text': ' 현재'}]


node: model
content: [{'type': 'text', 'text': ' 날'}]


node: model
content: [{'type': 'text', 'text': '씨'}]


node: model
content: [{'type': 'text', 'text': '를'}]


node: model
content: [{'type': 'text', 'text': ' 확인'}]


node: model
content: [{'type': 'text', 'text': '했습니다'}]


node: model
content: [{'type': 'text', 'text': '.\n\n'}]


node: model
content: [{'type': 'text', 'text': '-'}]


node: model
content: [{'type': 'text', 'text': ' 결과'}]


node: model
content: [{'type': 'text', 'text': ':'}]


node: model
content: [{'type': 'text', 'text': ' "'}]


node: model
content: [{'type': 'text', 'text': 'San'}]


node: model
content: [{'type': 'text', 'text': ' Francisco'}]


node: model
content: [{'type': 'text', 'text': '에는'}]


node: model
content: [{'type': 'text', 'text': ' 항상'}]


node: model
content: [{'type': 'text', 'text': ' 햇'}]


node: model
content: [{'type': 'text', 'text': '살'}]


node: model
content: [{'type': 'text', 'text': '이'}]


node: model
content: [{'type': 'text', 'text': ' 가'}]


node: model
content: [{'type': 'text', 'text': '득'}]


node: model
content: [{'type': 'text', 'text': '해'}]


node: model
content: [{'type': 'text', 'text': '요'}]


node: model
content: [{'type': 'text', 'text': '!"\n\n'}]


node: model
content: [{'type': 'text', 'text': '원'}]


node: model
content: [{'type': 'text', 'text': '하'}]


node: model
content: [{'type': 'text', 'text': '시면'}]


node: model
content: [{'type': 'text', 'text': ' 더'}]


node: model
content: [{'type': 'text', 'text': ' 자세'}]


node: model
content: [{'type': 'text', 'text': '한'}]


node: model
content: [{'type': 'text', 'text': ' 정보를'}]


node: model
content: [{'type': 'text', 'text': ' 알려'}]


node: model
content: [{'type': 'text', 'text': '드'}]


node: model
content: [{'type': 'text', 'text': '릴'}]


node: model
content: [{'type': 'text', 'text': '게'}]


node: model
content: [{'type': 'text', 'text': '요'}]


node: model
content: [{'type': 'text', 'text': '.'}]


node: model
content: [{'type': 'text', 'text': ' 예'}]


node: model
content: [{'type': 'text', 'text': '를'}]


node: model
content: [{'type': 'text', 'text': ' 들'}]


node: model
content: [{'type': 'text', 'text': '면'}]


node: model
content: [{'type': 'text', 'text': '\n'}]


node: model
content: [{'type': 'text', 'text': '-'}]


node: model
content: [{'type': 'text', 'text': ' 현재'}]


node: model
content: [{'type': 'text', 'text': ' 온'}]


node: model
content: [{'type': 'text', 'text': '도'}]


node: model
content: [{'type': 'text', 'text': ','}]


node: model
content: [{'type': 'text', 'text': ' 체'}]


node: model
content: [{'type': 'text', 'text': '감'}]


node: model
content: [{'type': 'text', 'text': '온'}]


node: model
content: [{'type': 'text', 'text': '도'}]


node: model
content: [{'type': 'text', 'text': '\n'}]


node: model
content: [{'type': 'text', 'text': '-'}]


node: model
content: [{'type': 'text', 'text': ' 바'}]


node: model
content: [{'type': 'text', 'text': '람'}]


node: model
content: [{'type': 'text', 'text': ' 세'}]


node: model
content: [{'type': 'text', 'text': '기'}]


node: model
content: [{'type': 'text', 'text': '와'}]


node: model
content: [{'type': 'text', 'text': ' 방향'}]


node: model
content: [{'type': 'text', 'text': '\n'}]


node: model
content: [{'type': 'text', 'text': '-'}]


node: model
content: [{'type': 'text', 'text': ' 상대'}]


node: model
content: [{'type': 'text', 'text': ' 습'}]


node: model
content: [{'type': 'text', 'text': '도'}]


node: model
content: [{'type': 'text', 'text': '\n'}]


node: model
content: [{'type': 'text', 'text': '-'}]


node: model
content: [{'type': 'text', 'text': ' 강'}]


node: model
content: [{'type': 'text', 'text': '수'}]


node: model
content: [{'type': 'text', 'text': ' 확'}]


node: model
content: [{'type': 'text', 'text': '률'}]


node: model
content: [{'type': 'text', 'text': '('}]


node: model
content: [{'type': 'text', 'text': '비'}]


node: model
content: [{'type': 'text', 'text': '/'}]


node: model
content: [{'type': 'text', 'text': '눈'}]


node: model
content: [{'type': 'text', 'text': ' 가능'}]


node: model
content: [{'type': 'text', 'text': '성'}]


node: model
content: [{'type': 'text', 'text': ')\n'}]


node: model
content: [{'type': 'text', 'text': '-'}]


node: model
content: [{'type': 'text', 'text': ' 앞으로'}]


node: model
content: [{'type': 'text', 'text': '의'}]


node: model
content: [{'type': 'text', 'text': ' 주'}]


node: model
content: [{'type': 'text', 'text': '간'}]


node: model
content: [{'type': 'text', 'text': ' 예'}]


node: model
content: [{'type': 'text', 'text': '보'}]


node: model
content: [{'type': 'text', 'text': '\n\n'}]


node: model
content: [{'type': 'text', 'text': '어'}]


node: model
content: [{'type': 'text', 'text': '떤'}]


node: model
content: [{'type': 'text', 'text': ' 정보를'}]


node: model
content: [{'type': 'text', 'text': ' 원'}]


node: model
content: [{'type': 'text', 'text': '하시'}]


node: model
content: [{'type': 'text', 'text': '나요'}]


node: model
content: [{'type': 'text', 'text': '?'}]


node: model
content: []


node: model
content: []


node: model
content: []

Custom updates

도구가 실행되는 동안 업데이트를 스트리밍하려면 get_stream_writer를 사용할 수 있습니다.

도구 내부에 get_stream_writer를 추가하면 LangGraph 실행 컨텍스트 외부에서는 도구를 호출할 수 없습니다.

from langchain.agents import create_agent
from langgraph.config import get_stream_writer
 
 
def get_weather(city: str) -> str:
    """지정된 도시의 날씨를 가져옵니다."""
 
    writer = get_stream_writer()
 
    # 임의의 데이터 스트리밍
    writer(f"도시 {city}에 대한 데이터 조회 중")
    writer(f"도시 {city}에 대한 데이터 획득")
 
    return f"{city}에는 항상 햇살이 가득해요!"
 
 
agent = create_agent(
    model="openai:gpt-4.1-nano",
    tools=[get_weather],
)
 
for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "샌프란시스코 날씨는 어때요?"}]},
    stream_mode="custom",
):
    print(chunk)
도시 샌프란시스코에 대한 데이터 조회 중
도시 샌프란시스코에 대한 데이터 획득

Stream multiple modes

스트림 모드를 리스트로 전달하여 여러 스트리밍 모드를 지정할 수 있습니다: stream_mode=["updates", "custom"]:

for stream_mode, chunk in agent.stream(
    {"messages": [{"role": "user", "content": "샌프란시스코 날씨는 어때요?"}]},
    stream_mode=["updates", "custom"],
):
    print(f"stream_mode: {stream_mode}")
    print(f"content: {chunk}")
    print("\n")
stream_mode: updates
content: {'model': {'messages': [AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 62, 'total_tokens': 82, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4.1-nano-2025-04-14', 'system_fingerprint': 'fp_950f36939b', 'id': 'chatcmpl-CYYqyjfnLopXDZnSR6Wks9h7TEFDI', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--84f0384c-7637-4c77-94a2-f400b3b16004-0', tool_calls=[{'name': 'get_weather', 'args': {'city': '샌프란시스코'}, 'id': 'call_fTkg4PGZezMqz2zBvfJ7p2FV', 'type': 'tool_call'}], usage_metadata={'input_tokens': 62, 'output_tokens': 20, 'total_tokens': 82, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}


stream_mode: custom
content: 도시 샌프란시스코에 대한 데이터 조회 중


stream_mode: custom
content: 도시 샌프란시스코에 대한 데이터 획득


stream_mode: updates
content: {'tools': {'messages': [ToolMessage(content='샌프란시스코에는 항상 햇살이 가득해요!', name='get_weather', id='e9f2531e-0609-4944-a46a-708d809636d0', tool_call_id='call_fTkg4PGZezMqz2zBvfJ7p2FV')]}}


stream_mode: updates
content: {'model': {'messages': [AIMessage(content='샌프란시스코는 항상 햇살이 가득해요! 좋은 하루 보내세요!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 24, 'prompt_tokens': 108, 'total_tokens': 132, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4.1-nano-2025-04-14', 'system_fingerprint': 'fp_950f36939b', 'id': 'chatcmpl-CYYqzi7jUObJmAuEPU6e4dxi6dpM4', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--a76d6d98-c4d7-4ff7-9ba1-0e80e0632180-0', usage_metadata={'input_tokens': 108, 'output_tokens': 24, 'total_tokens': 132, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}

Disable streaming

일부 애플리케이션에서는 특정 모델에 대해 개별 토큰의 스트리밍을 비활성화해야 할 수 있습니다.

이는 멀티 에이전트 시스템에서 어떤 에이전트가 출력을 스트리밍할지 제어할 때 유용합니다.

스트리밍을 비활성화하는 방법을 알아보려면 모델 가이드를 참조하세요.