6장. RAG 의 원리와 로컬 모델로 옮기기
문서를 조각으로 나누기
반찬을 반찬통에 나눠 담을 때는 한 가지 반찬을 한 통에 담습니다. 큰 덩어리를 아무 데서나 칼로 자르면 한 반찬이 두 통에 반씩 들어가 꺼내 먹기 불편합니다. 문서를 검색 단위로 나눌 때도 뜻이 한 덩어리인 곳에서 끊어야 합니다.
왼쪽은 조항마다 끊은 조각이고, 오른쪽은 아래 예시처럼 60글자마다 자른 조각입니다. 오른쪽 1번처럼 "50만원까지"가 두 조각으로 갈라지면, 어느 조각을 찾아도 답이 반만 들어 있습니다. 2번처럼 앞 조각의 끝을 조금 겹쳐 자르면 이 문제를 덜 수 있습니다. 여기서 반찬이 조항, 반찬통이 문서 조각입니다.
나누는 방법
| 방법 | 끊는 곳 | 좋은 점 | 아쉬운 점 |
|---|---|---|---|
| 구조로 나누기 | 빈 줄, 조항, 제목 | 뜻이 한 덩어리로 남는다 | 문서마다 구조가 다르다 |
| 글자 수로 자르기 | 정한 글자 수마다 | 어떤 문서에도 쓴다 | 문장이 중간에서 잘린다 |
| 겹쳐 자르기 | 글자 수마다, 앞 조각과 겹치게 | 잘린 곳의 뜻을 살린다 | 조각 수와 중복이 는다 |
이 장의 문서는 조항이 빈 줄로 나뉘어 있어 구조로 나눕니다. 조각의 크기를 고르는 법과 여러 방법의 견주기는 7장에서 자세히 다룹니다.
두 방법 해 보기
text = ("제14조(결제 수단) 신용카드, 계좌이체, 간편결제, 휴대폰 결제를 쓸 수 있습니다. 휴대폰 결제는 한 달에 50만원까지입니다."
"\n\n제15조(무이자 할부) 5만원 이상을 신용카드로 결제하면 3개월까지 무이자 할부가 됩니다.")
by_rule = text.split("\n\n") # 빈 줄에서 끊는다
by_size = [text[i:i + 60] for i in range(0, len(text), 60)] # 60글자마다 자른다
print(len(by_rule), "조각:", [c[:12] for c in by_rule])
print(len(by_size), "조각:", by_size[0][-12:], "|", by_size[1][:12])빈 줄로 나누면 조항 두 개가 그대로 두 조각이 됩니다. 60글자로 자르면 세 조각이 되고, 첫 조각은 "…휴대폰 결제는 한 달에"에서 끝납니다. 답인 "50만원까지입니다"는 다음 조각으로 넘어가 문장이 잘립니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
"\n\n" | 줄바꿈 두 번, 곧 빈 줄이다. 이 문서는 조항 사이에 빈 줄이 있다 |
text.split("\n\n") | 빈 줄이 있는 곳마다 글을 잘라 목록으로 만든다 |
range(0, len(text), 60) | 0, 60, 120 처럼 60씩 건너뛴 시작 위치를 만든다 |
text[i:i + 60] | i 번째부터 60글자를 잘라 낸다. 끝에 60글자가 안 남으면 남은 만큼만 나온다 |
c[:12], by_size[0][-12:] | 앞 12글자, 뒤 12글자만 보여 준다. 조각이 어디서 시작하고 끝나는지 보려는 것이다 |
이 코드가 뜻하는 것
긴 김밥 한 줄을 써는 두 방법과 같습니다. 재료가 바뀌는 곳에서 써는 방법과, 자로 재서 똑같은 길이로 써는 방법입니다. 자로 재면 크기는 고르지만 재료 한가운데가 잘릴 수 있습니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
text = (...) | 조항 두 개를 빈 줄로 이은 글이다 | 썰 김밥 한 줄 |
by_rule = ... | 빈 줄에서 끊는다 | 재료가 바뀌는 곳에서 썬다 |
by_size = ... | 60글자마다 자른다 | 자로 재서 똑같이 썬다 |
첫째 print | 조각 수와 각 조각의 앞부분을 찍는다 | 몇 조각인지, 조각마다 무엇으로 시작하는지 본다 |
둘째 print | 첫 조각의 끝과 둘째 조각의 앞을 나란히 찍는다 | 잘린 자리를 들여다본다 |
결과에서 빈 줄로 나눈 두 조각은 "제14조…", "제15조…"로 시작해 조항 하나씩을 온전히 담습니다. 60글자로 자른 쪽은 "휴대폰 결제는 한 달에"와 "50만원까지입니다" 사이가 끊겼습니다.
작게 줄여서 눈으로 보기
표에 나온 "겹쳐 자르기"를 짧은 문장으로 해 봅니다. 12글자씩 자르되, 다음 조각을 4글자 겹치게 시작합니다.
text = "휴대폰 결제는 한 달에 50만원까지입니다."
size, overlap = 12, 4
# 겹치지 않게: 12글자마다 새로 시작한다
plain = [text[i:i + size] for i in range(0, len(text), size)]
# 겹쳐서: 다음 조각을 12-4 = 8글자 뒤에서 시작한다
step = size - overlap
overlapped = [text[i:i + size] for i in range(0, len(text), step)]
print(plain)
print(overlapped)['휴대폰 결제는 한 달에', ' 50만원까지입니다.']
['휴대폰 결제는 한 달에', '한 달에 50만원까지입', '원까지입니다.']| 결과 | 읽는 법 |
|---|---|
| 첫째 줄(겹치지 않게) | 두 조각이다. "한 달에"와 "50만원"이 서로 다른 조각에 있어 어느 조각도 답을 다 담지 못한다 |
| 둘째 줄(겹쳐서) | 세 조각이다. 가운데 조각 "한 달에 50만원까지입"이 앞뒤 낱말을 함께 담는다 |
| 겹친 부분 | "한 달에", "원까지입"이 두 조각에 함께 들어 있다. 겹친 만큼 조각 수와 중복이 는다 |
겹쳐 자르면 잘린 자리의 뜻을 살릴 수 있지만, 조각이 둘에서 셋으로 늘었습니다. 표의 "아쉬운 점"이 바로 이것입니다.
주의할 점
- 조각에 조항 번호와 제목을 남겨 둡니다. 답에 "제14조에 따르면"처럼 근거를 보여 줄 수 있고, 제목의 낱말이 검색을 돕습니다.
- 표, 목록, 머리글은 나누기 전에 글로 풀거나 한 덩어리로 묶습니다. 표의 한 줄만 떨어져 나가면 무엇에 대한 숫자인지 알 수 없습니다.