허허의 오늘은 뭐 먹지?
LangChain과 OpenAI를 활용한 대화형 문서 검색 만들기 본문
LangChain과 OpenAI Embeddings를 활용해 텍스트 데이터를 벡터화하고, 이를 바탕으로 사용자의 질문에 맞는 답변을 제공하는 대화형 문서 검색 시스템을 구현하려고 한다.
벡터 데이터베이스인 Chroma를 이용해 텍스트의 의미 기반 검색이 가능하도록 설계하며, GPT 모델을 통해 문맥에 맞는 자연스러운 응답을 제공하는 목적이다.
1. 벡터 저장소 및 텍스트 분할
벡터 저장소(Vector Store)는 텍스트 데이터를 임베딩 벡터로 변환한 후 이를 저장하고 검색하는 데이터베이스이다.
여기서는 OpenAI의 임베딩(Embedding) 모델을 사용하여 문서를 벡터로 변환하고, Chroma를 사용해 벡터 데이터를 저장 및 관리한다.
벡터(Vector)란 무엇인가?
벡터란 텍스트 데이터를 수치화하여 다차원 공간에서 표현하는 수학적 개체이다. 각 단어, 문장, 혹은 문서가 벡터로 변환되면 서로의 의미적 유사성을 수치로 비교할 수 있다.
예를 들어, “고양이”와 “강아지”는 둘 다 동물이므로 벡터 공간에서 가깝게 위치하지만, “고양이”와 “자동차”는 의미가 다르기 때문에 멀리 떨어져 있다. 벡터의 개념은 영화 추천 시스템, 음악 추천 시스템 등 다양한 분야에서 사용된다.
예시
• “사과”와 “바나나”는 둘 다 과일이기 때문에 벡터 공간에서 가까운 위치에 있다.
• “사과”와 “자동차”는 관련성이 낮아 벡터 공간에서 멀리 떨어져 있다.
이러한 벡터화된 표현을 바탕으로 시스템은 텍스트 간의 유사성을 계산한다.
임베딩(Embedding)이란 무엇인가?
임베딩은 텍스트를 벡터로 변환하는 과정이다. 각 단어나 문장을 다차원 벡터로 변환하여 컴퓨터가 의미적으로 유사한 텍스트를 수치화할 수 있게 한다. 비슷한 의미의 텍스트는 벡터 공간에서 가까운 좌표를 가지며, 거리가 짧을수록 유사하다는 의미이다.
예를들어 지도에서 서울과 인천이 가깝게 표시되듯이, “강아지”와 “고양이”처럼 의미가 비슷한 단어는 벡터 공간에서도 가까이 위치한다. 반대로 “축구”와 “은행”처럼 연관성이 낮은 단어는 멀리 떨어져 있다.
임베딩의 실제 예시
• “AI”라는 단어의 벡터는 머신러닝, 딥러닝과 가까운 좌표에 위치한다.
• 반면 “AI”와 농업 관련 단어는 멀리 떨어져 있어 연관성이 낮음을 의미한다.
임베딩은 단순한 키워드 매칭을 넘어, 의미적으로 유사한 정보를 검색할 수 있도록 한다.
간단하게 코드로 구현하면 아래와 같다.
import os
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain.text_splitter import CharacterTextSplitter
# OpenAI API 키 설정
os.environ["OPENAI_API_KEY"] = "YOUR_API_KEY"
# 벡터 저장소 경로 및 임베딩 모델 초기화
vectorstore_path = "./data/vectorstore"
embeddings = OpenAIEmbeddings()
# 벡터 저장소 로드 또는 새로 생성
if os.path.exists(vectorstore_path):
vectorstore = Chroma(persist_directory=vectorstore_path, embedding_function=embeddings)
else:
loader = TextLoader('sample_doc.txt')
documents = loader.load()
# 텍스트 분할 설정 (청크 크기 및 겹침 설정)
text_splitter = CharacterTextSplitter(chunk_size=200, chunk_overlap=50)
texts = text_splitter.split_documents(documents)
# 벡터 저장소 생성 및 저장
vectorstore = Chroma.from_documents(documents=texts, embedding=embeddings, persist_directory=vectorstore_path)
긴 문서를 효율적으로 처리하기 위해 chunk_size=200으로 설정해 200자 단위로 문서를 나누고, 청크 간에 chunk_overlap=50을 적용해 겹치는 부분이 생기도록 설정한다. 이는 문맥을 유지하는 데 중요한 역할을 한다.
2. 검색 체인 및 문서 결합 체인 설정
검색 체인은 사용자의 질문과 가장 유사한 문서 청크들을 벡터 저장소에서 검색하는 역할을 한다.
체인을 만들고 실제 설정하는 부분은 아래와 같다.
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# 프롬프트 템플릿 설정
prompt = ChatPromptTemplate.from_messages([
("system", "You are an AI assistant that MUST ONLY use the given context..."),
("human", "CHAT HISTORY: {chat_history} QUESTION: {input}"),
("assistant", "알고 있는 내용을 말씀드릴게요:")
])
# OpenAI GPT 모델 설정
llm = ChatOpenAI(model_name="gpt-4o-mini", streaming=True)
# 문서 결합 체인 생성
document_chain = create_stuff_documents_chain(
llm=llm,
prompt=prompt,
document_variable_name="context"
)
# 검색 체인 설정
retrieval_chain = create_retrieval_chain(
retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),
document_chain=document_chain
)
create_stuff_documents_chain
create_stuff_documents_chain은 LangChain에서 검색된 여러 문서 청크를 결합하여 하나의 응답을 생성하는 체인이다. 검색된 개별 청크만으로는 정보가 불완전할 수 있으므로, 이를 결합하여 풍부한 맥락을 바탕으로 GPT 모델이 답변을 생성하도록 돕는다.
1. 사용자가 입력한 질문과 관련된 문서를 검색해 각 청크를 가져온다.
2. 여러 개의 청크를 결합하여 문맥을 풍부하게 만든다.
3. 결합된 문서를 바탕으로 GPT 모델이 자연스럽고 정확한 답변을 생성한다.
단일 문서나 청크에서 답변을 추출하면 정보가 부족할 수 있으나, 여러 개의 청크를 결합하면 더 정확하고 구체적인 답변을 생성할 수 있다.
검색체인
사용자의 질문에 따라 벡터 저장소에서 관련성 높은 문서 청크를 검색하려는 목적으로 search_kwargs={"k": 3} 은 검색된 3개의 문서 청크를 기반으로 응답을 생성하게 한다.
search_kwargs는 벡터 저장소에서 검색할 문서 청크의 개수를 결정하는 매개변수로, k 값으로 설정된다. 예를 들어 search_kwargs={"k": 3}은 관련 문서 청크 3개를 검색한다는 의미이다.
작은 k 값은 OpenAI API 호출 비용과 메모리 소모가 적지만, 너무 작으면 정보가 부족할 수 있다. 반대로 큰 k 값은 많은 데이터를 결합해 풍부한 응답을 제공하지만, API 호출 횟수와 메모리 소모가 늘어 비용이 증가할 수 있다.
K값은 대부분의 경우 3~5개의 문서를 검색하는 것이 적절하며, 문서의 길이와 시스템의 요구에 따라 최적의 값을 실험적으로 결정해야 한다.
3. 대화형 루프
대화형 루프는 사용자의 입력을 받아 관련 문서를 검색하고, GPT 모델을 사용해 최종 응답을 생성한다. 대화 내역을 유지하여 연속된 문맥에서 질문과 응답을 처리할 수 있다.
복잡하진 않으나 이렇게 프로그램을 마무리 해본다.
chat_history = []
print("대화를 시작합니다. 종료하려면 'quit' 또는 '종료'를 입력하세요.")
while True:
question = input("\n질문을 입력하세요: ")
if question.lower() in ['quit', '종료']:
print("대화를 종료합니다.")
break
print("\n답변: ", end="", flush=True)
for chunk in retrieval_chain.stream({"input": question, "chat_history": chat_history}):
if "answer" in chunk:
print(chunk["answer"], end="", flush=True)
print()
# 응답 저장 및 대화 내역 업데이트
result = retrieval_chain.invoke({"input": question, "chat_history": chat_history})
chat_history.append((question, result["answer"]))
'SW > 메모' 카테고리의 다른 글
Component and Connector View 에 대해 (0) | 2025.01.22 |
---|---|
DB Lock을 방지하기 위한 처리 (Python) (0) | 2025.01.22 |
추천 - 협업 필터링(Collaborative Filtering) (0) | 2025.01.22 |
추천시스템에서의 알고리즘 정리 (0) | 2025.01.22 |
git 비밀번호를 자꾸 물어볼때. (0) | 2023.08.09 |