본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 10. 20:24

프롬프트 배치(Prompt Batching)가 내 LLM 앱의 비용을 더 높였던 이유

요약

LLM 기반 문서 번역 파이프라인에서 비용 절감을 위해 시도한 프롬프트 배치(Prompt Batching)가 오히려 비용과 지연 시간을 증가시킨 사례를 분석합니다. 배치 처리 중 발생하는 JSON 응답 오류가 전체 배치의 재시도를 유발하여 효율성을 저해하는 문제를 다룹니다.

핵심 포인트

  • 프롬프트 배치는 API 호출 횟수를 줄이지만 토큰 사용량과 비용을 높일 수 있음
  • 배치 내 단일 항목 오류가 전체 배치의 재시도를 유발하여 지연 시간 급증
  • JSON 응답의 무결성 검증 및 정교한 폴백(Fallback) 로직 설계의 중요성

저는 LLM (Large Language Model) 기반의 문서 번역 파이프라인을 위한 비용 최적화 작업을 진행하고 있었습니다.

그 시점에서 LLM 번역 흐름은 여전히 매우 직접적이었습니다. 추출된 하나의 텍스트 세그먼트(segment)가 하나의 API 호출(API call)이 되는 방식이었습니다.

작동은 했지만, 비용 측면에서 이상적이지는 않았습니다.

텍스트 세그먼트가 많은 문서의 경우, API 호출 횟수가 선형적으로 증가했습니다. 따라서 최적화 아이디어는 간단했습니다. 여러 개의 텍스트 세그먼트를 하나의 프롬프트(prompt)로 배치(batch)하는 것이었습니다.

더 쉽게 설명하자면:

모든 텍스트 세그먼트에 대해 매번 하나의 API 호출을 보내는 대신, 여러 세그먼트를 하나의 요청(request)으로 그룹화하는 것입니다. 이론적으로 API 호출 횟수가 줄어들면 비용이 낮아지고 처리 속도가 빨라져야 합니다.

그것이 계획이었습니다.

하지만 첫 번째 실제 벤치마크(benchmark)에서, 이 "최적화"는 시스템을 더 비싸게 만들고 훨씬 더 느리게 만들었습니다.

베이스라인 (The Baseline)

테스트에는 동일한 입력 파일이 사용되었습니다:

  • 파일: sample_10p.pdf
  • 언어 쌍: zh-TW -> en
  • 모델: gpt-4.1-nano

배치(batching)를 적용하기 전, 시스템은 API 호출당 하나의 세그먼트를 번역했습니다.

지표 (Metric)배치 미적용 (No batching)
세그먼트 (Segments)160
...

이것은 단순하고 예측 가능했습니다. 160개의 세그먼트는 160개의 API 호출을 의미했습니다.

문제 또한 명확했습니다. 비용을 줄이고 싶다면, LLM 호출 횟수를 줄이는 것이 가장 먼저 시도해야 할 일이었습니다.

처음 시도한 것

첫 번째 구현에서는 프롬프트 배치(prompt batching)를 추가했습니다.

아이디어는 키가 지정된 JSON(keyed JSON)을 사용하여 최대 20개의 텍스트 세그먼트를 하나의 요청으로 그룹화하는 것이었습니다:

keyed_subset = {str(idx): text for idx, text in enumerate(masked_subset)}

kwargs = {
...

언뜻 보기에 결과는 더 좋아 보였습니다. API 호출이 160회에서 107회로 줄어들었기 때문입니다.

하지만 비용과 지연 시간(latency)은 더 악화되었습니다.

지표 (Metric)배치 미적용 (No batching)첫 번째 배치 적용 (First batching)
세그먼트 (Segments)160140
...

즉, 배치를 통해 API 호출은 33% 감소했지만, 비용은 37% 증가했습니다.

이 부분이 혼란스러운 지점이었습니다.

대시보드에는 API 호출이 줄어들었다고 나왔습니다. 하지만 최종 청구 예상액은 더 높았고, 총 처리 시간은 4배 이상 느려졌습니다.

그래서 질문은 이것이 되었습니다: 추가 비용은 어디에서 발생한 것인가?

무엇이 잘못되었나?

배치 크기(Batch size)는 20이었습니다.

140개의 세그먼트(segments)가 있다면, 시스템은 다음과 같이 처리해야 했습니다:

140 / 20 = 7 batch calls

하지만 이 7번의 배치 호출(batch calls) 중 5번이 검증(validation)에 실패했습니다.

JSON 응답에서 ID 하나가 누락되었을 때, 기존의 폴백(fallback) 로직은 전체 배치를 항목별로 하나씩 재시도(retry)했습니다:

for i in range(len(subset)):
    key = str(i)
    if key in keyed_translations:
...

즉, 번역 하나가 누락되면 성공한 19개의 번역까지 버려지고 20개의 세그먼트 전체를 다시 시도하게 된다는 의미입니다.

재구성된 호출 횟수는 대시보드와 일치했습니다:

7 batch calls
5 failed batches x 20 per-item retries = 100 retry calls

...

결과적으로 107번의 API 호출 중 100번이 재시도였습니다.

그것이 진짜 비용을 증폭시킨 원인이었습니다.

JSON 모드만으로는 충분하지 않았다

첫 번째 구현에서는 다음을 사용했습니다:

"response_format": {"type": "json_object"}

이는 모델에게 유효한 JSON을 반환하라고 요청할 뿐이었습니다.

모든 필수 ID가 포함되어 있다는 것을 보장하지는 않았습니다.

프롬프트에는 "어떤 ID도 건너뛰지 마세요"라고 명시했지만, 프롬프트 지침은 어디까지나 지침일 뿐입니다. 구조적인 강제성(structural enforcement)을 갖지는 못합니다.

로그를 살펴보면, 누락된 ID들은 주로 배치의 끝부분 근처에서 나타났습니다:

ID 19 missing
ID 18 missing
ID 12 missing
...

이러한 패턴은 긴 구조화된 출력(structured outputs)이 뒷부분으로 갈수록 품질이 저하되는 현상과 일치했습니다.

다음에 변경한 사항

해결책은 세 가지 부분으로 구성되었습니다.

첫째, OpenAI 엔드포인트(endpoint)의 경우, 응답 형식(response format)을 json_object에서 엄격한 json_schema로 변경했습니다.

keys = [str(i) for i in range(n_items)]

return {
...

이제 모든 예상되는 ID가 필수(required) 항목으로 나열됩니다.

OpenAI가 아닌 엔드포인트의 경우, 호환성이 다르기 때문에 시스템은 여전히 최선의 노력을 다하는 json_object 모드를 사용합니다.

둘째, 폴백(fallback) 방식이 부분적(partial)으로 바뀌었습니다.

배치 전체를 재시도하는 대신, 코드가 성공한 번역은 유지하고 누락된 ID만 재시도합니다:

missing = [i for i, v in enumerate(translated) if v is None]

if missing:
...

셋째, 이제 배치 요청 시 max_tokens를 설정하고 잘림(truncation) 여부를 확인합니다:

if choice.finish_reason == "length" and len(items) > 1:
    mid = len(items) // 2
    left = self._request_batch_keyed(items[:mid], context, tracker)
...

따라서 잘린(truncated) 배치는 개별 항목에 대한 폴백(fallback)으로 바로 넘어가는 대신, 더 작은 배치로 분할되어 재시도됩니다.

결과

수정 후, 동일한 벤치마크를 다시 실행했습니다.

지표 (Metric)최초 배치 방식 (First batching)수정된 배치 방식 (Fixed batching)배치 미사용 (No batching)
API 호출 수 (API calls)1077160
............

수정된 버전은 마침내 원래의 목표를 달성했습니다:

  • API 호출 수가 160회에서 7회로 감소
  • 예상 비용이 $0.0024에서 $0.0017로 감소
  • 소요 시간이 30.4초에서 22.1초로 감소
  • 폴백(Fallback) 발생률이 0%로 감소

교훈

교훈은 간단합니다: 배칭(batching)이 자동으로 더 저렴해지는 것은 아닙니다.

배치 응답이 부분적으로 실패할 수 있다면, 폴백 전략(fallback strategy)은 배칭 전략만큼이나 중요합니다.

구조화된 LLM 워크플로우(structured LLM workflows)를 위해서는 다음과 같은 세부 사항이 중요합니다:

  1. 엔드포인트가 지원하는 경우 스키마 강제(schema enforcement)를 사용하세요.
  2. 필수 필드에 대해 프롬프트 지시사항(prompt instructions)에만 의존하지 마세요.
  3. 부분적인 성공 결과(partial successes)를 유지하세요.
  4. 누락된 항목만 재시도하세요.
  5. finish_reason을 확인하세요.
  6. 단순히 API 호출 횟수만이 아니라 실제 비용을 측정하세요.

이 사례의 경우, 첫 번째 최적화는 요청 수는 줄였지만 비용은 증가시켰습니다.

진정한 최적화는 단순히 배칭을 하는 것이 아니었습니다.

배치 출력을 신뢰할 수 있게 만드는 것이었습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0