랭체인을 활용한 RAG의 전체적인 흐름은 다음과 같다.

DocumentLoader 를 통해 url, pdf, 혹은 docx 형태의 텍스트 문서를 들고와서 그걸 VectorDB에 저장하기 위해 Chunk 단위로 자르는 과정이 들어가는데, 이를 TextSplitter 모듈로 수행한다.
TextSplitter
TextSplitter의 종류에는 크게 두 가지가 있다. 하나는 CharacterTextSplitter, 또 다른 하나는 RecursiveCharacterTextSplitter.
CharacterTextSplitter: 가장 간단한 형태로, 문자 개수 혹은 줄바꿈 같이 특정한 기준으로 텍스트를 잘라준다. 그러나 이런 경우엔 token 수가 고려되지 않아 RAG용으론 좋지 않다.RecursiveCharacterTextSplitter: 텍스트 분할 시 재귀적으로(Recursive) 여러 레벨의 기준을 적용할 수 있는 모듈이다. 문단 기준, 문장 기준, 특정 구분자 기준으로 나눌 수 있으므로 token 개수를 맞출 수 있다는 장점이 있다.RecursiveCharacterTextSplitter 테스트
먼저 필요한 모듈을 불러온 후, 테스트용 문자열을 만든다. 그리고 해당 모듈로 문자열을 잘라서 결과가 어떻게 나오나 확인해본다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=10)
# 테스트용 텍스트
texts = [
"안녕하세요. 반갑습니다. 저는 한국인입니다. 저는 한국어를 할 수 있습니다." * 5,
"Hello, nice to meet you. I am Korean. I can speak Korean."
]
metadatas = [{"source": "text1"}, {"source": "text2"}]
documents = splitter.create_documents(texts, metadatas=metadatas)
print(documents)
결과
[Document(metadata={'source': 'text1'}, page_content='안녕하세요. 반갑습니다. 저는 한국인입니다. 저는 한국어를 할 수 있습니다.안녕하세요.'),
Document(metadata={'source': 'text1'}, page_content='반갑습니다. 저는 한국인입니다. 저는 한국어를 할 수 있습니다.안녕하세요. 반갑습니다.'),
Document(metadata={'source': 'text1'}, page_content='반갑습니다. 저는 한국인입니다. 저는 한국어를 할 수 있습니다.안녕하세요. 반갑습니다.'),
Document(metadata={'source': 'text1'}, page_content='반갑습니다. 저는 한국인입니다. 저는 한국어를 할 수 있습니다.안녕하세요. 반갑습니다.'),
Document(metadata={'source': 'text1'}, page_content='반갑습니다. 저는 한국인입니다. 저는 한국어를 할 수 있습니다.'),
Document(metadata={'source': 'text2'}, page_content='Hello, nice to meet you. I am Korean. I can speak'),
Document(metadata={'source': 'text2'}, page_content='can speak Korean.')]
결과는 Document 객체를 담은 리스트로 나온다.
파일에 적용하기
Docx2txtLoader를 이용해 불러온다.# Docx2txtLoader
from langchain.document_loaders import Docx2txtLoader
path = '파일위치/파일명.docx'
loader = Docx2txtLoader(path)
docx_data = loader.load_and_split()
# docx_data 구조
# [
# Document(metadata={'source': '파일경로'}, page_content='...'),
# Document(metadata={'source': '파일경로'}, page_content='...'),
# Document(metadata={'source': '파일경로'}, page_content='...'),
# Document(metadata={'source': '파일경로'}, page_content='...'),
# ]
create_documents() 의 인풋 파라미터
texts: list[str]metadatas: list[dict]에 맞게 데이터를 가공한다.
그리고 적절한 chunk_size, chunk_overlap, length_function 값으로 RecursiveCharacterTextSplitter 객체를 만들고 create_documents() 함수로 결과물을 얻는다.
# 데이터 가공
page_contents= []
metadatas = []
for i in range(len(docx_data)):
page_contents.append(docx_data[i].page_content)
metadatas.append(docx_data[i].metadata)
# 객체 생성
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=100, length_function=len
)
docx_data=splitter.create_documents(page_contents, metadatas)
print(docx_data)
토큰 크기에 맞춰서 자르기
TextSplitter는 기본적으로 len() 함수로 글자 수를 계산해서 자른다. 그런데 LLM 프롬프트에 넣기 위해서는 최대 허용 토큰을 넘지 않아야 한다. 그러면 기존의 len() 함수가 아닌 해당 문자열의 토큰 개수를 세주는 함수를 사용해야 한다. 여기서 토큰은 문자열을 특정 단위로 잘라 숫자들의 집합인 벡터로 만든 것을 의미하는데, gpt-3.5, gpt-4 등 모델마다 토큰을 만드는 인코딩 모델이 달라서 내가 사용하고자 하는 모델이 어떤 인코딩 모델을 사용하는지 알아야 LLM 프롬프트에 올리기 전에 이 문자열이 얼마만큼의 토큰을 차지하는지 계산할 수 있다. gpt-4o-mini 모델의 경우에는 o200k_base 를 인코딩 모델로 사용하고 있다.링크
다행히도 문자열의 토큰 개수를 세주는 함수는 t=iktoken 패키지를 이용해 쉽게 만들 수 있다. 이를 RecursiveCharacterTextSplitter() 의 length_function 패러미터에 넣어주면 TextSplitter는 문자열의 개수가 아닌 토큰 개수를 기준으로 잘라줄 수 있다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import Docx2txtLoader
from pprint import pprint
import tiktoken
def token_len(text) -> int:
tokens = tiktoken.encoding_for_model('gpt-4o-mini').encode(text)
return len(tokens)
path = '파일위치/파일명.docx'
loader = Docx2txtLoader(path)
docx_data = loader.load_and_split()
page_contents= []
metadatas = []
for i in range(len(docx_data)):
page_contents.append(docx_data[i].page_content)
metadatas.append(docx_data[i].metadata)
# length_function을 len() 이 아닌 token_len() 으로 사용.
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=100, length_function=token_len
)
docx_data=splitter.create_documents(page_contents, metadatas)
pprint(docx_data)