본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 26. 12:38

ChatGPT API에서 긴 문장 요약이 중간에 끊기는 문제: finish_reason으로 감지하여 분할하기

요약

ChatGPT API 사용 시 출력 토큰 제한으로 인해 요약이 중간에 끊기는 문제를 해결하는 방법을 다룹니다. finish_reason을 통한 미완성 감지와 텍스트 분할(Chunking) 및 오버랩(Overlap) 기법을 활용한 단계적 요약 전략을 제시합니다.

핵심 포인트

  • finish_reason이 'length'인 경우 출력이 미완성임을 인지해야 함
  • 출력 토큰 상한에 도달하면 에러 없이 문장이 끊길 수 있음
  • 긴 문장은 텍스트를 분할하여 부분 요약 후 재정리하는 방식이 효과적임
  • 분할 시 문맥 유지를 위해 청크 간 오버랩(Overlap) 설정이 필수적임

지난주, 30분 분량의 회의 녹취록을 ChatGPT에 그대로 붙여넣고 "결정 사항과 숙제를 뽑아줘"라고 부탁했습니다. 돌아온 요약은 깔끔했습니다. 결정 사항이 3개 있었고, 담당자와 기한도 붙어 있었습니다. 그대로 회의록으로서 팀에 공유했습니다.

문제는 다음 날 발생했습니다. 회의 후반부에 결정된 "판촉 캠페인 예산 범위"가 요약에서 통째로 빠져 있었던 것입니다.

에러는 한 번도 발생하지 않았습니다. 이 점이 까다로운 부분이었습니다.

긴 문장을 AI에게 통째로 던지는 작업은 대개 중간에 부실해집니다. 요약, 회의록, 긴 메일에 대한 답장, 규정 해석 등. 입력이 길수록 출력도 어딘가에서 막힙니다.

막히는 정체는 출력 측의 상한(limit)입니다. 입력(Prompt)은 모델의 컨텍스트 윈도우(Context Window)에 들어오더라도, 생성할 수 있는 출력 토큰(Output Token)에는 별도의 상한이 있습니다. 그 지점에 도달하면 모델은 사과도 하지 않고 멈추지도 않은 채, 쓸 수 있었던 부분까지의 문장을 그대로 반환합니다.

실제로 직접 테스트해 보았습니다. 출력 상한을 일부러 16토큰으로 제한하고, 모모타로의 줄거리를 300자로 요청했습니다.

import json, urllib.request
from pathlib import Path
def _key():
...

돌아온 내용은 이것입니다.

finish_reason = length
본문 = 모모타로는, 일본의 옛날이야기로, 복숭아 속에서 태어

"모모타로는, 일본의 옛날이야기로, 복숭아 속에서 태어"에서 문장이 멈춰 있습니다. 하지만 예외(Exception)는 발생하지 않았습니다. finish_reasonlength로 되어 있을 뿐입니다. 평소에 이 부분을 읽는 사람은 적을 것이라 생각합니다. 저도 읽지 않았습니다.

즉, 처음에 회의록에서 일어난 일도 마찬가지입니다. 후반부에 접어들 무렵 출력 상한에 도달했고, 쓸 수 있었던 부분까지를 "완성된 요약"인 것처럼 반환했던 것입니다. 깔끔해 보였던 이유는, 빠진 부분이 처음부터 없었던 것처럼 정돈되어 있었기 때문이었습니다.

대책은 두 단계입니다. 첫 번째는 단순하게, 매번 finish_reason을 확인하는 것입니다. length라면 "이것은 미완성"이라고 취급하여 처리를 중단하거나 상한을 높입니다. 받은 문자열을 그대로 신뢰하지 않는 것이 출발점입니다.

def ask(prompt, max_tokens=1000, model="gpt-4o-mini"):
body = {"model": model,
"messages": [{"role": "user", "content": prompt}],
...

이렇게 하면 "모르는 사이에 누락된 요약을 공유하는" 사고는 방지할 수 있습니다. 다만, 상한을 높이는 것만으로는 부족한 상황이 있습니다. 원문이 정말 길면 출력을 아무리 늘려도 한 번에 담을 수 없습니다.

두 번째가 핵심입니다. 긴 문장은 먼저 분할한 다음, 부분별로 요약하고, 마지막에 부분 요약들을 다시 정리합니다.

나눌 때 한 가지 주의할 점이 있습니다. 경계 부분에서 문맥이 끊기지 않도록, 인접한 청크(Chunk)를 아주 조금씩 겹치게(Overlap) 만드는 것입니다. 겹치지 않으면, 딱 경계에 걸쳐 있던 "담당자는 다나카, 기한은 월말"과 같은 한 문장이 앞에도 뒤에도 속하지 못한 채 사라집니다.

def split_text(text, max_chars=1500, overlap=150):
if len(text) <= max_chars:
return [text]
...

단락(빈 줄 구분)을 우선하여 나누고 있으므로, 문장 중간에서 뚝 끊어지는 일은 적습니다. overlap=150을 통해 이전 청크의 끝 150자를 다음 청크의 시작 부분에 붙입니다.

요약의 흐름은 다음과 같습니다.

def summarize_long(transcript):
parts = split_text(transcript, max_chars=1500, overlap=150)
partials = [ask(f"다음 회의록의 일부에서 '결정 사항'과 '숙제(담당·기한 포함)'만 불렛 포인트로 추출해줘.\n\n{c}")
...

의사적으로 만든 2000자 정도의 회의록(의제 5개, 각 의제마다 결정 및 숙제 포함)으로 실행하면, 2개의 청크로 나뉘어 최종적으로 다음과 같이 반환되었습니다.

### 결정 사항
- 신규 서비스의 요금 체계는 다음 달부터 진행한다.
- 채용 계획은 다음 달부터 진행한다.
...

5개의 의제가 하나도 빠짐없이 남았고, 중복 없이 정리되었습니다. 처음에 사라졌던 "판촉 캠페인"도 제대로 포함되어 있습니다.

한 가지. overlap을 추가하면 청크가 max_chars를 약간 초과합니다. 직접 검증해 보니 900자 상한이어야 할 청크가 928자가 되었습니다. 상한에 딱 맞춰 운용한다면, 겹치는 부분을 고려하여 max_chars를 조금 작게 설정하는 것이 안전합니다. 이 부분이 은근히 중요합니다.

또 하나 더 있습니다. 부분 요약들을 하나로 합치는 마지막 호출에서도, 부분 요약의 양이 많으면 출력이 끊깁니다. 따라서 통합 단계에서도 ask를 통해 finish_reason을 확인해야 합니다. 마지막까지 감지망을 늦추지 않는 것이 중요합니다.

인자(argument) 이름에도 주의가 필요합니다. 최신 모델에서는 max_tokens가 아니라 max_completion_tokens를 사용합니다. 예전 이름을 그대로 사용하면 에러가 발생하거나 무시될 수 있습니다.

긴 문장을 AI에게 통째로 던져서 돌아온 답변은, 우선 "전부 포함되어 있는가"를 의심하십시오. 이것을 습관화하는 것만으로도 회의록이나 문의 요약에서 내용을 놓치는 일이 상당히 줄어듭니다. 핵심은 단 하나, finish_reason입니다. 깔끔해 보이는 요약일수록 내용이 빠져 있어도 알아채기 어렵기 때문입니다.

나누고, 부분별로 수행하고, 합친다. 이 3단계 방식은 요약뿐만 아니라 긴 문장의 번역이나 항목 추출(extraction)에도 그대로 적용할 수 있습니다. 오늘 당신이 하는 업무 중 가장 긴 텍스트를 떠올려 보세요. 그것이 단 한 번의 호출로 수용 가능한가요? 의심스럽다면, 바로 분할(splitting)을 실행할 차례입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0