반응형
왜 청크로 나누어야 할까? (청킹의 이유)
- 문서가 너무 크기 때문
GPT나 임베딩 모델이 한 번에 처리할 수 있는 토큰(token) 수에는 제한이 있음 - 검색 정확도를 높이기 위해서
전체 문서를 통으로 벡터화하면 "어느 부분이 관련 있는지"를 찾기 어렵다
→ 그래서 문서를 문단, 문장 등 "작은 단위"로 나눠서 그 조각들을 각각 벡터화함
→ 검색할 땐 관련된 조각만 찾아서 답변으로 활용할 수 있다 - RAG (Retrieval-Augmented Generation) 기반 챗봇에 필수
예: "우리 회사 정책이 어떻게 돼?"라는 질문에 문서 중 관련 청크를 찾아서 GPT에게 전달해주면
→ GPT가 맥락에 맞게 답변해주는 구조
벡터 DB(Vector Database)란?
문자나 문서의 의미(semantic)를 숫자 벡터로 변환해 저장하는 데이터베이스
- 예: "사과"와 "배"는 과일이니까 벡터 공간에서도 가까이 있음
- 유사한 벡터를 빠르게 검색할 수 있는 데 특화됨.
- 일반 RDB는 '이름이 똑같은 것'을 찾는 거라면
- 벡터 DB는 '의미가 비슷한 것'을 찾아주는 것
- 대표적인 벡터 DB 예시:
- Pinecone, Weaviate, FAISS (Facebook), Qdrant, Milvus
- 하지만 현재 포스트에서는 ChromaDB ( 오픈소스 벡터 데이터베이스) 를 사용할 것이다
프로세스....
- 문서를 작게 쪼개서 (청킹)
- 각 조각을 의미 기반의 숫자 벡터로 변환하고 (임베딩)
- 그걸 벡터 DB에 저장해두면
- 나중에 질문이 들어올 때, 관련 있는 청크를 찾아서
- LLM에 전달해주고
- 모델이 문서 기반의 정확한 답을 생성해주는 구조 (RAG)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
# 1. RecursiveCharacterTextSplitter로 청크 단위로 나누기
text_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200)
# 청크 크기와 중복 설정
chunks = []
for doc in docs:
# 각 문서의 page_content를 청크로 나누기
split_chunks = text_splitter.create_documents([doc.page_content])
chunks.extend(split_chunks)
print(f"총 {len(chunks)}개의 청크가 생성되었습니다.")
# 2. OpenAIEmbeddings로 임베딩 생성
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 모델 명시
# 3. ChromaDB에 저장
vector_db = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_Web", # 디렉토리 명시
collection_metadata={'hnsw:space': 'l2'} # l2 메트릭 설정
)
print("청크가 ChromaDB에 임베딩 및 저장되었습니다.")
참고로 Chroma DB는 오픈 소스이고 LangChain은 그걸 연동해주는 걸 도와주는 역할을 한다.
또한 벡터화는 OpenAI 의 임베딩 모델을 사용한다
l2 메트릭??
벡터간의 유사도를 계산할 때 L2 메트릭을 사용한다고 설정하였다. L2 메트릭은 유클라디안 거리라고도 한다.
벡터 DB에서 유사도 검색할 때 자주 씀
- 사용자가 입력한 쿼리 벡터와 DB에 저장된 청크 벡터 사이의 거리를 계산해서
- 거리가 가까울수록 더 유사한 것으로 간주

다음 포스트에서는 RAG를 위한 프롬프팅과 chain을 구성하며 마무리 포스트를 지을 예정이다.
반응형