왜 후처리가 필요한가

LLM에게 "코드만 출력해"라고 지시해도 실제 응답에는 설명 문장, 마크다운 펜스, 언어 태그, 앞뒤 공백이 섞여 들어온다. 이 원문을 그대로 exec()나 컴파일러, DB 마이그레이션에 넘기면 구문 오류가 난다. 프롬프트만으로 형식을 100% 강제하기 어렵기 때문에, 파이프라인에는 결정론적(deterministic) 후처리 단계가 반드시 있어야 한다. 모델을 바꾸거나 온도를 올리는 것보다 파서를 견고하게 만드는 편이 훨씬 안정적이다.

흔히 깨지는 패턴

실무에서 반복적으로 마주치는 오염 유형은 다음과 같다.

  • 펜스 언어 태그: ```python처럼 첫 줄에 언어명이 붙는다.
  • 설명 혼입: 코드 블록 앞뒤에 "다음은 예시입니다" 같은 문장이 붙는다.
  • 다중 블록: 한 응답에 여러 개의 ``` 블록이 나뉘어 온다.
  • 펜스 누락: 짧은 응답에서 펜스 없이 코드만 뱉는다.
  • 스마트 따옴표: " 대신 “ ”가 들어와 파싱이 깨진다.

정규식 기반 추출

가장 먼저 시도할 것은 마크다운 펜스 추출이다. 언어 태그를 캡처 그룹으로 분리하고, 블록이 없으면 원문을 그대로 반환하는 폴백을 둔다.

import re

FENCE = re.compile(r"```([a-zA-Z0-9_+-]*)\n(.*?)```", re.DOTALL)

def extract_code_blocks(text: str):
    blocks = [(lang or "text", body.strip())
              for lang, body in FENCE.findall(text)]
    # 펜스가 전혀 없으면 원문 전체를 하나의 블록으로 취급
    if not blocks:
        return [("text", text.strip())]
    return blocks

def pick_largest(text: str) -> str:
    # 설명 조각을 배제하려면 가장 긴 블록을 선택
    return max((b for _, b in extract_code_blocks(text)),
              key=len, default="")

정제와 정규화

추출 후에는 실행 전에 문자 수준 정규화를 거친다. 스마트 따옴표, 비가시 공백(zero-width space, NBSP), 남은 펜스 잔여물을 제거한다.

import unicodedata

REPL = {"\u201c": '"', "\u201d": '"',
        "\u2018": "'", "\u2019": "'",
        "\u00a0": " ", "\u200b": ""}

def sanitize(code: str) -> str:
    code = unicodedata.normalize("NFKC", code)
    for bad, good in REPL.items():
        code = code.replace(bad, good)
    # 실수로 남은 펜스 라인 제거
    lines = [ln for ln in code.splitlines()
             if not ln.strip().startswith("```")]
    return "\n".join(lines).strip() + "\n"

추출 전략 비교

전략장점주의점
정규식 펜스 파싱빠르고 의존성 없음중첩 펜스에 취약
마크다운 AST 파서중첩·인라인 정확라이브러리 의존, 오버헤드
구조화 출력(tool/JSON)파싱 불필요, 필드 분리모델·API 지원 필요

검증 게이트를 붙여라

정제된 코드가 문법적으로 유효한지 실행 전에 확인한다. Python이면 ast.parse, SQL이면 파서, JSON이면 json.loads로 게이트를 건다. 검증에 실패하면 재시도하되, 같은 프롬프트를 반복하지 말고 실패한 코드와 에러 메시지를 함께 넣어 자기 수정(self-correction)을 유도한다.

import ast

def is_valid_python(code: str) -> bool:
    try:
        ast.parse(code)
        return True
    except SyntaxError:
        return False

운영 시 주의점

후처리를 강하게 걸수록 실패 원인이 파서 뒤로 숨는다. 원문 응답을 로그에 남겨 파싱 실패율을 추적하고, "가장 긴 블록 선택" 같은 휴리스틱이 정답을 버리는 경우가 없는지 표본 검토하라. 가능하면 프롬프트로 형식을 유도하되 최종 신뢰는 후처리와 검증 게이트에 두는 이중 방어가 안전하다. 근본적으로는 구조화 출력(JSON 스키마, tool 호출)으로 전환해 파싱 자체를 없애는 것이 가장 견고한 해법이며, 정규식 추출은 그 전환이 어려운 환경의 실용적 대안으로 두는 편이 좋다.