지난 글 끝에서 하나를 남겨 두었다. 글자를 다 살려 놓아도 그것만으로는 안 되는 자리, 표다.
표는 글자를 잃어서 망가지는 것이 아니라 자리를 잃어서 망가진다. 그래서 파싱이 완벽해도, OCR 정확도가 100%여도, 그다음 단계에서 여전히 무너질 수 있다.
표에서 뜻을 만드는 것은 자리다
실적표에서 「11」이라는 숫자 하나를 보자. 이 숫자가 무엇인지는 셀 안에 안 적혀 있다. 왼쪽으로 훑어 「영업이익」을 만나고 위로 훑어 「2025년」을 만나야 비로소 뜻이 생긴다. 값은 셀에 있지만 의미는 교차점에 있다.
줄글로 펴면 그 교차 관계가 사라진다. 남는 것은 숫자 넷이 나란히 늘어선 문자열이고, 어느 숫자가 어느 행·열의 것인지는 순서로 추측해야 한다.
모델은 이 추측을 꽤 잘한다. 2열짜리 작은 표라면 대개 맞힌다. 문제는 틀릴 때 조용히 틀린다는 점이다. 열이 다섯 개를 넘거나, 중간에 빈 셀이 있어 값 개수가 열 개수와 안 맞거나, 숫자 형식이 섞여 있으면 추측이 어긋난다. 그런데 결과는 「모르겠습니다」가 아니라 그냥 다른 숫자다.
청킹은 표를 두 번 망가뜨린다
표가 청킹 단계를 지나면 문제가 하나 더 붙는다.
헤더가 첫 청크에만 남는다. 200행짜리 표를 800자씩 자르면 헤더 행은 첫 조각에만 들어간다. 둘째 조각부터는 숫자만 늘어선 글이 되고, 그 조각이 검색되면 열 이름이 아예 없는 근거가 모델에 간다.
행 한가운데서 잘린다. 글자 수로 자르므로 행 경계를 지킬 이유가 없다. 「영업이익 8 1」에서 끊기고 다음 조각이 「1 …」로 시작하면, 11이라는 값이 두 조각으로 갈라진다.
둘 다 대응은 같은 방향이다. 표는 글자 수로 자르지 않는다. 청킹 전략에서 문단 경계를 지키라고 한 것과 같은 이야기인데, 표에서는 경계가 행이고 여기에 규칙이 하나 더 붙는다.
def chunk_table(header, rows, max_rows=20):
"""표는 행 단위로 자르고, 조각마다 헤더를 다시 붙인다."""
for i in range(0, len(rows), max_rows):
yield render_markdown(header, rows[i:i + max_rows])
조각마다 헤더를 다시 붙이는 것이 핵심이다. 헤더가 반복되면 색인이 조금 커지지만, 열 이름 없는 청크가 검색되는 것보다 훨씬 낫다. 맥락 붙이기에서 청크마다 문서 제목을 붙였던 것과 같은 발상이고, 표에서는 그 대상이 헤더 행이다.
색인에 넣는 세 가지 방식
표를 어떤 모양으로 저장하느냐에 따라 답할 수 있는 질문이 달라진다.
마크다운 표 그대로 넣는 방식이 가장 단순하다. 파이프 기호로 열을 가른 형태는 모델이 잘 읽고, 구조가 글자에 남아 있어 교차 관계가 보존된다. 표가 화면 한 장에 들어갈 크기라면 이것으로 충분하다. 한계는 크기다 — 수백 행짜리 표는 문맥 창에 안 들어가고, 검색 단위로도 너무 크다.
행마다 한 문장으로 펴는 방식은 검색에 유리하다. 「2025년 영업이익은 11억 원입니다」라는 문장은 사용자의 질의와 모양이 닮아 있어 벡터 검색이 잘 잡고, 키워드 검색도 「영업이익」·「2025」를 그대로 찾는다. 행 하나가 청크 하나가 되므로 크기 문제도 없다. 한계는 행끼리의 관계다 — 「매출이 가장 큰 부문」이나 「전 부문 합계」는 행 하나만 봐서는 답이 안 나온다.
표는 표로 두고 질의를 만들어 실행하는 방식은 그 한계를 넘는다. 표를 데이터베이스 테이블로 적재해 두고, 질문이 오면 모델이 질의문을 짓고, 그 결과를 근거로 답한다. 집계와 비교가 정확해지는데 — 숫자를 세는 일을 모델이 아니라 데이터베이스가 하기 때문이다. 에이전틱 RAG에서 다룬 도구 호출이 여기에 그대로 쓰인다. 한계는 준비 비용이다. 스키마를 만들어 두어야 하고, 표가 정형이어야 한다.
셋 중 하나를 고르는 것이 아니다. 같은 표를 마크다운으로도, 행 문장으로도 색인해 두면 색인이 두 배가 될 뿐 서로 방해하지 않는다. 자주 물어보는 큰 표만 데이터베이스에 추가로 적재하는 식으로 섞어 쓰는 것이 보통이다.
질문이 방식을 정한다
어떤 질문이 오느냐를 먼저 세어 보면 무엇을 만들지가 정해진다.
| 질문 유형 | 예 | 맞는 방식 |
|---|---|---|
| 한 칸 찾기 | 2025년 영업이익은 | 행 문장 |
| 행 사이 비교 | 매출이 가장 큰 부문은 | 질의 실행 |
| 집계 | 전 부문 합계는 | 질의 실행 |
| 여러 해 추이 | 3년간 어떻게 변했나 | 질의 실행 또는 표 통째 |
| 표 자체의 해석 | 이 표가 말하는 것은 | 마크다운 표 통째 |
한 칸 찾기가 대부분이면 행 문장만으로 끝난다. 실제 서비스에서 이 비율이 높은 경우가 많으니, 질의 로그를 먼저 세어 보는 것이 순서다. 집계 질문이 몇 건 안 되는데 데이터베이스 적재 파이프라인을 만드는 것은 낭비다.
병합 셀과 다단 헤더
여기까지는 표가 반듯한 격자라는 전제였다. 실제 문서의 표는 그렇지 않다.
| 구분 | 2025년 || 2024년 ||
| | 상반기 | 하반기 | 상반기 | 하반기 |
| 매출 | 66 | 72 | 58 | 62 |
헤더가 두 줄이고 위 줄의 「2025년」이 두 칸에 걸쳐 있다. 이 표에서 「72」의 뜻은 「매출 · 2025년 · 하반기」인데, 그러려면 헤더 두 줄을 합쳐 읽어야 한다.
대응은 헤더를 미리 펴 두는 것이다. 파싱 단계에서 열 이름을 「2025년 하반기」처럼 하나로 합쳐 격자를 반듯하게 만든 뒤에 색인한다. 나중에 모델이 알아서 합쳐 읽기를 기대하지 말고, 색인에 들어가기 전에 해결한다.
세로 방향 병합도 같다. 「구분」 열에서 「매출」이 세 행에 걸쳐 있으면, 아래 두 행에는 그 값이 안 적혀 있다. 행 문장을 만들 때 그 빈칸을 위에서 물려받아 채워야 한다 — 안 채우면 「상반기는 72억 원입니다」처럼 주어 없는 문장이 나온다.
이런 표는 문서 파싱에서 다룬 파서 선택과도 맞물린다. 병합 정보를 살려 주는 파서를 써야 이 작업이 가능하고, 격자를 이미 뭉갠 뒤에 받으면 되살릴 방법이 없다.
답변에서 숫자를 다루는 규칙
표에서 뽑은 숫자로 답할 때 지킬 것이 셋이다.
모델에게 계산을 시키지 않는다. 「합계는 얼마인가」에 근거 표만 주고 더하게 하면 대개 맞지만 가끔 틀린다. 그리고 틀린 합계는 근거 표를 봐도 안 보인다 — 표에 없는 숫자라서 대조할 자리가 없기 때문이다. 계산은 코드나 데이터베이스가 하고 모델은 그 결과를 옮긴다.
어느 셀에서 왔는지 함께 낸다. 「11억 원(실적표 · 영업이익 · 2025년)」처럼 행과 열 이름을 붙이면, 읽는 사람이 그 자리를 원문에서 확인할 수 있다. 숫자만 있는 답은 확인할 방법이 없다.
단위와 기준을 잃지 않는다. 표 제목에 「단위: 억 원」이 적혀 있고 셀에는 「11」만 있는 경우가 흔하다. 행 문장을 만들 때 그 단위를 문장에 넣지 않으면 11이 무엇인지 알 수 없게 된다. 표 위아래의 주석 줄 — 단위, 기준일, 「잠정치」 같은 말 — 은 표와 같은 청크에 담는다.
정리
- 표의 뜻은 셀이 아니라 행과 열이 만나는 자리에 있다. 펴면 그 대응이 사라진다
- 표는 글자 수가 아니라 행 단위로 자르고, 조각마다 헤더를 다시 붙인다
- 색인 방식은 셋이고 답할 수 있는 질문이 다르다. 함께 두어도 서로 방해하지 않는다
- 질의 로그를 세어 보고 정한다. 한 칸 찾기가 대부분이면 행 문장으로 끝난다
- 병합 셀과 다단 헤더는 색인 전에 펴 둔다. 나중에 모델이 합쳐 읽기를 기대하지 않는다
- 계산은 모델이 아니라 코드가 하고, 답에는 셀 위치와 단위를 함께 적는다
읽어주셔서 감사합니다. 😊

