6장에서 만든 RAG 파이프라인은 답을 내놓긴 했습니다. 문제는 그 답이 어떤 데이터를 근거로 나왔느냐입니다.
로더가 읽지 못하는 것
6장에서 내려받은 원본 파일을 살펴보면 표가 이미지로 삽입되어 있습니다. 종합부동산세법 제9조(세율 및 세액)의 과세표준 구간별 세율표가 통째로 그림입니다.
LangChain의
Docx2txtLoader는python-docx패키지를 활용하기 때문에 마이크로소프트 워드로 작성된 표는 읽어올 수 있지만, 문서에 첨부된 이미지는 읽어올 수 없습니다.
세율표를 못 읽으니 세금 계산을 제대로 할 리가 없습니다. 검색 품질 이전에 저장할 데이터 자체가 비어 있는 상황입니다.
표를 마크다운으로 바꾸기
해결책은 첨부된 이미지를 gpt-4o, claude-3.5-sonnet 같은 멀티모달 LLM으로 마크다운으로 변환하는 것입니다. ChatGPT에 표 이미지를 올리고 “첨부된 표를 마크다운과 HTML 형식으로 변환해 줄 수 있나요?”라고 물으면 됩니다.
여기서 왜 하필 마크다운인가가 이 장의 핵심입니다.
HTML이나 XML과 같은 형식도 표를 표현할 수 있지만, 마크다운을 활용해야 토큰을 아끼고 비용을 절감할 수 있습니다.
책은 이를 OpenAI 토크나이저로 직접 확인합니다. 같은 세율표를 두 형식으로 변환해 토큰 수를 비교한 결과입니다.
| 형식 | 토큰 수 | 문자 수 |
|---|---|---|
| 마크다운 | 269 | 583 |
| HTML | 408 | 785 |
HTML이 마크다운보다 약 1.5배의 토큰을 사용합니다. <table>, <thead>, <tr>, <td> 같은 태그가 전부 토큰을 잡아먹기 때문입니다. RAG에서 검색된 청크는 매 질문마다 프롬프트에 실려 나가므로, 이 차이는 호출할 때마다 반복되는 비용입니다.
문서가 많다면 py-zerox
수작업 전처리는 문서가 간단할 때 이야기입니다.
문서의 양이 많다면 DOCX 대신 PDF 형식을 활용해서 py-zerox 같은 패키지를 사용하는 것이 유리합니다. py-zerox는 멀티모달 LLM을 활용해 PDF 문서를 마크다운으로 변환해 주는 라이브러리입니다.
단점도 분명합니다. 문서 전체에 멀티모달 LLM을 구동하기 때문에 시간이 오래 걸리고 비용이 많이 나옵니다. 다만 방대한 양의 문서를 처리할 때는 수동 전처리보다 효율적입니다.
정리하면 선택 기준은 이렇습니다.
| 상황 | 방법 |
|---|---|
| 문서가 적고 간단하다 | ChatGPT 같은 도구로 수동 전처리 |
| 문서가 방대하다 | PDF + py-zerox로 일괄 변환 (시간·비용 감수) |
전처리된 문서로 다시 만들기
기존 문서 대신 전처리된 문서를 쓰려면 바꿀 곳은 파일 경로 한 줄뿐입니다.
loader = Docx2txtLoader('../documents/law_markdown.docx') # 파일 경로가 변경된다.
document_list = loader.load_and_split(text_splitter=text_splitter)
벡터 저장소도 다른 경로에 새로 만듭니다. 기존 저장소와 섞이지 않도록 컬렉션 이름과 디렉터리를 모두 구분합니다.
from langchain_chroma import Chroma
vector_store = Chroma.from_documents(
documents=document_list,
embedding=embedding,
collection_name='tax-markdown',
persist_directory="./tax-markdown"
)
같은 코드를 실행해도 기존과 다른 답변이 나옵니다. 이번에는 세율표를 제대로 읽어서 구간별 세율을 나열하고 계산까지 합니다.
2주택을 소유하고 있는 경우, 주택분 종합부동산세는 다음과 같은 세율이 적용됩니다. 과세표준 3억 원 이하: 1천분의 5 / 3억 원 초과 6억 원 이하: 150만 원 + (3억 원을 초과하는 금액의 1천분의 7) / … 각 집의 가치는 10억 원이므로, 총 과세표준은 20억 원입니다. … 따라서 총 세액은 960만 원 + 1040만 원 = 2000만 원입니다.
6장의 답변(5억 4천만 원)에 비하면 훨씬 그럴듯합니다. 세율 구간을 정확히 인용하고 누진 계산 구조도 맞습니다.
그런데 이 답도 틀렸다
이 장이 끝나는 방식이 좋습니다. 책은 여기서 만족하지 않습니다.
세액이 계산된 것처럼 보이지만, 사실은 그렇지 않습니다.
원본 문서에 따르면 주택에 대한 종합부동산세의 과세표준은 이렇게 정해집니다.
납세의무자별로 주택의 공시가격을 합산한 금액에서 다음 각 호의 금액을 공제한 금액에 부동산 시장의 동향과 재정 여건 등을 고려하여 100분의 60부터 100분의 100까지의 범위에서 대통령령으로 정하는 공정시장가액비율을 곱한 금액으로 한다. 다만, 그 금액이 영보다 작은 경우에는 영으로 본다.
즉 집값 10억 × 2채 = 20억을 그대로 과세표준으로 쓰면 안 됩니다. 실제로는 이런 순서를 거쳐야 합니다.
공시가격 합산 → 공제액 차감 → 공정시장가액비율(60~100%) 곱하기 → 과세표준 → 세율 적용
따라서 사용자가 세금에 관한 질문을 했을 때 공시가격, 공제액, 공정시장가액비율을 바탕으로 답변을 생성해야 합니다.
데이터를 잘 저장했는데도 답이 틀렸다는 것은, 이제 문제가 R(검색)이 아니라 A(증강)와 프롬프트 설계로 넘어갔다는 뜻입니다. 다음 장에서 LLM이 원하는 답변을 생성하도록 LangChain을 어떻게 활용할지 다룹니다.
정리
- 로더가 무엇을 못 읽는지 먼저 확인한다.
Docx2txtLoader는 워드 표는 읽지만 이미지로 삽입된 표는 읽지 못한다. - 이미지 표는 멀티모달 LLM으로 마크다운 변환한다. HTML은 같은 표에 1.5배(408 vs 269) 토큰을 쓰고, 그 차이는 질문할 때마다 반복된다.
- 문서가 방대하면 py-zerox로 PDF를 일괄 변환한다. 느리고 비싸지만 수작업보다 낫다.
- 전처리된 문서로 갈아타는 비용은 경로 한 줄과 새 컬렉션뿐이다.
- 답이 그럴듯해 보인다고 맞는 게 아니다. 세율표를 제대로 읽어도 과세표준 산정 규칙(공시가격·공제·공정시장가액비율)을 모르면 여전히 틀린 답이 나온다.