LangChain 및 Elasticsearch를 활용한 RAG 챗봇
고용주 정책과 같은 사용자 지정 데이터에 관한 질문에 답변할 수 있는 RAG 챗봇을 구축하는 방법을 확인해 보세요. 챗봇은 LangChain의 ConversationalRetrievalChain을 사용합니다.
Colab에서 열기노트북 다운로드In this notebook we'll build a chatbot that can respond to questions about custom data, such as policies of an employer.
The chatbot uses LangChain's ConversationalRetrievalChain and has the following capabilities:
- Answer questions asked in natural language
- Run hybrid search in Elasticsearch to find documents that answer the question
- Extract and summarize the answer using OpenAI LLM
- Maintain conversational memory for follow-up questions
Requirements 🧰
For this example, you will need:
- An Elastic deployment
- We'll be using Elastic Cloud for this example (available with a free trial)
- OpenAI account
Use Elastic Cloud
If you don't have an Elastic Cloud deployment, follow these steps to create one.
- Go to Elastic cloud Registration and sign up for a free trial
- Select Create Deployment and follow the instructions
Locally install Elasticsearch
If you prefer to run Elasticsearch locally, the easiest way is to use Docker. See Install Elasticsearch with Docker for instructions.
Install packages 📦
First we pip install the packages we need for this example.
%pip install -qU langchain openai langchain-elasticsearch tiktokenInitialize clients 🔌
Next we input credentials with getpass. getpass is part of the Python standard library and is used to securely prompt for credentials.
from getpass import getpass
# https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id
ELASTIC_CLOUD_ID = getpass("Elastic Cloud ID: ")
# https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#creating-an-api-key
ELASTIC_API_KEY = getpass("Elastic Api Key: ")
# https://platform.openai.com/api-keys
OPENAI_API_KEY = getpass("OpenAI API key: ")Load and process documents 📄
Time to load some data! We'll be using the workplace search example data, which is a list of employee documents and policies.
import json
from urllib.request import urlopen
url = "https://raw.githubusercontent.com/elastic/elasticsearch-labs/main/notebooks/generative-ai/data/workplace-docs.json"
response = urlopen(url)
workplace_docs = json.loads(response.read())
print(f"Successfully loaded {len(workplace_docs)} documents")Chunk documents into passages 🪓
As we're chatting with our bot, it will run semantic searches on the index to find the relevant documents. In order for this to be accurate, we need to split the full documents into small chunks (also called passages). This way the semantic search will find the passage within a document that most likely answers our question.
We'll use LangChain's RecursiveCharacterTextSplitter and split the documents' text at 800 characters with some overlap between chunks.
from langchain.text_splitter import RecursiveCharacterTextSplitter
metadata = []
content = []
for doc in workplace_docs:
content.append(doc["content"])
metadata.append({"name": doc["name"], "summary": doc["summary"]})
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=512,
chunk_overlap=256,
)
docs = text_splitter.create_documents(content, metadatas=metadata)
print(f"Split {len(workplace_docs)} documents into {len(docs)} passages")Let's generate the embeddings and index the documents with them.
from langchain_elasticsearch import ElasticsearchStore
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)
vector_store = ElasticsearchStore.from_documents(
docs,
es_cloud_id=ELASTIC_CLOUD_ID,
es_api_key=ELASTIC_API_KEY,
index_name="workplace-docs",
embedding=embeddings,
)Chat with the chatbot 💬
Let's initialize our chatbot. We'll define Elasticsearch as a store for retrieving documents and for storing the chat session history, OpenAI as the LLM to interpret questions and summarize answers, then we'll pass these to the conversational chain.
from langchain.llms import OpenAI
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ElasticsearchChatMessageHistory
from uuid import uuid4
retriever = vector_store.as_retriever()
llm = OpenAI(openai_api_key=OPENAI_API_KEY)
chat = ConversationalRetrievalChain.from_llm(
llm=llm, retriever=retriever, return_source_documents=True
)
session_id = str(uuid4())
chat_history = ElasticsearchChatMessageHistory(
es_cloud_id=ELASTIC_CLOUD_ID,
es_api_key=ELASTIC_API_KEY,
session_id=session_id,
index="workplace-docs-chat-history",
)Now we can ask questions from our chatbot!
See how the chat history is passed as context for each question.
# Define a convenience function for Q&A
def ask(question, chat_history):
result = chat({"question": question, "chat_history": chat_history.messages})
print(
f"""[QUESTION] {question}
[ANSWER] {result["answer"]}
[SUPPORTING DOCUMENTS] {list(map(lambda d: d.metadata["name"], list(result["source_documents"])))}"""
)
chat_history.add_user_message(result["question"])
chat_history.add_ai_message(result["answer"])
# Chat away!
print(f"[CHAT SESSION ID] {session_id}")
ask("What does NASA stand for?", chat_history)
ask("Which countries are part of it?", chat_history)
ask("Who are the team's leads?", chat_history)💡 Try experimenting with other questions or after clearing the workplace data, and observe how the responses change.
Once we're done, we can clean up the chat history for this session...
chat_history.clear()... or delete the indices.
vector_store.client.indices.delete(index="workplace-docs")
vector_store.client.indices.delete(index="workplace-docs-chat-history")ObjectApiResponse({'acknowledged': True})