SHIFTONE

검색은 24문항 다 맞췄는데 답은 틀려요 — RAG PDF 표 추출, '과장'과 '130,000원' 사이 여섯 줄

사내 규정 PDF 의 표를 텍스트로 뽑으면 '과장'과 '130,000원' 사이에 여섯 줄이 끼어요. 파싱 4종 × 청킹 2종을 24문항으로 재봤더니 검색은 다 맞추는데 근거 조각이 깨졌고, 표를 행마다 한 문장으로 풀자 청킹을 바꿔도 24/24 였어요. 색인은 55% 커졌고요.

사무실 책상 위에 노란 포스트잇 색인이 잔뜩 붙은 서류 뭉치가 여러 단으로 쌓여 있는 사진 — 색인에 넣기 전의 종이 규정 문서들

"과장 숙박비 한도가 얼마예요?" 사내 규정 챗봇에 물었더니 25,000원이라고 답해요. 규정 표에는 130,000원이에요. 근거 문서는 맞는 PDF, 맞는 페이지고요. 검색은 틀리지 않았어요. 표가 글자로 풀리는 순간 줄이 섞였을 뿐이에요.

청킹 글에서 "조각이 자기 이름을 잃는다"고 썼는데, 표는 한 단계 앞인 파싱에서 이미 잃고 있었어요.

PDF 에는 표가 없어요. 좌표에 놓인 글자만 있어요

pypdf 문서는 PDF 에 시맨틱 레이어가 없고, 최악의 경우 글자 하나하나가 절대 좌표로 놓여 어디가 열이고 행인지 알 수 없다고 말해요. 텍스트 추출기는 그린 순서대로 글자를 모아요. 행 순서로 그린 표와 열 순서로 그린 표는 화면에선 같지만 텍스트는 달라요. 열 순서로 그린 출장 경비 표는 이렇게 나와요.

직급
사원
대리
과장
차장
부장
임원
숙박비(1박)
100,000원
110,000원
130,000원

'과장'과 '130,000원' 사이에 여섯 줄이 끼어 있어요. 더 내려가면 '30,000원'이 일비 열과 식비 열에 한 번씩 나와요. 사람은 줄을 세어 풀 수 있지만, 그 세는 일을 모델에게 맡기는 거예요.

파싱 네 가지 × 청킹 두 가지를 24문항으로 재봤어요

가상의 사내 규정 4건을 PDF 로 만들었어요. 표는 5개 32행, 그중 2개는 일부러 열 순서로 그렸어요. 질문 24개 중 18개는 표에서, 6개는 본문에서 답을 찾아요. 검색기는 토크나이저 글과 같은 순수 파이썬 BM25 고, 1위 조각만 봤어요.

판정은 두 가지예요. 값 포함은 1위 조각에 정답 문자열이 있는지예요. 한 줄 근거는 행 이름 · 열 이름 · 값이 한 줄에 같이 있거나, 마크다운 표처럼 구조가 명시된 채 같은 조각에 있는지예요.

파싱 넷은 아래 표의 이름 그대로예요. 표 인식은 pdfplumber 로 격자선을 읽었어요. 청킹은 섹션과 200자 고정이에요.

같은 표, 두 경로 — 조각 안에서 '과장'과 '130,000원'이 몇 줄 떨어져 있나 PDF 의 표좌표에 놓인글자들 텍스트 추출 표 인식 그린 순서대로 텍스트과장차장 … 숙박비(1박)100,000원 … 130,000원 청크행 이름과 값 사이 6줄열 이름은 더 멀리200자에서 잘리면 사라져요 LLM줄을 세어 추측25,000원? 행마다 한 문장출장 경비 한도 —직급 과장, 숙박비 130,000원,일비 25,000원, … 청크어디서 잘라도행 · 열 · 값이 한 줄색인은 55% 커져요 LLM그대로 읽어요130,000원 실측: 파싱 4종 × 청킹 2종, 24문항 BM25 1위 조각 기준. 모델 생성 단계는 이번에 재지 않았어요.

검색은 전부 맞췄어요. 깨진 건 근거 조각이었어요

파싱청킹값 포함한 줄 근거
텍스트 추출(pypdf)섹션24/246/24
텍스트 추출(pypdf)200자 고정19/246/24
레이아웃 보존(pdftotext -layout)섹션24/2424/24
레이아웃 보존(pdftotext -layout)200자 고정13/249/24
표 인식 → 마크다운 표섹션24/2424/24
표 인식 → 마크다운 표200자 고정15/2410/24
표 인식 → 행 문장섹션24/2424/24
표 인식 → 행 문장200자 고정24/2424/24

첫 줄이 도입부 장면이에요. 텍스트 추출은 24문항 전부 정답 값이 든 조각을 1위로 올렸는데, 한 줄 근거는 본문 질문 6개뿐이에요. 표 질문 18개는 값은 있지만 어느 행 어느 열인지가 없어요. 골든셋으로 검색만 재면 이 줄은 만점이에요.

레이아웃과 마크다운 표는 청킹 한 번에 무너졌어요

둘 다 섹션 단위로 자르면 완벽했는데, 200자 고정으로 바꾸자 값 포함부터 13, 15로 떨어졌어요.

2. 출장 경비 한도
출장 경비는 직급별로 아래 한도 안에서 실비 지급한다.
| 직급 | 숙박비(1박) | 일비 | 식비(1일) | 교통 등급 |
| --- | --- | --- | --- | --- |

머리말만 남고 행은 다음 조각으로 넘어갔어요. 거기엔 '과장 | 130,000원'은 있는데 '숙박비'가 없어요. 표는 열 이름을 머리말 한 줄에 몰아 두는 구조라서, 머리말과 떨어진 행은 어디서 자르든 이름을 잃어요. 섹션 청킹이 그걸 가려 줬을 뿐이에요.

그래서 표는 행마다 한 문장으로 풀기로 했어요

머리말을 행마다 다시 적어 주면 어디서 잘라도 한 줄 안에 행 · 열 · 값이 남아요. 표 인식은 pdfplumber 의 격자선 전략으로 하고, 섹션 제목을 앞에 붙였어요.

# 결정: 표의 색인 단위는 셀도 표도 아니고 '행'이다
with pdfplumber.open("rules.pdf") as pdf:
    tables = [t for p in pdf.pages for t in p.extract_tables()]

def rows_to_sentences(section, table):
    header, *rows = table
    for row in rows:
        cells = ", ".join(f"{h} {v}" for h, v in zip(header, row))
        yield f"{section} — {cells}"
# 출장 경비 한도 — 직급 과장, 숙박비(1박) 130,000원, 일비 25,000원, …

달라진 건 표의 마지막 두 줄이에요. 200자 고정에서도 24/24 가 됐고, 열 순서로 그린 표도 행 순서 표와 같아졌어요. 대신 머리말이 행마다 반복되니 색인 글자 수가 1,429자에서 2,220자로 55% 늘었어요. 마크다운 표는 30% 였고요.

AS-ISTO-BE
표 처리텍스트 추출기에 맡김격자선으로 표 인식 뒤 행 문장
조각 안 근거값은 있고 이름은 몇 줄 밖행 · 열 · 값이 한 줄
청킹 의존섹션으로 자를 때만 안전어디서 잘라도 같음
색인 크기기준+55%

도구는 가벼운 걸 썼어요. 실제로는 레이아웃 모델로 표를 읽는 Docling(IBM 이 만들어 LF AI & Data 재단에 둔 MIT 프로젝트, 이번 주 v2.141.0) 을 앞단에 두고 같은 행 문장 변환을 거는 게 저희 기본형이에요.

바로 믿으면 안 되는 세 가지

하나, 표가 작고 깨끗해요. 병합 셀 · 두 줄 머리말 · 격자선 없는 표는 안 쟀어요.

둘, 모델 생성 단계는 안 쟀어요. 한 줄 근거는 문자열 규칙이에요. 모델이 여섯 줄을 세어 맞출 수도 있지만, 그걸 운에 맡기지 않는 게 요지예요.

셋, 스캔본은 다른 문제예요. pypdf 도 pdfplumber 도 이미지에서 글자를 못 읽어요. 스캔 PDF 는 OCR 이 먼저예요.

틀린 건 검색이 아니었다는 말로 돌아와서

25,000원이라고 답한 챗봇의 검색 로그는 만점이었을 거예요. 틀린 자리는 표가 글자로 풀리던 순간이에요. 그래서 저희는 색인 전에 pdftotext 를 한 번 돌려 '과장' 옆에 무엇이 있는지 눈으로 봐요. 완성된 규칙은 아니에요. 병합 셀은 아직 재는 중이에요.

자주 묻는 질문

Q. 마크다운 표로 넣고 청크를 크게 잡으면 되지 않나요? A. 섹션 청킹에선 그것도 24/24 였어요. 다만 표가 청크 한도보다 길어지면 같은 방식으로 깨져요.

Q. 색인이 55% 커지는데 괜찮나요? A. 표가 든 섹션에만 적용하면 체감은 그보다 작아요.

Q. 표가 어떻게 뽑히는지 먼저 보는 방법이 있나요? A. pdftotext 와 pdftotext -layout 결과를 나란히 보세요. 행 이름 옆자리가 다르면 표 인식이 필요해요.

Sources

도입부의 25,000원 장면은 설명용 구성이고 고객사 사례가 아니에요. 표의 수치는 가상 규정 4건 · 24문항 · 순수 파이썬 BM25 로 저희가 직접 돌린 값이고, 모델 생성 단계는 포함하지 않았어요.