LLM 앱 운영 #4 배칭 — 급하지 않은 작업은 반값에

4 분 소요

3편까지의 절감은 실시간 요청 안에서의 일이었습니다. 이번에는 질문을 바꿉니다. 이 요청, 정말 지금 답이 필요한가요? 야간에 쌓인 문서 분류, 주간 리포트 생성, 평가셋 채점 같은 작업은 몇 시간 뒤에 결과가 나와도 아무 문제가 없습니다. Batches API는 그런 작업을 비동기로 받아 가는 대신 입력과 출력 모든 토큰을 50% 할인해 줍니다. 조건 없는 반값이라서, 해당하는 작업이 있다면 가장 손쉬운 절감입니다.

어떤 작업이 배치감인가 #

기준은 하나, 지연 요구사항입니다. 사람이 화면 앞에서 기다리면 실시간, 아니면 배치 후보입니다.

실시간으로 남길 것배치로 보낼 것
챗봇 응답, 에이전트 작업쌓인 문서의 분류·요약·임베딩용 전처리
사용자가 누른 “요약” 버튼야간 일괄 리포트, 주간 다이제스트
검색 시점의 쿼리 변환RAG 심화 6편 평가셋의 정기 채점
신규 데이터의 사전 라벨링·태깅

배치의 계약 조건도 같이 알아 둡니다. 한 배치에 최대 10만 건, 대부분 1시간 안에 끝나지만 보장은 24시간 이내, 결과는 29일간 보관됩니다. “대부분 빠르지만 최악은 24시간"이라는 폭 때문에, 마감이 빠듯한 작업은 배치감이 아닙니다.

제출하고, 기다리고, 수거하기 #

배치의 단위는 개별 요청에 custom_id를 붙인 묶음입니다. 흐름은 제출 → 폴링 → 결과 수거의 세 단계입니다.

submit_batch.py
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

batch = client.messages.batches.create(
    requests=[
        Request(
            custom_id=f"doc-{doc.id}",          # 결과를 되찾을 열쇠
            params=MessageCreateParamsNonStreaming(
                model="claude-haiku-4-5",        # 2편의 라우팅이 여기서도 적용된다
                max_tokens=256,
                system=CLASSIFY_SYSTEM,
                messages=[{"role": "user", "content": doc.text}],
            ),
        )
        for doc in pending_docs
    ]
)
print(batch.id, batch.processing_status)
collect_batch.py
import time

while True:
    b = client.messages.batches.retrieve(batch.id)
    if b.processing_status == "ended":
        break
    time.sleep(60)

for result in client.messages.batches.results(batch.id):
    if result.result.type == "succeeded":
        msg = result.result.message
        text = next((blk.text for blk in msg.content if blk.type == "text"), "")
        save_classification(result.custom_id, text)    # custom_id로 원본과 연결
    elif result.result.type == "errored":
        retry_later(result.custom_id)                   # 서버 에러는 재제출 대상

운영 포인트 세 가지입니다.

  • custom_id가 생명선입니다. 결과는 제출 순서와 무관하게 옵니다. 원본 데이터와 잇는 유일한 끈이 custom_id이므로, 재실행해도 안전하게 같은 ID가 나오는 규칙(문서 ID 기반 등)으로 만듭니다.
  • 건별로 성패가 갈립니다. 배치 전체가 성공하거나 실패하는 것이 아니라, 결과에 succeeded/errored/expired가 섞여 옵니다. 에러 건만 골라 다음 배치에 재제출하는 흐름을 처음부터 둡니다.
  • 다른 절감과 겹쳐 적용됩니다. 요청 params는 일반 호출과 같으므로 2편의 모델 라우팅이 그대로 적용되고, 같은 시스템 프롬프트를 쓰는 요청들이 모이면 3편의 캐싱도 함께 동작합니다. haiku + 배치라면 opus 실시간 대비 10분의 1 이하의 단가가 됩니다.

운영 패턴 — 큐를 사이에 두기 #

배치를 일회성 스크립트가 아니라 상시 운영으로 만들 때의 표준 모양은 큐입니다.

배치 파이프라인
이벤트 발생 → 작업 큐(테이블)에 적재
                └─ (cron, 예: 매시) 쌓인 것을 모아 배치 제출
                       └─ (cron) 끝난 배치를 수거해 결과 반영, 실패 건 재적재

애플리케이션은 “분류해 줘"라는 행을 테이블에 넣을 뿐이고, 제출과 수거는 주기 작업이 맡습니다. 이 구조의 장점은 자연스러운 완충입니다. 트래픽이 몰려도 실시간 경로의 한도(5편의 주제)를 건드리지 않고, 배치 크기도 모아서 조절할 수 있습니다. 1편의 계측 래퍼와 같은 형태로 배치 결과의 usage 도 로깅해 두면, 기능별 비용 대시보드에 배치 작업도 같은 기준으로 잡힙니다.

실시간을 배치로 바꾸는 발상 #

이미 배치인 작업을 옮기는 것을 넘어, 제품 설계를 바꿔 배치로 보내는 경우도 있습니다. 예를 들어 “업로드하면 즉시 요약"을 “요약이 준비되면 알림"으로 바꿀 수 있다면, 그 기능의 비용은 반값이 되고 트래픽 급증에도 강해집니다. 모든 기능이 가능하지는 않지만, 1편 로그에서 비용 상위 기능을 볼 때 “이건 정말 동기여야 하나"를 한 번씩 묻는 것은 공짜입니다.

흔히 걸려 넘어지는 곳 #

  • 마감 있는 작업을 배치에 건다 — 보장은 24시간입니다. “아침 9시까지 꼭"인 작업을 전날 밤 배치에 걸면 가끔 약속을 어기게 됩니다. 마감이 있으면 여유를 두거나 실시간으로 둡니다.
  • 결과 수거를 잊는다 — 제출은 했는데 수거 작업이 죽어 있으면 비용만 내고 결과는 29일 뒤 사라집니다. 수거 cron의 동작 여부도 모니터링 대상입니다.
  • custom_id를 일련번호로 만든다 — 재실행하면 다른 번호가 붙는 ID는 결과를 원본과 못 잇게 만듭니다. 원본 데이터에서 결정적으로 나오는 ID를 씁니다.

마무리 #

이번 글에서는 비실시간 작업을 반값 경로로 옮겼습니다.

  • 기준은 지연 요구사항입니다. 사람이 기다리지 않는 작업은 배치 후보이고, 모든 토큰이 50% 할인됩니다.
  • custom_id 설계, 건별 성패 처리, 수거 자동화가 배치 운영의 세 기둥입니다.
  • 라우팅·캐싱과 겹쳐 적용되며, 큐를 사이에 둔 파이프라인이 상시 운영의 표준 모양입니다.

비용 3부작(라우팅, 캐싱, 배칭)이 끝났습니다. 다음 주제는 돈이 아니라 멈추지 않는 것입니다. 다음 글인 “LLM 앱 운영 #5 신뢰성 — 레이트리밋, 재시도, 폴백"에서 한도와 장애 앞에서 버티는 구조를 만들겠습니다.

X