출력 파싱 — AI 응답을 코드가 읽을 수 있게 만드는 기술

…

한줄 요약

AI 출력은 기본적으로 자유 형식 텍스트다 — 이걸 코드가 쓸 수 있는 구조로 바꾸는 것이 출력 파싱이고, 이게 생각보다 훨씬 어렵다.

본문

문제의 시작

의료 AI 시스템이 감별진단을 내려준다고 하자. 코드는 이 결과를 받아서 EMR에 저장하고 싶다.

AI 응답이 이렇게 오면 어떻게 할 것인가:

"흉통 환자에서 고려할 감별진단은 다음과 같습니다.
첫째로 급성 심근경색(AMI)을 배제해야 합니다. 특히...
또한 대동맥 박리도 생각해볼 수 있으며...
폐색전증의 가능성도 있습니다. Wells score를 고려하세요.
기흉의 경우 호흡음 청진이 중요하며..."

자연스러운 텍스트다. 사람은 읽을 수 있다. 코드는? 여기서 진단명을 추출해서 배열로 만드는 것은 매우 어렵다.


해결책 1: JSON 모드 강제

현대 LLM API는 대부분 “JSON 모드”를 지원한다. AI가 반드시 유효한 JSON을 출력하도록 강제하는 기능이다.

시스템 프롬프트에 이렇게 넣는다:

항상 다음 JSON 형식으로만 응답하라:
{
  "diagnoses": [
    {
      "name": "질환명",
      "priority": 1,
      "icd10": "코드",
      "reasoning": "근거",
      "urgency": "emergent|urgent|routine"
    }
  ],
  "recommended_workup": ["검사1", "검사2"],
  "confidence": "high|medium|low"
}

그러면 AI가 이런 식으로 응답한다:

{
  "diagnoses": [
    {
      "name": "급성 심근경색",
      "priority": 1,
      "icd10": "I21.9",
      "reasoning": "흉통, 발한, ECG 변화",
      "urgency": "emergent"
    },
    {
      "name": "대동맥 박리",
      "priority": 2,
      "icd10": "I71.0",
      "reasoning": "찢어지는 흉통, 혈압 차이",
      "urgency": "emergent"
    }
  ],
  "recommended_workup": ["ECG", "Troponin", "Chest CT"],
  "confidence": "high"
}

코드가 바로 파싱해서 쓸 수 있다.


해결책 2: Structured Outputs

2024년 OpenAI가 “Structured Outputs”를 도입했다. JSON 모드보다 강력하다 — JSON 스키마를 미리 정의하면 AI가 반드시 그 스키마를 따르는 출력만 생성한다.

from pydantic import BaseModel
from typing import Literal

class Diagnosis(BaseModel):
    name: str
    priority: int
    icd10: str
    urgency: Literal["emergent", "urgent", "routine"]

class DiagnosisResponse(BaseModel):
    diagnoses: list[Diagnosis]
    confidence: Literal["high", "medium", "low"]

# API 호출 시 스키마 지정
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[...],
    response_format=DiagnosisResponse,
)

타입 안전성이 보장된다. 필드가 빠지거나 타입이 맞지 않으면 API 레벨에서 오류가 난다.


파싱이 실패하는 상황들

JSON이 잘렸다: 출력 토큰 제한에 걸리면 JSON이 중간에 끊긴다. { "diagnoses": [{"name": "급... 이런 식으로.

대응: max_tokens를 충분히 잡고, 응답 끝이 }로 끝나는지 확인.

마크다운이 섞였다: AI가 JSON 주변에 ```json을 붙이는 경우. 정규식으로 추출해야 한다.

import re, json

def extract_json(text: str) -> dict:
    # 마크다운 코드블록 제거
    match = re.search(r'```(?:json)?\s([\s\S]?)```', text)
    if match:
        text = match.group(1)
    return json.loads(text.strip())

키 이름이 달라졌다: “diagnoses” 대신 “diagnosis”, “Diagnoses”, “진단” 등으로 오는 경우. 파싱 실패.

대응: 스키마를 시스템 프롬프트에 두 번 강조하거나, Structured Outputs 사용.

숫자가 문자열로 왔다: "priority": "1" (문자열) vs "priority": 1 (숫자). 타입 체크 필수.


파싱 실패 시 복구 전략

def parse_with_fallback(response: str) -> dict | None:
    # 1차 시도: 직접 JSON 파싱
    try:
        return json.loads(response)
    except json.JSONDecodeError:
        pass

    # 2차 시도: 마크다운 제거 후 파싱
    try:
        cleaned = extract_json(response)
        return cleaned
    except Exception:
        pass

    # 3차 시도: AI에게 "JSON만 다시 줘" 재요청
    try:
        retry_response = ask_ai_to_reformat(response)
        return json.loads(retry_response)
    except Exception:
        pass

    # 실패 처리: 로그 남기고 사람에게 알림
    log_parse_failure(response)
    return None

출력 파싱의 미래

Anthropic, OpenAI, Google 모두 Structured Outputs를 강화하는 방향으로 가고 있다. 점점 더 파싱 실패 확률이 줄어들고 있다.

하지만 완전히 없어지지는 않을 것이다. 복잡한 출력, 긴 문서, 예상치 못한 케이스는 항상 생긴다.

출력 파싱 하네스는 AI 시스템이 프로덕션에서 안정적으로 동작하는지를 결정하는 핵심 레이어다.