- Published on
OpenAI Batch API로 대량 요약 비용 90% 절감하기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 로그, CS 티켓, 회의록, 기사 크롤링 결과처럼 “요약”은 만들수록 쌓이고, 쌓일수록 비용이 폭증합니다. 특히 실시간 API 호출로 수천~수만 건을 처리하면 요청당 오버헤드(네트워크, 레이트리밋 회피, 재시도)까지 더해져 운영이 괴로워집니다.
이때 유효한 선택지가 OpenAI Batch API입니다. 한 번에 요청들을 묶어 비동기 처리하고, 대량 처리에 최적화된 과금/처리 모델을 활용해 대규모 요약 비용을 체감상 90% 수준까지 절감(워크로드/모델/프롬프트에 따라 다름)할 수 있습니다. 이 글에서는 “왜 싸지는지”보다 더 중요한 어떻게 설계해야 실제로 절감이 나오는지를 중심으로, 입력 파일 구성부터 결과 병합, 실패 재처리, 비용 추적까지 끝까지 설명합니다.
Batch API가 대량 요약에 유리한 이유
대량 요약은 다음 특성이 있습니다.
- 대부분 비실시간이어도 됨(예: 하루 1회 리포트 생성, 백필 백로그 처리)
- 입력이 크고(긴 텍스트), 요청 수가 많음
- 실패가 일부 섞여도 전체 파이프라인은 계속 돌아가야 함
Batch는 이런 워크로드에서 장점이 큽니다.
- 요청을 묶어 처리
- 개별 HTTP 요청을 수만 번 날리는 대신, JSONL 파일 한 개로 “작업 묶음”을 제출합니다.
- 비동기 처리로 레이트리밋/스파이크 부담 감소
- 실시간 QPS를 맞추기 위한 큐잉, 토큰 버짓 계산, 백오프 설계가 단순해집니다.
- 운영 단순화
- “요청 제출”과 “결과 수거”가 분리되므로, 장애 격리/재처리 설계가 쉬워집니다.
- 비용 최적화 여지 확대
- 대량 요약은 프롬프트/출력 길이를 표준화하기 쉽고, Batch로 돌리면 “항상 같은 정책”으로 비용을 강하게 통제할 수 있습니다.
핵심: Batch는 단순히 싸게 처리하는 기능이 아니라, 비실시간 대량 처리 파이프라인을 표준화해서 비용과 실패율을 동시에 낮추는 도구입니다.
전체 아키텍처: “수집 → 청크/정규화 → Batch 제출 → 결과 병합 → 저장”
대량 요약 파이프라인을 운영용으로 만들 때는 아래 5단계를 분리하는 게 좋습니다.
- 원문 수집: DB/오브젝트 스토리지에 원문 저장(중복 제거 키 포함)
- 정규화/청크: 너무 긴 문서는 토큰 기준으로 분할하고, 메타데이터를 붙임
- Batch 입력(JSONL) 생성: 요청 단위를 표준화(
custom_id로 추적) - Batch 제출 및 상태 폴링: 완료 시 output 파일 다운로드
- 결과 병합 및 저장: 요약 결과를 DB에 upsert, 실패 건은 재큐잉
이 구조는 “실시간 호출”에도 적용되지만, Batch에서는 특히 custom_id 설계가 생명입니다. 결과가 JSONL로 돌아오기 때문에, 나중에 어떤 문서/청크의 결과인지 안정적으로 조인해야 합니다.
JSONL 입력 포맷 설계(중요)
Batch는 보통 “요청 1줄 = 작업 1개” 형태의 JSONL을 입력으로 받습니다. 각 줄에는 최소한 다음이 필요합니다.
custom_id: 내부 추적용 ID(문서ID, 청크ID, 버전 포함 권장)method: 보통POSTurl: 호출할 엔드포인트 경로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단계 요약입니다.
- 청크 요약(Map 단계)
- 문서를 토큰 기준으로
N개 청크로 나누고 각 청크를 짧게 요약
- 최종 합성(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와 응답(또는 에러)이 담기므로, 다음처럼 처리합니다.
- 한 줄씩 스트리밍 파싱(파일이 매우 클 수 있음)
custom_id파싱해서doc_id,chunk_id,version추출- 성공이면 요약 텍스트를 저장
- 실패면 에러 코드/메시지 저장 후 재처리 큐로 이동
파이썬에서 “한 줄씩 읽기”는 단순하지만, 반드시 예외를 삼키지 말고 실패 라인을 별도 파일로 떼어두는 게 좋습니다.
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로 파이프라인을 재구성해 보세요.