RAG · 검색증강생성수정 2026-08-07

RAG 품질 Q&A — 파싱 · 관련성 검증 · 자기교정 25문답

사내 문서 RAG 정확도가 낮을 때 어디부터 보는지, 표와 그림을 살리는 방법, 그리고 검증 루프로 할루시네이션을 구조로 막는 설계를 정리한다.

"사내 문서 RAG 정확도가 낮다"는 문제는 대부분 모델이 아니라 그 앞에서 시작된다. 파싱 단계에서 사라진 정보는 뒤에서 복구되지 않고, 검색이 빗나가면 프롬프트를 아무리 다듬어도 그럴듯한 오답이 나온다.

이 글은 품질이 무너지는 두 지점 — 입력 품질검증 부재 — 에 대한 질문을 모았다. 파싱 산출물을 어떻게 진단하는지, 표와 그림을 어떻게 살리는지, 그리고 판정을 파이프라인에 넣어 할루시네이션을 구조로 막는 방법이다. 전체 맥락은 문서 파싱 시리즈와 Agentic RAG 시리즈에 있다.

Q. 사내 문서 RAG 정확도가 낮은데 원인을 어디서 찾나

검색기나 모델보다 파싱 산출물을 먼저 본다. 순서를 지키는 것이 전부다.

도식을 탭하면 확대해서 볼 수 있습니다

파싱된 마크다운을 열어 표가 구조로 남았는지, 차트가 텍스트로 번역됐는지, 머리말·꼬리말이 제거됐는지 확인한다. 여기서 정보가 사라졌으면 뒤 단계로는 복구가 안 된다. 그다음이 청킹, 그다음이 검색, 모델은 마지막이다.

대부분의 팀이 4번부터 시작해서 시간을 쓴다. 1번에서 표가 깨져 있으면 2~4번은 볼 필요가 없다.

Q. 표가 많은 문서는 어떻게 처리하나

두 벌로 저장한다. 하나는 마크다운 원본, 하나는 서술형 해설이다.

저장물형태용도
표 마크다운기계적 변환, 설명 금지답변 생성용 — 원본 수치 전달
서술형 해설제목·요약·엔티티·가상질문검색용 — 임베딩 대상

이 분리가 필요한 이유는 명확하다. 표 마크다운만 임베딩하면 숫자·기호가 많아 자연어 질의와 유사도가 낮아 검색이 안 되고, 서술문만 저장하면 답변할 때 정확한 수치가 없다.

질의는 서술문에 걸리고, 답변 생성은 마크다운의 원본 수치를 쓴다. 재현율을 위해 저장 중복을 감수한 의도적 설계다. 구현은 레이아웃 파서와 Document Graph Parser에 있다.

Q. 그림과 차트는 버려도 되나

보고서는 핵심 수치가 차트에 있는 경우가 많다. 실제 분포를 보면 답이 나온다.

category개수비중
paragraph32269%
header + footer6213% — 버릴 것
table + chart + figure429% — 정량 정보 대부분이 여기
heading1388%

21페이지 산업 리포트를 파싱한 결과다. 표·차트·그림은 9%뿐이지만 정량 정보의 대부분이 거기 있다. VLM으로 해설을 생성해 텍스트로 만들면 검색 대상이 된다.

반대로 header·footer 13%는 의도적으로 버린다. 남기면 청크의 13%가 "회사명·페이지번호" 노이즈가 된다. 카테고리별 처리 방침은 파싱 노드 파이프라인에 정리했다.

Q. OCR은 항상 켜야 하나

아니다. 텍스트 레이어가 있는 PDF에 OCR을 켜면 오히려 오탈자가 늘고 비용도 오른다.

문서 유형OCR이유
텍스트 레이어가 있는 PDF이미 정확한 텍스트가 있는데 이미지 인식으로 덮어쓸 이유가 없다
스캔본·이미지 PDF텍스트 레이어가 없어 다른 방법이 없다

기본값을 ocr: False로 두고 문서 유형에 따라 분기하는 것이 맞다. "정확도가 오르니까 켠다"는 직관이 여기서는 반대로 작동한다.

Q. 파싱 비용은 어떻게 통제하나

파싱은 1회성 비용, 검색은 상시 비용이라는 구분에서 출발한다.

비용 항목통제 수단
파싱 API앞 N페이지만 시험 파싱하는 옵션
VLM 호출배치 처리(10개 단위), figure/chart/table에만 호출
재실행파싱 결과를 pickle/JSON으로 캐시 → 청킹·임베딩 실험 때 재파싱하지 않음
모델 선택해설 생성은 소형 멀티모달 모델로 충분

세 번째가 실험 비용을 한 자릿수로 떨어뜨린다. 파싱은 페이지당 수 센트에 수 분이 걸리므로, 청킹 전략을 바꿔 볼 때마다 재파싱하면 비용이 배로 든다.

파싱에 페이지당 몇 센트를 더 쓰더라도 검색 정확도가 오르면 전체적으로 이득이다. 반대로 문서가 자주 바뀐다면 변경 감지 후 변경분만 재파싱하는 구조가 필요하다.

Q. 커스텀 로더는 언제 만드나

표준 로더가 없는 포맷이거나, 파싱 결과에 도메인 후처리가 필요할 때다.

class CustomDocumentLoader(BaseLoader):
    def __init__(self, file_path: str) -> None:
        self.file_path = file_path

    def lazy_load(self) -> Iterator[Document]:  # <-- 인자를 받지 않는다
        ...

규약이 하나 있다. 모든 설정은 __init__으로 받는다. lazy_load에 매개변수를 넣지 않는다.

이 규약 덕분에 로더는 "설정이 끝난 객체"가 되어 파이프라인 어디에 꽂아도 추가 인자 없이 동작한다. 어기면 로더를 호출하는 쪽이 매번 그 로더의 사정을 알아야 한다. HWP 같은 비표준 포맷의 전체 구현은 커스텀 Document Loader에 있다.

Q. 대용량 문서는 어떻게 처리하나

배치 분할이 기본이고, 파일명에 페이지 범위를 인코딩한다.

report_0000_0009.pdf
report_0010_0019.pdf
얻는 것내용
부분 실패 격리100p 중 1p가 실패해도 해당 배치만 재시도
병렬 처리배치별 동시 호출 가능
페이지 번호 복원파일명만 파싱하면 전역 페이지 번호가 나온다
API 한도 회피한 번에 보낼 수 있는 페이지 수 제한 대응

세 번째가 핵심이다. API는 보낸 파일 기준으로 페이지를 매기므로 4049페이지를 잘라 보내면 응답은 110페이지로 온다. 파일명 끝 9글자에서 시작 페이지를 읽어 오프셋을 더하면 별도 매핑 테이블 없이 복원된다.

Q. 가상질문(hypothetical questions)은 왜 넣나

질의-문서 유사도보다 질의-질의 유사도가 높기 때문이다.

"이 표를 보고 사용자가 물어볼 법한 질문"을 미리 생성해 저장하면, 실제 사용자 질문과 직접 매칭되어 재현율이 올라간다.

비교유사도
사용자 질문 ↔ 표 마크다운낮음 — 숫자·기호가 많다
사용자 질문 ↔ 표 서술문중간
사용자 질문 ↔ 저장된 가상질문높음 — 같은 형태끼리 비교

프롬프트에서 <hypothetical_questions> 태그로 별도 항목을 만들어 뽑아낸다.

Q. 요약할 때 숫자가 사라지는 문제는 어떻게 막나

프롬프트에 명시적으로 요구하고, 원본을 함께 보관한다. 둘 다 필요하다.

5. Summary must include important entities, numerical values.
Be sure to include numerical values, proper nouns, terms, and terminologies.

표 전용 프롬프트에는 이 지시를 SYSTEM과 USER 양쪽에 중복해서 넣는다. 요약은 본질적으로 정보를 버리는 연산이고, 모델이 가장 먼저 버리는 것이 숫자이기 때문이다.

그리고 요약본만 저장하지 않는다. 원본 마크다운을 metadata에 함께 보관해서 답변 생성 시 원본 수치를 참조하게 한다.

Q. 파싱 품질을 어떻게 측정하나

파싱 단계와 검색 단계의 지표가 다르다.

단계지표
파싱① header/footer 제거율 ② 표 element 대비 마크다운 변환 성공률 ③ 그림 대비 해설 생성률
검색정답 청크가 top-k에 들어오는 재현율을 먼저, 그다음 정밀도

순서가 중요하다. 재현율이 낮은 상태에서 정밀도를 개선해 봐야 애초에 후보에 없는 정답은 올라오지 않는다.

배포 전 체크리스트는 이렇게 잡는다.

점검 항목실패 시 증상
header/footer/footnote가 제거되었는가청크에 회사명·페이지번호 반복
표가 마크다운 구조로 남아 있는가숫자만 나열된 텍스트
그림·차트에 해설 텍스트가 붙었는가"자료에 없음" 응답 다발
페이지 번호가 전역 기준으로 복원됐는가출처 인용이 엉뚱한 페이지
표가 청크 경계에서 잘리지 않았는가표 절반만 검색됨

Q. 파싱 파이프라인을 왜 그래프로 만드나

상태 가시성·병렬성·재개 세 가지 때문이다.

이점절차형 스크립트에서는
상태 가시성어느 단계에서 무엇이 만들어졌는지 로그로만 추정
병렬 실행이미지 크롭·표 처리가 독립인데 순차로 묶임
중단·재개100페이지 중 70페이지에서 실패하면 처음부터 다시
조건 분기배치 루프를 명령형으로 짜야 함

세 번째가 비용에 직결된다. 파싱이 20초에 $0.21이면 재실행마다 그만큼을 다시 낸다. 체크포인터가 있으면 실패 지점부터 재시작한다.

Q. 프롬프트를 YAML로 빼는 이유는

프롬프트는 코드가 아니라 설정이기 때문이다.

코드에 f-string으로 박으면YAML로 빼면
문구 하나 바꾸는 데 배포 필요파일 편집만으로 변경
변경 이력이 코드 diff에 묻힘프롬프트 단위 diff·리뷰·롤백
SYSTEM/USER 구분이 흐려짐역할별 파일 분리
언어·변수 하드코딩{language}·{context} 변수화

버전을 파일명에 붙이고 이전 버전을 지우지 않는 것도 중요하다. 프롬프트 변경은 릴리스이고, 모델을 안 바꿔도 프롬프트 한 줄로 시스템 동작이 바뀌므로 형상관리가 없으면 장애 원인을 재현할 수 없다. 프롬프트 4종의 설계 의도는 멀티모달 프롬프트 설계에, v1→v4 개정 기록은 Agentic RAG와 관련성 검증에 있다.

Q. 할루시네이션을 어떻게 줄이나

프롬프트로 억제하는 데는 한계가 있어 파이프라인에 판정 지점을 넣는다.

도식을 탭하면 확대해서 볼 수 있습니다

검색 결과를 그대로 믿지 않고 별도 저비용 모델로 질문↔문서 관련성을 yes/no로 판정한다. no면 쿼리를 다시 만들어 재검색한다. 여기에 검색 문서를 통째로 넣지 않고 근거 문장만 골라 넣으면 무관한 문맥에서 억지로 연결하는 경우가 줄어든다.

핵심은 환각을 생성 단계가 아니라 검색·검증 단계에서 막는 것이다.

Q. Naive RAG와 Agentic RAG는 무엇이 다른가

경로가 고정이냐, 매 턴 결정되느냐다.

구분Chain RAGAgentic RAG
실행 경로고정 (검색 → 생성)LLM이 매 턴 결정
검색 횟수항상 1회0회~N회
새 검증 추가파이프라인 재설계도구 하나 추가
비용·지연예측 가능예측 어려움 (상한 필요)
실패 모드조용한 오답루프 폭주 / 조기 종료

Naive RAG는 검색이 빗나가도 그대로 답을 만든다. Agentic RAG는 판정 결과에 따라 재검색으로 되돌아가는 분기가 생긴다. 대신 비용과 지연이 예측 불가해지므로 반복 상한과 폴백을 반드시 같이 설계한다.

Q. 검증 루프를 넣으면 비용이 많이 들지 않나

성공 케이스에서는 판정 1회분만 추가된다.

상황추가 비용
검색이 성공 (평상시)저비용 모델 판정 1회
검색이 실패재검색 × 반복 횟수

비용 구조가 **"평상시 소액 고정비 + 실패 시 변동비"**다. 실제로 정상 흐름은 검색·압축·판정 3회 호출로 끝나고, 판정이 계속 실패하는 경우에만 반복이 누적된다. 이것이 검증 루프가 실무에서 성립하는 이유다.

판정은 저비용 모델에 temperature 0으로 돌리므로 부담이 더 작아진다.

Q. 문맥 압축기가 요약을 잘못하면 더 위험하지 않나

그래서 압축기에 생성 권한을 주지 않는다.

검색 결과를 줄 단위로 쪼개 번호를 붙이고, LLM은 관련 있는 줄의 번호만 반환하게 한다.

indexed_contexts = [f"[{i+1}] {contexts[i]}" for i in range(len(contexts))]
# LLM 출력: '3,4,5,9,10,15,17,19,21,23'
selected = [contexts[int(i) - 1] for i in answer.split(",")]
항목원문 재생성인덱스 반환
원문 충실도요약·의역이 섞일 수 있음한 글자도 변형되지 않음
할루시네이션압축 단계에서 새 환각 유입구조적으로 불가능
출력 토큰선택 문장 전체20자 내외
실패 모드조용한 왜곡잘못된 인덱스 → 즉시 티가 남

판정·선별 컴포넌트에는 생성 권한을 주지 않는다는 것이 일반 원칙이다. 토큰 효과와 한계는 문맥 추출과 자기교정 루프에서 수치로 다뤘다.

Q. 관련성 검증은 무엇과 무엇을 비교하나

축이 세 개이고 각각 잡아내는 실패가 다르다.

평가 쌍잡아내는 실패배치
A질문 ↔ 검색문서검색 실패 — 엉뚱한 문서온라인 (재검색 트리거)
B답변 ↔ 검색문서근거 없는 확장 (groundedness 붕괴)오프라인 샘플링
C답변 ↔ 질문동문서답오프라인 샘플링

세 축을 다 실시간으로 돌리면 판정 호출이 3배가 된다. 보통 A축만 요청 경로에 넣고 나머지 둘은 샘플링해 품질 지표로 쓴다. 그래야 지연 예산을 A축만 계산하면 된다.

Q. 압축을 먼저 하나, 검증을 먼저 하나

압축 후 검증이 유리하다. 판정기가 정제된 문맥을 보게 되기 때문이다.

순서판정 입력판정 정확도
검증만청크 전량 (예: 61줄)노이즈에 흔들림
압축 → 검증정제 문맥 (예: 10줄)근거 밀도가 높아 상승

다만 새 위험이 생긴다. 압축에서 근거가 잘리면 잘못된 no 판정이 나온다. 압축기가 관련 문장을 놓치면 판정기는 "관련 없다"고 답할 수밖에 없다.

두 단계를 한 번의 LLM 호출로 합치는 확장도 가능하지만 권하지 않는다. 왕복 1회를 줄이는 대신 어느 단계가 틀렸는지 분리할 수 없게 되고, 압축과 판정이 같은 추론 안에서 서로 오염된다. 관측 가능성을 파는 거래는 손해다.

Q. 에이전트가 지시한 단계를 건너뛰면 어떻게 진단하나

프롬프트 / 도구 / 실행기 세 층으로 나눠 본다.

도식을 탭하면 확대해서 볼 수 있습니다
트레이스 상태문제 층대응
판정 도구 호출 기록이 없다프롬프트핵심 규칙을 앞뒤에 이중 배치
호출은 됐는데 결과를 안 따른다도구description 개선, 반환 스키마를 구조화 객체로 강제
정상 동작인데 끊긴다실행기반복 상한 확인, 폴백 설계

이 진단은 답변 텍스트가 아니라 tool_calls 필드를 봐야만 가능하다. 관찰 지점이 응답 본문이 아니라 메시지 메타데이터에 있다.

Q. 반복 상한에 걸리면 어떻게 처리하나

상한 도달을 정상 경로로 취급해 폴백 답변을 낸다.

기본 동작은 답변 없이 종료 메시지만 나오는 것이다. 서비스에서는 "문서에서 근거를 찾지 못했습니다"를 명확히 응답해야 한다.

그리고 상한 계산에 함정이 있다.

오해사실
max_iterations는 재시도 횟수도구 호출 횟수
20이면 20번 재검색검색+판정이 1회차이므로 10번

검증 도구를 늘리면 실질 재시도 횟수가 그만큼 줄어든다. 도구 3개를 쓰면 20 상한에서 6~7회밖에 못 돈다. 재검색 상한과 전체 상한을 별도로 관리하는 편이 안전하다.

Q. 검증 루프 로직은 어떻게 테스트하나

판정 결과를 항상 no로 고정하는 스텁 도구를 만든다.

@tool
def check_relevance_fake(input: str, context: list[str]) -> bool:
    """This is a tool for checking relevance between the user's query and retrieved context."""
    if len(input) % 2 == 0:
        return "no"
    else:
        return "no"

두 분기 모두 "no"를 반환한다. 관련성이 절대 통과되지 않는 상황을 만들어 루프가 실제로 도는지, 언제 멈추는지를 관찰하려는 것이다.

이 스텁으로 돌려 보면 실제 문제가 드러난다. 재작성된 쿼리 10개가 어순과 수식어만 바뀐 동의어 나열이었고, 상한에 도달하자 답변 없이 종료됐다.

루프 로직은 정상 케이스에서는 절대 드러나지 않는다. 검증 루프를 설계할 때 스텁을 함께 만들어 두는 것이 이식할 만한 습관이다. 실제 트레이스 관측 결과는 문맥 추출과 자기교정 루프에 있다.

Q. 재검색 쿼리가 계속 비슷하게 나오면 어떻게 하나

LLM은 "다른 쿼리"를 요구받으면 어휘만 바꾼다. 전략을 명시해야 한다.

개선안방식
이전 쿼리 이력 전달시도한 쿼리 목록을 프롬프트에 누적 제공
재작성 전략 명시"① 상위어로 넓히기 ② 하위어로 좁히기 ③ 동의어 치환 ④ 문서 어휘로 바꾸기" 순서 지정
검색 파라미터 변주k 확대, 다른 인덱스, 키워드 검색 병행
시도 상한 분리재검색 상한과 전체 상한을 별도 관리

같은 어휘의 변형은 같은 문서를 계속 검색하므로 재시도가 의미를 갖지 못한다.

Q. 문서 파서는 무엇을 기준으로 고르나

기능 비교가 아니라 네 가지 요건으로 고른다.

기준확인할 것
표 구조 보존colspan/rowspan이 살아 있는 HTML을 주는가
좌표 제공원본 위치 추적·시각화·재크롭이 가능한가
언어 지원한국어 문서의 인식 품질
페이지 단가문서량 × 재파싱 빈도로 총비용 계산

선택지는 상용 레이아웃 분석 API, 파싱 지시문을 지원하는 상용 파서, 무료지만 구조가 없는 라이브러리, 클라우드 사업자의 문서 인식 서비스 등이 있다. 표가 답변 품질에 중요하면 속도를 포기하고 구조 보존 쪽을 택한다.

Q. 해설 생성에 왜 소형 모델을 쓰나

추론 난이도가 낮고 호출 수가 많은 작업이기 때문이다.

작업 특성모델 선택
이미지·표를 보고 설명 생성난이도 낮음, 호출 수 많음 → 소형
최종 답변 생성난이도 높음, 호출 수 적음 → 대형

표·그림 1개당 호출 1회이므로 문서 하나에 수십 회가 나간다. 여기서 대형 모델을 쓰면 비용이 배로 든다.

표 수치 판독 정확도가 문제되면 표에만 상위 모델을 쓰는 하이브리드가 가능하다. 전부 올리거나 전부 내리는 선택만 있는 것이 아니다.

Q. LLM 제품 품질을 무엇으로 측정하나

세 계층으로 나눈다.

계층지표나쁠 때 손대는 곳
검색Recall@k청크 크기, 임베딩 모델, k, 하이브리드
검색Precision@k리랭커, 필터링, score_threshold
검색관련성 통과율검색 품질 전반
생성Groundedness프롬프트 제약, 문맥 압축, 인용 강제
생성Answer Relevance질문 재해석, 프롬프트
운영재검색률높으면 검색 품질 신호
운영상한 도달률높으면 폴백 설계 신호
운영요청당 토큰문맥 압축, 캐싱
운영p95 지연도구 호출 수, 병렬화, 스트리밍

운영 계층 두 지표를 신호로 읽는 것이 특히 유용하다. 전제는 트레이스 수집이다.

용어 정리

용어
layout parsing페이지를 픽셀이 아니라 의미 단위 블록(제목·문단·표·그림)으로 분해하는 것
element레이아웃 파싱의 최소 단위. category + content + coordinates + page + id
VLMVision Language Model. 이미지를 이해하고 텍스트로 설명하는 모델
hypothetical questions"이 표를 보고 물어볼 법한 질문". 질문-질문 유사도로 적중률을 올린다
Naive RAG검색 → 생성으로 끝나는, 검증 단계가 없는 RAG
Agentic RAG검색·검증·압축을 도구로 만들고 LLM이 매 턴 경로를 정하는 RAG
Relevance Check검색 결과·답변이 질문에 실제로 맞는지 별도 모델이 판정하는 단계
Groundedness답변이 얼마나 문서 근거에 묶여 있는지의 정도
Context Extractor검색 문서 전체가 아니라 근거 문장만 골라내는 도구
max_iterations실행기가 허용하는 최대 도구 호출 횟수. 왕복 횟수가 아니다