메인 콘텐츠로 건너뛰기
한 줄 요약: 단일 호출로 모델에 수만 자의 출력을 요청할 때(에피소드 개요, 장편 소설, 대용량 번역, 대규모 코드 등), 비스트리밍을 사용하지 말고 응답을 스트리밍하십시오. 클라이언트 읽기 타임아웃은 전체 생성 시간이 아니라 데이터 이벤트 간 간격(수십 초 — 90120초가 안전한 값입니다)으로 설정해야 합니다. max_tokens에 여유를 두고, 텍스트를 사용하기 전에 stop_reason를 확인하십시오. 이 네 가지를 지키면 긴 출력 호출 시 “아무것도 반환되지 않는” 문제가 발생하지 않습니다. 2만3만 자를 초과하는 경우에는 한 가지를 더 추가하여, 단일 연결을 20분 동안 유지하는 대신 작업을 여러 번의 호출로 분할하십시오.
이 문서는 모든 대형 모델(OpenAI, Claude, Gemini, Grok 등)에 적용됩니다. 코드 예제는 Claude의 네이티브 /v1/messages를 사용하며, OpenAI 호환 형식에 따른 차이점은 별도로 안내합니다.

먼저 알아야 할 세 가지

  1. 1만 단어를 출력하는 경우 실제 생성에 10~20분이 걸리는 것은 정상입니다. 모델은 추론/사고 단계와 함께 수만 개의 문자를 token 단위로 생성하므로 엔드투엔드 지연 시간이 실제로 깁니다. 이는 게이트웨이가 느린 것이 아니라 생성 자체가 느린 것입니다.
  2. 비스트리밍은 전체 결과를 버퍼링한 후 전송합니다. 비스트리밍(stream 생략 또는 false)에서는 서버가 모델의 전체 생성이 완료될 때까지 기다린 다음 전체 본문을 한 번에 전송해야 합니다. 그 몇 분 동안 클라이언트의 읽기 타임아웃이 만료될 수 있으며, 생성 시간이 길수록 결과가 도착하기 전에 연결이 끊길 가능성이 커집니다. 또한 예외가 빈 상태인 경우가 많아(httpx.ReadError의 str(e)이 비어 있음) 원인을 확인할 수 없습니다.
  3. 연결이 끊겨도 과금되므로 무분별하게 재시도하면 이중으로 과금됩니다. 서버가 출력을 생성한 시점에 결과가 사용자에게 도달하지 않았더라도 호출에는 과금됩니다. 이미 본문의 일부를 받은 후 재시도하면 모델이 다시 실행되고 비용도 다시 지불하게 됩니다.

스트리밍을 사용하고 비스트리밍은 사용하지 마십시오

스트리밍(stream: true)을 사용하면 첫 번째 바이트가 몇 초 내에 도착하며, 그 후에는 수십 초마다 데이터 이벤트가 도착합니다. 읽기 타임아웃은 여러 분 동안 실행되는 생성 전체가 아니라 이벤트 간 공백만 포괄하면 됩니다. 이것이 스트리밍이 긴 출력을 안정적으로 제공하는 핵심 이유입니다. 두 프로토콜은 서로 다른 종결자를 사용합니다. 혼동하지 마십시오. 적응형 thinking이 활성화되면 Claude 네이티브는 먼저 type: "thinking" 블록을 내보내고(증분은 thinking_delta), 그다음 text 블록을 내보냅니다. 렌더링할 때는 thinking_delta 및 text_delta을 별도로 라우팅하고 thinking을 본문 텍스트에 연결하지 마십시오. 최소 Claude 네이티브 /v1/messages 스트리밍 예시(기본 httpx, 줄 단위 SSE 파싱):
공식 Anthropic SDK를 사용하는 경우 base_url을 https://api.apiyi.com로 지정하고 client.messages.stream(...).get_final_message()을 사용하십시오. SDK가 SSE 파싱, 타임아웃 및 stop_reason을 처리합니다. 위의 httpx 버전은 SDK를 추가하고 싶지 않은 경우를 위한 것입니다.

읽기 타임아웃은 전체 시간이 아닌 이벤트 간격에 맞춰 설정하십시오

많은 사용자가 전체 생성 과정을 감당하기 위해 읽기 타임아웃을 매우 큰 값(예: 1800초)으로 설정하지만, 여전히 타임아웃 오류를 겪곤 합니다. 비스트리밍 방식에서는 해당 값 하나로 전체 생성 시간을 버텨내야 하므로 사소한 지연만 발생해도 연결이 끊어지기 때문입니다. 올바른 접근법은 스트리밍과 함께 이벤트 간격에 맞춘 읽기 타임아웃을 설정하는 것입니다. 측정 기준치(claude-opus-5가 약 1.5만 자 입력으로부터 약 2만 자의 에피소드 개요를 생성하는 경우): 따라서 90~120초의 읽기 타임아웃을 설정하면 여유를 두고 최대 이벤트 간격을 충분히 처리할 수 있으며, 굳이 수 분에 달하는 큰 값을 지정할 필요가 없습니다. 3단계 타임아웃을 사용하면 단계를 분리하여 각각 크기를 지정할 수 있습니다:

클라이언트별 설정 방법

많은 클라이언트 라이브러리가 읽기별(per-read) 타임아웃과 전체(total) 타임아웃을 모두 제공하며, 두 명칭이 유사한 경우가 많습니다. 긴 출력을 처리할 때는 전자(읽기별 타임아웃)를 설정하고, 후자는 설정하지 않거나 무제한으로 두십시오:

stream도 멈출 수 있습니다: 도중에 수신이 중단되는 연결

스트리밍은 “전송 전 전체 버퍼링” 문제를 해결하지만, 주의해야 할 또 다른 장애 유형이 있습니다: stream 도중 연결이 멈추는 현상입니다.
  • 증상: 처음에는 텍스트가 정상적으로 수신되다가 더 이상 아무것도 들어오지 않지만, 연결에 오류가 발생하거나 닫히지도 않습니다. 클라이언트는 자체 타임아웃에 도달할 때까지 대기 상태에 머뭅니다. 타임아웃이 1시간이라면 꼬박 1시간을 대기하게 되며, 결국 처음에 수신된 작은 일부만 얻게 됩니다.
  • 원인: 리전 간 및 국가 간 네트워크 경로에서는 장시간 열려 있는 연결이 중간 네트워크 장비에서 멈출 수 있습니다. 서버는 생성을 마치고 이미 오래전에 연결을 닫았지만, 종료 신호가 클라이언트에 전혀 도달하지 않는 것입니다. 연결이 오래 지속될수록 이러한 현상이 발생할 가능성이 높아집니다 — 20분 동안 유지된 연결은 3분 유지된 연결보다 이러한 문제를 겪을 확률이 훨씬 높습니다.
  • 과금: 서버는 생성을 완료하고 정상적으로 과금합니다(“먼저 알아두어야 할 세 가지”의 3번 항목 참조).
대응 방법:
  1. 읽기 타임아웃을 이벤트 간격에 맞게 설정합니다(이전 섹션 참조). 120초 읽기 타임아웃을 설정하면 멈춤 현상이 발생해도 2분 후에 재시도할 수 있지만, 1시간 읽기 타임아웃을 설정하면 꼬박 1시간이 소요됩니다. 이것이 가장 중요합니다.
  2. 각 연결 시간을 단축합니다: 매우 긴 출력은 여러 번의 호출로 분할합니다 — 아래의 “매우 긴 출력을 여러 번의 호출로 분할”을 참조하십시오.
  3. 중단된 stream을 멈춘 지점부터 재개합니다 — 아래의 “재시도 전략”을 참조하십시오.
읽기 타임아웃을 “총 생성 시간 + 여유 시간”(1800초, 3600초 등)으로 설정하지 마십시오. 정상적으로 작동할 때는 차이가 없지만, 연결이 멈추었을 때 실패했다는 사실을 인지하기까지 그만큼 긴 시간을 기다려야 합니다.

max_tokens에 충분한 여유를 두고 stop_reason을 확인합니다

긴 출력은 쉽게 max_tokens 한도에 도달하여 잘립니다. 이는 thinking이 활성화된 Claude와 같은 모델에서 특히 그러합니다. — thinking 자체가 max_tokens 예산을 소모하며, 긴 내용은 이 예산을 모두 소진할 수 있습니다.
  • max_tokens를 64000에서 시작합니다 (높은 노력 수준 / 심층 thinking에서는 더 많이 제공합니다. claude-opus-5는 128K 출력을 지원합니다).
  • 응답을 사용하기 전에 stop_reason를 확인합니다:
    • end_turn — 정상적으로 완료되었으며 텍스트가 완전합니다. 이것만 성공입니다.
    • max_tokens — 잘렸으므로 텍스트가 불완전하거나 아예 비어 있을 수 있습니다. 이는 잘림이지 “빈 결과”가 아닙니다. max_tokens를 늘리고 재시도합니다.
    • refusal — 안전 정책에 따라 거부되었습니다. 별도로 처리합니다.
str(e)만으로 또는 “텍스트가 비어 있음”으로 성공 여부를 판단하면 오해할 수 있습니다. 빈 본문은 대개 max_tokens 잘림입니다.

매우 긴 출력을 여러 번의 호출로 분할하기

스트리밍을 사용하더라도 20분 동안 실행되는 단일 호출에서 5만 자를 요청하는 것은 가장 취약한 패턴입니다. 진행 도중 네트워크 문제가 조금만 발생해도 전체 작업이 무효화되며, 전체 분량에 대해 과금됩니다. 출력이 2만~3만 자를 초과하면 다음과 같이 분할하십시오.
  1. 먼저 개요 / 에피소드 목록 / 챕터 목록을 생성합니다(짧은 호출).
  2. 그런 다음 각 호출을 1만~2만 자 미만으로 유지하면서 호출당 하나의 챕터 또는 에피소드를 생성합니다. 내용의 일관성이 유지되도록 개요 및 각 파트에 필요한 컨텍스트(등장인물 설정, 이전 파트가 끝난 방식 등)를 포함하십시오.
  3. 개요가 확정되면 각 파트는 독립적이므로 병렬로 실행할 수 있습니다.

재시도 전략

긴 출력에 대해서는 재시도를 보수적으로 적용하여, “실패 시 재시도”가 “이중 과금 및 두 번째 장시간 실행”으로 이어지지 않도록 하십시오:
  • 응답 헤더가 도착하기 전의 실패 및 5xx / 429에 대해서만 재시도하십시오(백오프 적용, 최대 2회). 이는 연결 문제 또는 일시적인 문제이므로 재시도가 타당합니다.
  • 본문의 일부를 이미 수신한 후 stream이 끊기면, 처음부터 다시 시작하지 말고 중단된 위치부터 이어받으십시오. 서버는 이미 해당 내용을 생성하고 과금했습니다. 다시 시작하면 처음부터 다시 실행되어 비용이 중복 지불됩니다. 이미 수신한 텍스트를 보관하여 지금까지 작성된 내용으로서 새 호출에 포함하고 모델이 계속 이어 작성하도록 하십시오. 이렇게 하면 남은 부분에 대해서만 비용을 지불하게 됩니다.
  • 읽기 타임아웃(연결 중단)도 동일한 방식으로 처리하십시오: 이미 상당한 양의 텍스트를 확보했다면 이어받기를 수행하고, 거의 수신하지 못했다면 전체 호출을 재시도하십시오.
  • 대조 및 문제 해결을 위해 응답 헤더의 request id를 기록하십시오.
이어받기 요청은 다음과 같습니다(모든 모델에서 지원됩니다):

시나리오 치트 시트

30k자를 초과하는 단일 작업의 경우, 챕터나 에피소드별로 분할하십시오(“매우 긴 출력을 여러 번의 호출로 분할” 참조). 항상 stream을 사용하십시오. api.apiyi.com(또는 백업 b.apiyi.com)를 사용하고, api-cf.apiyi.com는 사용하지 마십시오(CDN 노드가 약 100초 시점에 524를 반환하므로 긴 요청을 처리할 수 없습니다).

관련 문서

API 시간 초과를 방지하는 방법

시나리오별 시간 초과 값

스트리밍과 비스트리밍 비교

장단점과 선택 방법

Claude 사고 및 추론 강도

적응형 사고, 추론 강도 단계, max_tokens 및 잘림

Claude 응답 처리

네이티브 응답 형식, SSE 이벤트, stop_reason