토큰 길이를 넘겨 요청이 실패할 때, 잘라내기 vs 요약 압축

토큰 길이를 넘겨

토큰 길이를 넘겨 요청이 실패했다면, 예산과 시간이 허락하는 한 요약 압축을 먼저 시도해 보시는 편이 낫습니다. 다만 숫자나 코드, 인용문처럼 한 글자도 틀리면 안 되는 정보가 많은 대화라면 잘라내기가 오히려 더 안전한 선택이 됩니다. 이 글에서는 두 방식이 실제로 어떻게 동작하는지 코드로 확인하고, 각각이 유리한 조건을 구체적으로 짚어보겠습니다.

왜 토큰 길이를 넘기면 요청이 실패할까요

GPT나 Claude 같은 모델은 한 번에 처리할 수 있는 입력과 출력의 총량이 정해져 있는데, 이를 컨텍스트 윈도우라고 부릅니다. 이 한도는 글자 수가 아니라 토큰 수 기준이라, 한글은 영어보다 같은 의미를 표현하는 데 토큰을 더 많이 씁니다. 관련 개념은 위키백과의 거대 언어 모델 문서에도 정리되어 있습니다.

한도를 넘기면 API는 요청 자체를 거부합니다. OpenAI Python SDK(v1 기준)에서는 아래처럼 예외가 발생합니다.

openai.BadRequestError: Error code: 400 - {'error': {'message': "This model's maximum context length is 128000 tokens. However, your messages resulted in 132480 tokens. Please reduce the length of the messages.", 'type': 'invalid_request_error', 'code': 'context_length_exceeded'}}

에러 코드 context_length_exceeded가 뜨면 재시도해도 소용이 없습니다. 입력 자체를 줄이거나 압축한 뒤 다시 보내야 요청이 통과됩니다. 정확한 에러 코드 목록은 OpenAI 공식 에러 코드 문서에서 확인하실 수 있습니다.

잘라내기(Truncation)가 정보를 지우는 방식

개발자가 노트북 화면의 코드 에러를 확인하는 모습

잘라내기는 대화 기록을 토큰 단위로 세어서 한도를 넘는 앞부분(또는 뒷부분)을 통째로 버리는 방법입니다. 계산이 단순하고 추가 API 호출이 없어서 지연 시간이 거의 늘지 않습니다. 토큰 수 계산에는 OpenAI가 공개한 tiktoken 라이브러리를 씁니다.

import tiktoken

encoding = tiktoken.encoding_for_model("gpt-4o")

def count_tokens(text: str) -> int:
    return len(encoding.encode(text))

def truncate_by_tokens(text: str, max_tokens: int) -> str:
    tokens = encoding.encode(text)
    if len(tokens) <= max_tokens:
        return text
    # 최근 대화를 우선 보존하고 앞부분을 버립니다
    return encoding.decode(tokens[-max_tokens:])

history_text = "\n".join(m["content"] for m in conversation_history)
print(count_tokens(history_text))          # 132480
trimmed = truncate_by_tokens(history_text, 120_000)
print(count_tokens(trimmed))               # 120000

이 방식의 약점은 명확합니다. 버려진 구간에 사용자가 앞서 정한 조건이나 이름, 수치가 들어 있었다면 그 정보는 그냥 사라집니다. 예를 들어 대화 초반에 "예산은 300만 원입니다"라고 말했는데 그 부분이 잘려나가면, 모델은 이후 답변에서 그 조건을 전혀 반영하지 못합니다.

요약 압축이 잘라내기보다 나은 순간

요약 압축은 한도를 넘길 것 같은 오래된 대화 구간을 별도 모델 호출로 요약해서, 원문 대신 요약문만 다음 요청에 포함시키는 방식입니다. 정보를 완전히 버리지 않고 압축한다는 점이 잘라내기와 다릅니다.

def summarize_old_turns(old_messages: list[dict]) -> str:
    joined = "\n".join(f"{m['role']}: {m['content']}" for m in old_messages)
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "다음 대화를 핵심 사실과 결정사항 위주로 5문장 이내로 요약해줘."},
            {"role": "user", "content": joined},
        ],
    )
    return resp.choices[0].message.content

# 사용 예
old_turns, recent_turns = split_by_threshold(conversation_history, keep_recent=20)
summary = summarize_old_turns(old_turns)
new_history = [{"role": "system", "content": f"이전 대화 요약: {summary}"}] + recent_turns

이 방식은 "예산은 300만 원"처럼 오래된 조건도 요약문 안에 살아남을 가능성이 큽니다. 다만 요약 자체가 별도의 API 호출이라 비용과 지연 시간이 추가로 발생하고, 대화가 길어질 때마다 반복해서 요약을 다시 만들어야 하는 부담도 생깁니다.

직접 코드로 두 방식을 비교해 보세요

같은 대화 기록(약 132,000토큰)을 두 방식으로 각각 처리했을 때 나타나는 차이를 표로 정리했습니다.

디지털 데이터 압축 및 요약 개념을 나타내는 추상 이미지

항목 잘라내기 요약 압축
추가 API 호출 없음 요약용 호출 1회 이상 필요
처리 속도 즉시 처리 요약 모델 응답 대기 시간 추가
정보 손실 잘린 구간 전부 소실 세부 수치·정확한 문구는 뭉개질 수 있음
적합한 대화 최근 맥락만 중요한 단순 질의응답 장기 프로젝트, 조건이 누적되는 상담형 대화
비용 추가 비용 없음 요약 모델 토큰 비용 추가

숫자만 보면 요약 압축이 항상 유리해 보이지만, 계약서 문구나 코드 스니펫처럼 한 글자 단위 정확도가 필요한 대화에서는 요약 과정에서 세부 내용이 바뀔 위험이 있습니다. 이런 경우엔 통째로 자르더라도 남은 부분만큼은 원문 그대로 유지하는 잘라내기가 더 예측 가능합니다.

요약 압축을 썼는데 왜 숫자와 코드가 틀리게 나올까요?

요약 모델은 문장을 짧게 줄이는 과정에서 세부 수치나 변수명을 근사치로 바꾸는 경향이 있습니다. "재고 1,247개"가 요약 후 "재고 약 1,200개"로 바뀌는 식입니다. 이 오차가 이후 답변에 그대로 반영되면 사용자는 원문과 다른 결과를 받게 됩니다.

이 문제를 줄이려면 요약 대상에서 숫자·코드·고유명사가 담긴 메시지는 제외하고, 해당 메시지만 원문 그대로 최근 대화 쪽에 남겨 잘라내기 방식과 함께 쓰는 편이 안전합니다. 즉 두 방식은 양자택일이 아니라 대화 성격에 따라 섞어 쓰는 조합으로 접근하시는 것이 좋습니다.

대화 성격에 따라 잘라내기와 요약 압축을 섞어 쓰는 기준

단순 질의응답이나 고객센터 봇처럼 최근 몇 턴만 중요한 서비스라면 잘라내기만으로도 충분합니다. 구현이 단순하고 추가 비용이 없다는 점이 실무에서는 큰 장점입니다.

반대로 여러 세션에 걸쳐 조건이 누적되는 상담형 서비스나 장기 프로젝트 어시스턴트라면, 오래된 구간은 요약 압축으로 넘기고 최근 20턴 안팎은 원문 그대로 유지하는 하이브리드 구조를 권해드립니다. 먼저 tiktoken으로 현재 토큰 수를 확인하고, 한도의 80% 선에서 요약 트리거를 걸어보시면 두 방식의 장단점을 실제 트래픽으로 비교해 보실 수 있습니다.

GPT·Claude·Gemini API 실제 월 비용 계산: 1만 요청이면 얼마 나올까 (+캐싱·배치 절감법)

Leave a Comment