정규식 vs LLM: 구조화된 텍스트 파싱
구조화된 텍스트(퀴즈, 폼, 인보이스, 문서)를 파싱하기 위한 실용적인 의사 결정 프레임워크입니다. 핵심 통찰: 정규식이 95-98%의 케이스를 저렴하고 결정론적으로 처리합니다. 값비싼 LLM 호출은 나머지 예외 케이스에만 예약하세요.
활성화 시점
- 반복되는 패턴이 있는 구조화된 텍스트 파싱 (질문, 폼, 테이블)
- 텍스트 추출을 위해 정규식과 LLM 중에서 결정해야 할 때
- 두 접근법을 결합한 하이브리드 파이프라인을 구축할 때
- 텍스트 처리에서 비용/정확도 균형을 최적화할 때
의사 결정 프레임워크
텍스트 형식이 일관되고 반복적인가?
├── 예 (>90%가 패턴을 따름) → 정규식으로 시작
│ ├── 정규식이 95%+ 처리 → 완료, LLM 불필요
│ └── 정규식이 <95% 처리 → 예외 케이스에만 LLM 추가
└── 아니오 (자유 형식, 가변성이 높음) → LLM 직접 사용
아키텍처 패턴
원본 텍스트
│
▼
[정규식 파서] ─── 구조 추출 (95-98% 정확도)
│
▼
[텍스트 클리너] ─── 노이즈 제거 (마커, 페이지 번호, 인공물)
│
▼
[신뢰도 점수기] ─── 낮은 신뢰도의 추출 플래그 지정
│
├── 높은 신뢰도 (≥0.95) → 직접 출력
│
└── 낮은 신뢰도 (<0.95) → [LLM 검증기] → 출력
구현
1. 정규식 파서 (대부분 처리)
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class ParsedItem:
id: str
text: str
choices: tuple[str, ...]
answer: str
confidence: float = 1.0
def parse_structured_text(content: str) -> list[ParsedItem]:
"""정규식 패턴을 사용하여 구조화된 텍스트를 파싱합니다."""
pattern = re.compile(
r"(?P<id>\d+)\.\s*(?P<text>.+?)\n"
r"(?P<choices>(?:[A-D]\..+?\n)+)"
r"Answer:\s*(?P<answer>[A-D])",
re.MULTILINE | re.DOTALL,
)
items = []
for match in pattern.finditer(content):
choices = tuple(
c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", match.group("choices"))
)
items.append(ParsedItem(
id=match.group("id"),
text=match.group("text").strip(),
choices=choices,
answer=match.group("answer"),
))
return items
2. 신뢰도 점수 매기기
LLM 검토가 필요할 수 있는 항목 플래그 지정:
@dataclass(frozen=True)
class ConfidenceFlag:
item_id: str
score: float
reasons: tuple[str, ...]
def score_confidence(item: ParsedItem) -> ConfidenceFlag:
"""추출 신뢰도를 점수화하고 문제를 플래그 지정합니다."""
reasons = []
score = 1.0
if len(item.choices) < 3:
reasons.append("few_choices")
score -= 0.3
if not item.answer:
reasons.append("missing_answer")
score -= 0.5
if len(item.text) < 10:
reasons.append("short_text")
score -= 0.2
return ConfidenceFlag(
item_id=item.id,
score=max(0.0, score),
reasons=tuple(reasons),
)
def identify_low_confidence(
items: list[ParsedItem],
threshold: float = 0.95,
) -> list[ConfidenceFlag]:
"""신뢰도 임계값보다 낮은 항목을 반환합니다."""
flags = [score_confidence(item) for item in items]
return [f for f in flags if f.score < threshold]
3. LLM 검증기 (예외 케이스만)
def validate_with_llm(
item: ParsedItem,
original_text: str,
client,
) -> ParsedItem:
"""LLM을 사용하여 신뢰도가 낮은 추출을 수정합니다."""
response = client.messages.create(
model="claude-haiku-4-5-20251001", # 검증을 위한 가장 저렴한 모델
max_tokens=500,
messages=[{
"role": "user",
"content": (
f"다음 텍스트에서 질문, 선택지, 답을 추출하세요.\n\n"
f"텍스트: {original_text}\n\n"
f"현재 추출: {item}\n\n"
f"필요시 수정된 JSON을 반환하거나, 정확하면 'CORRECT'를 반환하세요."
),
}],
)
# LLM 응답을 파싱하고 수정된 항목 반환...
return corrected_item
4. 하이브리드 파이프라인
def process_document(
content: str,
*,
llm_client=None,
confidence_threshold: float = 0.95,
) -> list[ParsedItem]:
"""전체 파이프라인: 정규식 -> 신뢰도 검사 -> 예외 케이스에 대한 LLM."""
# 1단계: 정규식 추출 (95-98% 처리)
items = parse_structured_text(content)
# 2단계: 신뢰도 점수 매기기
low_confidence = identify_low_confidence(items, confidence_threshold)
if not low_confidence or llm_client is None:
return items
# 3단계: LLM 검증 (플래그 지정된 항목만)
low_conf_ids = {f.item_id for f in low_confidence}
result = []
for item in items:
if item.id in low_conf_ids:
result.append(validate_with_llm(item, content, llm_client))
else:
result.append(item)
return result
실제 측정치
프로덕션 퀴즈 파싱 파이프라인 (410개 항목):
| 측정치 | 값 |
|---|---|
| 정규식 성공률 | 98.0% |
| 낮은 신뢰도 항목 | 8개 (2.0%) |
| 필요한 LLM 호출 | ~5회 |
| 전체 LLM 대비 비용 절감 | ~95% |
| 테스트 커버리지 | 93% |
모범 사례
- 정규식으로 시작하세요 — 완벽하지 않은 정규식이라도 개선할 기준선을 제공합니다
- 신뢰도 점수 매기기를 사용하여 LLM 도움이 필요한 부분을 프로그래밍 방식으로 식별하세요
- 가장 저렴한 LLM을 검증에 사용하세요 (Haiku-급 모델이면 충분합니다)
- 파싱된 항목을 절대 변경하지 마세요 — 정리/검증 단계에서 새 인스턴스를 반환하세요
- 파서에는 TDD가 잘 맞습니다 — 먼저 알려진 패턴에 대한 테스트를 작성하고, 이후 예외 케이스를 처리하세요
- 측정치를 기록하세요 (정규식 성공률, LLM 호출 횟수) — 파이프라인 상태를 추적합니다
피해야 할 안티패턴
- 정규식이 95%+ 케이스를 처리할 수 있는데도 모든 텍스트를 LLM에 보내는 것 (비용이 많이 들고 느림)
- 자유 형식, 가변성이 높은 텍스트에 정규식을 사용하는 것 (LLM이 더 적합)
- 신뢰도 점수 매기기를 건너뛰고 정규식이 "그냥 잘 작동할 것"이라고 기대하는 것
- 정리/검증 단계에서 파싱된 객체를 변경하는 것
- 예외 케이스를 테스트하지 않는 것 (잘못된 입력, 누락된 필드, 인코딩 문제)
사용 시기
- 퀴즈/시험 문제 파싱
- 폼 데이터 추출
- 인보이스/영수증 처리
- 문서 구조 파싱 (헤더, 섹션, 테이블)
- 비용이 중요한 반복 패턴이 있는 모든 구조화된 텍스트