RAG (Retrieval-Augmented Generation) 기법을 Langchain으로 구현하는 방법에 대해 알아보자.
1. RAG 개념 소개
- RAG는 질문-답변 시스템이나 대화형 AI에서 매우 유용한 기법으로, 외부 데이터 소스를 기반으로 필요한 정보를 검색(retrieve)하고, 이를 바탕으로 답변을 생성(generate)하는 방식이다. Langchain은 이런 구조를 쉽게 구현할 수 있도록 다양한 툴과 체인을 제공한다.
from dotenv import load_dotenv
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
load_dotenv()
embedding_function = OpenAIEmbeddings()
먼저 환경 변수를 로드하고, 문서를 처리할 벡터 스토어와 임베딩 기능을 준비한다. 이 과정에서 dotenv
로 API 키 같은 환경 설정을 불러오고, Chroma
와 OpenAIEmbeddings
를 사용하여 데이터를 벡터로 변환한다.
2. 문서 임베딩 및 데이터베이스 생성
- 여기서는
Document
객체로 문서를 준비하고, 이를 기반으로 벡터 데이터베이스를 생성한다.
docs = [
Document(page_content="the dog loves to eat pizza", metadata={"source": "animal.txt"}),
Document(page_content="the cat loves to eat lasagna", metadata={"source": "animal.txt"}),
]
db = Chroma.from_documents(docs, embedding_function)
이 코드에서 Document
클래스는 텍스트 콘텐츠와 관련된 메타데이터를 함께 저장한다. 이 문서들은 Chroma
데이터베이스에 벡터화되어 저장된다. 이후 쿼리를 통해 이 데이터베이스에서 관련된 문서를 검색할 수 있게 된다.
3. Retrieval 부분: 질문에 대한 문서 검색
- 데이터베이스에서 관련된 문서를 검색하기 위한
retriever
를 설정한다.
retriever = db.as_retriever()
result = retriever.invoke("What does the dog want to eat?")
# [Document(metadata={'source': 'animal.txt'}, page_content='the dog loves to eat pizza'), Document(metadata={'source': 'animal.txt'}, page_content='the cat loves to eat lasagna')]
여기서 invoke
메서드를 통해 "What does the dog want to eat?"라는 질문에 대해 데이터베이스에서 관련된 문서를 검색할 수 있다. RAG의 첫 번째 단계인 retrieval이 완료된 것이다.
4. Prompt 템플릿과 모델 설정
- 검색된 문서를 바탕으로 답변을 생성하기 위해
ChatPromptTemplate
와 OpenAI의ChatOpenAI
모델을 활용한다.
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
Prompt 템플릿은 검색된 문서의 내용을 context
로 넣어주고, question
을 함께 전달합니다. OpenAI의 GPT 모델을 사용하여 적절한 답변을 생성한다.
5. Runnable Chain 사용법
chain을 실행하는 방식은 아래와 같이 두 가지 방식으로 실행할 수 있다.
1) 질문을 기반으로 한 문서 검색
- Langchain에서는 체인 구조를 통해 여러 작업을 순차적으로 처리할 수 있다. 여기서는 검색된 결과를 프로세스 체인에 연결하는 방식으로 사용한다.
retrieval_chain = (
{
"context": (lambda x: x["question"]) | retriever,
"question": itemgetter("question"),
}
| prompt
| model
| StrOutputParser()
)
result = retrieval_chain.invoke({"question": "What does the dog want to eat?"})
print(result) # Pizza
구성:
- 이 방식에서는 질문 내용을 기반으로 문서를 검색하는 흐름입니다.
"context"
키에는lambda x: x["question"]
가 들어가 있는데, 이는 입력된 딕셔너리에서 "question" 값을 추출한 후, 이를retriever
로 전달하여 문서 검색을 수행합니다."question"
에는itemgetter("question")
를 사용해, 딕셔너리에서 "question" 키에 해당하는 값을 직접 추출해 프로세스에 넘겨줍니다.
핵심 아이디어:
- 질문 내용 자체를 활용한 검색: 이 방식의 큰 특징은 질문이 곧 검색 쿼리로 사용된다는 점입니다.
lambda
함수가 "question" 값을 추출해 이를retriever
로 전달함으로써, 사용자가 입력한 질문에 맞는 문서를 찾습니다. - 자동 변환(coercion): Langchain에서는 딕셔너리와 같은 비-Runnable 객체도 자동으로
Runnable
객체로 변환해 사용하므로, 이 구조는 자동으로 파이프라인에서 동작합니다.
장점:
- 질문에 맞춘 문서 검색: 질문이 복잡하거나 특정 문맥을 요구할 때, 해당 질문을 기반으로 관련 문서를 찾는 데 효과적입니다.
- 유연한 체인 구성: 질문에서 값을 추출하고 검색에 활용하는 등 다양한 전처리 작업을 추가할 수 있습니다.
실행
question
을 dictionary
로 전달하여 실행한다.
result = retrieval_chain.invoke({"question": "What does the dog want to eat?"})
2) 질문을 그대로 전달하고 검색 기반 답변 생성
retrieval_chain = (
{
"context": retriever,
"question": RunnablePassthrough(),
}
| prompt
| model
| StrOutputParser()
)
result = retrieval_chain.invoke("What does the dog want to eat?")
print(result) # Pizza
구성:
- 이 방식에서는 질문을 그대로 전달하여 체인을 실행합니다.
"context"
에retriever
만 전달하므로, 질문을 그대로 retriever로 넘겨줍니다. 이 때, 추가적인 전처리 없이 retriever가 동작하게 됩니다."question"
에는RunnablePassthrough()
를 사용하여 입력된 질문을 가공하지 않고 그대로 전달합니다. 이 방식은 질문에 변형 없이 바로 답변을 생성하는 데 중점을 둡니다.
핵심 아이디어:
- 질문을 직접 검색에 사용: 이 방식은 질문을 그대로
retriever
에 넘겨, 검색에 필요한 문서를 가져오도록 설계되었습니다. 전처리 과정 없이, 입력된 질문을 바탕으로 검색이 이루어집니다. - 질문 그대로 사용:
RunnablePassthrough()
는 입력된 데이터를 그대로 전달하는 역할을 하므로, 질문을 변경하거나 가공할 필요가 없을 때 유용합니다.
장점:
- 간결성: 질문을 그대로 사용하고자 할 때, 별도의 처리가 필요 없어서 빠르고 직관적으로 사용할 수 있습니다.
- 사용 예: 질문이 비교적 간단하거나, 질문 자체를 가공할 필요 없이 바로 검색해야 하는 경우에 적합합니다.
실행
question을 dictionary가 아닌 직접 입력하여 실행한다.
result = retrieval_chain.invoke("What does the dog want to eat?")
반응형