RAG의 전체 구성 과정 (LangChain 실습)
RAG의 전체 구성 과정
- 문서의 내용을 읽는다.
- 문서를 쪼갠다.
- 임베딩
- 질문이 있을 때 백터 데이터베이스에 유사도 검색을 한다.
- 유사도 검색으로 가져온 문서를 LLM에 질문과 같이 전달한다.
1. 문서의 내용을 읽는다. (Document Loader 사용)
Document Loader란?
→ LangChain에서 다양한 형식의 원본 문서를 읽어서 표준화된 Document 객체로 변환해주는 컴포넌트
# Document 객체의 구조
Document(
page_content = "여기에 실제 텍스트 내용이 들어감...",
metadata = {"source" : "./tax.docx"}
)
from langchain_community.document_loaders import Docx2txtLoader
loader = Docx2txtLoader("./tax.docx")
2. 문서를 쪼갠다. (Text Splitter 사용)
문서를 쪼개는 이유는 아래와 같다.
- 토큰 수 초과로 답변을 생성하지 못할 수 있고
- 문서가 길면 답변 생성이 오래 걸림
Text Splitter란? → 긴 문서를 작은 조각(chunk)으로 쪼개는 컴포넌트이다. Document Loader가 문서를 읽어온 다음 Text Splitter가 그 문서를 잘게 나눠주는 역할을 한다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 1500, # 문서를 쪼갤 때 하나의 chunk의 최대 길이
chunk_overlap = 200 # 문서를 쪼갤 때 chunk간의 겹치는 길이
)
document_list = loader.load_and_split(text_splitter=text_splitter)
# 나눠진 문서를 배열 형태로 받을 수 있다.
3. 임베딩
임베딩이란? → 텍스트를 컴퓨터가 의미 단위로 비교할 수 있는 숫자 벡터로 변환하는 과정이다.
예를들면 "소득세 계산 방법"과 "세금 산출 방식"은 사람 눈에는 비슷한 의미지만 컴퓨터에게는 그냥 다른 문자열일 뿐이다.
이런 텍스트를 예를들어 [0.03, -0.21, 0.88, ...]처럼 수백~수천 차원의 숫자 배열로 바꿔주는데 의미가 비슷한 문장일수록 벡터 공간에서 서로 가까운 위치에 놓이도록 학습되어 있다.
from langchain_upstage import UpstageEmbeddings
load_dotenv()
embeddings = UpstageEmbeddings(model="solar-embedding-2-query")
4. 데이터베이스에서 유사도 검색 (ChromaDB)
ChromaDB란? → 임베딩(벡터)를 저장하고 나중에 유사도 검색을 해주는 벡터 데이터베이스이다.
일반 DB는 "정확히 일치하는 값"을 찾는데 특화되어 있지만 벡터 DB는 "의미상 가장 비슷한 것"을 찾는 데 특화되어 있다. 저장된 수많은 벡터들 중에서 질문 벡터와 거리가 가장 가까운 것들을 빠르게 찾아내는 게 핵심 기능이다.
from langchain_chroma import Chroma
database = Chroma(
collection_name="chroma_tax_v2",
embedding_function=embeddings,
persist_directory="./chroma_v2"
)
collection_name: 벡터DB 안에서 데이터 묶음을 구분하는 이름embedding_function: Chroma는 텍스트를 직접 벡터로 바꾸는 능력은 없음 → 텍스트가 들어오면 "이 함수(모델)를 써서 벡터로 변환해" 라고 알려주는 역할persist_directory: 벡터 데이터가 실제로 저장되는 디스크 경로
5. LLM에게 전달 (retrieved QA chain 사용)
retrieved QA chain란? → Retriever(문서 검색기)가 찾아온 관련 문서를 LLM에 넘겨서 그 문서 내용을 근거로 질문에 답하는 문장을 생성하게 만드는 체인
langchain hub란? 다른 사람들이 만들어 놓은 프롬프트(prompt), 체인(chain), 에이전트 설정 등을 검색하고 가져와 쓸 수 있는 공유 저장소
# 동작 흐름
qa_chain.invoke({"query" : "연봉 5천만원인 직장인의 소득세는 얼마인가요?"})
1. retriever가 ChromaDB에서 질문과 유사한 문서 조각 검색
2. 검색된 문서(context) + 질문(question)을 prompt에 끼워넣음
3. 완성딘 프롬프트를 llm에 전달
4. LLM이 문서 내용을 근거로 답변 생성
결과: {'query': '...', 'result': '연봉 5천만원인 직장인의 소득세는 554만원입니다.'}
# query와 llm 생성
query = "연봉 5천만원인 직장인의 소득세는 얼마인가요?"
from langchaian_upstage import ChanUpstage
llm = ChatUpstage()
# 프롬프트 가져오기
from langsmith import Client
client = Client()
prompt = client.pull_prompt(
'rlm/rag-prompt',
include_model=False,
dangerously_pull_public_prompt=True
)
Client: LangSmith 서버에 접속하는 클라이언트 객체 생성client.pull_prompt: rlm 이라는 사용자가 올려둔 rag-prompt 라는 프롬프트 템플릿을 원격에서 다운로드include_model=False: 프롬프트에 딸려올 수 있는 모델 설정은 제외하고 순수 프롬프트 텍스트만 가져옴dangerously_pull_public_prompt=True: 공개 프롬프트를 신뢰하고 가져오겠다는 명시적 동의
# RetrievalQA 체인 조립
from langchain_classic.chains import RetrievalQA
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
retriever = database.as_retriever(),
chain_type_kwargs={"prompt": prompt}
)
retriever=database.as_retriever(): ChromaDB(database)를 검색 도구로 변환, 질문이 들어오면 여기서 유사한 문서를 찾아옴chain_type_kwargs={"prompt": prompt}: 검색된 문서(context)와 질문(question)을 조합할 때 방금 위에서 가져온rlm/rag-prompt템플릿을 사용하라고 지정
# 최종적으로 qa_chain이 하는 일
ai_message = qa_chain({"query": query})
ai_message
# 결과
{'query': '연봉 5천만원인 직장인의 소득세는 얼마인가요?',
'result': '연봉 5천만원인 직장인의 소득세는 554만원입니다. (지방세 포함)'}
👀 프로세스 시각화
✅ pinecone으로 DB 변경하기
pinecone이란? → Chroma와 같은 벡터 데이터베이스 서비스이지만 Chroma는 inbase인 반면 pinecone을 클라우드 기반 관리형 서비스이다.
from langchain_pinecone import PineconeVectorStore
# 데이터를 처음 저장할 때
index_name = 'tax-upstage-index'
database = PineconeVectorStore.from_documents(
document_list,
embedding,
index_name=index_name
)