토큰 스트리밍 이해
스트리밍 API가 생성되는 대로 부분 완성을 전송하는 방식과 OpenAI의 stream=True 매개변수 작동 방식을 이해하고, 스트리밍이 사용자 경험을 개선하는 시점을 알아봅니다.
토큰 스트리밍 이해은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
사용자 경험에서 스트리밍이 중요한 이유
스트리밍이 없으면 애플리케이션은 아무것도 표시하기 전에 LLM이 전체 응답을 생성할 때까지 기다려야 하며, 긴 답변의 경우 대개 5~30초가 걸립니다. 스트리밍을 사용하면 요청을 보낸 후 200~500ms 안에 첫 토큰이 나타나고, 이후 토큰도 생성되는 즉시 스트리밍됩니다. 전체 생성 시간은 같더라도 사용자가 느끼는 경험은 기다리는 과정에서 흥미로운 실시간 생성 효과로 바뀌어 반응성이 크게 향상됩니다.
LLM이 토큰을 생성하는 방식
LLM은 자기회귀 방식으로 작동합니다. 즉, 한 번에 하나의 토큰을 생성하며 각 새 토큰은 이전의 모든 토큰을 조건으로 합니다. API가 요청을 받으면 GPU는 프롬프트 처리가 끝난 직후 첫 토큰의 샘플링을 시작합니다. 이후 각 토큰은 대략 동일한 시간이 걸립니다. 스트리밍은 모든 토큰을 버퍼링한 후 마지막에 완성된 문자열을 보내는 대신, 각 토큰이 샘플링되는 즉시 클라이언트로 전송합니다.
# Conceptual model of autoregressive generation
prompt = 'The capital of France is'
# Step 1: process full prompt, predict next token
# token_1 = sample(logits) → ' Paris'
# Step 2: append token_1 to context, predict next
# token_2 = sample(logits) → '.'
# Step 3: append token_2 to context, predict next
# token_3 = sample(logits) → '<|end|>'
# Total time: time_to_process_prompt + n_tokens * time_per_token
# With streaming: first token arrives after time_to_process_prompt (TTFT)
# Without streaming: everything arrives after TTFT + n_tokens * time_per_tokenTTFT와 TPOT: 두 가지 지연 시간 지표
스트리밍은 서로 다른 두 가지 지연 시간 개념을 도입합니다. TTFT (첫 토큰까지의 시간)는 요청을 보낸 시점부터 첫 토큰을 받을 때까지의 지연 시간으로, 프롬프트 처리 시간의 영향을 크게 받습니다. TPOT (출력 토큰당 시간)은 연속된 토큰 사이의 시간으로, 모델 크기와 하드웨어에 따라 결정됩니다. TTFT는 UI가 얼마나 빠르게 응답하는지에 영향을 주고, TPOT는 텍스트가 얼마나 매끄럽게 스트리밍되는지에 영향을 줍니다. 두 지표를 모두 관측 시스템에서 별도로 추적해야 합니다.
import time
from openai import OpenAI
client = OpenAI()
def measure_streaming_latency(prompt: str):
t_start = time.perf_counter()
t_first_token = None
token_times = []
stream = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
t_now = time.perf_counter()
if t_first_token is None:
t_first_token = t_now
print(f'TTFT: {(t_first_token - t_start) * 1000:.0f}ms')
else:
token_times.append(t_now - token_times[-1] if token_times else t_now - t_first_token)
token_times.append(t_now)
print(f'TPOT avg: {1000 * (token_times[-1] - t_first_token) / max(len(token_times)-1, 1):.1f}ms')stream=True 매개변수
OpenAI SDK에서 스트리밍을 활성화하려면 chat.completions.create 호출에서 stream=True를 설정해야 합니다. 응답 유형은 ChatCompletion 객체에서 Stream[ChatCompletionChunk] 반복자로 변경됩니다. 각 청크에는 delta가 포함되며, 여기에 content 문자열 조각이 들어 있거나 토큰이 도구 호출이거나 스트림이 끝날 때는 None이 들어 있습니다.
from openai import OpenAI
client = OpenAI()
# Non-streaming: wait for complete response
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': 'Explain RAG in one paragraph.'}],
)
full_text = response.choices[0].message.content
# Streaming: receive tokens incrementally
stream = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': 'Explain RAG in one paragraph.'}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta: # delta can be None for non-content chunks
print(delta, end='', flush=True)
print() # newline at end전체 응답 누적
많은 애플리케이션 흐름에서는 반응성을 위해 토큰을 UI로 스트리밍하는 동시에, 로깅, 캐싱 또는 추가 파이프라인 단계와 같은 후속 처리에 사용할 전체 응답 텍스트도 누적해야 합니다. 패턴은 간단합니다. 스트림을 반복하면서 각 청크를 클라이언트에 출력하거나 전달하고, 동시에 콘텐츠를 전체 문자열로 이어 붙이세요.
def stream_and_accumulate(prompt: str) -> str:
stream = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
stream=True,
)
full_text = ''
finish_reason = None
for chunk in stream:
choice = chunk.choices[0]
delta = choice.delta.content
if delta:
print(delta, end='', flush=True) # real-time display
full_text += delta # accumulate
if choice.finish_reason:
finish_reason = choice.finish_reason
print() # newline
print(f'Finished: {finish_reason}, total chars: {len(full_text)}')
return full_text사용 통계와 함께 스트리밍
기본적으로 스트리밍 응답에는 토큰 사용량 통계(프롬프트 토큰, 완성 토큰)가 포함되지 않습니다. 이를 포함하려면 stream_options={'include_usage': True}를 전달하세요. 사용량 데이터는 콘텐츠 스트림이 끝난 후 최종 청크로 도착합니다. 이는 운영 애플리케이션에서 비용 추적과 속도 제한 모니터링에 중요합니다.
stream = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': 'What is a vector database?'}],
stream=True,
stream_options={'include_usage': True}, # include token counts
)
full_text = ''
usage = None
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
full_text += chunk.choices[0].delta.content
if chunk.usage: # arrives in the final chunk
usage = chunk.usage
if usage:
print(f'Prompt tokens: {usage.prompt_tokens}')
print(f'Completion tokens: {usage.completion_tokens}')
print(f'Total tokens: {usage.total_tokens}')스트리밍을 사용하지 않아야 하는 경우
스트리밍이 항상 적합한 것은 아닙니다. 다음과 같은 경우에는 스트리밍을 피하세요. (1) JSON 파싱이나 도구 호출 감지처럼 응답으로 무엇인가를 수행하기 전에 전체 응답이 필요한 경우, (2) 응답이 매우 짧아(30토큰 미만) 스트리밍 오버헤드로 절약되는 시간보다 지연이 더 커지는 경우, (3) 개별 응답 지연 시간보다 처리량이 중요한 일괄 처리로 많은 요청을 처리하는 경우입니다. 이러한 경우에는 표준 비스트리밍 호출이 더 간단하고 속도도 동일합니다.
Anthropic 및 Gemini API를 사용한 스트리밍
스트리밍은 OpenAI뿐 아니라 모든 주요 LLM 제공업체 API에서 사용할 수 있습니다. 방식은 비슷하지만 SDK 인터페이스는 조금씩 다릅니다. Anthropic의 Python SDK는 컨텍스트 관리자로 client.messages.stream()을 사용하는 반면, Gemini는 generate_content(stream=True)를 사용합니다. 제공업체에 종속되지 않는 애플리케이션을 구축할 때는 공통 생성기 함수 뒤에 스트리밍 인터페이스를 추상화하세요.
import anthropic
ant_client = anthropic.Anthropic(api_key='YOUR_KEY')
# Anthropic streaming
with ant_client.messages.stream(
model='claude-sonnet-4-5',
max_tokens=1024,
messages=[{'role': 'user', 'content': 'Explain hybrid search briefly.'}],
) as stream:
for text in stream.text_stream:
print(text, end='', flush=True)
# Final message with usage stats
final_msg = stream.get_final_message()
print(f'\nInput tokens: {final_msg.usage.input_tokens}')
print(f'Output tokens: {final_msg.usage.output_tokens}')생성기 기반 스트리밍 인터페이스
깔끔한 아키텍처 패턴은 토큰 문자열을 전달하는 Python 생성기 함수로 스트리밍을 감싸는 것입니다. 이렇게 하면 스트리밍 로직과 소비 로직이 분리됩니다. 호출자는 생성기를 반복하면서 파일에 쓰거나, WebSocket으로 전달하거나, 문자열에 누적할 수 있으며, 스트리밍 코드는 출력이 어떻게 사용되는지 알 필요가 없습니다. 이는 대부분의 운영 스트리밍 API의 기반입니다.
from typing import Generator
def stream_completion(
messages: list[dict],
model: str = 'gpt-4o-mini',
**kwargs,
) -> Generator[str, None, None]:
stream = client.chat.completions.create(
model=model,
messages=messages,
stream=True,
**kwargs,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
yield delta
# Usage: pipe to stdout
for token in stream_completion([{'role': 'user', 'content': 'Hello!'}]):
print(token, end='', flush=True)
# Usage: accumulate
full = ''.join(stream_completion([{'role': 'user', 'content': 'Hello!'}]))터미널 및 CLI 애플리케이션에서의 스트리밍
터미널 애플리케이션에서 스트리밍 출력은 입력하는 것처럼 보입니다. 생성되는 즉시 각 문자가 나타납니다. 핵심 요구 사항은 모든 print 호출에서 flush=True를 사용하는 것입니다. 플러시하지 않으면 Python이 줄바꿈이 나올 때까지 출력을 버퍼링하므로 스트리밍의 목적이 사라집니다. 출력 형식을 더 세밀하게 제어하려면 sys.stdout.write(token)을 사용한 다음 sys.stdout.flush()를 호출할 수도 있습니다.
import sys
def stream_to_terminal(messages: list[dict]):
stream = client.chat.completions.create(
model='gpt-4o-mini',
messages=messages,
stream=True,
)
token_count = 0
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
sys.stdout.write(delta) # no newline added
sys.stdout.flush() # MUST flush or output buffers
token_count += 1
print() # final newline
print(f'({token_count} tokens generated)')스트리밍과 오류 복구
스트리밍에서는 클라이언트에 이미 일부 토큰을 보낸 후 스트림 중간에 실패할 수 있으므로 오류 처리가 복잡해집니다. 권장 패턴은 스트림 반복을 try/except 블록으로 감싸고, 오류가 발생하면 클라이언트에 오류 신호를 보내거나 스트림을 깔끔하게 닫는 것입니다. 서버가 스트리밍을 시작한 후 생성 도중 멈추는 상황에 대비하여 전체 스트림에 항상 시간 제한을 설정하세요.
import signal
def stream_with_timeout(messages, timeout_seconds=30):
def timeout_handler(signum, frame):
raise TimeoutError('LLM stream timed out')
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout_seconds)
try:
stream = client.chat.completions.create(
model='gpt-4o-mini',
messages=messages,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
yield delta
except TimeoutError:
yield '\n[Response timed out]'
except Exception as e:
yield f'\n[Error: {str(e)}]'
finally:
signal.alarm(0) # cancel timeout빠른 확인
이 레슨에서 배운 LLM 토큰 스트리밍 내용을 확인해 보세요.
레슨 요약
이 레슨에서는 다음을 배웠습니다. 스트리밍은 생성된 각 토큰을 샘플링하는 즉시 클라이언트로 전송하여 체감 응답성을 크게 향상하고, TTFT와 TPOT는 별도로 추적해야 하는 두 가지 핵심 지연 시간 지표이며, stream=True는 OpenAI SDK의 응답을 for 루프로 소비하는 청크 반복자로 변경합니다. 깔끔하고 재사용 가능한 인터페이스를 위해 스트림을 생성기 함수로 감싸십시오. 다음으로 Python SDK를 사용한 비동기 스트리밍을 구현하겠습니다.
자주 묻는 질문
“토큰 스트리밍 이해” 강의는 무료인가요?
네 — “토큰 스트리밍 이해” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“토큰 스트리밍 이해”에서 뭘 배우나요?
스트리밍 API가 생성되는 대로 부분 완성을 전송하는 방식과 OpenAI의 stream=True 매개변수 작동 방식을 이해하고, 스트리밍이 사용자 경험을 개선하는 시점을 알아봅니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“토큰 스트리밍 이해” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.