Intro

OpenAI API를 이용해 답변을 실시간 스트리밍(stream)으로 받아오는 AI 에이전트를 개발하던 중이었다. ChatGPT처럼 한 토큰씩 타이핑치듯이 출력되는 화면을 기대했지만, 예상과 달리 응답이 한 문단씩 뭉텅이로 끊겨서 출력되는 현상을 발견했다.
문제 원인 : 표준 출력의 버퍼링
문제의 원인은 바로 표준 출력(stdout) 의 버퍼링(buffering)이었다. 파이썬의 print() 함수는 호출될 때마다 즉시 화면에 글자를 뿌리는 것이 아니다. 내부적으로 출력 버퍼라는 임시 공간에 내용을 먼저 쌓아두었다가, 특정 조건이 만족될 때 OS의 표준 출력으로 데이터를 내보내게 된다.
버퍼에 대해서 궁금하다면 - 버퍼의 개념과 다양한 버퍼 방식
대표적인 버퍼 방출(flush) 조건들은 다음과 같다.
- 출력 버퍼가 정해진 용량만큼 가득 찼을 때
- 줄바꿈(
\n)이 발생했을 때 - 프로그램이 정상적으로 종료될 때
- 강제로 버퍼를 비우도록 명령할 때 (flush)
왜 평소에는 몰랐을까?
- 바로
print(end="")조건 때문
평소에 반복문에서 print()를 사용할 때에는 실시간으로 한 줄씩 잘 출력된다. 그 이유는 print()함수의 end 옵션의 기본값이 바로 줄바꿈 문자(\n)이기 때문이다. 앞선 단락에서 소개했든 줄바꿈이 발생할 때에는 버퍼가 비워지고 출력이 발생하게 된다. 때문에 평소에 이런 현상을 접하지 못한 것이었다.
하지만 LLM 스트리밍에서는 토큰을 공백 없이 이어붙이기 위해 print(..., end="")형식을 사용했다. 이 경우에는 줄바꿈이 일어나지 않으므로, 데이터는 계속 출력 버퍼에 쌓이다가, 버퍼 용량이 가득 차는 시점에 한꺼번에 표준 출력에 전달되는 것이다. 이게 바로 문단 단위 출력의 원인이었다.
이를 좀 더 쉽게 체감할 수 있게 예시 코드를 준비해보았다. 실행해보도록 하자.
- 줄바꿈을 하는 경우
1
2
3
4
5
6
7
# 줄바꿈을 하는 경우 -> 바로 출력됨
import time
gen = (x for x in range(1000))
for it in gen:
print(it)
time.sleep(0.02)
- 줄바꿈을 하지 않는 경우
1
2
3
4
5
6
7
# 줄바꿈을 하지 않는 경우 -> 버퍼가 차는 것을 기다렸다가 출력됨
import time
gen = (x for x in range(1000))
for it in gen:
print(it, end="")
time.sleep(0.02)
해결
- 버퍼를 즉시 비우는
flush=True옵션을 적용
파이썬의 print 함수에서는 출력 버퍼를 즉시 비우는 flush 라는 옵션이 있다. 이 옵션은, print 내용을 바로 OS로 전달해 출력되도록 하는 역할을 한다. 이를 적용해서 LLM 의 stream 을 정상적으로 구현할 수 있었다.
1
2
3
4
# 예시 코드
for chunk in response:
print(chunk, end="", flush=True)
...

Reference
https://docs.python.org/ko/3.13/library/functions.html#print
Comments