LangChain에서 출력 스트리밍하기
LCEL 체인을 통해 토큰 스트리밍을 구현하여 전체 응답을 기다리는 대신 각 단어가 도착하는 즉시 애플리케이션에 표시하고, 체감 지연 시간을 줄입니다.
LangChain에서 출력 스트리밍하기은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
스트리밍이 중요한 이유
스트리밍이 없으면 사용자는 LLM이 생성을 완료할 때까지 빈 화면을 바라보며 기다려야 합니다. 긴 응답을 생성하는 데는 5~30초가 걸릴 수 있습니다. 스트리밍을 사용하면 토큰이 생성되는 즉시 표시되어 즉각적인 피드백을 제공합니다. 이를 통해 사용자가 느끼는 응답성이 크게 향상됩니다. LangChain의 LCEL은 .stream()을 호출하면 전체 체인에 걸쳐 스트리밍을 자동으로 전달합니다.
.stream()을 사용한 기본 스트리밍
모든 LCEL 체인은 청크의 반복자를 반환하는 .stream() 메서드를 제공합니다. StrOutputParser로 끝나는 체인에서는 각 청크가 문자열의 일부입니다. 청크를 순회하면서 도착하는 즉시 출력하거나 생성하면 됩니다. 스트리밍은 HTTP 수준에서 이루어지며, OpenAI API에서 전달된 각 토큰은 도착하는 즉시 파서를 거쳐 전달됩니다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
chain = (
ChatPromptTemplate.from_template('Explain {topic} in detail.')
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
# Stream tokens to stdout
for chunk in chain.stream({'topic': 'quantum entanglement'}):
print(chunk, end='', flush=True)
print() # final newline.astream()을 사용한 비동기 스트리밍
.astream()은 .stream()의 비동기 버전입니다. 비동기 반복자를 반환하며 async for로 소비합니다. 요청 처리기가 코루틴인 FastAPI, Starlette 및 기타 비동기 웹 프레임워크에서는 이것이 올바른 접근 방식입니다. 비동기 처리기에서 동기 스트리밍을 사용하면 이벤트 루프가 차단됩니다.
import asyncio
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
chain = (
ChatPromptTemplate.from_template('Write a poem about {subject}')
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
async def stream_response():
async for chunk in chain.astream({'subject': 'the ocean'}):
print(chunk, end='', flush=True)
asyncio.run(stream_response())StreamingResponse를 사용한 FastAPI 스트리밍
FastAPI에서는 비동기 생성기를 media_type='text/plain'과 함께 StreamingResponse로 감싸 브라우저에 텍스트 토큰을 스트리밍할 수 있습니다. 서버 전송 이벤트(SSE)에는 media_type='text/event-stream'을 사용하고 각 청크를 data: ...\n\n 형식으로 지정하세요. 그러면 브라우저는 전체 응답을 기다리지 않고 생성되는 토큰을 즉시 받습니다.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def generate_stream(topic: str):
async for chunk in chain.astream({'topic': topic}):
yield chunk
@app.get('/stream')
async def stream_endpoint(topic: str):
return StreamingResponse(
generate_stream(topic),
media_type='text/plain'
)
# SSE format for frontend EventSource
async def sse_stream(topic: str):
async for chunk in chain.astream({'topic': topic}):
yield f'data: {chunk}\n\n'중간 단계를 거치는 스트리밍
LCEL 체인은 스트리밍을 지원하는 각 단계를 거쳐 스트리밍을 전달합니다. StrOutputParser는 스트리밍을 인식하므로 청크를 즉시 통과시킵니다. 하지만 JsonOutputParser와 같은 일부 파서는 구문 분석 전에 전체 출력을 버퍼링해야 하므로 스트리밍이 중단됩니다. LangChain은 이를 명확하게 처리합니다. 어떤 단계가 스트리밍과 호환되지 않으면 해당 단계는 출력을 누적한 후 다음 단계로 전달합니다.
from langchain_core.output_parsers import JsonOutputParser
# This chain does NOT stream token by token
# JsonOutputParser must buffer the full response before parsing JSON
json_chain = (
ChatPromptTemplate.from_template('Return JSON: {task}')
| ChatOpenAI(model='gpt-4o-mini')
| JsonOutputParser() # buffers until complete
)
# But partial JSON streaming IS possible with streaming_json_parser
for partial in json_chain.stream({'task': 'list 3 colors'}):
print(partial) # prints partial dict as it fills in세밀한 제어를 위한 astream_events
.astream_events()는 최종 출력뿐 아니라 체인의 모든 단계에 대한 이벤트를 내보내는 더 세밀한 스트리밍 API를 제공합니다. 각 이벤트에는 kind 필드(on_chain_start, on_llm_stream, on_chain_end)와 data 페이로드가 있습니다. 이를 사용하면 도구 호출 결과, 중간 추론, 최종 출력을 UI의 서로 다른 영역으로 각각 스트리밍할 수 있습니다.
async def stream_with_events(question: str):
async for event in chain.astream_events(
{'question': question},
version='v2'
):
kind = event['event']
if kind == 'on_llm_stream':
chunk = event['data']['chunk'].content
print(chunk, end='', flush=True)
elif kind == 'on_chain_end':
print('\n[Done]')
elif kind == 'on_tool_start':
print(f'\n[Tool: {event["name"]}]')스트리밍 출력 버퍼링
사용자에게 토큰을 스트리밍하는 동시에 완전한 응답을 캡처하여 로깅하거나 추가로 처리해야 할 때가 있습니다. 목록 누적기와 함께 .astream()을 사용하세요. 반복이 끝난 후 청크를 결합하면 전체 텍스트를 얻을 수 있습니다. 이 패턴을 사용하면 스트리밍 출력을 실시간으로 표시하면서 분석, 캐싱 또는 평가를 위해 완전한 응답도 저장할 수 있습니다.
async def stream_and_capture(question: str) -> str:
full_response = []
async for chunk in chain.astream({'question': question}):
print(chunk, end='', flush=True) # stream to user
full_response.append(chunk) # also collect
print() # newline
complete = ''.join(full_response)
await log_response(question, complete) # log full text
return complete도구 호출을 사용한 스트리밍
모델이 스트리밍 응답에서 도구 호출을 생성하면 함수 인수가 토큰 조각으로 도착합니다. 도구 호출이 완료되어 실행할 수 있을 때까지 JSON 인수 문자열을 버퍼링해야 합니다. LangChain은 에이전트 실행기에서 이를 자동으로 처리하지만, 사용자 지정 스트리밍 루프를 구축하는 경우 finish_reason을 확인하고 tool_call.function.arguments 조각을 누적해야 합니다.
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def stream_with_tools(prompt: str):
tool_call_buffer = {}
async with client.chat.completions.stream(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
tools=[weather_tool_schema]
) as stream:
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.tool_calls:
for tc in delta.tool_calls:
idx = tc.index
if idx not in tool_call_buffer:
tool_call_buffer[idx] = ''
if tc.function.arguments:
tool_call_buffer[idx] += tc.function.arguments스트리밍 취소와 시간 제한
장시간 스트리밍 응답에는 취소 기능이 필요합니다. 비동기 Python에서는 스트림을 감싸는 asyncio.Task를 취소할 수 있습니다. FastAPI에서는 StreamingResponse를 사용할 때 클라이언트 연결 끊김에 따른 취소를 프레임워크가 자동으로 처리합니다. OpenAI 클라이언트의 timeout 매개변수로 시간 제한을 설정하거나 스트림을 asyncio.wait_for()로 감싸 최대 시간이 지나면 중단하세요.
import asyncio
async def stream_with_timeout(question: str, timeout: float = 30.0):
async def _stream():
async for chunk in chain.astream({'question': question}):
yield chunk
try:
async for chunk in asyncio.timeout(_stream(), timeout):
print(chunk, end='', flush=True)
except asyncio.TimeoutError:
print('\n[Stream timed out after 30 seconds]')
except asyncio.CancelledError:
print('\n[Stream cancelled by client disconnect]')JavaScript를 사용한 클라이언트 측 SSE
프런트엔드에서는 브라우저의 기본 EventSource API가 서버 전송 이벤트를 소비합니다. FastAPI 엔드포인트가 data: token\n\n 청크를 내보내면 EventSource는 각 청크마다 message 이벤트를 발생시킵니다. 각 토큰이 도착할 때 DOM에 추가하면 타자기 효과를 만들 수 있습니다. 더 세밀하게 제어하려면 response.body.getReader()와 함께 fetch()를 사용해 스트리밍에 완전히 접근할 수 있습니다.
// Frontend JavaScript (not Python)
const source = new EventSource('/stream?topic=quantum+computing');
const outputDiv = document.getElementById('output');
source.onmessage = (event) => {
outputDiv.textContent += event.data;
};
source.onerror = () => {
source.close();
outputDiv.textContent += ' [done]';
};
// Alternative: fetch with ReadableStream
const response = await fetch('/stream?topic=ai');
const reader = response.body.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) break;
outputDiv.textContent += new TextDecoder().decode(value);
}스트리밍 모범 사례
스트리밍을 구현할 때는 다음 모범 사례를 따르세요. 버퍼링을 방지하려면 stdout에 출력할 때 항상 flush=True를 사용하세요. 스트리밍 중 정확한 토큰 수가 필요하면 stream_usage=True를 설정하세요. 클라이언트가 연결을 닫을 시점을 알 수 있도록 SSE 스트림 끝에 data: [DONE]\n\n 센티널을 내보내세요. curl --no-buffer로 스트리밍 엔드포인트를 테스트하여 토큰이 점진적으로 도착하는지 확인하세요.
# Complete SSE endpoint with DONE sentinel
async def sse_generator(question: str):
try:
async for chunk in chain.astream({'question': question}):
# Escape any newlines in the chunk
safe_chunk = chunk.replace('\n', ' ')
yield f'data: {safe_chunk}\n\n'
finally:
yield 'data: [DONE]\n\n'
@app.get('/chat/stream')
async def chat_stream(question: str):
return StreamingResponse(
sse_generator(question),
media_type='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
)빠른 확인
LangChain에서 스트리밍 출력을 이해했는지 확인해 보세요.
레슨 요약
이 레슨에서는 다음을 배웠습니다. stream() 및 astream()을 사용하면 토큰 청크가 생성되는 즉시 순회할 수 있어 전체 응답을 기다리는 긴 시간을 없앨 수 있습니다. SSE 형식의 FastAPI StreamingResponse는 브라우저 클라이언트에 토큰을 실시간으로 전달합니다. 또한 astream_events()는 도구 호출과 중간 출력을 포함하여 체인의 각 단계에 대한 세밀한 이벤트 훅을 제공합니다. 다음으로는 여러 차례에 걸친 대화를 위한 메모리 관리를 살펴보겠습니다.
자주 묻는 질문
“LangChain에서 출력 스트리밍하기” 강의는 무료인가요?
네 — “LangChain에서 출력 스트리밍하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“LangChain에서 출력 스트리밍하기”에서 뭘 배우나요?
LCEL 체인을 통해 토큰 스트리밍을 구현하여 전체 응답을 기다리는 대신 각 단어가 도착하는 즉시 애플리케이션에 표시하고, 체감 지연 시간을 줄입니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“LangChain에서 출력 스트리밍하기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- LangChain 아키텍처와 핵심 추상화
- LCEL로 체인 구축하기
- 분기 및 병렬 체인
- LangChain에서 출력 스트리밍하기