- Published on
Responses API 400 context_length_exceeded 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 OpenAI Responses API를 붙이다 보면 어느 순간 갑자기 400과 함께 context_length_exceeded가 터집니다. 특히 RAG(검색 증강 생성)로 문서를 여러 개 붙이거나, 대화 히스토리를 전부 누적하는 방식으로 구현했을 때 재현이 쉽습니다. 문제는 “프롬프트가 좀 길었나?” 수준이 아니라, 모델별 컨텍스트 윈도우 한도를 초과하면 요청 자체가 거절된다는 점입니다.
이 글에서는 context_length_exceeded의 정확한 의미, 어디서 토큰이 새는지(시스템/개발자 메시지, 툴 스키마, 첨부 문서, 출력 토큰 예약), 그리고 현업에서 가장 많이 쓰는 해결 패턴(max_output_tokens 제한, 히스토리 슬라이딩, 요약 캐시, RAG top-k/청크 튜닝, 사전 토큰 계산, 오류 재시도 전략)을 코드와 함께 정리합니다.
> 참고: Responses API에서 400이 나는 다른 케이스(필드 형식, tool 정의 오류, input 구조 오류 등)는 별도 글로 정리해두었습니다. OpenAI Responses API 400 invalid_request_error 원인과 해결
1) 에러 메시지의 정체: “입력만”이 아니라 “입력+출력 예약”
context_length_exceeded는 단순히 input이 길다는 뜻이 아닙니다. 대부분의 API/SDK는 다음을 합산한 총량이 모델 한도를 넘으면 실패합니다.
- 시스템/개발자 지시문(상수처럼 보이지만 토큰을 먹음)
- 사용자 입력(질문)
- 대화 히스토리(이전 질답)
- 툴 호출 관련 텍스트(함수 스키마/인자/결과)
- RAG로 붙인 문서 본문
- 모델이 생성할 출력 토큰을 위한 여유분(예:
max_output_tokens)
즉, max_output_tokens를 크게 잡아두면 입력이 그만큼 짧아야 합니다. “입력은 한도 미만인데도” 에러가 나는 이유가 여기서 나옵니다.
2) 가장 흔한 원인 6가지(체크리스트)
2.1 히스토리를 무제한으로 누적
채팅 UI에서 흔히 하는 실수:
- 매 요청마다 지난 모든 메시지를 그대로
input에 포함 - 시스템 프롬프트도 매번 길게 포함
해결: 슬라이딩 윈도우(최근 N턴만) 또는 요약 메모리로 전환.
2.2 RAG 문서를 “그냥 다 붙임”
검색 결과 top-k를 1020개로 뽑고, 청크도 12k 토큰으로 크게 가져오면 폭발합니다.
해결: top-k 축소 + 청크 크기 축소 + 중복 제거 + 질문과 무관한 섹션 제거.
2.3 툴 스키마/JSON Schema가 과도하게 큼
Responses API에서 tool 정의(특히 JSON Schema)를 길게 넣으면 매 요청 토큰을 많이 차지합니다.
해결: 스키마 최소화(필드 축소, 설명 축약), 툴 수 줄이기, 정말 필요한 요청에서만 툴 제공.
2.4 출력 토큰을 과도하게 예약
max_output_tokens=2048 같은 값을 상수로 박아두면, 입력이 조금만 길어져도 한도를 초과할 수 있습니다.
해결: 상황별로 동적 조절(요약이면 256, 분석이면 1024 등).
2.5 “로그/디버그 텍스트”를 프롬프트에 포함
서버 로그, SQL 결과 전체, HTML 원문, 에러 스택트레이스를 그대로 붙이면 토큰이 순식간에 늘어납니다.
해결: 필요한 부분만 발췌 + 구조화(핵심 필드만) + 길이 제한.
2.6 멀티턴에서 tool 결과(특히 문서) 재주입
툴 호출로 가져온 긴 결과를 다음 턴에도 계속 포함하면 누적됩니다.
해결: 툴 결과는 저장해두고, 다음 턴에는 요약/참조 키만 넣기.
3) 빠른 해결 1순위: max_output_tokens부터 줄여라
가장 즉효가 큰 조치가 max_output_tokens 조절입니다. 입력이 길어지는 상황(RAG, 긴 히스토리)에서는 출력 토큰을 적절히 제한해야 요청이 통과합니다.
from openai import OpenAI
client = OpenAI()
resp = client.responses.create(
model="gpt-4.1-mini",
input=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "아래 문서를 읽고 핵심만 5줄로 요약해줘..."},
],
# 요약 작업이면 과도한 출력 예약이 필요 없음
max_output_tokens=300,
)
print(resp.output_text)
운영에서는 “항상 1024” 같은 고정값 대신, 요청 유형별로 상한을 두는 편이 안전합니다.
4) 근본 해결: 토큰 예산(Token Budget) 설계
4.1 입력 예산을 먼저 정하고, 초과분은 잘라낸다
실무적으로는 다음처럼 예산을 나눕니다.
- 모델 컨텍스트 한도:
CONTEXT_LIMIT - 출력 예약:
OUTPUT_BUDGET - 시스템/상수 프롬프트:
SYSTEM_BUDGET(실측 기반) - 남는 입력 예산:
INPUT_BUDGET = CONTEXT_LIMIT - OUTPUT_BUDGET - SYSTEM_BUDGET - SAFETY_MARGIN
그리고 INPUT_BUDGET을 넘는 부분은 (1) 문서 줄이기 → (2) 히스토리 줄이기 → (3) 그래도 넘으면 요약 순으로 처리합니다.
4.2 tiktoken으로 사전 토큰 계산(가드레일)
아래 코드는 대략적인 토큰 수를 계산해 초과 전에 잘라내는 패턴입니다(모델별 인코딩은 상황에 맞게 선택).
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
def count_tokens(text: str) -> int:
return len(enc.encode(text))
def trim_to_tokens(text: str, max_tokens: int) -> str:
ids = enc.encode(text)
ids = ids[:max_tokens]
return enc.decode(ids)
# 예: RAG 문서가 너무 길면 상한으로 컷
doc = open("doc.txt", "r", encoding="utf-8").read()
print("doc tokens:", count_tokens(doc))
doc_trimmed = trim_to_tokens(doc, 1200)
주의할 점:
- 실제 API의 토큰 산정은 메시지 구조/역할/툴 정의 등을 포함해 더 복잡할 수 있습니다.
- 그래도 사전 차단 목적에는 충분히 효과적입니다.
5) RAG에서 context_length_exceeded를 줄이는 튜닝 포인트
RAG는 “검색 품질”과 “토큰 비용/한도”가 트레이드오프입니다. 다음 순서로 줄이면 품질 손실을 최소화할 수 있습니다.
5.1 top-k를 줄이기 전에 “중복 제거”
동일 문서의 비슷한 청크가 여러 개 들어오면 낭비입니다.
- 같은
doc_id에서 상위 1~2개만 채택 - cosine 유사도가 너무 비슷하면 하나만 남김
5.2 청크 크기/오버랩 조정
- 청크가 크면 한 번에 많이 먹지만, top-k를 줄여도 정보 손실이 큼
- 청크가 너무 작으면 top-k가 커져야 해서 또 길어짐
권장 접근:
- 300~800 토큰 수준의 청크로 시작
- overlap은 10~20% 정도로 최소화
5.3 “질문에 필요한 섹션만” 발췌
문서 전체를 붙이지 말고, 다음처럼 서버에서 전처리합니다.
- 제목/헤더/요약/결론 우선
- 표/로그/코드 블록은 필요한 부분만
RAG 품질이 갑자기 떨어지는(정확도 급락) 케이스는 검색/정규화 문제일 때도 많습니다. 관련해서는 PostgreSQL pgvector RAG 검색 품질 급락 원인과 해결 체크리스트도 함께 보면 원인 분리가 빨라집니다.
6) 대화 히스토리 관리: 슬라이딩 + 요약 메모리 조합
6.1 최근 N턴만 유지(슬라이딩 윈도우)
가장 간단하고 안정적입니다.
def build_messages(system_prompt: str, history: list[dict], user_msg: str, keep_last_turns: int = 6):
# history: [{"role":"user"|"assistant", "content":"..."}, ...]
trimmed = history[-keep_last_turns*2:] # user+assistant를 1턴으로 가정
return (
[{"role": "system", "content": system_prompt}] +
trimmed +
[{"role": "user", "content": user_msg}]
)
단점: 오래된 정보가 사라짐.
6.2 요약 메모리(장기 기억)
오래된 히스토리는 한 번 요약해서 “메모리”로 저장하고, 매 요청에는 메모리만 포함합니다.
- 메모리 요약은 별도 요청(짧은 max_output_tokens)으로 생성
- 메모리는 200~400 토큰 정도로 제한
실전 패턴:
memory(요약) +recent_history(최근 몇 턴) +current_question
7) 에러가 났을 때의 운영 대응: 자동 축소 재시도
사용자 경험 관점에서는 “에러로 종료”보다 “자동으로 줄여서 재시도”가 낫습니다. 다만 무한 재시도는 금지하고, 단계적으로 축소합니다.
아래는 간단한 단계적 폴백 예시입니다.
from openai import OpenAI
client = OpenAI()
def call_with_fallback(model, system_prompt, user_msg, docs, history):
# 1) 기본 시도
attempts = [
{"k": 6, "doc_tokens": 1800, "max_out": 800},
{"k": 4, "doc_tokens": 1200, "max_out": 600},
{"k": 2, "doc_tokens": 800, "max_out": 400},
]
last_err = None
for a in attempts:
try:
selected_docs = docs[:a["k"]]
context = "\n\n".join(selected_docs)
# 문서 토큰 컷(예시는 단순 문자열 컷; 실제로는 tiktoken 기반 권장)
context = context[: a["doc_tokens"] * 4] # 매우 러프한 근사
messages = [
{"role": "system", "content": system_prompt},
{"role": "system", "content": f"[CONTEXT]\n{context}"},
*history[-8:],
{"role": "user", "content": user_msg},
]
return client.responses.create(
model=model,
input=messages,
max_output_tokens=a["max_out"],
)
except Exception as e:
last_err = e
continue
raise last_err
핵심은 “문서 수/문서 길이/출력 토큰/히스토리 길이”를 한 번에 다 줄이지 말고, 품질 손실이 적은 축부터 단계적으로 줄이는 것입니다.
8) 스트리밍은 해결책이 아니다(하지만 체감 개선에는 도움)
stream=true(또는 SDK 스트리밍)를 켠다고 컨텍스트 한도가 늘어나지는 않습니다. 요청이 모델에 들어가기 전에 이미 한도 초과면 400으로 실패합니다.
다만 스트리밍은 다음에 도움이 됩니다.
- 출력이 길어질 때 TTFB/체감 지연 감소
- 프록시/로드밸런서 환경에서 타임아웃 이슈를 줄이기 위한 튜닝 포인트 제공
프록시 뒤에서 SSE가 끊기거나 499/502가 늘어나는 케이스는 별도의 네트워크/버퍼링 이슈이므로, 해당 증상이 함께 있다면 아래 글의 체크리스트가 유용합니다.
9) 최종 점검: 재현 가능한 진단 로그 남기기
context_length_exceeded를 빨리 잡으려면 “현재 요청이 왜 길어졌는지”를 수치로 남겨야 합니다.
권장 로깅 항목:
- 히스토리 메시지 개수/문자 수(또는 토큰 수)
- RAG 문서 개수/총 토큰 수
- 시스템 프롬프트 토큰 수(상수지만 버전별로 변할 수 있음)
max_output_tokens- 사용자 입력 토큰 수
그리고 배포 후에는 다음을 알람으로 묶으면 좋습니다.
context_length_exceeded발생률- 요청당 평균 입력 토큰/출력 토큰 추정치
- RAG top-k 평균/95퍼센타일
10) 정리: 가장 실용적인 해결 순서
운영에서 바로 적용 가능한 우선순위는 다음과 같습니다.
max_output_tokens를 작업 유형별로 낮추고 동적으로 조절- 히스토리 슬라이딩 윈도우 적용(최근 N턴만)
- RAG: top-k 축소 전에 중복 제거, 청크 크기/오버랩 튜닝
- 문서/로그/HTML 원문은 발췌·요약 후 주입
- tiktoken으로 토큰 예산 가드레일 구축
- 에러 발생 시 단계적 축소 재시도(품질 손실 최소화)
이 흐름대로만 정리해도 Responses API 400 context_length_exceeded는 대부분 “재발 방지 가능한 형태”로 잡힙니다. 특히 RAG/멀티턴/툴 호출이 결합된 서비스라면, 토큰 예산을 기능 요구사항이 아니라 SLO의 일부로 다루는 것이 장기적으로 가장 비용이 덜 듭니다.