개발자들은 이제 간단한 Python 루프를 통해 LLM(대규모 언어 모델)이 생성한 잘못된 형식의 JSON을 수정하는 비용을 복구 시도 횟수를 2회로 제한함으로써 제어할 수 있습니다. 생성, 파싱, 검증, 재시도, 그리고 수락 또는 실패로 이어지는 이 패턴은 "아마도 유효할 것 같은 JSON"을 측정 가능한 성공률로 전환하고, 토큰 사용량이 걷잡을 수 없이 늘어나는 것을 방지합니다.
복구 루프가 중요한 이유
LLM은 종종 "JSON을 반환하라"는 지시를 받지만, 출력 결과에는 구문 오류, 누락된 키 또는 비즈니스 규칙을 위반하는 값이 포함되는 경우가 빈번합니다. 엄격한 페이로드를 기대하는 다운스트림 시스템은 이러한 출력을 받으면 충돌하거나 잘못된 결과를 생성합니다. 이러한 문제를 포착하고 수정하는 체계적인 방법이 없다면, 팀은 높은 실패율을 감수하거나 모델이 올바른 결과를 낼 때까지 반복적으로 프롬프트를 입력하며 비용을 낭비해야 합니다.
세 가지 검증 계층
신뢰할 수 있는 루프는 검증을 다음과 같이 세 단계로 분리합니다:
- Syntax (구문) – 문자열이 JSON으로 파싱되는가?
- Shape (구조) – 최상위 객체에 예상된 키가 정확히 포함되어 있는가?
- Semantic (의미론적) – 값이 도메인 제약 조건을 준수하는가 (예: "category"가 미리 정의된 집합에 속하는가)?
실패하는 첫 번째 단계에서 멈춤으로써, 루프는 모델에 정확한 오류 메시지를 전달할 수 있으며, 이는 다음 시도에서 올바르게 수정될 확률을 획기적으로 높여줍니다.
최소한의 Python 예제
다음 코드는 표준 라이브러리만을 사용합니다. 작은 스키마(category와 summary 키를 가진 딕셔너리)와 허용된 카테고리 목록을 정의합니다.
import json
CATEGORIES = {"bug", "feature", "question"}
def validate(raw: str):
try:
value = json.loads(raw)
except json.JSONDecodeError as exc:
return None, f"syntax: {exc.msg}"
if not isinstance(value, dict):
return None, "shape: root must be an object"
if set(value) != {"category", "summary"}:
return None, "shape: expected category and summary"
if value["category"] not in CATEGORIES:
return None, "semantic: invalid category"
if not isinstance(value["summary"], str) or not value["summary"].strip():
return None, "semantic: summary must be text"
return value, None
복구 루프 자체는 시도 횟수에 엄격한 상한선을 둡니다:
MAX_ATTEMPTS = 2
raw = model_response # initial LLM output
for attempt in range(MAX_ATTEMPTS + 1):
value, error = validate(raw)
if error is None:
break
if attempt == MAX_ATTEMPTS:
raise RuntimeError(f"failed after {MAX_ATTEMPTS} retries: {error}")
# Send the error and schema back to the model for a fix
raw = ask_model_to_repair(raw, error)
JSON이 첫 번째 통과에서 검증을 통과하면 루프는 즉시 종료됩니다. 그렇지 않으면 모델에 간결한 페이로드(원래 출력, 정확한 오류 문자열, 예상 스키마)를 전달하여 재시도합니다. 두 번의 시도가 실패하면 프로세스는 명확한 예외와 함께 중단됩니다.
비용 효율적인 복구를 위한 두 가지 규칙
복구 프롬프트를 간결하게 유지하세요 – 원본 JSON, 오류 메시지, 스키마만 포함하세요. 긴 대화 기록을 추가하면 토큰 사용량이 늘어나고 모델을 혼란스럽게 하여, 정확도를 높이지 못한 채 재시도당 비용만 높일 수 있습니다.
검증과 사실 확인을 분리하세요 – 파서는 구조적 문제를 감지할 수 있지만, "summary" 문장이 사실인지 확인할 수는 없습니다. 사실 확인은 나중 단계에서 이루어져야 합니다. 거짓된 주장을 "수정"하려고 시도하는 것은 단지 거짓말을 더 깔끔하게 보이게 만들 뿐입니다.
성공 측정하기
각 시도의 오류 유형(구문, 구조, 의미론적)과 루프의 성공 여부를 기록하면 구체적인 지표를 얻을 수 있습니다. 즉, 제한된 재시도 내에서 사용 가능한 상태가 된 LLM 응답의 비율입니다. 팀은 이를 시간에 따라 추적하거나, 프롬프트 스타일을 비교하거나, 다양한 스키마 정의를 실험할 수 있습니다.
잠재적인 단점
루프에 상한을 두는 것은 모델의 원본 품질이 낮을 경우 제한에 도달한 후 일부 잘못된 출력이 폐기됨을 의미하며, 이는 전체 실패율을 높일 수 있습니다. 모든 응답이 중요한 환경에서는 개발자가 추가 토큰을 소모하더라도 더 높은 재시도 상한을 선호할 수 있습니다. 트레이드오프는 명확합니다. 더 높은 커버리지를 위해 더 많은 비용을 지불할 것인가, 아니면 예측 가능한 상한선 내에서 더 타이트한 예산을 유지할 것인가입니다.
다음에 주목할 점
- 복구를 위한 프롬프트 엔지니어링 – 오류 메시지 형식을 개선하면 필요한 재시도 횟수를 줄일 수 있습니다.
- 대안 파서 – 일부 라이브러리는 사소한 문제를 자동으로 수정할 수 있는 관대한 파싱을 제공합니다. 이러한 라이브러리의 비용과 제한된 루프의 비용을 비교해 볼 가치가 있습니다.
- 모델 업데이트 – 최신 LLM 버전은 별도의 처리 없이도 더 깨끗한 JSON을 생성할 수 있으므로, 재시도 제한을 더 낮출 수 있을 것입니다.
제한된 JSON 복구 루프는 개발자에게 비용 급증 없이 모호한 LLM 출력을 신뢰할 수 있는 데이터로 바꿀 수 있는 실용적인 도구를 제공합니다. 검증을 핵심 단계로 취급하고 재시도 횟수를 제한함으로써, 팀은 예산과 다운스트림 시스템 모두를 만족시킬 수 있습니다.
