Skip to content

AI 연재 1/9 — LLM 호출 하나를 '파이프라인'으로 만든다는 것 ​

LLM API를 처음 붙이면 신기하다. 프롬프트 던지면 그럴듯한 답이 온다. 문제는 그걸 서비스에 넣는 순간부터다. 응답 JSON이 가끔 깨지고, 타임아웃 뒤에 재시도했더니 같은 처리가 두 번 일어나고, 어느 날부터 판단이 미묘하게 달라진다.

이 연재는 그런 지점에서 출발한다. 총 9편이고, 3편씩 세 덩어리로 나눈다.

  • 1~3편: AI 처리 파이프라인과 판단 품질 보장 (지금 읽는 글)
  • 4~6편: 에이전트 워크플로우, RAG, 에이전트 심화
  • 7~9편: 대화형 UX의 한계와 동적 UI

세 덩어리를 관통하는 가상의 예시가 하나 있다. 고객이 채팅으로 "보상 신청"을 하면, 서류를 받고, AI가 1차 판단을 하고, 사람이 검토하고, 결과를 안내하는 서비스다. 특정 회사나 산업의 실제 시스템이 아니라 설명용으로 일반화한 시나리오다.

미리 밝혀 둘 게 있다. 나는 이 시나리오를 운영해 본 적이 없다. 그래서 "우리 팀은 이렇게 했다" 식의 이야기는 쓰지 않는다. 대신 공식 문서, 논문, 규제 문서에서 직접 확인한 것만 근거로 삼고, 확인하지 못한 건 확인하지 못했다고 쓴다. 직접 해본 건 나닮이라는 개인 프로젝트에서 OpenAI를 붙여 본 정도다. 그 얘기는 필요한 곳에서만 짧게 한다.


단계로 쪼갠다 ​

보상 신청 하나를 LLM에 통째로 던지면 어떻게 될까. "이 신청 승인해도 돼?"라는 질문 하나에 서류 해석, 약관 적용, 금액 산정, 사기 징후 판단이 한꺼번에 들어간다. 틀렸을 때 어디서 틀렸는지 알 수 없다.

그래서 보통 이렇게 쪼갠다.

  1. 입력 정리: 업로드된 서류에서 필드를 뽑는다 (추출)
  2. 분류: 신청 유형이 뭔지 정한다
  3. 판단: 유형별 기준에 따라 승인/보류/거절 후보를 낸다
  4. 후처리: 금액 계산, 규칙 검증, 사람 검토 필요 여부 결정

Anthropic의 "Building effective agents"(2024-12)는 이런 구조를 워크플로우 패턴으로 정리한다. 고정된 하위 작업으로 깔끔하게 나뉘면 prompt chaining이 이상적이라고 하고, 입력을 분류해 전문 경로로 보내는 건 routing이라고 부른다. 이 글에서 가장 마음에 든 문장은 성공의 기준이 "가장 정교한 시스템"이 아니라 "필요에 맞는 올바른 시스템"이라는 부분이다. 4편에서 다시 다룬다.

단계 사이에는 코드로 짠 게이트를 둔다. 추출된 금액이 숫자인지, 날짜가 미래가 아닌지, 필수 서류가 빠지지 않았는지. LLM이 아니라 평범한 코드가 검사한다. 이 게이트가 있어야 앞 단계의 오류가 뒤로 번지지 않는다. Anthropic도 에이전트처럼 자유도가 큰 구조에서는 오류가 누적(compounding)될 수 있다고 경고한다.

그리고 중간 산출물을 반드시 저장해 둔다. 이건 2편(평가)과 3편(감사 로그)의 전제 조건이라서 지금 아니면 나중에 고치기 어렵다. 결과가 이상하다는 제보가 들어왔을 때 그때의 입력과 중간 결과가 남아 있지 않으면 재현 자체가 안 되기 때문이다.


구조화 출력: 형식은 보장하지만 의미는 아니다 ​

단계 사이에서 JSON을 주고받으려면 응답 형식이 믿을 만해야 한다. 예전에는 프롬프트에 "JSON으로만 답해"라고 쓰고 파싱 실패 시 재시도했다. 지금은 모델 쪽에서 스키마 준수를 지원한다.

공식 문서에서 확인한 내용이다.

  • Anthropic의 Structured Outputs는 현재 GA이고, JSON 스키마를 지정하는 출력 설정과 도구에 strict: true를 붙이는 방식이 있다. 지원하는 스키마는 enum, const, anyOf, $ref 등이고, 재귀 스키마나 minimum/maximum, minLength/maxLength 같은 제약은 지원하지 않는다고 적혀 있다.
  • OpenAI는 Structured Outputs를 JSON mode의 발전형으로 설명한다. JSON mode는 유효한 JSON만 보장하고, Structured Outputs는 스키마 준수(필수 필드 누락이나 잘못된 enum 값이 없음)를 보장한다. 안전상 거부는 refusal 필드로 프로그램에서 감지할 수 있다.

여기서 실무적으로 중요한 포인트가 몇 가지 있다.

첫째, 지원하지 않는 제약은 내가 코드로 검증해야 한다. 예를 들어 confidence가 0~1 사이인지는 스키마가 아니라 애플리케이션 코드가 확인한다.

둘째, 거부와 잘림을 따로 처리해야 한다. Anthropic 문서에 따르면 stop_reason이 refusal이면 스키마를 따르지 않을 수 있고, max_tokens로 잘려도 불완전한 출력이 나온다. 200 응답이라고 안심하면 안 된다.

셋째, 첫 요청에는 스키마를 컴파일하는 지연이 있다. 스키마를 자주 바꾸면 이 지연이 반복된다. 이건 문서에 적힌 사실이지만, 내 환경에서 얼마나 체감되는지는 재 보지 않았다.

그리고 가장 중요한 하나. 스키마를 통과했다는 건 문법이 맞다는 뜻이지 판단이 맞다는 뜻이 아니다. {"decision": "approve"}는 완벽하게 유효한 JSON이다. 그게 옳은 결정인지는 별개 문제고, 그 별개 문제가 2편의 주제다.

python
from pydantic import BaseModel, Field
from typing import Literal

class ClaimDecision(BaseModel):
    decision: Literal["approve", "hold", "reject", "insufficient_info"]
    reasons: list[str]            # 근거: 어떤 서류의 어느 부분인지
    evidence_refs: list[str]      # 서류 ID/페이지 같은 참조
    confidence: float = Field(ge=0, le=1)  # 스키마 지원 여부와 무관하게 코드에서 검증

def validate(raw: dict) -> ClaimDecision:
    d = ClaimDecision.model_validate(raw)   # 형식 + 범위
    if d.decision == "approve" and not d.evidence_refs:
        raise ValueError("근거 없는 승인")   # 의미 규칙은 코드가 확인
    return d

enum에 insufficient_info를 넣은 건 의도적이다. 모델에게 "모름"을 말할 자리를 주지 않으면 필드를 억지로 채운다. 이건 출처가 있는 사실이라기보다 내 설계 습관에 가깝다. 의견이 갈릴 수 있다.


재시도, 타임아웃, 멱등성 ​

LLM 호출은 느리고 가끔 실패한다. 그러면 재시도를 넣게 되는데, 여기서 사고가 난다. 타임아웃이 났는데 실제로는 서버에서 처리가 끝났다면? 재시도가 부수 효과를 두 번 일으킨다. 알림이 두 번 나가거나 상태가 두 번 바뀐다.

멱등성 키는 이 문제의 고전적인 해법이다. Stripe API 문서가 가장 잘 정리돼 있다.

  • 키는 클라이언트가 만든다. V4 UUID를 권장하고 최대 255자다.
  • 서버는 첫 요청의 상태 코드와 본문을 키 기준으로 저장하고, 성공이든 실패든 같은 키로 다시 오면 같은 결과를 돌려준다.
  • 24시간이 지난 키는 삭제될 수 있다. 같은 키에 다른 파라미터가 오면 에러다.

LLM 파이프라인에 그대로 옮길 때 주의할 점이 있다. LLM의 출력 자체는 비결정적이라서 "같은 입력이면 같은 출력"이라는 의미의 멱등성은 성립하지 않는다. 그래서 현실적인 설계는 이렇다. 처리 단위(job)에 멱등 키를 붙이고, 완료된 결과를 저장한 뒤, 재시도 때는 새로 호출하지 않고 저장된 결과를 반환한다. 부수 효과(알림 발송, 상태 변경)는 LLM 호출 단계와 분리해서 그 키로 보호한다. 이건 Stripe 문서의 원칙을 내가 LLM 파이프라인에 적용해 본 해석이다.

재시도 정책은 이 정도가 무난하다고 생각한다.

  • 재시도 대상은 429, 5xx, 타임아웃. 4xx와 스키마 위반은 재시도하지 않는다 (같은 입력에 같은 실패가 반복될 가능성이 높다)
  • 지수 백오프에 지터를 섞는다
  • 재시도 횟수와 전체 시간 예산(deadline)에 상한을 둔다
  • 한도를 넘으면 수동 검토 큐로 넘긴다

단계마다 재시도하면 곱셈으로 폭증한다는 점도 조심하자. 4단계 파이프라인에서 각 단계가 3번씩 재시도하면 최악의 경우 호출이 늘어나는 폭이 크다. OWASP의 LLM Top 10(2025)에서 LLM10 Unbounded Consumption이 따로 항목으로 있는 이유가 이런 비용 폭주다.


아직 정리 안 된 고민 ​

단계를 몇 개로 쪼개는 게 적당한지는 솔직히 모르겠다. 너무 크게 두면 디버깅이 안 되고, 너무 잘게 쪼개면 단계마다 지연과 비용이 붙고 오류 전파 경로만 늘어난다. 지금으로서는 "사람이 중간 결과를 보고 판단할 수 있는 단위"가 기준이라고 생각하는데, 이건 근거 있는 규칙이 아니라 감이다.

다음 편은 쪼갠 파이프라인이 실제로 잘 동작하는지 어떻게 아는가, 즉 평가와 관측성이다.


연재 목차: 1편(현재) · 2편 품질 평가와 관측성

참고자료 ​

멸종 위기 개발자