RAG & LANCHAIN (1)- 기초 문법

이영락·2024년 8월 21일
0

인공지능 공부

목록 보기
1/33

RAG를 이용한 외부 문서 참조 챗봇 모델 구현


목차

  1. RAG란?
  2. 데이터 적재 및 분할
  3. Embedding과 캐싱
  4. 벡터 저장 및 검색
  5. 프롬프트와 Chain 설계
  6. 결과 및 분석
  7. 기타 고려 사항

RAG란?

RAG(Retrieval-Augmented Generation)는 AI 모델이 사전 학습된 데이터 외에도 외부 문서를 참조하여 더 정확하고 신뢰성 있는 정보를 제공할 수 있도록 하는 기술입니다. 이를 통해 최신 정보나 사용자 맞춤 정보를 제공할 수 있습니다.

원본 데이터 -> 적재(load) -> 분할(split) -> 임베딩 -> 벡터 공간 저장(vector store & retriever)

from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import UnstructuredFileLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings, CacheBackedEmbeddings
from langchain.vectorstores import FAISS
from langchain.storage import LocalFileStore
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough, RunnableLambda

llm = ChatOpenAI(
    temperature=0.1,
)

cache_dir = LocalFileStore("./.cache/practice/")

splitter = CharacterTextSplitter.from_tiktoken_encoder(
    separator="\n",
    chunk_size=600,
    chunk_overlap=100,
)

loader = UnstructuredFileLoader("./files/운수 좋은 날.txt")

docs = loader.load_and_split(text_splitter=splitter)

embeddings = OpenAIEmbeddings()

cached_embeddings = CacheBackedEmbeddings.from_bytes_store(embeddings, cache_dir)

vectorstore = FAISS.from_documents(docs, cached_embeddings)

retriever = vectorstore.as_retriever()

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """
            You are a helpful assistant. 
            Answer questions using only the following context. 
            If you don't know the answer just say you don't know, don't make it up:
            \n\n
            {context}",
            """
        ),
        ("human", "{question}"),
    ]
)

chain = (
    {
        "context": retriever,
        "question": RunnablePassthrough(),
    }
    | prompt
    | llm
)

result = chain.invoke("김첨지는 학생을 어디로 데려다 주었나?")
print(result)

데이터 적재 및 분할

문서를 LLM에 제공할 때 문서 전체를 한 번에 전달하면 프롬프트가 과도하게 길어질 수 있습니다. 이를 방지하기 위해 문서를 적절한 크기로 분할하여 필요한 부분만 LLM에 전달하는 방식이 필요합니다.

코드 설명:

splitter = CharacterTextSplitter.from_tiktoken_encoder(
    separator="\n",
    chunk_size=600,
    chunk_overlap=100,
)
loader = UnstructuredFileLoader("./files/운수 좋은 날.txt")
docs = loader.load_and_split(text_splitter=splitter)
  • CharacterTextSplitter: 문서를 지정한 문자(여기서는 "\n")를 기준으로 분할합니다.
  • UnstructuredFileLoader: 다양한 형식의 파일을 로드할 수 있는 기능을 제공합니다.
  • load_and_split(): 파일을 로드하고 분할된 문서를 반환합니다.

Embedding과 캐싱

Embedding은 자연어를 벡터로 변환하는 작업으로, 이를 통해 컴퓨터가 텍스트의 의미를 이해할 수 있게 됩니다. 변환된 문서는 벡터 공간에 저장되며, 이후 관련 문서를 검색할 때 사용됩니다.

코드 설명:

embeddings = OpenAIEmbeddings()
cache_dir = LocalFileStore("./.cache/practice/")
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(embeddings, cache_dir)
  • OpenAIEmbeddings: OpenAI에서 제공하는 Embedding 서비스.
  • CacheBackedEmbeddings: 동일한 문서에 대해 반복적으로 임베딩하는 것을 방지하기 위해 캐시를 사용하여 효율성을 높입니다.

벡터 저장 및 검색

벡터 공간에 저장된 문서는 사용자가 쿼리를 입력할 때 연관성이 높은 문서를 검색하여 LLM에 전달할 수 있도록 합니다.

코드 설명:

vectorstore = FAISS.from_documents(docs, cached_embeddings)
retriever = vectorstore.as_retriever()
  • FAISS: 로컬에서 실행 가능한 벡터 검색 라이브러리로, 벡터를 저장하고 검색하는 기능을 제공합니다.
  • retriever: 사용자의 쿼리와 연관된 문서를 찾아오는 역할을 합니다.

프롬프트와 Chain 설계

LLM에 전달할 프롬프트는 적절하게 구성되어야 하며, 모델이 답변할 때 발생할 수 있는 환각 현상을 방지하기 위한 문구를 추가합니다.

코드 설명:

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """
            You are a helpful assistant. 
            Answer questions using only the following context. 
            If you don't know the answer just say you don't know, 
            don't make it up:
            \n\n
            {context}
            """,
        ),
        ("human", "{question}"),
    ]
)
  • ChatPromptTemplate: LLM에 전달될 프롬프트를 구성합니다.

결과 및 분석

구현된 체인을 통해 사용자 질문에 대한 정확한 답변을 도출할 수 있습니다. 예를 들어, "김첨지는 학생을 어디로 데려다 주었나?"라는 질문에 "남대문 정거장"이라는 답변을 도출할 수 있습니다.


기타 고려 사항

  • 문서와 쿼리, 프롬프트 지시문 등은 영어로 작성하는 것이 좋습니다. 한국어의 경우 정확도가 떨어질 수 있습니다.
  • Embedding과 retriever의 원리에 대한 추가적인 공부가 필요합니다.

이 글을 바탕으로 RAG 방식을 통해 외부 문서를 참조하는 챗봇 모델을 구축하는 방법에 대해 이해할 수 있을 것입니다.

LangChain을 활용한 대화형 AI 챗봇 구축


목차

  1. LangChain 소개
  2. LLM 설정
  3. 메모리 사용
  4. 프롬프트 템플릿 작성
  5. 체인 구성 및 실행
  6. 오프 더 쉘프 체인 사용
  7. 결과 및 분석

1. LangChain 소개

LangChain은 대형 언어 모델(LLM)을 사용하여 애플리케이션을 구축하기 위한 오픈 소스 프레임워크입니다. 다양한 컴포넌트를 체인 형태로 연결하여, LLM을 쉽게 다루고 사용자에게 맞춤형 서비스를 제공하는 것이 핵심 목적입니다. 이 글에서는 LangChain의 주요 기능 중 하나인 Model I/O를 중심으로, 대화형 AI 챗봇을 만드는 방법을 설명합니다.


2. LLM 설정

LLM(대형 언어 모델)을 설정하는 것은 LangChain에서 중요한 단계입니다. 여기서는 ChatOpenAI를 사용하며, 이 모델은 기본적으로 GPT-3.5-turbo를 사용합니다.

코드 설명:

llm = ChatOpenAI(temperature=0.1)
  • temperature: 생성된 텍스트의 다양성을 조절하는 파라미터로, 값이 클수록 창의적인 응답을, 작을수록 정확한 응답을 생성합니다.

3. 메모리 사용

대화형 AI가 문맥을 이해하고 적절한 답변을 제공하기 위해서는 이전 대화 내용을 기억하는 기능이 필요합니다. 이를 위해 LangChain에서는 다양한 메모리 옵션을 제공합니다.

코드 설명:

memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=80,
    memory_key="chat_history",
    return_messages=True,
)
  • ConversationSummaryBufferMemory: 이전 대화를 요약하여 저장하며, 메모리의 용량이 초과될 경우 자동으로 요약합니다.
  • max_token_limit: 메모리에 저장될 최대 토큰 수를 설정합니다. 이 값을 초과하면 대화 내용이 요약됩니다.

load_memory

def load_memory(_):
    return memory.load_memory_variables({})["chat_history"]

RunnablePassthrough을 사용하여 chain을 만들 때 메모리의 채팅 기록을 반환하는 함수가 필요하다. chian을 invoke() 하였을 때 값이 자동으로 전달되기에 input을 반드시 선언해야 한다. 여기서는 필요하지 않으므로 언더바_로 값을 무시한다.


4. 프롬프트 템플릿 작성

LangChain은 프롬프트를 쉽게 생성할 수 있는 기능을 제공합니다. 이를 통해 다양한 상황에 맞는 프롬프트를 만들 수 있습니다.

코드 설명:

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful AI talking to human"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{question}"),
])
  • ChatPromptTemplate: 프롬프트를 템플릿 형태로 작성하여, 필요할 때마다 재사용할 수 있습니다.
  • MessagesPlaceholder: 메모리에 저장된 대화 기록을 프롬프트에 포함시키는 역할을 합니다.

6. 체인(Chain) 이해하기

LangChain의 가장 중요한 기능 중 하나는 다양한 컴포넌트를 체인 형태로 묶어 데이터를 처리하는 것입니다. 이는 LCEL(LangChain Expression Language)을 통해 구현되며, 리눅스의 파이프라인과 유사한 방식으로 동작합니다. LCEL을 사용하면 반환된 값을 다음 컴포넌트의 입력으로 전달할 수 있으며, 스트리밍, 비동기적 작동, 병렬 실행 등의 이점을 제공합니다.

기본 체인 구성

다음은 기본 체인 구성 예시입니다:

chain = RunnablePassthrough.assign(chat_history=load_memory) | prompt | llm
  • RunnablePassthrough: 입력된 데이터를 그대로 다음 컴포넌트에 전달하거나, 새로운 데이터를 추가하여 전달합니다. 여기서는 chat_history 키에 load_memory 함수의 반환값을 추가합니다.
  • prompt: 전달된 데이터를 기반으로 프롬프트를 구성합니다.
  • llm: 구성된 프롬프트를 LLM에 전달하고, 결과를 반환합니다.

체인 실행

체인을 실행하는 함수 invoke_chain()는 다음과 같이 구성됩니다:

def invoke_chain(question):
    result = chain.invoke({"question": question})
    memory.save_context(
        {"input": question},
        {"output": result.content},
    )
  • chain.invoke(): 체인을 실행하여, 프롬프트에 전달될 변수명과 값을 지정한 후 LLM에 전달합니다. LLM의 반환값이 최종적으로 반환됩니다.
  • memory.save_context(): 대화의 입력과 출력 기록을 메모리에 저장합니다.

체인의 흐름은 다음과 같습니다:

  1. 사용자 입력 전달: chain.invoke({"question": question})를 통해 사용자의 질문이 question 키로 전달됩니다.
  2. RunnablePassthrough: 앞에서 전달받은 값을 그대로, 또는 추가된 값과 함께 다음 컴포넌트로 전달합니다. 여기서는 chat_history에 대해 load_memory를 호출한 반환값을 전달합니다.
  3. 프롬프트 완성: 프롬프트가 완성되기 위해서는 questionchat_history 두 키가 필요합니다.
  4. LLM 호출: 완성된 프롬프트가 LLM으로 전달되고, LLM의 응답이 최종 반환값이 됩니다.

오프 더 쉘프 체인

LangChain은 미리 만들어진 체인인 "off-the-shelf chain"을 제공합니다. 이러한 체인을 사용하면 더욱 간단하게 작업을 수행할 수 있지만, LCEL로 직접 구현한 방식보다 커스터마이징이 어렵다는 단점이 있습니다.

chain = LLMChain(
    llm=llm,
    memory=memory,
    prompt=prompt,
    verbose=True
)

chain.predict(question="My name is Nam")
chain.predict(question="What is my name?")
  • LLMChain: LLM, 메모리, 프롬프트를 자동으로 연결하여 동작하는 체인을 생성합니다.
  • verbose: True로 설정 시 콘솔에 로그가 출력되어 디버깅에 유용합니다.

오프 더 쉘프 체인은 간단하지만, 작동 방식이 모호할 수 있으며 직접 구현한 체인보다 커스터마이징이 제한적입니다. 그러나 기본적인 작업에는 충분히 유용합니다.


참고자료
https://velog.io/@udonehn/RAG를-적용한-질의응답-챗봇-LangChanin

profile
AI Engineer / 의료인공지능

0개의 댓글

관련 채용 정보