
사용자가 300행짜리 단가표가 든 한글 문서를 올리고 「품목군별 평균 단가를 알려 줘」라고 물으면, 문서 검색(RAG)은 질문과 비슷한 청크 몇 개를 돌려줍니다. 문서는 4,000자 단위로 잘려 색인되므로 표도 여러 청크로 나뉘고, 무슨 열인지 알려 주는 머리글은 첫 청크에만 있습니다. 모델은 받은 청크 안의 행으로 평균을 내고, 그 값을 표 전체의 평균처럼 답합니다. 숫자가 틀려도 답의 모양은 멀쩡합니다.
그래서 업로드한 문서 속 표를 집계해야 할 때는 그 표를 대화에서만 보이는 임시 테이블로 올리고 SQL로 셉니다. 행은 서버가 파일에서 직접 읽고, 머리글과 열 타입도 서버가 정합니다. 이 글은 검색과 파일 읽기로는 왜 합계가 맞지 않는지, 표를 테이블로 만드는 규칙을 어디에 두었는지, 임시 테이블이 어디에 놓이고 어떤 실행 경로를 타는지 다룹니다. DB에 있는 데이터를 세는 문제는 「RAG로는 건수와 합계를 셀 수 없다, 원천 DB에 SQL로 묻는다」에서 다룹니다.
색인용 글에서 표는 머리글과 떨어진다
문서 검색이 표를 찾으려면 표를 글로 펼쳐 색인해야 합니다. 한글(HWPX) 문서 네 종을 실제로 풀어 보니 표 셀의 글자가 문서 전체 글자의 30~45%였습니다. 표를 펴지 않으면 그만큼이 검색에서 빠집니다. 그래서 파서는 표를 한 행에 한 줄, 칸은 탭으로 구분한 글로 펼칩니다.
펼친 표는 검색에 걸리지만 집계에 필요한 구조는 잃습니다. 형식마다 잃는 것이 조금씩 다릅니다.
- 머리글이 떨어진다. 문서는 글자 수 기준 고정 창(
document.chunkSize기본 4,000자,document.chunkOverlap기본 500자)으로 잘린다. 긴 표의 두 번째 청크에는 「A 볼트 3200」 같은 행만 남고, 3200이 무슨 열의 값인지 알려 주는 머리글은 첫 청크에 있다. - 표가 제자리에 없다. 워드와 한글 문서의 색인용 글은 본문 문단을 모두 쓴 뒤에 표 행을 붙인다. 「품목별 단가는 다음과 같다」는 문장과 그 표가 색인 안에서 멀리 떨어진다.
- 칸 경계가 사라진다. PDF는 페이지 글자만 추출하고 표를 따로 인식하지 않는다. 한글 바이너리(HWP)도 읽는 라이브러리가 표를 평문으로 눌러 셀 경계가 남지 않는다.
검색은 질문과 비슷한 청크를 고를 뿐, 조건에 맞는 행을 전부 모아 주지 않습니다. 평균을 내려면 표의 모든 행이 머리글과 함께 있어야 하는데, 색인은 둘 다 보장하지 않습니다.
읽은 행을 모델이 세면 합계를 확인할 수 없다
첨부 파일을 읽는 도구는 색인을 거치지 않습니다. 워드·한글·엑셀 파일의 표를 행·칸 배열로 돌려주므로 머리글과 값의 대응이 지켜집니다. 그래도 이 배열을 받아 모델이 직접 세게 하면 두 군데에서 어긋납니다.
첫째, 모델이 받은 행이 표 전체라는 보장이 없습니다. 결과 하나가 컨텍스트에 실을 수 있는 크기를 넘으면 서버가 줄여서 싣고, 줄였다는 사실을 모델에게 알립니다(「큰 도구 결과는 프로바이더가 자르기 전에 줄이고, 줄였다고 알린다」). 대화가 길어져 압축되면 오래된 읽기 결과는 표의 개수와 위치만 남기고 셀을 버립니다. 행 수백 개를 다음 라운드까지 컨텍스트에 들고 있을 수 없습니다.
둘째, 합계는 같은 표에 매번 같은 값이 나와야 하는 계산입니다. 모델이 숫자 수백 개를 읽고 더하면 그 값을 확인할 길이 없습니다. 코드로 셀 수 있는 일은 코드가 셉니다.
코드로 세는 길은 지금도 있습니다. 샌드박스에는 워드·한글·엑셀 문서 라이브러리가 들어 있어, 모델이 첨부 원본을 다시 열고 Python으로 셀 수 있습니다. 파일 하나를 한 번 세는 데는 이 길로 충분합니다. 다만 스크립트가 질문마다 표를 새로 해석합니다. 첫 행을 머리글로 볼지, 1,234를 숫자로 읽을지를 스크립트마다 다시 정하므로, 같은 표에 두 번 물으면 서로 다른 규칙으로 셀 수 있습니다. 첨부한 실적 엑셀을 회사 DB의 주문과 거래처 코드로 맞춰 보는 질문이라면, 스크립트가 DB 결과 파일과 첨부 파일을 직접 짝지어야 합니다. 같은 일을 SQL 한 문장으로 할 수 있는데도 그렇습니다.
표를 테이블로 만드는 규칙은 서버에 둔다
표를 올리는 일은 표 올리기 도구가 맡습니다. 모델은 첨부 파일과 표 위치만 지정합니다. 엑셀이면 시트, 워드·한글·파워포인트면 몇 번째 표인지를 넘깁니다. 서버는 파일 읽기 도구와 같은 파서로 그 표를 읽어 테이블을 만듭니다. 모델이 행을 인자로 다시 쓰지 않으므로 옮겨 적다 값이 바뀌는 일이 없고, 표가 커도 인자는 좌표 몇 개입니다.
테이블의 모양은 서버가 정해진 규칙으로 정합니다.
| 정하는 것 | 규칙 |
|---|---|
| 컬럼 이름 | 표의 첫 행. 워드·한글 표에는 머리글 표시가 없어 첫 행을 머리글로 읽는다 |
| 숫자 컬럼 | 한 컬럼의 값이 전부 「숫자로 바꿔도 표기가 그대로인」 값일 때만 숫자 타입으로 만든다 |
| 글자 컬럼 | 007, 1,234, 3%처럼 숫자로 바꾸면 표기가 달라지는 값이 하나라도 섞이면 글자 타입으로 둔다 |
007을 숫자로 만들면 품목 코드의 앞자리 0이 사라집니다. 그래서 테이블에는 원문 표기를 그대로 저장하고, 쉼표가 든 금액처럼 숫자로 세야 하는 값은 모델이 SQL에서 변환해 셉니다. 저장한 값이 원문과 다르면 결과를 원문과 대조할 수 없습니다.
문서 표는 데이터 표보다 신청서, 결재란, 사양표 같은 양식이 많습니다. 양식을 테이블로 올리면 「기관명」 같은 입력 라벨이 컬럼 이름이 되고 빈 입력칸이 행이 됩니다. 그래서 다음 가운데 하나라도 걸리면 서버는 올리기를 거절하고 이유를 돌려줍니다.
- 3열 미만이다. 문서의 2열 표는 대개 「항목 | 값」 양식이다.
- 머리글에 빈 칸이 있다.
- 같은 머리글이 되풀이된다. 가로 병합 셀을 파서가 칸마다 펼친 흔적이다.
- 행마다 칸 수가 다르다. 병합 셀이 칸 하나로 실린 흔적이다.
이 규칙을 서버에 두면 같은 표는 언제 올려도 같은 테이블이 됩니다.
임시 테이블은 대화마다 만드는 작업 스키마에 놓인다
임시 테이블은 대화마다 하나씩 만드는 작업 스키마에 놓입니다. 회사 DB는 읽기 전용 계정으로만 붙기 때문에(「읽기 전용과 쿼리 취소는 DB 서버에 닿아야 한다」) 작업 스키마를 그쪽에 만들 수 없습니다. 작업 스키마는 OwlAI 쪽 DB에 있고, 회사 DB의 조회 결과도 같은 스키마로 올릴 수 있습니다. SQL 실행 결과는 CSV 스필 파일로 남으므로(「조회 결과는 모델이 다시 받아 적지 않게 파일 경로로 넘긴다」), 표 올리기 도구가 그 파일 경로를 받아 테이블로 만듭니다. 첨부 엑셀과 주문 DB 결과가 한 스키마에 나란히 있으면 거래처 코드로 조인하고 차이를 세는 일을 SQL 한 문장으로 합니다.
질의는 회사 DB와 같은 읽기 전용 실행 도구를 지납니다. 같은 문장 판정, 실행 시간 예산, 결과 행 상한, 감사 기록이 걸립니다. 작업 스키마에 붙는 계정은 그 대화의 스키마만 볼 수 있어 다른 대화의 임시 테이블에는 닿지 않습니다. 작업 스키마는 대화에 딸린 자원이라 관리자의 DB 연결 목록에는 나오지 않습니다. 결과는 다른 SQL 결과처럼 답 안의 표와 스필 파일로 나갑니다.
임시 테이블은 원본이 아니라 작업 공간입니다. 원본 파일은 객체 저장소에 남고(「재배포 뒤에도 남아야 할 파일은 객체 저장소에만 둔다」) 테이블은 그 파일에서 언제든 다시 만들 수 있습니다. 그래서 임시 테이블의 수명은 스필 파일과 같게 두었습니다. 기한이 지나 지워진 뒤 오래된 대화에서 다시 물으면 모델이 원본에서 표를 다시 올립니다. 대화를 지우면 작업 스키마도 함께 지워집니다.
설계에서 고른 것
색인용 글은 지금처럼 표를 본문 뒤에 붙입니다. 표를 문서 순서대로 펼치고 표 경계를 따라 청크를 자르면 머리글이 떨어지는 일이 줄고, 숫자 하나를 찾는 검색은 더 정확해집니다. 대신 기존 문서를 모두 다시 색인해야 하고, 그래도 검색은 표의 모든 행을 돌려주지 않습니다. 집계는 임시 테이블이 맡으므로 색인은 지금의 펼침 방식을 유지했습니다.
양식 판정은 표의 모양만 봅니다. 머리글이 다 차 있고 칸 수가 고른 3열 이상 양식은 테이블로 올라가고, 병합 셀이 섞인 데이터 표는 거절됩니다. 거절된 표는 샌드박스에서 원본을 직접 열어 세는 길이 남습니다. 판정 조건을 더 붙여 이런 경계 사례까지 가르는 방법도 있지만, 조건이 늘수록 같은 표가 어느 쪽으로 갈지 예측하기 어려워집니다. 그래서 조건은 실제 양식 문서로 다시 확인한 뒤에만 바꿉니다.
임시 테이블은 대화 하나의 것입니다. 다른 대화나 다른 사람과 공유되지 않고, 원본 파일이 바뀌어도 이미 올린 테이블은 따라 바뀌지 않습니다. 한 번 묻고 끝날 표라면 이 범위로 충분합니다. 여러 사람이 매달 같은 실적표를 묻는다면 그 표를 DB로 옮겨 연결하고 스키마 분석을 돌리는 편이 맞습니다.
메모리와 검색의 다른 글

엔지니어링 노트 · 메모리와 검색
개인 메모리는 매 턴 싣고, 고칠 때는 통째로 다시 쓴다
사용자가 「이슈 트래커에서 제 계정은 이 ID예요. 앞으로 내 이슈는 이걸로 찾아 주세요」라고 하면 모델은 「기억하겠습니다」라고 답합니다.

엔지니어링 노트 · 메모리와 검색
대화에서 바로잡은 컬럼 뜻, 테이블 메모로 다음 사람에게 싣기
고객 테이블의 상태 컬럼 STATCD 에는 1, 2, 9가 들어 있습니다. 스키마만 봐서는 9가 해지라는 것을 알 수 없어서, 「활성 고객 수」를 물으면 모델은 해지 고객까지 세어 보고서에 올립니다.

엔지니어링 노트 · 메모리와 검색
조사 붙은 검색어는 전문 검색에 안 걸린다, 낱말을 OR로 잇는다
테이블 메모에 「반품 주문은 ORDRTN 테이블에 따로 쌓인다」라고 적혀 있고, 사용자가 「지난달 반품 주문 건수를 알려 줘」라고 묻습니다.