Einführung
Nicht allzu lange ist es her, dass ich versucht habe, einen einfachen benutzerdefinierten Chatbot zu erstellen, der vollständig auf meinem CPU laufen würde.
Die Ergebnisse waren erschreckend, die Anwendung stürzte häufig ab. Dies ist jedoch keine schockierende Entwicklung. Wie sich herausstellte, ist es das Programmieräquivalent dazu, ein Kleinkind einen Berg erklimmen zu lassen, ein 13-Milliarden-Parameter-Modell auf einem 600-Dollar-Computer unterzubringen.
Dieses Mal habe ich einen ernsthafteren Versuch unternommen, einen Forschungs-Chatbot mit einem End-to-End-Projekt zu erstellen, das AWS nutzt, um die für den Aufbau der Anwendung benötigten Modelle zu hosten und den Zugriff darauf zu ermöglichen.
Der folgende Artikel beschreibt meine Bemühungen, RAG (Retrieval-Augmented Generation) zu nutzen, um einen leistungsstarken Forschungs-Chatbot zu erstellen, der Fragen mit Informationen aus Forschungsarbeiten beantwortet.
Ziel
Ziel dieses Projekts ist es, einen Frage-Antwort-Chatbot mit dem RAG-Framework zu erstellen. Er wird Fragen mit dem Inhalt von PDF-Dokumenten aus dem arXIV-Repository beantworten.
Bevor wir in das Projekt einsteigen, betrachten wir die Architektur, den Tech-Stack und das Verfahren zum Aufbau des Chatbots.
Chatbot-Architektur
Das obige Diagramm veranschaulicht den Workflow für die LLM-Anwendung (Large Language Model).
Wenn ein Nutzer eine Abfrage in einer Benutzeroberfläche eingibt, wird die Abfrage mit einem Embedding-Modell transformiert. Dann ruft die Vektor-Datenbank die ähnlichsten Embeddings ab und sendet sie zusammen mit dem eingebetteten Abfrage an das LLM. Das LLM nutzt den bereitgestellten Kontext, um eine präzise Antwort zu generieren, die dem Nutzer in der Benutzeroberfläche angezeigt wird.
Tech-Stack
Für den Aufbau der RAG-Anwendung mit den in der Architektur gezeigten Komponenten werden mehrere Tools benötigt. Die wichtigsten Tools sind folgende:
- Amazon Bedrock
Amazon Bedrock ist ein serverloser Dienst, der Nutzern über eine API den Zugriff auf Modelle ermöglicht. Da er ein Pay-as-you-go-System verwendet und nach der Anzahl der verwendeten Token berechnet wird, ist er für Entwickler sehr bequem und kostengünstig.
Bedrock wird für den Zugriff sowohl auf das Embedding-Modell als auch auf das LLM verwendet. Für die Konfiguration muss zunächst ein IAM-Benutzer mit Zugriff auf den Dienst erstellt werden. Außerdem muss im Voraus der Zugriff auf die gewünschten Modelle gewährt werden.
- FAISS
FAISS ist eine beliebte Bibliothek im Data-Science-Bereich und wird in diesem Projekt zur Erstellung der Vektor-Datenbank verwendet. Sie ermöglicht eine schnelle und effiziente Abrufung relevanter Dokumente basierend auf einem Ähnlichkeitsmaß. Sie ist außerdem kostenlos, was immer hilfreich ist.
- LangChain
Das LangChain-Framework erleichtert die Erstellung und Nutzung der RAG-Komponenten (z.B. Vektor-Store, LLM).
- Chainlit
Die Chainlit-Bibliothek wird für die Entwicklung der Benutzeroberfläche des Chatbots verwendet. Sie ermöglicht es Nutzern, mit minimalem Code eine ästhetische Frontend zu erstellen und bietet Funktionen, die für Chatbot-Anwendungen geeignet sind.
Hinweis: Der technische Teil des Artikels enthält Code-Snippets von Chainlit-Operationen, geht aber nicht auf die Syntax oder Funktionsweise dieser Operationen ein.
- Docker
Für Portabilität und einfache Bereitstellung wird die Anwendung mit Docker containerisiert.
Verfahren
Die Entwicklung der LLM-Anwendung erfordert folgende Schritte. Jeder Schritt wird einzeln untersucht.
- Laden der PDF-Dokumente
- Aufbau des Vektor-Stores
- Erstellen der Retrieval-Kette
- Entwerfen der Benutzeroberfläche
- Ausführen der Chatbot-Anwendung
- Ausführen der Anwendung in einem Docker-Container
Schritt 1 – Laden der PDF-Dokumente
ArXIV ist ein Repository mit einer Vielzahl kostenloser, quelloffener Artikel und Arbeiten zu Themen von Wirtschaft bis Ingenieurwesen. Die Backend-Daten der Anwendung werden aus einigen Dokumenten über LLMs aus dem Repository bestehen.
Nachdem die ausgewählten Dokumente in einem Verzeichnis gespeichert wurden, werden sie mit LangChains PyPDFDirectoryLoader geladen und mit dem RecursiveCharacterTextSplitter in Textabschnitte unterteilt.
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import PyPDFDirectoryLoader
def load_pdfs(chunk_size=3000, chunk_overlap=100):
# load the pdf documents
loader=PyPDFDirectoryLoader("PDF Documents")
documents=loader.load()
# split the documents into chunks
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size,
chunk_overlap=chunk_overlap)
docs = text_splitter.split_documents(documents=documents)
return docs
Schritt 2 – Aufbau des Vektor-Stores
Die in Schritt 1 erstellten Textabschnitte werden mit Amazons Titan Text Embeddings Modell in Embeddings umgewandelt. Darauf kann per Code mit boto3, dem Amazon SDK für Python, zugegriffen werden. Das Titan-Modell kann über die model_id identifiziert werden, die in der Bedrock-Dokumentation angegeben ist.
from langchain_community.embeddings import BedrockEmbeddings
from langchain_community.vectorstores.faiss import FAISS
def create_vector_store(docs):
# Set up bedrock client
bedrock = create_client()
bedrock_embeddings=BedrockEmbeddings(model_id='amazon.titan-embed-text-v1', client=bedrock)
# create and save the vector store
vector_store = FAISS.from_documents(docs, bedrock_embeddings)
vector_store.save_local("faiss_index")
return None
Die eingebetteten Abschnitte werden in einem FAISS-Vektor-Store gespeichert, der lokal als „faiss_index“ gespeichert wird.
Schritt 3 – Laden des LLM
Das LLM für die Anwendung wird Metas 13 Milliarden Parameter großes Llama 2 Modell sein. Genau wie das Embedding-Modell wird das LLM über Amazon Bedrock zugänglich gemacht.
from langchain.llms.bedrock import Bedrock
def create_llm(bedrock_client):
# load llama2
llm = Bedrock(model_id='meta.llama2-13b-chat-v1',
client=bedrock_client,
streaming=True,
model_kwargs={'temperature':0})
return llm
Ein bemerkenswerter Parameter ist die temperature
, die die Zufälligkeit der Ausgabe des Modells beeinflusst. Da die Anwendung für die Forschung konzipiert ist, wird die Zufälligkeit minimiert, indem temperature auf 0 gesetzt wird.
Schritt 4 – Erstellen der Retrieval-Kette
In LangChain ist eine „Kette“ ein Wrapper, der eine Reihe von Ereignissen in einer bestimmten Reihenfolge ermöglicht. In dieser RAG-Anwendung erhält die Kette die Nutzerabfrage und ruft die ähnlichsten Abschnitte aus dem Vektor-Store ab. Die Kette sendet dann die eingebettete Abfrage und die abgerufenen Abschnitte an das geladene LLM, das unter Verwendung des bereitgestellten Kontexts eine Antwort generiert.
from langchain.llms.bedrock import Bedrock
from langchain.memory import ChatMessageHistory, ConversationBufferMemory
from langchain.chains import RetrievalQA, ConversationalRetrievalChain
# load llm
llm = create_llm(bedrock_client=bedrock_client)
# load embeddings and vector store
bedrock_embeddings=BedrockEmbeddings(model_id='amazon.titan-embed-text-v1', client=bedrock_client)
vector_store = FAISS.load_local('faiss_index', bedrock_embeddings, allow_dangerous_deserialization=True)
# create memory history
message_history = ChatMessageHistory()
memory = ConversationBufferMemory(
memory_key="chat_history",
output_key="answer",
chat_memory=message_history,
return_messages=True,
)
# create qa chain
qa_chain = ConversationalRetrievalChain.from_llm(llm,
chain_type='stuff',
retriever=vector_store.as_retriever(search_type='similarity', search_kwargs={"k":3}),
return_source_documents=True,
memory=memory)
Die Kette integriert auch den ConversationBufferMemory, der es dem Chatbot ermöglicht, sich an vorherige Abfragen zu erinnern. So kann der Nutzer Folgefragen stellen.
Ein weiterer erwähnenswerter Hyperparameter ist das k
für den Retriever, das angibt, wie viele Embeddings aus dem Vektor-Store abgerufen werden sollen. Für diesen Anwendungsfall setzen wir k auf 3, d.h. die LLM-Anwendung verwendet 3 Embeddings als Kontext, um jede Abfrage zu beantworten.
Schritt 5 – Erstellen der Benutzeroberfläche
Bisher wurden die Backend-Komponenten der Anwendung entwickelt, daher ist es an der Zeit, an der Frontend zu arbeiten. Chainlit erleichtert den Aufbau von Benutzeroberflächen für LangChain-Anwendungen, da der vorhandene Code nur mit zusätzlichen Chainlit-Befehlen modifiziert werden muss.
Chainlit wird zur Erstellung der Funktion verwendet, die die Kette einrichtet.
import chainlit as cl
@cl.on_chat_start
async def create_qa_chain():
# create client
bedrock_client = create_client()
# load llm
llm = create_llm(bedrock_client=bedrock_client)
# load embeddings and vector store
bedrock_embeddings=BedrockEmbeddings(model_id='amazon.titan-embed-text-v1', client=bedrock_client)
vector_store = FAISS.load_local('faiss_index', bedrock_embeddings, allow_dangerous_deserialization=True)
# create memory history
message_history = ChatMessageHistory()
memory = ConversationBufferMemory(
memory_key="chat_history",
output_key="answer",
chat_memory=message_history,
return_messages=True,
)
# create qa chain
qa_chain = ConversationalRetrievalChain.from_llm(llm,
chain_type='stuff',
retriever=vector_store.as_retriever(search_type='similarity', search_kwargs={"k":3}),
return_source_documents=True,
memory=memory
)
# add custom messages to the user interface
msg = cl.Message(content="Loading the bot...")
await msg.send()
msg.content = "Hi, Welcome to the QA Chatbot! Please ask your question."
await msg.update()
cl.user_session.set('qa_chain' ,qa_chain)
Es wird auch zur Erstellung der Funktion verwendet, die die Kette nutzt, um Antworten zu generieren und an den Nutzer zu senden.
import chainlit as cl
@cl.on_message
async def generate_response(query):
qa_chain = cl.user_session.get('qa_chain')
res = await qa_chain.acall(query.content, callbacks=[cl.AsyncLangchainCallbackHandler(
stream_final_answer=True,
)])
# extract results and source documents
result, source_documents = res['answer'], res['source_documents']
# Extract all values associated with the 'metadata' key
source_documents = str(source_documents)
metadata_values = re.findall(r"metadata={'source': '([^']*)', 'page': (\d+)}", source_documents)
# Convert metadata_values into a single string
pattern = r'PDF Documents|\\'
metadata_string = "\n".join([f"Source: {re.sub(pattern, '', source)}, page: {page}" for source, page in metadata_values])
# add metadata (i.e., sources) to the results
result += f'\n\n{metadata_string}'
# send the generated response to the user
await cl.Message(content=result).send()
Die Chainlit-Dekoratoren sind eine notwendige Ergänzung. Der on_chat_start
-Dekorator definiert die Operationen, die ausgeführt werden sollen, wenn die Chat-Sitzung gestartet wird (d.h. Einrichten der Kette), während der on_message
-Dekorator die Operationen definiert, die ausgeführt werden sollen, wenn der Nutzer eine Abfrage eingibt (d.h. Senden der Antwort).
Darüber hinaus integriert der Code die Verwendung von async
und await
-Befehlen, so dass die Aufgaben asynchron gehandhabt werden.
Da die LLM-Anwendung für die Forschung konzipiert ist, enthält die generierte Antwort nach der Ähnlichkeitssuche die Quellen der aus dem Vektor-Store abgerufenen Embeddings. Dies macht die generierten Antworten zitierfähig und daher glaubwürdiger in den Augen des Nutzers.
Schritt 6 – Ausführen der Chatbot-Anwendung
Nachdem alle Komponenten im Chatbot-Workflow erstellt wurden, kann die Anwendung ausgeführt und getestet werden. Mit Chainlit kann eine Sitzung mit einer einfachen Zeile gestartet werden:
chainlit run <app.py>
Der Chatbot läuft nun! Er zeigt die im Code bereitgestellte Nachricht beim Start der Sitzung.
Testen wir ihn mit einer einfachen Abfrage:
Wenn eine Abfrage eingegeben wird, ist die Antwort präzise und verständlich. Außerdem enthält sie die Quellen der 3 Vektor-Embeddings, die zur Generierung der Antwort verwendet wurden, einschließlich des Namens des Dokuments und der Seitenzahl.
Um sicherzustellen, dass der Chatbot sich an vorherige Abfragen erinnert, können wir eine Folgefrage stellen.
Hier fragen wir nach „einem anderen Beispiel“, ohne zusätzliche Informationen darüber zu geben, welches Beispiel benötigt wird. Da der Bot sich erinnert, weiß er, dass sich die Abfrage auf vorgefertigte LLMs bezieht.
Insgesamt arbeitet die Anwendung auf einem zufriedenstellenden Niveau. Ein Aspekt, der in einem Artikel nicht demonstriert werden kann, ist die deutlich geringere Rechenleistung, die zum Ausführen des Chatbots benötigt wird. Da AWS das Embedding-Modell und das LLM hostet, besteht kein Risiko von Abstürzen durch übermäßige CPU-Auslastung.
Schritt 7 – Containerisieren der Anwendung
Obwohl der Chatbot läuft, bleibt noch ein Schritt übrig. Die LLM-Anwendung muss noch mit Docker für eine einfachere Portabilität und Versionskontrolle containerisiert werden.
Der erste Schritt für die Containerisierung ist die Entwicklung der Dockerfile.
In dieser Dockerfile erstellen wir ein Python-Image als Basis, definieren Argumente für die Access Key ID und den geheimen Zugriffsschlüssel von AWS, installieren die requirements.txt-Datei im Container, kopieren das aktuelle Verzeichnis in den Container und führen die Chainlit-Anwendung aus.
Von hier an ist es ziemlich einfach. Der Aufbau des Docker-Images erfolgt mit einem Einzeiler:
docker build --build-arg AWS_ACCESS_KEY_ID=<your_access_key_id> --build-arg AWS_SECRET_ACCESS_KEY=<your_secret_access_key> -t chainlit_app .
Der obige Befehl erstellt ein Image mit dem Namen chainlit_app. Es enthält die AWS-Zugriffs-Key-ID und den AWS-Geheimschlüssel als Argumente, da sie benötigt werden, um über die API auf die Modelle in Amazon Bedrock zuzugreifen.
Schließlich kann die Anwendung in einem Docker-Container ausgeführt werden:
docker run -d --name chainlit_app -p 8000:8000 chainlit_app
Die Anwendung läuft nun auf Port 8000! Da die Anwendung lokal ausgeführt wird, wird der Chatbot unter http://localhost:8000/ gehostet.
Sehen wir nach, ob die RAG-Komponenten (einschließlich der AWS Bedrock-Modelle) noch funktionieren, indem wir eine Abfrage stellen.
Es funktioniert genauso wie erwartet!
Nächste Schritte
Der aktuelle Chatbot kann Abfragen mit angemessener Leistung und zu geringen Kosten beantworten. Die Anwendung wird jedoch immer noch lokal ausgeführt und verwendet Standardparameter. Es gibt also noch Möglichkeiten, die Leistung und Benutzerfreundlichkeit des Chatbots weiter zu verbessern.
- Rigorose Tests durchführen
Die LLM-Anwendung scheint effektiv zu arbeiten, die Antworten sind präzise und akkurat. Dennoch muss das Tool noch rigorosen Tests unterzogen werden, bevor es als benutzbar gelten kann.
Die Tests dienen in erster Linie dazu sicherzustellen, dass die Antwortgenauigkeit maximiert und Halluzinationen minimiert werden.
- Fortschrittliche RAG-Techniken implementieren
Wenn der Chatbot bestimmte Arten von Fragen nicht beantworten kann oder generell schlecht abschneidet, wäre es ratsam, den Einsatz fortschrittlicher RAG-Techniken in Betracht zu ziehen, um bestimmte Aspekte des Workflows wie den Abruf von Inhalten aus der Vektor-Datenbank zu verbessern.
- Frontend aufpolieren
Derzeit verwendet das Tool das Standardfrontend von Chainlit. Um das Tool ästhetischer und intuitiver zu gestalten, kann das UI-Design weiter angepasst werden.
Außerdem kann die Zitierfunktion des Chatbots (d.h. die Identifizierung der Quelle der Antwort) verbessert werden, indem ein Hyperlink bereitgestellt wird, damit der Nutzer sofort zu der Seite gehen kann, die die benötigten Informationen enthält.
- In der Cloud bereitstellen
Wenn dieses Tool einer größeren Nutzerbasis angeboten werden soll, wäre der nächste Schritt, es auf einem Remote-Server mit Cloud-Plattformen wie Amazon EC2 und Amazon ECS zu deployen. Mit vielen Cloud-Plattformen sind hohe Skalierbarkeit, Verfügbarkeit und Leistung erreichbar, aber da dieses Tool AWS Bedrock nutzt, wäre der natürliche nächste Schritt, andere Ressourcen im AWS-Ökosystem zu nutzen.
Fazit
Bei der Arbeit an diesem Projekt war ich überwältigt davon, wie weit sich der Bereich Data Science entwickelt hat. NLP-Anwendungen, die generative KI nutzen, wären vor 5 Jahren nur schwer zu erstellen gewesen, da es erhebliche Zeit, Geld und Manpower erfordert hätte.
Im Jahr 2024 können solche Tools mit nur einem Menschen, etwas Code und minimalen Kosten (das ganze Projekt hat bisher weniger als 1 Dollar gekostet) erstellt werden. Man fragt sich, was in den kommenden Jahren möglich sein wird.
Für diejenigen, die mehr über den Quellcode des Projekts erfahren möchten, besuchen Sie bitte das GitHub-Repository:
anair123/Building-a-Research-Chatbot-with-AWS-and-Llama-2 (github.com)
Vielen Dank fürs Lesen!
Referenzen
- Stehle, J., Eusebius, N., Khanuja, M., Roy, M., & Pathak, R. (n.d.). Getting started with Amazon Titan text embeddings in Amazon bedrock … https://aws.amazon.com/blogs/machine-learning/getting-started-with-amazon-titan-text-embeddings/