6장. 문서 기반 챗봇
텍스트 나누기(청킹)
피자를 한 판 통째로 입에 넣을 수는 없고, 너무 잘게 부수면 무슨 피자인지 모릅니다. 보통은 여덟 조각쯤으로 자르고, 칼이 지나간 자리의 토핑이 양쪽 조각에 조금씩 걸치게 둡니다. 청킹이 이 일입니다. 쪽 하나를 검색하기 좋은 크기로 자르되, 경계의 글이 양쪽 조각에 조금 겹치게 둡니다.
긴 막대가 쪽 하나의 글이고, 그 아래 세 막대가 잘린 조각입니다. 위에서 내려오는 점선은 조각이 글의 어디서 시작하는지 가리킵니다. 막대의 길이가 chunk_size(조각의 최대 글자 수), 이웃한 막대가 겹친 페리윙클(연보라) 부분이 chunk_overlap(겹치는 글자 수)입니다. 여기서 피자 한 판이 쪽의 글, 한 조각의 크기가 chunk_size, 양쪽에 걸친 토핑이 chunk_overlap 입니다. 비유와 다른 점은, RecursiveCharacterTextSplitter 는 정확한 자리에서 칼질하지 않고 문단(\n\n), 줄(\n), 빈칸( ) 순서로 자를 만한 곳을 찾아 자른다는 것입니다. 그래서 조각의 길이가 들쭉날쭉하고, 겹침도 정한 수보다 짧을 수 있습니다.
조각이 너무 크거나 작으면
| 조각 크기 | 좋은 점 | 나쁜 점 |
|---|---|---|
| 너무 작다(문장 반쪽) | 질문과 딱 맞는 곳만 집는다 | 뜻이 잘려 앞뒤를 모른다. 조각 수가 많아 색인이 커진다 |
| 알맞다(조항 하나) | 한 조각에 한 가지 규정이 온전히 든다 | 문서마다 알맞은 크기를 찾아야 한다 |
| 너무 크다(쪽 하나) | 앞뒤 맥락이 다 들어 있다 | 여러 규정이 섞여 검색이 흐려진다. 프롬프트가 길어진다 |
RecursiveCharacterTextSplitter 의 인자
| 인자 | 뜻 | 이 장의 값 |
|---|---|---|
chunk_size | 조각 하나의 최대 길이(글자 수) | 100 ~ 1000, 기본으로 200 |
chunk_overlap | 이웃 조각이 겹칠 최대 글자 수 | 30 |
separators | 자를 곳을 찾는 차례. 기본은 문단, 줄, 빈칸, 글자 | 기본값 그대로 |
split_text(글) | 글 하나를 글 조각 목록으로 | 본문 예시 |
split_documents(목록) | Document 목록을 조각 Document 목록으로. metadata 를 그대로 물려준다 | 실습 2 |
짧은 예시
약관 제5조 한 덩어리를 chunk_size=60, chunk_overlap=20 으로 잘라 봅니다. 겹친 곳이 보이도록 일부러 작게 잡았습니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = ("출발 10일 전까지 취소하면 낸 돈을 전액 돌려준다. 출발 7일 전부터 9일 전까지는 상품 금액의 10%, "
"3일 전부터 6일 전까지는 30%, 1일 전과 2일 전은 50%를 취소 수수료로 뗀다.")
splitter = RecursiveCharacterTextSplitter(
chunk_size=60, # 조각 하나의 최대 글자 수
chunk_overlap=20, # 이웃 조각과 겹칠 최대 글자 수
)
chunks = splitter.split_text(text)
print("원래 글자 수:", len(text), "/ 조각 수:", len(chunks))
for c in chunks:
print(f"{len(c):3}자 | {c}")원래 글자 수: 108 / 조각 수: 3
59자 | 출발 10일 전까지 취소하면 낸 돈을 전액 돌려준다. 출발 7일 전부터 9일 전까지는 상품 금액의 10%,
59자 | 9일 전까지는 상품 금액의 10%, 3일 전부터 6일 전까지는 30%, 1일 전과 2일 전은 50%를 취소
28자 | 1일 전과 2일 전은 50%를 취소 수수료로 뗀다.108자짜리 글이 59자, 59자, 28자의 세 조각이 되었습니다. 첫 조각의 끝 "9일 전까지는 상품 금액의 10%,"가 둘째 조각의 맨 앞에 다시 나오고, 둘째 조각의 끝 "1일 전과 2일 전은 50%를 취소"가 셋째 조각의 앞에 다시 나옵니다. 이것이 겹침입니다. 분할기는 빈칸에서 잘랐기 때문에 낱말이 반으로 잘리지는 않았지만, 문장은 "50%를 취소"와 "수수료로 뗀다" 사이에서 끊겼습니다. 겹침이 없었다면 "1일 전과 2일 전은 50%"라는 규정이 두 조각에 반씩 흩어졌을 것입니다.
코드가 의미하는 것
이 코드는 피자 한 판에 칼 크기와 걸칠 토핑의 양을 정해 주고 잘라 보는 일입니다. 분할기를 만들고(크기와 겹침 정하기), 글을 자르고, 조각마다 길이를 재는 세 동작입니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
RecursiveCharacterTextSplitter(chunk_size=60, …) | 조각 하나의 최대 글자 수를 60 으로 정한다 | 검색 한 번에 꺼내 올 근거의 크기를 정하는 일입니다. 이 숫자가 검색의 정확도와 프롬프트의 길이를 함께 좌우합니다 |
chunk_overlap=20 | 이웃 조각이 최대 20자까지 겹치게 한다 | 경계에서 잘린 규정이 어느 한 조각에는 온전히 남게 하는 보험입니다 |
splitter.split_text(text) | 글 하나를 글 조각 목록으로 나눈다 | 색인 단계의 "나누기" 칸입니다. 이 조각 하나하나가 나중에 임베딩 하나, 검색 결과 하나가 됩니다 |
len(c) | 조각마다 글자 수를 잰다 | 정한 크기를 넘지 않는지, 조각이 들쭉날쭉한지 확인합니다 |
결과에서 읽을 것은 두 가지입니다.
- 조각은 60자를 넘지 않지만 길이가 같지 않습니다. 59자, 59자, 28자입니다. 분할기가 빈칸에서 자를 곳을 찾기 때문에 정한 크기보다 조금 짧게 끊기고, 마지막 조각은 남은 글만큼만 됩니다.
- 겹친 글은 두 조각에 모두 들어갑니다. "1일 전과 2일 전은 50%를 취소"가 둘째와 셋째 조각에 함께 있습니다. 손님이 "하루 전 취소"를 물으면 어느 조각을 찾아도 이 규정을 볼 수 있습니다.
주의할 점
chunk_size는 토큰이 아니라 글자 수입니다(기본length_function=len). 한글은 글자 하나가 토큰 하나 안팎이라 대략 비슷하지만 같지는 않습니다.- 겹침을 크게 하면 같은 글이 여러 조각에 들어가 색인이 커지고, 검색 결과에 거의 같은 조각이 둘씩 나올 수 있습니다. 조각 크기의 10~20% 안팎에서 시작해 봅니다.
- 약관, 법령, 설명서처럼 "제N조", "1.", 소제목으로 짜인 문서는 글자 수보다 그 짜임대로 자르는 것이 낫습니다. 실습 2에서 조항 단위 나누기와 견줍니다.