Published on

OpenAI Batch API로 대량 요약 비용 90% 절감하기

Authors

서버 로그, CS 티켓, 회의록, 기사 크롤링 결과처럼 “요약”은 만들수록 쌓이고, 쌓일수록 비용이 폭증합니다. 특히 실시간 API 호출로 수천~수만 건을 처리하면 요청당 오버헤드(네트워크, 레이트리밋 회피, 재시도)까지 더해져 운영이 괴로워집니다.

이때 유효한 선택지가 OpenAI Batch API입니다. 한 번에 요청들을 묶어 비동기 처리하고, 대량 처리에 최적화된 과금/처리 모델을 활용해 대규모 요약 비용을 체감상 90% 수준까지 절감(워크로드/모델/프롬프트에 따라 다름)할 수 있습니다. 이 글에서는 “왜 싸지는지”보다 더 중요한 어떻게 설계해야 실제로 절감이 나오는지를 중심으로, 입력 파일 구성부터 결과 병합, 실패 재처리, 비용 추적까지 끝까지 설명합니다.

Batch API가 대량 요약에 유리한 이유

대량 요약은 다음 특성이 있습니다.

  • 대부분 비실시간이어도 됨(예: 하루 1회 리포트 생성, 백필 백로그 처리)
  • 입력이 크고(긴 텍스트), 요청 수가 많음
  • 실패가 일부 섞여도 전체 파이프라인은 계속 돌아가야 함

Batch는 이런 워크로드에서 장점이 큽니다.

  1. 요청을 묶어 처리
  • 개별 HTTP 요청을 수만 번 날리는 대신, JSONL 파일 한 개로 “작업 묶음”을 제출합니다.
  1. 비동기 처리로 레이트리밋/스파이크 부담 감소
  • 실시간 QPS를 맞추기 위한 큐잉, 토큰 버짓 계산, 백오프 설계가 단순해집니다.
  1. 운영 단순화
  • “요청 제출”과 “결과 수거”가 분리되므로, 장애 격리/재처리 설계가 쉬워집니다.
  1. 비용 최적화 여지 확대
  • 대량 요약은 프롬프트/출력 길이를 표준화하기 쉽고, Batch로 돌리면 “항상 같은 정책”으로 비용을 강하게 통제할 수 있습니다.

핵심: Batch는 단순히 싸게 처리하는 기능이 아니라, 비실시간 대량 처리 파이프라인을 표준화해서 비용과 실패율을 동시에 낮추는 도구입니다.

전체 아키텍처: “수집 → 청크/정규화 → Batch 제출 → 결과 병합 → 저장”

대량 요약 파이프라인을 운영용으로 만들 때는 아래 5단계를 분리하는 게 좋습니다.

  1. 원문 수집: DB/오브젝트 스토리지에 원문 저장(중복 제거 키 포함)
  2. 정규화/청크: 너무 긴 문서는 토큰 기준으로 분할하고, 메타데이터를 붙임
  3. Batch 입력(JSONL) 생성: 요청 단위를 표준화(custom_id로 추적)
  4. Batch 제출 및 상태 폴링: 완료 시 output 파일 다운로드
  5. 결과 병합 및 저장: 요약 결과를 DB에 upsert, 실패 건은 재큐잉

이 구조는 “실시간 호출”에도 적용되지만, Batch에서는 특히 custom_id 설계가 생명입니다. 결과가 JSONL로 돌아오기 때문에, 나중에 어떤 문서/청크의 결과인지 안정적으로 조인해야 합니다.

JSONL 입력 포맷 설계(중요)

Batch는 보통 “요청 1줄 = 작업 1개” 형태의 JSONL을 입력으로 받습니다. 각 줄에는 최소한 다음이 필요합니다.

  • custom_id: 내부 추적용 ID(문서ID, 청크ID, 버전 포함 권장)
  • method: 보통 POST
  • url: 호출할 엔드포인트 경로
  • body: 모델/입력/파라미터

아래는 요약 전용 템플릿 예시입니다. (엔드포인트/필드는 사용 중인 OpenAI SDK/버전에 따라 달라질 수 있으니, 실제 적용 시 최신 문서에 맞춰 조정하세요.)

{"custom_id":"doc_9812#chunk_0001#v1","method":"POST","url":"/v1/responses","body":{"model":"gpt-4.1-mini","input":[{"role":"system","content":[{"type":"text","text":"너는 기술 문서를 요약하는 어시스턴트다. 출력은 한국어로, 5~7문장으로 핵심만."}]},{"role":"user","content":[{"type":"text","text":"[원문]\n...긴 텍스트...\n\n[요약 요구]\n- 핵심 주장\n- 숫자/지표\n- 실행 항목"}]}],"max_output_tokens":220,"temperature":0.2}}}

custom_id에 반드시 넣어야 하는 것

  • 문서 식별자: doc_9812
  • 청크 식별자: chunk_0001
  • 프롬프트/정책 버전: v1 (나중에 재생성/비교 필수)

이렇게 해두면

  • 결과 파일에서 custom_id만으로 DB upsert 가능
  • 프롬프트 바꾸고 재실행해도 충돌 없이 공존 가능
  • 일부 실패만 골라 재처리 가능

긴 문서 요약: 청크 전략이 비용을 좌우한다

대량 요약 비용을 줄이려면 모델 선택보다 먼저 입력 토큰을 줄여야 합니다. 실무에서 가장 흔한 실패는 “그냥 원문 전체를 넣고 요약”입니다.

권장 전략은 2단계 요약입니다.

  1. 청크 요약(Map 단계)
  • 문서를 토큰 기준으로 N개 청크로 나누고 각 청크를 짧게 요약
  1. 최종 합성(Reduce 단계)
  • 청크 요약들만 모아서 최종 요약 생성

이 구조는

  • 원문 전체를 계속 넣지 않으니 총 토큰이 감소
  • 실패 시 특정 청크만 재처리 가능
  • Batch에 매우 잘 맞음(작업 단위가 균질해짐)

간단한 청크 분할 코드(파이썬)

토크나이저는 모델별로 다르지만, 운영에서는 “대략적인 길이 제한”만 잘 지켜도 효과가 큽니다.

from dataclasses import dataclass

@dataclass
class Chunk:
    idx: int
    text: str

def chunk_by_chars(text: str, max_chars: int = 6000, overlap: int = 200):
    # 토큰 기반이 가장 좋지만, 간단히 문자 수로 근사
    chunks = []
    start = 0
    i = 0
    while start < len(text):
        end = min(len(text), start + max_chars)
        chunk = text[start:end]
        chunks.append(Chunk(idx=i, text=chunk))
        i += 1
        start = end - overlap
        if start < 0:
            start = 0
    return chunks

문자 기반 근사는 언어/문서 성격에 따라 오차가 있지만, “원문 전체 투입”보다는 대부분 낫습니다. 가능하면 운영 단계에서는 모델에 맞는 토큰 카운팅으로 바꾸는 게 좋습니다.

Batch 입력 파일 생성: 실패 재처리를 위한 “멱등성”

Batch는 비동기이므로, 중간에 잡이 재시작되거나 동일 문서가 중복 제출될 수 있습니다. 따라서 저장 계층은 멱등(upsert) 이 되어야 합니다.

다음 원칙을 추천합니다.

  • 요약 결과 테이블의 유니크 키를 doc_id + chunk_id + prompt_version + model로 구성
  • 결과 저장 시 INSERT ... ON DUPLICATE KEY UPDATE(MySQL) 같은 upsert 사용
  • custom_id를 그대로 키로 쓰는 것도 좋음

만약 DB 쓰기 경합이 생기면 데드락/락 대기가 증가할 수 있습니다. 대량 upsert에서 데드락이 보이면 아래 글의 접근이 도움이 됩니다.

Batch 실행과 상태 관리: 폴링은 “지수 백오프 + 타임아웃”

Batch는 제출 후 완료까지 시간이 걸립니다. 운영에서는 상태 폴링 로직이 필요하고, 여기서도 재시도/타임아웃 설계가 중요합니다.

  • 폴링 간격: 10초, 20초, 40초… 지수 백오프
  • 전체 타임아웃: 예를 들어 2시간
  • 완료 상태가 아니면 다음 폴링
  • 실패 상태면 input 파일과 함께 원인/실패 라인만 따로 떼어 재제출

분산 시스템에서 재시도/타임아웃을 잘못 잡으면 “더 큰 장애”를 만듭니다. 재시도 설계 감각을 잡는 데는 아래 글도 참고할 만합니다.

결과 파일 처리: output JSONL을 안전하게 병합하기

Batch 결과는 보통 JSONL로 내려옵니다. 각 줄에 custom_id와 응답(또는 에러)이 담기므로, 다음처럼 처리합니다.

  1. 한 줄씩 스트리밍 파싱(파일이 매우 클 수 있음)
  2. custom_id 파싱해서 doc_id, chunk_id, version 추출
  3. 성공이면 요약 텍스트를 저장
  4. 실패면 에러 코드/메시지 저장 후 재처리 큐로 이동

파이썬에서 “한 줄씩 읽기”는 단순하지만, 반드시 예외를 삼키지 말고 실패 라인을 별도 파일로 떼어두는 게 좋습니다.

import json

def iter_jsonl(path: str):
    with open(path, "r", encoding="utf-8") as f:
        for line_no, line in enumerate(f, start=1):
            line = line.strip()
            if not line:
                continue
            try:
                yield json.loads(line)
            except json.JSONDecodeError as e:
                raise RuntimeError(f"JSONL parse error at line {line_no}: {e}")

# 예시: 결과에서 custom_id 기반으로 분기
for item in iter_jsonl("batch_output.jsonl"):
    custom_id = item.get("custom_id")
    # item["response"] 또는 item["error"] 형태는 환경에 따라 다를 수 있음
    if "error" in item:
        # 실패 저장 및 재처리 대상으로 기록
        continue
    # 성공 처리

“비용 90% 절감”이 나오는 실전 체크리스트

Batch로 바꿨는데도 비용이 크게 안 줄었다면, 보통 아래 중 하나입니다.

1) 출력 토큰이 통제되지 않는다

  • 요약은 max_output_tokens를 작게 고정하세요.
  • “요약은 짧아야 한다”를 시스템/유저 프롬프트에 명시하세요.

나쁜 예: “자세히 요약해줘”

좋은 예: “5~7문장, 숫자/지표는 유지, 불확실하면 추측 금지”

2) 원문을 그대로 넣는다

  • 2단계 요약(Map-Reduce)로 바꾸면 입력 토큰이 급감합니다.
  • 특히 “최종 합성” 단계는 청크 요약만 입력하므로 매우 저렴합니다.

3) 중복 문서를 계속 요약한다

  • 해시(예: sha256)로 원문 중복 제거
  • 동일 문서라도 프롬프트 버전이 같으면 재생성하지 않기

4) 실패 재처리가 전체 재실행으로 이어진다

  • 실패 라인만 모아 “재제출 JSONL”을 만들면 비용이 폭발하지 않습니다.

5) 모델을 과하게 쓴다

  • 대량 요약은 대개 “가벼운 모델 + 강한 포맷 제약”이 효율적입니다.
  • 정확도가 필요한 일부 문서만 상위 모델로 재요약하는 2단계 정책도 좋습니다.

운영 팁: 배치 잡을 CI로 돌릴 때 동시 실행을 막아라

대량 요약 배치를 GitHub Actions 같은 CI로 돌리면, 스케줄이 겹치거나 수동 실행이 겹쳐서 동일 배치를 중복 제출하는 사고가 자주 납니다. 이건 비용 절감의 적입니다.

  • “하루 1회” 배치는 동시 실행을 막고, 이전 실행을 취소하거나 대기시키세요.

관련해서 GitHub Actions의 동시 실행 제어는 아래 글이 실전적으로 도움이 됩니다.

예시: “청크 요약 Batch” JSONL 생성기

아래 코드는 문서 리스트를 받아 청크를 만들고, Batch 입력 JSONL을 생성합니다. 포인트는 custom_id 규칙과 “짧은 출력” 강제입니다.

import json
from hashlib import sha256

def make_custom_id(doc_id: str, chunk_idx: int, prompt_version: str, model: str):
    return f"{doc_id}#chunk_{chunk_idx:04d}#{prompt_version}#{model}"

def build_batch_jsonl(docs, out_path: str, model: str = "gpt-4.1-mini", prompt_version: str = "v1"):
    with open(out_path, "w", encoding="utf-8") as f:
        for doc in docs:
            doc_id = doc["doc_id"]
            text = doc["text"]

            # 중복 제거 키(예: 원문 해시). 실제로는 DB에 저장해 재요약 방지
            content_hash = sha256(text.encode("utf-8")).hexdigest()[:12]

            chunks = chunk_by_chars(text, max_chars=6000, overlap=200)
            for ch in chunks:
                custom_id = make_custom_id(doc_id, ch.idx, prompt_version, model)

                body = {
                    "model": model,
                    "input": [
                        {
                            "role": "system",
                            "content": [
                                {
                                    "type": "text",
                                    "text": "너는 기술 문서를 요약한다. 출력은 한국어. 5~7문장. 불확실하면 추측하지 말고 '알 수 없음'으로 표시."
                                }
                            ],
                        },
                        {
                            "role": "user",
                            "content": [
                                {
                                    "type": "text",
                                    "text": f"doc_id={doc_id}, hash={content_hash}, chunk={ch.idx}\n\n[원문]\n{ch.text}\n\n[요약 규칙]\n- 핵심 주장\n- 수치/지표 유지\n- 실행 항목이 있으면 bullet로 3개 이내"
                                }
                            ],
                        },
                    ],
                    "max_output_tokens": 220,
                    "temperature": 0.2,
                }

                line = {
                    "custom_id": custom_id,
                    "method": "POST",
                    "url": "/v1/responses",
                    "body": body,
                }
                f.write(json.dumps(line, ensure_ascii=False) + "\n")

# 사용 예시
docs = [
    {"doc_id": "doc_1001", "text": "긴 기술 문서 내용..."},
    {"doc_id": "doc_1002", "text": "또 다른 문서..."},
]
build_batch_jsonl(docs, "batch_input.jsonl")

이 파일을 Batch에 제출하고, 완료 후 output JSONL을 받아 custom_id 기준으로 저장하면 됩니다.

마무리: Batch는 “대량 처리의 표준 운영 모델”이다

OpenAI Batch API로 대량 요약을 돌리면, 단순히 요청을 묶는 것 이상의 효과가 납니다.

  • 비용 통제: 출력 길이/프롬프트 정책을 표준화하기 쉬움
  • 실패 격리: 실패 라인만 재처리 가능
  • 운영 단순화: 레이트리밋 맞추는 실시간 큐잉 지옥에서 벗어남

“비용 90% 절감”은 마케팅 문구가 아니라, 입력 토큰 절감(청크/2단계 요약) + 중복 제거 + 실패 재처리 최소화 + 배치 운영 표준화가 함께 맞물릴 때 현실적으로 도달 가능한 수치입니다. 대량 요약이 돈 먹는 하마가 됐다면, 먼저 Batch로 파이프라인을 재구성해 보세요.