상류에서 소음을 잘라내라 — DART report_nm blacklist 사례
RAG 품질 개선 얘기가 나오면 사람들이 제일 먼저 손을 대는 곳은 대개 이렇다.
1. 임베딩 모델을 바꿔볼까
2. Reranker를 붙여볼까
3. Retriever를 fine-tune 해볼까
세 개 다 유효하다. 그런데 이걸 다 시도하기 전에, 훨씬 싸고 훨씬 큰 효과를 내는 곳이 있다. 상류(source) 쪽에서 소음을 잘라내는 것이다.
이번 글은 그 관점 — 상류 노이즈 컷(source denoising) — 을 정리한다. 실제 프로젝트에서 DART 공시 데이터를 다루면서 만든 blacklist 규칙이 사례다.
왜 상류가 우선인가
RAG 파이프라인은 이렇게 흐른다.
[원본 문서] → [파싱] → [청킹] → [임베딩] → [저장]
↓
[질의] → [검색] → [Rerank] → [생성]
Retriever fine-tuning은 저 흐름의 오른쪽 절반을 튜닝한다. 하지만 원본 문서 자체에 노이즈가 섞여 있으면, 아래처럼 된다.
좋은 문서: 임베딩 잘 되고, 검색되고, 답에 잘 쓰임
노이즈 문서: 임베딩도 되고, 검색되고, 답에 노이즈로 섞임 ← 문제
Retriever가 하는 일은 "저장된 것 중에서" 잘 뽑는 것. 저장되기 전에 노이즈가 잘려 있으면 retriever 튜닝을 안 해도 품질이 올라간다. 반대로 상류에 노이즈가 남아 있으면 retriever를 아무리 튜닝해도 그 노이즈를 뽑아 온다.
순서로 보면 이렇다.
1) 상류 소음 컷 ← 이 편
2) chunking / metadata
3) generation 프롬프트 ← 다음 편
4) retriever fine-tune ← rag-retriever/04
1번은 대개 코드 20~50줄로 끝난다. 4번은 GPU와 pair 데이터가 필요하다. 가성비가 완전히 다르다.
사례: DART report_nm blacklist
프로젝트에서 한국 상장사 공시(DART)를 실적 분석 뉴스 판단의 근거로 쓴다. DART는 공시 유형이 매우 다양하다.
정기공시 (사업보고서, 반기, 분기)
주요사항보고 (합병, 유상증자, 배당 등)
발행공시 (증권신고서)
지분공시 (대량보유상황, 임원 매매)
기타공시 (자기주식 취득/처분 등)
자율공시 (IR 자료 등)
각 공시에는 report_nm이라는 필드가 있다. 예를 들어:
"[정기]사업보고서(2026.03)"
"[기재정정]주요사항보고서(자기주식취득)"
"[첨부추가]대량보유상황보고서"
문제는 이 중 상당수가 분석 목적으로는 소음이라는 점이다.
[첨부추가] → 원 문서에 첨부만 추가. 실질 정보 없음
[기재정정] → 오타 수정. 원 문서와 90% 이상 중복
[변경] → 사소한 지분 변동 신고
[유상증자] → 재무 흐름에 영향 있지만 뉴스 판단에는 노이즈가 큼
이 문서들이 검색에 걸리면 어떻게 되나?
질의: "삼성전자 3분기 실적은?"
top-10 검색 결과:
1위: 3분기 사업보고서 ← 진짜 답
2위: [기재정정] 3분기 사업보고서 ← 1위와 90% 중복
3위: [첨부추가] 3분기 사업보고서 ← 첨부만 다름
4위: 반기보고서 ← 관련 있음
5위: [기재정정] 반기보고서 ← 4위와 중복
...
이러면 실질적 top-10이 top-4로 쪼그라든다. 컨텍스트가 중복으로 채워지고, LLM은 같은 내용을 반복해서 보게 된다.
해결: report_nm 기반 blacklist
파이프라인의 상류에서 다음 패턴을 컷했다.
BLACKLIST_PATTERNS = [
r"^\[첨부추가\]",
r"^\[기재정정\]",
r"^\[변경\]",
r".*자기주식(취득|처분).*",
r".*임원.주요주주.*소유상황.*",
]
파싱 단계에서 이 패턴에 걸리는 공시는 인덱스에 아예 안 넣는다.
def should_ingest(report_nm: str) -> bool:
return not any(re.match(p, report_nm) for p in BLACKLIST_PATTERNS)50줄이 안 되는 로직. Retriever 튜닝이 필요 없다.
blacklist 이후 실측
같은 evaluation set에서 blacklist 도입 전후를 비교했다.
blacklist 前 blacklist 後
context_precision 0.61 0.78
faithfulness 0.919 0.937
평균 컨텍스트 문서 수 9.4개 6.1개
context_precision이 0.17p 뛰었다. 이건 top-k 안의 문서 중 실제로 답에 쓰이는 비율이다. 노이즈 문서를 애초에 안 뽑아왔기 때문이다.
faithfulness도 소폭 올랐다. 컨텍스트에 노이즈가 줄어드니까 LLM이 헷갈리지 않고 정답 근거만 골라서 인용한다.
그리고 부수 효과가 두 개 있다.
1) 저장 비용이 줄었다 (임베딩 개수 감소)
2) 검색 latency가 줄었다 (인덱스 크기 감소)
이 두 개는 계산상 예측되던 효과지만, 실제로 인덱스 크기가 22% 줄었고 검색 latency는 p50 기준 15% 빨라졌다.
상류 컷의 다른 축들
report_nm blacklist는 상류 소음 컷의 한 사례일 뿐이다. 같은 관점으로 다른 축도 있다.
1. 문서 파싱 품질
XBRL → 재무 데이터 구조화가 명확. 파싱 정확도 높음
HTML → 광고, 네비게이션 등 노이즈 섞임. 정제 필수
PDF → 표/이미지 손실. OCR 필요한 경우 노이즈 확
같은 사업보고서라도 XBRL로 파싱하면 노이즈가 적고, HTML/PDF로 하면 많다. 노이즈 컷은 어떤 포맷을 우선 파싱할지 결정하는 것부터 시작한다.
2. Metadata enrichment
문서에 date, source, category, ticker 같은 메타데이터를 박아둔다.
검색 시 필터로 쓴다.
메타데이터는 상류 컷의 "부드러운 버전"이다. blacklist가 "아예 안 넣는다" 라면, metadata filter는 "일단 넣되 필요 없으면 검색에서 제외한다". 둘을 같이 쓴다.
3. Chunking 경계
"이 청크가 어느 문서·어느 섹션에서 온 것인가"가 메타데이터로 붙어
있으면, 나중에 답 검증 단계에서 노이즈 청크를 사람 눈으로 골라내기 쉽다.
이건 rag-retriever/03 에서 다뤘던 청킹 얘기와 이어진다.
언제 blacklist를 다시 열어봐야 하나
Blacklist는 한 번 정하고 방치하면 안 된다. 다음 상황에서 다시 열어본다.
1) 판정 지표(faithfulness, context_precision)가 하락할 때
2) evaluation set에서 "정답 문서가 검색 안 됨" 케이스가 늘 때
3) 새로운 공시 유형이 추가될 때 (제도 변경, 신규 필드 도입)
4) 도메인이 확장될 때 (예: 미국 상장사 → 한국 상장사 추가)
특히 2번이 무섭다. blacklist가 실수로 정답 문서를 잘라내고 있을 수 있다. 정기적으로 "필터에 걸린 문서 중 잘못 걸린 게 있나"를 샘플로 사람이 본다.
다음 편
상류에서 소음을 잘라내면 컨텍스트 quality가 좋아진다. 하지만 좋은 컨텍스트를 줘도 LLM이 그 컨텍스트를 무시하고 답을 지어낼 수 있다.
다음 편에서는 생성 쪽에서 grounded를 강제하는 프롬프트 패턴을 다룬다. 인용 마커 강제, Chain-of-Verification, Self-consistency 같은 것들이다. 이 편의 faithfulness 0.937을 어디까지 올릴 수 있는지가 다음 편의 주제다.