5. 벡터 임베딩 생성
OpenAI 모델을 사용했기 때문에 OpenAIEmbeddings를 이용해 텍스트를 벡터로 변환할 벡터 임베딩을 생성했다. langchain_openai 라이브러리에서 OpenAIEmbeddings 클래스를 불러온다.
from langchain_openai import OpenAIEmbeddings
# OpenAI 임베딩 모델 초기화
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
text-embedding-ada-002 모델을 사용하여 텍스트 데이터를 임베딩 벡터로 변환한다. 이 모델은 텍스트 데이터를 고차원 벡터로 변환하는 데 사용된다. 자연어 처리(NLP) 작업에서 텍스트를 벡터 공간으로 변환하여, 유사도 검색, 클러스터링, 분류 등 다양한 작업에 활용된다.
6. 벡터 스토어 생성
앞서 만든 벡터 임베딩과 청크된 문서를 활용하여 FAISS 벡터 스토어를 생성했다.
import faiss
from langchain_community.vectorstores import FAISS
vectorstore = FAISS.from_documents(documents=splits_RCT, embedding=embeddings)
FAISS(Facebook AI Similarity Search)는 고속 유사도 검색을 위한 라이브러리다. 주로 벡터 데이터베이스에서 빠르게 유사한 항목을 검색하는 데 사용된다.
FAISS의 역할을 알아보자. FAISS는 벡터화된 데이터를 저장하고, 주어진 벡터와 가장 유사한 벡터를 효율적으로 검색할 수 있도록 해준다. 이 때문에 텍스트 데이터가 벡터 형태로 변환되면, 의미가 비슷한 문서들을 빠르게 찾을 수 있다.
langchain_community.vectorstores는 LangChain의 확장 모듈인 Community 모듈에서 제공되는 것으로, 여러 종류의 벡터 데이터베이스와 연동하여 데이터를 저장하고 검색할 수 있는 기능을 제공한다. 이 모듈에서 FAISS 클래스를 제공한다. 여기서 FAISS는 벡터스토어로 사용된다.
FAISS.from_documents는 splits_RCT라는 문서 목록을 받아들여, 각 문서를 임베딩 모델(embeddings)을 사용하여 벡터로 변환한 후, 이 벡터들을 FAISS 벡터스토어에 저장하는 역할을 한다. splits_RCT는 우리가 pdf 문서들을 chunk 단위로 나눈 데이터다. 이 데이터를 벡터로 변환하여 검색할 수 있게 documents에 저장한다. embedding에는 텍스트 임베딩 모델을 지정한다. 여기서는 OpenAI의 text-embedding-ada-002 모델을 사용하여 문서 벡터를 생성한다.
즉, 이 코드는 주어진 문서들을 임베딩 모델을 통해 벡터화한 후, FAISS를 이용해 빠르게 검색할 수 있는 형태로 벡터를 저장하는 과정을 구현한다. 이를 통해 텍스트 데이터를 벡터화하고, 이후 유사도 검색 등을 효율적으로 수행할 수 있다.
7. FAISS를 Retriever로 변환
RAG 체인에서 사용할 수 있도록 FAISS를 retriever로 변환하자.
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 1})
RAG (Retrieval-Augmented Generation) 체인에서 FAISS를 retriever로 변환하는 이유는 검색 기반 텍스트 생성을 효율적으로 수행하기 위해서다. RAG 체인에서는 검색과 생성의 두 단계가 중요한 역할을 한다. 이를 통해 모델은 주어진 질문에 대한 답변을 더 잘 생성할 수 있도록 외부 지식을 활용한다.
RAG 체인의 작동 원리를 간단히 살펴보자.
- 검색 단계 (Retriever)
- 먼저 질문에 대해 관련 문서를 검색한다. 이때 FAISS 벡터스토어를 사용하여, 주어진 질문에 대해 가장 유사한 문서를 빠르게 찾아낸다.
- FAISS는 이미 문서들이 벡터로 변환되어 저장되어 있기 때문에, 유사도 검색이 매우 효율적이다.
- 생성 단계 (Generator)
검색된 문서들을 바탕으로 답변을 생성한다. 이때, 생성 모델은 검색된 문서에서 중요한 정보를 추출하여 이를 바탕으로 자연스러운 답변을 만든다.
왜 FAISS를 retriever로 사용할까?
- 고속 유사도 검색 : FAISS는 고차원 벡터에서 유사한 항목을 빠르게 찾을 수 있도록 최적화된 라이브러리다. 대규모 데이터셋에서 유사한 문서를 빠르게 검색할 수 있다는 점에서 RAG 체인의 검색 단계에서 중요한 역할을 한다.
- 외부 지식 활용 : RAG 체인에서는 모델이 외부의 대규모 지식을 활용할 수 있어야 한다. FAISS는 벡터 형태로 저장된 문서들을 빠르게 검색하여 외부 지식을 제공한다. 이렇게 검색된 문서는 모델의 응답을 개선하는 데 중요한 역할을 한다.
as_retriever() 메서드는 vectorstore를 retriever로 변환하는 역할을 한다. 이 메서드는 벡터 데이터베이스에서 검색을 수행할 수 있도록 변환해 준다. 여기서는 vectorstore는 우리가 6번에서 지정했듯이 FAISS 벡터 데이터베이스를 가리키는 객체다. 즉, vectorstore는 이전에 문서들이 벡터화된 후 저장된 데이터베이스다. 따라서 이 메서드는 벡터 데이터를 기반으로 유사도 검색을 할 수 있는 retriever를 생성한다.
search_type은 검색 방식에 대해 정의한다. "similarity"는 유사도 검색을 의미한다. 즉, 사용자가 질의를 입력하면 질의와 유사한 문서를 찾아주는 방식이다. 이는 FAISS와 같은 벡터 기반 검색을 사용할 때 일반적으로 쓰이는 설정이다. 벡터화된 문서와 입력된 질의 벡터 간의 유사도를 계산하여 가장 비슷한 문서를 반환한다.
search_kwargs는 검색 시 추가적인 매개변수를 설정하는 부분이다. 이는 여러 매개변수를 담을 수 있도록 설계된 딕셔너리다. 그래서 {"k": 1} 처럼 키-값 쌍 형태로 사용해야 한다. {"k": 1}은 검색 결과에서 가장 유사한 1개의 문서만 반환하도록 지정한 것이다. 여기서 k는 top-k 검색 결과에서 몇 개의 문서를 반환할 것인지 정의한다.
내용을 정리하면 아래와 같다.
이 코드는 vectorstore에 저장된 벡터 데이터베이스에서 가장 유사한 문서 1개를 검색하는 retriever를 설정하는 코드다. 사용자가 질의를 입력하면, search_type="similarity"에 의해 유사도 검색이 실행되고, k=1 설정에 따라 가장 유사한 문서 1개만 검색해서 반환한다.
예를 들어, 만약 사용자가 "What is the capital of France?"라는 질문을 입력하면, vectorstore.as_retriever()는 저장된 벡터 데이터베이스에서 이 질문과 가장 유사한 문서 1개를 검색하고 반환한다. 검색된 문서는 retriever를 통해 후속 처리나 생성 모델에 전달될 수 있다.
즉, retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 1})는 vectorstore에 저장된 벡터 데이터를 이용해 가장 유사한 문서를 1개 반환하는 retriever를 설정하는 코드다. 이 retriever는 RAG 체인 등에서 질문-응답 시스템의 검색 단계로 사용될 수 있다.
8. 프롬프트 템플릿 정의
프롬프트 템플릿을 정의해보자.
# 필요한 모듈을 langchain_core에서 임포트
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
# 프롬프트 템플릿 정의
contextual_prompt = ChatPromptTemplate.from_messages([
("system", "Answer the question using only the following context."),
("user", "Context: {context}\\n\\nQuestion: {question}")
])
ChatPromptTemplate 클래스는대화형 프롬프트 템플릿을 정의하는 클래스다. 시스템 메시지와 사용자 메시지를 설정하고, context와 question을 템플릿에서 사용할 수 있도록 정의한다. 즉, 사용자와 시스템 간의 대화 형태로 구성된 프롬프트를 정의하는 데 사용된다.
ChatPromptTemplate.from_messages() 함수는 대화에서 사용하는 템플릿을 만드는 메서드다. 두 가지 메시지 유형(system과 user)을 정의하며, 이를 통해 시스템은 특정 방식으로 응답하고 사용자는 질문과 문맥을 입력한다.
이 코드에서는 시스템과 사용자 메시지를 포함하는 템플릿을 만든다.
시스템 메시지는 프롬프트에서 어떤 방식으로 응답해야 하는지 명시한다. 여기서는 "다음 문맥만을 사용하여 질문에 답하라"는 지시가 포함된다. 즉, 사용자가 입력한 질문에 대한 답변을 제공하기 전에 Context만을 사용하라고 지정하고 있다.
사용자 메시지는 실제로 제공될 정보다. {context}는 문맥 정보이고, {question}은 사용자가 묻는 질문이다. 즉, context와 question을 변수로 사용하여 사용자로부터 정보를 받는다. 따라서 템플릿에서 {context}와 {question}는 나중에 실제 사용자가 제공한 데이터로 대체된다.
9. RAG 체인 구성
LangChain의 모델과 프롬프트를 연결하여 RAG 체인을 구성해보자. 먼저, 아래의 코드 전체를 요약하자면, Langchain을 사용하여 질문 응답 시스템을 구성하고, 중간 과정의 각 단계에서 디버깅을 추가하고, 문서 리스트를 텍스트로 변환하여 모델이 입력으로 받을 수 있게 처리하는 구조다.
class DebugPassThrough(RunnablePassthrough):
def invoke(self, *args, **kwargs):
# 부모 클래스의 invoke 메서드를 호출하여 처리된 결과를 받아온다.
output = super().invoke(*args, **kwargs)
# 처리된 결과를 출력하여 디버깅 용도로 확인한다.
print("Debug Output:", output)
# 처리된 결과를 그대로 반환한다.
return output
여기서는 RunnablePassthrough 클래스를 상속받은 _**DebugPassThrough**_ 클래스를 정의한다. 이 클래스는 입력된 데이터를 그대로 전달하면서, 중간 결과를 디버깅 용도로 출력한다.
class ContextToText(RunnablePassthrough):
def invoke(self, inputs, config=None, **kwargs): # config 인수도 받을 수 있도록 설정
# context의 각 문서를 텍스트로 결합한다.
context_text = "\n".join([doc.page_content for doc in inputs["context"]])
# 결합된 텍스트와 사용자 질문을 함께 반환한다.
return {"context": context_text, "question": inputs["question"]}
여기서는 문서 리스트를 텍스트로 변환하는 _**ContextToText**_ 클래스를 정의한다. 'RunnablePassthrough'를 상속받아, context의 각 문서를 텍스트로 결합하는 기능을 수행한다.
rag_chain_debug = {
"context": retriever, # retriever는 context를 가져오는 단계다.
"question": DebugPassThrough() # DebugPassThrough는 question을 그대로 전달하며 디버깅을 출력한다.
} | DebugPassThrough() | ContextToText() | contextual_prompt | model # 각 단계에 디버깅과 텍스트 변환을 추가한 파이프라인
RAG 체인에서 각 단계에 DebugPassThrough를 추가했다. rag_chain_debug는 질문 응답 시스템의 각 단계를 정의한 파이프라인이다. retriever는 문서에서 관련된 context를 가져온다. DebugPassThrough는 사용자의 질문이 잘 전달되는지 확인하고, 디버깅 출력을 확인한다. 이후 ContextToText가 문서 리스트를 텍스트로 변환하고, contextual_prompt로 위에서 정의한 템플릿을 사용하여 질문에 대한 답변을 생성하고, model로 모델을 호출하여 답변을 생성한다.
10. 챗봇 구동 확인
질문에 응답하는 챗봇을 구동하여 질문해보자. 그리고 같은 질문을 일반 chat gpt 혹은 Gemini에 질문해보고 답변을 비교해보고, 왜 RAG가 필요한지 간단히 markdown으로 서술해보자.
우선, 아래의 코드는 사용자로부터 질문을 입력받고, 이를 rag_chain_debug 체인을 통해 처리한 후 최종 응답을 출력하는 반복문이다.
while True:
# 사용자에게 질문을 입력하라는 메시지를 출력
print("========================")
query = input("질문을 입력하세요: ") # 사용자로부터 질문을 입력받음
# 'rag_chain_debug' 체인을 호출하여 질문을 처리하고 응답을 받음
response = rag_chain_debug.invoke(query)
# 'Final Response:'라는 메시지를 출력하여 최종 응답을 나타냄
print("Final Response:")
# 'response.content'는 모델이 반환한 응답의 내용을 출력한다.
print(response.content)
while문을 통해 무한 루프를 시작한다. 이 루프는 사용자가 질문을 입력할 때마다 계속 반복된다. 루프를 종료하려면 break 명령어를 사용하거나 프로그램을 강제로 종료해야 한다.
query = input("질문을 입력하세요: ")는 input() 함수를 사용하여 사용자가 질문을 입력할 수 있게 한다. 이 입력값은 query 변수에 저장된다.
rag_chain_debug.invoke(query)를 호출하여, 사용자가 입력한 질문(query)을 rag_chain_debug 체인을 통해 처리한다. 이 체인은 여러 단계를 거쳐서 질문에 대한 답변을 생성한다.
rag_chain_debug는 앞서 10. RAG 체인 구성에서 정의했다시피, 문서 검색, 질문 전달, 디버깅 출력, 텍스트 변환, 프롬프트 처리 등을 포함한 파이프라인으로 질문을 처리한다.
print("Final Response:")는 모델로부터 받은 최종 응답을 출력하기 전 "Final Response:"라는 메시지를 표시한다.
print(response.content)는 response는 처리된 결과이며, content는 모델의 응답을 포함하는 속성이다. 이 값을 출력하여 사용자가 입력한 질문에 대한 최종 응답을 표시한다.
즉, query는 사용자가 입력한 질문이고, response는 처리된 결과이며, content는 실제 응답 메시지다.
이제 챗봇을 실제로 구동시켜보자.
같은 질문을 ChatGPT에게도 해보고, Gemini에게도 해봤다.
여러 질문들을 해봤는데, 그 중 주목할만한 답변이 있는 질문은 '상장주식 대주주 기준이 50억원 이상으로 완화됐다며? 적용 시점을 알려줘.' 였다.
이 질문에 ChatGPT는 4개의 사이트에서 검색해서 답변을 가져왔다. 그래서 정확한 시점에 대해 알려줄 수 있었다.
그러나 Gemini는 정확한 시점을 대답하지 못했고, 관련된 설명만 늘어놓았다.
RAG를 활용해서 내가 만든 챗봇은 내가 vectorstore에 넣어둔 pdf 자료를 통해 답변을 생성해내기 때문에 정확한 시점을 알려주었다.
이렇듯, RAG를 사용하면 LLM이 외부 데이터, 여기서는 pdf 파일을 활용해서, 더 정확한 답변을 생성할 수 있게 된다. 최신 정보나 특정한 데이터에 대해 외부 정보를 반영해서 답변을 생성하기 때문에 최신 정보에 대한 답변도 잘하고, 더 정확하게 답변할 수 있는 것이다.
필수 과제를 완료했으니 도전 과제도 해보자.
'AI 부트캠프 > 챕터3(11.08~12.04)' 카테고리의 다른 글
TIL 40 (2) | 2024.11.16 |
---|---|
WIL 6 (0) | 2024.11.15 |
TIL 38 개인 과제 (3) | 2024.11.14 |
TIL 37 (1) | 2024.11.13 |
TIL 36 (2) | 2024.11.12 |