langchain / / 2024. 9. 19. 07:30

[langchain] LCEL로 RAG를 구현하는 기본 예제

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 키 같은 환경 설정을 불러오고, ChromaOpenAIEmbeddings를 사용하여 데이터를 벡터로 변환한다.

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 객체로 변환해 사용하므로, 이 구조는 자동으로 파이프라인에서 동작합니다.

장점:

  • 질문에 맞춘 문서 검색: 질문이 복잡하거나 특정 문맥을 요구할 때, 해당 질문을 기반으로 관련 문서를 찾는 데 효과적입니다.
  • 유연한 체인 구성: 질문에서 값을 추출하고 검색에 활용하는 등 다양한 전처리 작업을 추가할 수 있습니다.

실행

questiondictionary로 전달하여 실행한다.

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?")
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유