LLM 구조화 출력 운영 가이드: JSON 모드만 믿지 말고 스키마·검증·재시도 계층을 나눠라
LLM으로 업무 자동화를 만들 때 가장 자주 터지는 문제 중 하나는 JSON입니다. 데모에서는 모델이 예쁜 JSON을 잘 내놓습니다. 하지만 프로덕션에서는 필드가 빠지고, enum 값이 틀리고, 숫자가 문자열로 오고, 설명 문장이 JSON 앞뒤에 붙습니다. 이 상태로 후속 API나 DB 저장을 연결하면 장애가 납니다.
최근 구조화 출력 관련 자료들은 공통적으로 schema-constrained generation, function calling, validation loop를 함께 봐야 한다고 말합니다. MLflow 글은 deterministic extraction에는 schema-constrained generation을, agentic decision point에는 function calling을 함께 쓰는 패턴을 언급합니다. LangChain 문서도 structured output을 별도 기능으로 다룹니다. 핵심은 “JSON처럼 보이는 텍스트”가 아니라 “계약을 지키는 데이터”입니다.
참고 자료: MLflow structured outputs 글(https://mlflow.org/articles/structured-outputs-llm), LangChain structured output 문서(https://docs.langchain.com/oss/python/langchain/structured-output).
JSON 모드와 구조화 출력은 다르다
JSON 모드는 대체로 “문법적으로 JSON이 되도록” 돕는 기능입니다. 하지만 문법적으로 유효하다고 해서 비즈니스 스키마를 만족한다는 뜻은 아닙니다. required 필드가 빠질 수 있고, status가 paid/refunded/pending 중 하나여야 하는데 completed가 올 수 있습니다. amount가 number여야 하는데 "12,000원" 같은 문자열로 올 수도 있습니다.
구조화 출력은 한 단계 더 나아가야 합니다. 타입, 필수값, enum, 배열 길이, 중첩 객체, nullable 여부를 명시하고, 모델 응답이 이 계약을 지키도록 강제하거나 검증합니다. 가능하면 provider-side schema enforcement를 쓰고, 그렇지 않다면 애플리케이션 계층에서 validation을 반드시 둡니다.
실무에서는 “모델이 알아서 맞추겠지”가 가장 위험합니다. 모델은 자연어 생성기이고, 후속 시스템은 엄격한 타입 시스템입니다. 둘 사이에 계약 계층을 두지 않으면 장애는 사용자 요청이 다양해지는 순간 발생합니다.
스키마 설계: 모델이 맞추기 쉬운 구조로 만든다
좋은 스키마는 개발자가 보기 좋은 구조가 아니라 모델이 안정적으로 채울 수 있는 구조입니다. 너무 깊은 중첩, 애매한 필드명, 서로 겹치는 enum은 실패율을 높입니다. 예를 들어 category와 type이 비슷한 의미를 갖고 있으면 모델은 둘을 섞습니다.
필드명은 구체적으로 씁니다. summary보다 issue_summary, score보다 risk_score가 낫습니다. enum 값은 짧고 서로 구분되어야 합니다. high, medium, low처럼 명확한 값이 좋고, needs_review와 review_required처럼 비슷한 값은 피합니다. 날짜는 ISO 형식처럼 고정합니다. 금액은 숫자와 통화를 분리합니다.
또한 optional 필드를 남발하지 않는 것이 좋습니다. optional이 많으면 모델은 편한 대로 생략합니다. 정말 없어도 되는 값만 optional로 두고, 나머지는 null 허용 여부를 명확히 정합니다. 스키마는 문서가 아니라 운영 계약입니다.
검증 계층: 파싱 성공과 업무 성공을 분리한다
구조화 출력 검증은 두 단계로 나눠야 합니다. 첫째는 문법·타입 검증입니다. JSON 파싱, JSON Schema, Pydantic, Zod 같은 도구로 required, type, enum을 확인합니다. 둘째는 업무 규칙 검증입니다. 예를 들어 start_date가 end_date보다 앞서는지, refund_amount가 order_amount보다 크지 않은지, confidence가 낮으면 자동 실행하지 않는지 확인합니다.
많은 팀이 첫 번째 검증만 하고 끝냅니다. 하지만 실제 사고는 두 번째에서 많이 납니다. JSON은 맞지만 업무적으로 말이 안 되는 값이 들어오면 더 위험합니다. 시스템은 성공으로 처리하고 잘못된 액션을 실행하기 때문입니다.
검증 실패는 단순히 “다시 생성”으로 처리하지 말고 원인을 분류해야 합니다. 파싱 실패, 스키마 실패, 업무 규칙 실패, 안전 정책 실패를 나눕니다. 그래야 프롬프트 문제인지, 스키마 문제인지, 사용자 입력 문제인지 판단할 수 있습니다.
재시도 전략: 무한 재생성은 비용 폭탄이다
구조화 출력이 실패했을 때 재시도는 필요합니다. 하지만 무한 재시도는 비용과 지연 시간을 망칩니다. 최대 1~2회로 제한하고, 재시도 프롬프트에는 실패한 검증 메시지를 구체적으로 넣어야 합니다. “다시 JSON으로 작성해”보다 “field risk_score must be number between 0 and 100”이 훨씬 낫습니다.
재시도 후에도 실패하면 fallback을 정해야 합니다. 자동 실행을 멈추고 사람 검토 큐로 보내거나, 보수적인 기본값을 쓰거나, 사용자에게 추가 정보를 요청할 수 있습니다. 어떤 fallback을 쓸지는 기능 위험도에 따라 달라집니다. 게시글 태그 추천은 기본값으로 넘어가도 되지만, 환불 승인이나 의료 상담 분류는 사람 검토가 필요합니다.
재시도 로그도 남겨야 합니다. 어떤 필드에서 실패가 반복되는지 알면 스키마를 줄이거나 예시를 추가할 수 있습니다. 반복 실패 필드는 모델이 어려워하는 필드입니다.
에이전트와 구조화 출력: 결정과 실행을 분리한다
에이전트에서는 구조화 출력이 더 중요합니다. 모델이 도구를 고르고, 인자를 만들고, 실행 결과를 해석하기 때문입니다. 이때 결정 단계와 실행 단계를 분리해야 합니다. 모델은 “어떤 도구를 어떤 인자로 호출할지” 구조화 출력으로 제안하고, 애플리케이션은 스키마와 권한을 검증한 뒤 실행합니다.
예를 들어 delete_user 같은 도구가 있다면 모델이 바로 실행하게 두면 안 됩니다. 먼저 {action:"delete_user", user_id:"...", reason:"...", confidence:0.82} 같은 계획 객체를 만들게 하고, 서버가 권한·승인·위험도를 확인합니다. 위험도가 높으면 사람 승인으로 넘깁니다.
이 구조는 느려 보이지만 운영 사고를 줄입니다. 에이전트가 점점 많은 도구를 갖게 될수록 “모델 출력은 제안이고, 실행은 시스템 정책이 결정한다”는 경계를 유지해야 합니다.
실행 체크리스트
- JSON 문법 성공과 스키마 성공, 업무 규칙 성공을 별도 지표로 기록한다.
- 필드명은 구체적으로, enum은 짧고 겹치지 않게 설계한다.
- provider-side schema enforcement가 가능하면 우선 사용하고, 그래도 서버 검증을 둔다.
- 재시도는 최대 1~2회로 제한하고 검증 실패 메시지를 프롬프트에 넣는다.
- 실패 fallback을 기능 위험도별로 정한다: 기본값, 재질문, 사람 검토, 중단.
- 에이전트 도구 호출은 “계획 객체 생성 → 서버 검증 → 실행” 순서로 나눈다.