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)
Embedding은 자연어를 벡터로 변환하는 작업으로, 이를 통해 컴퓨터가 텍스트의 의미를 이해할 수 있게 됩니다. 변환된 문서는 벡터 공간에 저장되며, 이후 관련 문서를 검색할 때 사용됩니다.
embeddings = OpenAIEmbeddings()
cache_dir = LocalFileStore("./.cache/practice/")
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(embeddings, cache_dir)
벡터 공간에 저장된 문서는 사용자가 쿼리를 입력할 때 연관성이 높은 문서를 검색하여 LLM에 전달할 수 있도록 합니다.
vectorstore = FAISS.from_documents(docs, cached_embeddings)
retriever = vectorstore.as_retriever()
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}"),
]
)
구현된 체인을 통해 사용자 질문에 대한 정확한 답변을 도출할 수 있습니다. 예를 들어, "김첨지는 학생을 어디로 데려다 주었나?"라는 질문에 "남대문 정거장"이라는 답변을 도출할 수 있습니다.
이 글을 바탕으로 RAG 방식을 통해 외부 문서를 참조하는 챗봇 모델을 구축하는 방법에 대해 이해할 수 있을 것입니다.
LangChain은 대형 언어 모델(LLM)을 사용하여 애플리케이션을 구축하기 위한 오픈 소스 프레임워크입니다. 다양한 컴포넌트를 체인 형태로 연결하여, LLM을 쉽게 다루고 사용자에게 맞춤형 서비스를 제공하는 것이 핵심 목적입니다. 이 글에서는 LangChain의 주요 기능 중 하나인 Model I/O를 중심으로, 대화형 AI 챗봇을 만드는 방법을 설명합니다.
LLM(대형 언어 모델)을 설정하는 것은 LangChain에서 중요한 단계입니다. 여기서는 ChatOpenAI
를 사용하며, 이 모델은 기본적으로 GPT-3.5-turbo를 사용합니다.
llm = ChatOpenAI(temperature=0.1)
대화형 AI가 문맥을 이해하고 적절한 답변을 제공하기 위해서는 이전 대화 내용을 기억하는 기능이 필요합니다. 이를 위해 LangChain에서는 다양한 메모리 옵션을 제공합니다.
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=80,
memory_key="chat_history",
return_messages=True,
)
def load_memory(_):
return memory.load_memory_variables({})["chat_history"]
RunnablePassthrough을 사용하여 chain을 만들 때 메모리의 채팅 기록을 반환하는 함수가 필요하다. chian을 invoke() 하였을 때 값이 자동으로 전달되기에 input을 반드시 선언해야 한다. 여기서는 필요하지 않으므로 언더바_로 값을 무시한다.
LangChain은 프롬프트를 쉽게 생성할 수 있는 기능을 제공합니다. 이를 통해 다양한 상황에 맞는 프롬프트를 만들 수 있습니다.
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful AI talking to human"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}"),
])
LangChain의 가장 중요한 기능 중 하나는 다양한 컴포넌트를 체인 형태로 묶어 데이터를 처리하는 것입니다. 이는 LCEL(LangChain Expression Language)을 통해 구현되며, 리눅스의 파이프라인과 유사한 방식으로 동작합니다. LCEL을 사용하면 반환된 값을 다음 컴포넌트의 입력으로 전달할 수 있으며, 스트리밍, 비동기적 작동, 병렬 실행 등의 이점을 제공합니다.
다음은 기본 체인 구성 예시입니다:
chain = RunnablePassthrough.assign(chat_history=load_memory) | prompt | llm
chat_history
키에 load_memory
함수의 반환값을 추가합니다.체인을 실행하는 함수 invoke_chain()
는 다음과 같이 구성됩니다:
def invoke_chain(question):
result = chain.invoke({"question": question})
memory.save_context(
{"input": question},
{"output": result.content},
)
체인의 흐름은 다음과 같습니다:
chain.invoke({"question": question})
를 통해 사용자의 질문이 question
키로 전달됩니다.chat_history
에 대해 load_memory
를 호출한 반환값을 전달합니다.question
과 chat_history
두 키가 필요합니다.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?")
오프 더 쉘프 체인은 간단하지만, 작동 방식이 모호할 수 있으며 직접 구현한 체인보다 커스터마이징이 제한적입니다. 그러나 기본적인 작업에는 충분히 유용합니다.