왜 '싸게 시작하고, 필요할 때만 비싸게'가 정답인가

PDF 한 페이지를 PyMuPDF로 파싱하면 5밀리초, 비용 0원입니다. 같은 페이지를 비전 LLM에 던지면 10초, 1만 배 비용이 나가요. 문제는 이겁니다.

  • 질문이 산문(prose)에 있으면 → 싼 파서가 이깁니다.
  • 답이 표, 그림, 스캔 영역, 평탄화된 레이아웃 안에 숨어 있으면 → 싼 파서는 조용히 쓸모없는 결과를 리턴하고, LLM은 그 위에서 자신 있게 틀린 답을 냅니다.

EasyOCR 같은 클래식 OCR이 대표적이에요. 단어는 복원하는데 주변의 표 구조를 조용히 날려버립니다. 답변만 보면 멀쩡해 보이고, 원본 페이지랑 대조하기 전까지는 아무도 눈치채지 못하죠.

그래서 필요한 게 피드백 루프입니다. 가장 싼 파서로 시작 → 파이프라인의 각 체크포인트에서 결과를 평가 → 최소 하나라도 '이 질문엔 부족하다'고 플래그하면 그때 무거운 파서로 에스컬레이션. 이 글은 그중에서도 가장 비싸고 가장 불안정한 체크, 즉 LLM 자기평가(self-eval)를 어떻게 다뤄야 하는지에 대한 실전 기록입니다.

이 글은 엔터프라이즈 RAG 시스템을 4개 브릭(문서 파싱 / 질문 파싱 / 검색 / 생성)으로 쌓는 시리즈의 한 편입니다. 근거자료는 Towards Data Science 원문에서 확인하실 수 있어요.

Developer inspecting flat-parsed PDF table rows in a dataframe before RAG generation IT Technology Image

에스컬레이션 캐스케이드 — 8개 체크를 순서대로

파이프라인은 싼 체크가 먼저, 비싼 체크가 나중에 오도록 설계됩니다. 각 체크는 다음 체크보다 싸고, 더 신뢰할 수 있어야 해요.

# 체크 순서 (싼 것 → 비싼 것)
# 1. pre-parsing 메타데이터 라우팅 (거의 공짜)
# 2. parsing-time 신호: char density, flat-table fingerprint
# 3. chunk integrity
# 4. question parsing 단계의 intent-aware routing
# 5. retrieval score-gap
# 6. retrieval context-gap
# 7. LLM self-eval (context_structured 플래그) ← 이 글의 주제
# 8. post-generation groundedness (별도 LLM/NLI 판정)

# 실제 파이프라인에서 에스컬레이션을 트리거하는 부분
if not answer.context_structured:
    # PyMuPDF로 파싱된 페이지를 Azure DI로 재파싱
    page_df = parse_with_azure(
        pdf_path=pdf_path,
        page_num=answer.cited_page,
        method="azure_layout",  # parsing_method 컬럼에 기록됨
    )
    # 같은 질문으로 재생성
    answer = llm_answer_with_evidence(
        question=question,
        lines=page_df.text.tolist(),
    )

Case A: 평탄화된 표 → Azure DI로 에스컬레이션

Attention 논문(Transformer 원본) 9페이지 Table 3에서 base 행의 h 값을 묻는 질문을 던져봅시다. 정답은 8. PyMuPDF는 이 표를 13개 셀을 13개 라인으로 쪼개서 리턴합니다. LLM은 이걸 세로로 세면서 h 컬럼을 찾아야 하죠.

# 1차 생성 결과 (PyMuPDF 기반)
{
    "answer": "8",  # 값은 맞음
    "citation": "PyMuPDF L25 of page 9",
    "context_structured": False,  # ← 구조적 위험을 LLM이 스스로 플래그
}

값은 맞았지만 context_structured=False가 떴습니다. 오케스트레이터는 이 한 비트를 보고 Azure DI로 재파싱합니다. 비용은 약 $0.003, 4초. 그리고 같은 질문을 다시 던지면:

# 2차 생성 결과 (Azure DI 기반)
{
    "answer": "8",
    "citation": "Azure row: | base | 6 | 512 | 2048 | 8 | ... |",
    "context_structured": True,  # 구조를 신뢰
}

파이프의 마크다운 라인에서 h가 5번째 셀이라는 게 파이프(|)로 명시되니까, LLM이 컬럼을 셀 필요가 없어집니다. 같은 값, 하지만 이제 구조적으로 신뢰 가능해요.

Case B: 그림 → 비전 LLM으로 에스컬레이션

"Transformer의 아키텍처가 뭔가요?"라는 질문. 답은 3페이지 Figure 1(인코더-디코더 다이어그램)에 있습니다. PyMuPDF는 type='image' 행과 함께 placeholder만 리턴하죠. 1차 생성 결과:

{
    "answer": "주변 산문에 따르면... (불완전)",
    "context_structured": False,
    "caveat": "figure referenced but absent",  # 그림이 참조되었지만 파싱 안 됨
}

파이프라인은 image_df.image_id=1의 parsing_method='not_parsed'를 확인하고 비전 LLM으로 라우팅합니다. 결과는 6개 레이어, 멀티헤드 어텐션, residual connection, layer norm, cross-attention 브릿지까지 구조화된 설명으로 돌아오고, line_df에 parsing_method='vision_gpt4o'로 append됩니다. 논문 15페이지 중 이 한 페이지만 비전 모델을 봅니다.

Vision LLM extracting Transformer encoder-decoder diagram into structured text for RAG pipeline Dev Environment Setup

⚠️ 18회 스트레스 테스트가 밝힌 충격적인 사실

여기서부터가 이 글의 진짜 값어치입니다. 위의 두 케이스는 베스트 케이스예요. LLM self-eval이 잘 작동한 경우죠. 그런데 실제로 스트레스 테스트를 돌려보면 그림이 완전히 달라집니다.

실험 설계

  • 질문 3종: Q_easy(직접 라벨된 셀) / Q_inheritance(빈 셀, 컬럼 상속) / Q_far(표 마지막 행, 유사 숫자 다수)
  • 모델 3종: gpt-4o-mini, gpt-4o, gpt-4.1
  • 파서 2종: PyMuPDF, Azure DI
  • 총 18회 실행

결과 1: 틀린 답변 4개 중 4개가 cs=True

질문모델/파서답변context_structured
Q_inheritancemini/PyMuPDF16 (오답)True
Q_inheritance4o/PyMuPDF32 (오답)True
Q_inheritance4.1/PyMuPDF8 (정답)False
Q_farmini/PyMuPDF25.7 (오답, BLEU 컬럼)True

약한 모델이 컬럼을 잘못 정렬했을 때, 자기가 잘못 정렬했다는 걸 눈치채지 못합니다. 오히려 context_structured=True로 자신 있게 주장하죠. 유일하게 cs=False를 띄운 건 답도 맞춘 gpt-4.1이었습니다. 즉 LLM 신호가 발화한 유일한 순간이, 구조될 필요가 없었던 순간이었어요.

결과 2: 연속 스코어(0.0~1.0)도 도움 안 됨

바이너리 플래그 대신 context_structured_score를 0~1로 받아봤습니다.

  • 정답 평균: 0.919
  • 오답 평균: 0.920
  • 임계값 0.95 적용 시: 오답 3개 잡고, 정답 15개 중 9개를 오탐 (60% false alarm)

LLM의 구조 판정은 연속인 척하는 바이모달입니다. 스코어는 바이너리 플래그 대비 아무것도 사주지 않아요.

결과 3: rationale은 안정적, verdict는 불안정

같은 입력, 같은 temperature로 gpt-4.1을 3회 돌려봤습니다.

  • rationale(설명): 매번 일관됨 — "headers split across multiple lines", "each cell on its own line, not aligned"
  • verdict(플래그): 매번 흔들림

모델은 파싱이 왜 이상한지는 정확히 압니다. 그게 에스컬레이션할 만큼 심각한지는 안정적으로 판단하지 못해요. 그래서 실전 해법은 이렇습니다.

# rationale 텍스트에 결정적 규칙을 적용
ESCALATION_PATTERNS = [
    "split across multiple lines",
    "each cell on its own line",
    "blank cells",
    "not aligned",
]

def should_escalate(rationale: str) -> bool:
    return any(p in rationale.lower() for p in ESCALATION_PATTERNS)

바이너리나 스코어를 직접 읽는 대신, rationale 텍스트 위에 결정적 규칙을 얹는 것이 유일하게 안정적인 방법입니다.

결과 4: check 8 (별도 LLM groundedness)이 진짜 안전망

생성 후 별도 LLM(또는 NLI 모델)이 답변의 모든 주장이 인용 청크로 지지되는지 검증합니다. 판정자가 새롭기 때문에 생성 모델이 자기 답변을 방어할 유인이 없어요. check 7이 놓친 hallucination을 잡을 확률이 있는 유일한 체크입니다.

국내 SI/엔터프라이즈 환경에서의 주의점

국내 금융·공공 SI 환경에서 이 파이프라인을 돌릴 때 주의할 점이 있습니다. 감사(audit) 요건이 강한 문서(전자금융감독규정, 개인정보 처리방침, 규제 filing)는 lazy parsing 기본값을 예외로 두고 ingestion 시점에 full parse를 돌려야 합니다. 사후에 재현 가능한 기록이 남아야 하니까요. 반대로 사내 위키·기술 문서처럼 쿼리 수명이 짧고 반복 조회가 많은 문서는 lazy + 캐시가 압도적으로 유리합니다. 판단 기준은 하나예요 — "이 문서가 수명 동안 평균 몇 번 조회될 것인가?" 여러 번이면 eager, 몇 번이거나 모르면 lazy.

Enterprise RAG pipeline diagram showing PyMuPDF, Azure DI, and vision model escalation cascade Programming Illustration

정리 — 순서가 바뀌면 파이프라인이 무너진다

이 글의 핵심을 한 줄로 줄이면 이겁니다.

LLM self-eval은 마지막 방어선이지, 첫 번째 방어선이 아니다.

18회 스트레스 테스트가 이 순서를 협상 불가로 만듭니다. LLM의 바이너리 판정은 스스로의 컨텍스트 품질에 대해 안정적이지 않고, 별도의 fresh judge(check 8)만이 fabrication을 잡습니다.

실무 적용 3단계

  1. 싼 체크부터 깔아라. char density, flat-table fingerprint, chunk integrity — 이 세 개가 대부분의 실패를 공짜로 잡습니다.
  2. LLM self-eval은 rationale만 받아라. verdict는 결정적 규칙으로 직접 판정하세요. 바이너리·스코어를 그대로 믿지 마세요.
  3. check 8을 반드시 붙여라. 별도 LLM 또는 NLI 기반 groundedness 검증. 비용은 LLM 호출 한 번, 얻는 건 hallucination 방어.

데이터 모델이 곧 캐시

에스컬레이션이 일어날 때마다 line_df에 parsing_method='azure_layout' 또는 'vision_gpt4o' 행이 append됩니다. 다음 질문이 같은 페이지를 건드리면 이 행을 그대로 읽어요. 캐시는 별도 레이어가 아니라, retrieval이 이미 읽는 같은 테이블입니다. 가장 많이 조회된 문서의 가장 많이 조회된 영역이 자동으로 deep-parsed 버전을 갖게 되고, 나머지는 싼 파서로 남습니다. 시스템이 ingestion 시점의 추측이 아니라 실제 사용 패턴을 기반으로 deep parsing 투자 위치를 학습하는 거죠.

다음 단계 학습 방향

  • 캐스케이드 순서 최적화: 체크 1·2의 결정적 신호를 어떻게 튜닝하는지
  • check 8 구현: NLI 모델 vs. 별도 LLM 판정자의 비용·정확도 트레이드오프
  • cross-reference 처리: 문서 간 참조(Article 11 예고)가 같은 self-correction 프레임으로 어떻게 확장되는지

함께 보면 좋은 글

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.