6장. RAG 의 원리와 로컬 모델로 옮기기
실습 2 — 정책 문서를 조각으로 나누기
shop_policy.txt(가상 쇼핑몰의 이용 안내, 조항 25개)를 검색 단위의 조각으로 나눕니다. 빈 줄로 조항을 나누는 법과 글자 수로 자르는 법을 견줍니다. 모델은 쓰지 않습니다.
1단계. 문서 읽기
text = open("shop_policy.txt", encoding="utf-8").read()
print("글자 수:", len(text))
# 앞부분 250글자만 본다
print(text[:250])글자 수: 1885
가상 쇼핑몰 이용 안내
이 문서는 LLM 엔지니어링 교안의 연습용으로 지어낸 정책입니다.
제1조(배송 기간) 결제 후 1~3일 안에 도착합니다. 제주와 섬 지역은 2일 더 걸립니다. 주말과 공휴일에는 출고하지 않습니다.
제2조(배송비) 3만원 이상 주문하면 배송비가 무료입니다. 3만원보다 적으면 3,000원을 받습니다. 제주와 섬 지역은 3,000원이 더 붙습니다.
제3조(출고 마감) 평일 오후 2시까지 결제한 주문은 그날 출고합니다. 오2단계. 빈 줄로 조항 나누기
# 조항 사이에는 빈 줄이 있다. 맨 앞 덩어리는 문서 제목과 안내라 뺀다
chunks = [c.strip() for c in text.split("\n\n")[1:]]
lengths = [len(c) for c in chunks]
print("조각 수:", len(chunks))
print("글자 수: 가장 짧은", min(lengths), "· 가장 긴", max(lengths), "· 평균", round(sum(lengths) / len(lengths)))
print(chunks[13])조각 수: 25
글자 수: 가장 짧은 44 · 가장 긴 102 · 평균 71
제14조(결제 수단) 신용카드, 계좌이체, 간편결제, 휴대폰 결제를 쓸 수 있습니다. 휴대폰 결제는 한 달에 50만원까지입니다.3단계. 글자 수로 자르면
body = "\n\n".join(chunks)
size = 80
# 80글자마다 자른다. 조항의 경계를 보지 않는다
by_size = [body[i:i + size] for i in range(0, len(body), size)]
print("80글자 조각 수:", len(by_size))
# "50만원까지"가 들어 있던 곳이 어떻게 잘렸는지 찾아본다
for i, c in enumerate(by_size):
if "휴대폰 결제는" in c or "만원까지" in c:
print(i, repr(c))80글자 조각 수: 23
13 '. 휴대폰 결제는 한 달에 50만원까지입니다.\n\n제15조(무이자 할부) 5만원 이상을 신용카드로 결제하면 3개월까지 무이자 할부가 됩니다.\n\n제'repr(c) 는 조각 c 를 보이지 않는 글자까지 드러낸 모습으로 바꿉니다. repr 은 representation(표현)의 줄임말입니다. 글자가 실제로 어떻게 들어 있는지를 파이썬 코드에 적는 모습 그대로 보여 줍니다. 같은 조각의 앞부분을 print(c) 와 print(repr(c)) 로 찍어 견주면 이렇습니다.
print(13, c) → 13 . 휴대폰 결제는 한 달에 50만원까지입니다.
(빈 줄)
제15조(무이자 할부) 5만원
print(13, repr(c)) → 13 '. 휴대폰 결제는 한 달에 50만원까지입니다.\n\n제15조(무이자 할부) 5만원'| 보이는 것 | print(c) | print(repr(c)) |
|---|---|---|
| 줄바꿈 | 실제로 줄이 바뀐다. 조각이 어디서 끝나는지 헷갈린다 | \n 이라는 글자로 보인다. 빈 줄은 \n\n 이다 |
| 조각의 처음과 끝 | 알기 어렵다 | 작은따옴표 '…' 로 감싸 정확히 보인다 |
| 앞뒤의 빈칸, 탭 | 눈에 띄지 않는다 | ' 끝 ', 'a\tb' 처럼 드러난다 |
그래서 위 출력에서 세 가지가 한눈에 보입니다. 13번 조각은 '. 휴대폰… 처럼 문장 중간(마침표)에서 시작합니다. 안에 든 \n\n 은 빈 줄이므로, 제14조의 끝과 제15조가 한 조각에 섞였다는 뜻입니다. 끝은 '…\n\n제' 로, 다음 조항의 이름이 "제" 한 글자에서 잘렸습니다. print(c) 로 찍었다면 줄이 바뀌어 이 경계가 잘 보이지 않습니다. 이것이 4단계에서 조항 단위로 나누고 겹쳐 자르는 까닭입니다.
4단계. 겹쳐 자르기와 조항 정보 붙이기
import re
import pandas as pd
# 겹쳐 자르기: 80글자 조각을 60글자마다 시작해 앞 조각과 20글자씩 겹친다
overlap = [body[i:i + size] for i in range(0, len(body), 60)]
print("겹친 조각 수:", len(overlap), "·", "한 조각 안에 '한 달에 50만원까지'가 온전한가:",
any("한 달에 50만원까지" in c for c in overlap))
# 빈 줄로 나눈 조각에서 조항 번호와 제목을 뽑아 표로 만든다
rows = []
for c in chunks:
m = re.match(r"제(\d+)조\((.+?)\)", c) # "제14조(결제 수단)" 에서 14 와 결제 수단
rows.append({"조": int(m.group(1)), "제목": m.group(2), "글자수": len(c)})
print(pd.DataFrame(rows).head(6).to_string(index=False))겹친 조각 수: 31 · 한 조각 안에 '한 달에 50만원까지'가 온전한가: True
조 제목 글자수
1 배송 기간 72
2 배송비 82
3 출고 마감 70
4 해외 배송 99
5 선물 포장 64
6 주문 취소 76이 실습이 뜻하는 것
이 실습은 두꺼운 안내 책자를 서류철에 꽂기 좋게 나누는 일과 같습니다. 책자를 펼쳐 분량을 보고(1단계), 항목 사이의 빈 줄을 따라 항목별로 자르고(2단계), 같은 책자를 항목은 보지 않고 자로 재서 일정한 길이마다 잘라 보고(3단계), 끝으로 자른 곳이 조금씩 겹치게 여유를 두어 자르고 장마다 항목 번호와 제목 딱지를 붙입니다(4단계). 여기서 책자가 shop_policy.txt, 항목이 조항, 자로 잰 길이가 80글자, 딱지가 조항 번호와 제목입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 문서 읽기 | 글자 수: 1885 | 정책 문서 전체가 1,885글자입니다. 맨 앞에 지어낸 정책이라는 안내가 있고, 그 뒤로 "제1조(배송 기간)"처럼 조항이 이어집니다 |
| 2. 빈 줄로 나누기 | 조각 수: 25 | 조항 25개가 조각 25개가 되었습니다. 한 조각이 한 조항이라 뜻이 끊기지 않습니다 |
| 2. 조각 길이 | 가장 짧은 44 · 가장 긴 102 · 평균 71 | 조항마다 길이는 다르지만 모두 100글자 안팎으로 짧습니다 |
| 2. 14번째 조각 | 제14조(결제 수단) … 50만원까지입니다. | 휴대폰 결제 한도의 답이 이 조각 안에 온전히 들어 있습니다 |
| 3. 80글자로 자르기 | 80글자 조각 수: 23 | 길이로 자르니 23조각이 되었습니다 |
| 3. 잘린 모습 | '. 휴대폰 결제는 …\n\n제' | 조각이 문장 중간(". 휴대폰")에서 시작해 "제" 한 글자에서 끝납니다. 조항의 머리(제14조)가 앞 조각으로 떨어져 나갔습니다 |
| 4. 겹쳐 자르기 | 겹친 조각 수: 31 · … True | 20글자씩 겹치게 자르니 조각이 31개로 늘었고, "한 달에 50만원까지"가 한 조각 안에 온전히 남았습니다 |
| 4. 조항 표 | 1 배송 기간 72 … | 조각마다 번호와 제목을 뽑아 표로 만들었습니다. 나중에 답과 함께 "몇 조에 있다"는 근거를 보여 줄 수 있습니다 |
한 줄로 줄이면, 이 문서는 빈 줄로 나눈 조항 단위(25조각)가 뜻을 가장 잘 지키고, 글자 수로 자르면 문장이 중간에서 끊긴다는 것을 보인 실습입니다. 실습 3부터는 이 조항 25개를 검색 단위로 씁니다. 어느 나누는 법이 검색을 더 잘 되게 하는지는 이 실습에서 재지 않았고, 7장 실습 5에서 숫자로 견줍니다.
코드 설명
- 빈 줄로 나누면 조각이 25개로 조항 수와 같습니다. 조각은 44~102글자, 평균 71글자이고, 조각마다 "제14조(결제 수단)"처럼 번호와 제목으로 시작합니다.
- 80글자로 자르면 23조각입니다. 이번에는 "한 달에 50만원까지입니다"가 운 좋게 한 조각에 들어갔지만, 그 조각은 제14조의 중간(". 휴대폰 결제는")에서 시작해 "제14조(결제 수단)"라는 머리가 앞 조각에 남았고, 끝은 "제" 한 글자에서 잘려 제16조의 머리가 다음 조각으로 넘어갔습니다. 어디서 잘릴지 글자 수가 정하니, 다른 문서에서는 답이 갈라질 수 있습니다.
- 60글자마다 시작해 20글자씩 겹치면 31조각으로 늘고, "한 달에 50만원까지"가 온전히 든 조각이 있습니다. 겹치기는 조각 수(검색할 양)를 늘리는 대신 잘린 곳의 뜻을 살립니다.
re.match(r"제(\d+)조\((.+?)\)", c)는 조각 맨 앞의 "제14조(결제 수단)"에서 괄호로 묶은 두 부분(번호, 제목)을 뽑습니다. 이 정보가 있으면 답에 근거 조항을 보여 줄 수 있습니다.