✒️Door: @Jan Willem Altink
📅Datum: 16 april 2024
🕜Leestijd: 11 minuten
- Inleiding
- Visuele weergave van wat we gaan maken
- Vectorstore
- Chatbot
- Stap 0: het opzetten van de database
- Extensie
- Stap 1: het extracten van de data uit de pdf
- methode 1: met het Claude Opus model
- methode 2: met LlamaParse
- Stap 2: het opknippen van de data en het toevoegen van metadata
- Stap 3: Het embedden en opslaan van de data
- Meer grip op je data
- Stap 4: Langchain
- de chatfunctie
- de retriever
- de queryeditor
- De conversational chain
- Analyseren met Langsmith
- Conclusie en next steps
Inleiding
Een paar weekjes terug heb ik een blog geplaatst over LLM gedreven RAG applicaties. Dat was vooral een theoretische introductie. Hoe werkt het en wat kun je ermee? In deze blog een praktijk voorbeeld waarin we een VectorStore maken in een Postgres database, Vector Embeddings genereren met het nieuwe Cohere Embeddings model en met Langchain deze embeddings gebruiken om de contextwindow van een LLM te vullen met relevante data op basis van de chatgeschiedenis. Als laatste zullen we LangSmith gebruiken om de chains die we bouwen te analyseren. We doen dit alles aan de hand van een voorbeeld: de gebruikershandleiding van de Tesla Model Y. Deze handleiding bevat ruim 300 pagina’s aan informatie in zowel tekst, tabellen als beeld, en is daardoor een leuk voorbeeld om de werking van RAG te demonstreren.
Visuele weergave van wat we gaan maken
Vectorstore
Chatbot
Stap 0: het opzetten van de database
Zoals in deel 3 al uitgelegd is maken we bij RAG gebruik van een externe bron van data om aan de contextwindow van het LLM toe te voegen. Dit kan zo’n beetje elke vorm van data zijn; en dit kan zowel tijdelijk of eenmalig gebruik zijn als wat meer structureel. Vaak, metname bij semantische of textuele data, wordt gebruik gemaakt van Vector Embeddings. Het genereren van deze embeddings is een redelijk zware operatie en dus is het nuttig om de gegenereerde embeddings te bewaren in bijvoorbeeld een database.
Voor onze applicatie gaan we gebruik maken van LangChain. LangChain is een framework dat is ontworpen om het makkelijker te maken om taalmodellen te verbinden met data. LangChain bevat een aantal handige bibliotheken voor de integratie, evaluatie en productie van onze applicatie. Zo bevat LangChain ondermeer verscheidene methodes voor het verbinden met verschillende type databases, ondermeer MySQL, MongoDB en Postgres worden ondersteund. Wij kiezen voor die laatste. Om onze vector store in te kunnen richten hebben we toegang nodig tot een Postgres database met de extensie PGVector.
Ik vond het het makkelijkst om de database up and running te krijgen met Docker:
docker run --name tesla-container -e POSTGRES_USER=tesla -e POSTGRES_PASSWORD=tesla -e POSTGRES_DB=tesla -p 6024:5432 -d pgvector/pgvector:pg16
Maar er zijn meer manieren mogelijk, zie hier alternatieven.
Extensie
Let op! Ik had zelf niet direct de benodigde extensie in mijn Postgres database na het starten van de Docker container. Mocht je hetzelfde hebben kun je de volgende stappen doorlopen:
1: Zorg dat je onderstaande omgevingsvariabelen instelt in je ontwikkelomgeving via een .env
bestand:
PGVECTOR_HOST=localhost
PGVECTOR_PORT=6024
PGVECTOR_DATABASE=tesla
PGVECTOR_USER=tesla
PGVECTOR_PASSWORD=tesla
2: Voer dit Python script uit:
GitHub link: klik hier
Stap 1: het extracten van de data uit de pdf
Ik heb ervoor gekozen om de stappen die nu volgen zo klein mogelijk te maken, sommige scripts zouden efficienter zijn wanneer we ze met elkaar combineren, of niet elk tussenstapje op te slaan, maar ten behoeve van de modulariteit en de begrijpelijkheid gaan we de grote PDF stap voor stap prepareren om als bron te dienen voor onze chatbot. De moeilijkheid van PDFs zit hem er in dat de informatie die er in staat in verschillende vormen en op een ongestructureerde manier wordt aangeboden. Informatie kanin een tabel staan, maar ook in een reguliere tekst, of in een grafiek of infographic. We willen dat al deze informatie uit de PDF op het juiste moment als input dient voor onze chatbot, en dus moeten we zorgen dat we alle informatie er uit halen. Ik heb hieronder 2 manieren uitgewerkt om de PDF te converteren naar markdown en de afbeeldingen en tabellen te extraheren, omdat we die wellicht anders willen behandelen bij de verdere verwerking.
methode 1: met het Claude Opus model
Veel moderne LLMs hebben visuele capaciteiten. Je kunt ze vragen om een foto te beschrijven. In deze methode gaan we die capaciteiten benutten:
-We knippen de PDF op in losse pagina’s en slaan elke pagina op als afbeelding
-We converteren de afbeelding naar Base64
, en voegen die samen met een prompt toe aan een API call naar Claude Opus, het beste model van Anthropic (zorg dat je de API key toevoegt aan je .env
bestand)
methode 2: met LlamaParse
LlamaParse is een vrij nieuwe oplossing van LLamaIndex, die een goede oplossing voor het extraheren van informatie hebben gelanceerd, en hun gratis versie is redelijk royaal, je mag 1000 pagina’s per dag gratis converteren, daarbij hebben ze een out of de box markdown oplossing, hoewel wij de JSON versie zullen gebruiken, omdat die de mogelijkheid biedt om ook afbeeldingen te extraheren.
-We uploaden de PDF naar LlamaParse
-We extracten de tabellen
-We extracten de afbeeldingen
-We converteren het totaal naar JSon en halen uit de Json de markdown teksten
Stap 2: het opknippen van de data en het toevoegen van metadata
De volgende stap is dat we de database vullen met de data uit de PDFs. Daarvoor moeten we de data eerst opknippen in kleine stukjes, waarbij we ook nog willen zorgen dat die stukjes een beetje “logisch” geknipt zijn, dus idealiter geen halve zinnen of belangrijke stukjes die net niet bij het relevante stukje tekst gezet kunnen worden. Dat is ook de reden dat we eerst hebben geconverteerd naar markdown, omdat we daarmee de originele alinea opdeling etc. behouden hebben. We kunnen daardoor gebruik maken van de indeling die de makers van de PDF oorspronkelijk hebben bedacht. Een andere stap die we hier nemen is dat we de stukjes informatie die we na het opknippen krijgen verrijken met wat metadata, zoals de pagina waarop deze informatie oorspronkelijk stond, zodat we bij het retrieven eventueel ook die hele pagina nog makkelijk terug kunnen vinden.
Een andere stap die we hier nemen is dat we de geextracte speciale informatie (in dit voorbeeld tabellen) laten samenvatten door een LLM. Dat doen we omdat de samenvatting van de tabel bij een vector similarity search of bij een hybride search variant waarschijnlijk hoger scoort dan de individuele chunk met alleen maar cijfertjes en de informatie uit deze tabellen voor heel veel search query’s wel super relevant is.
Stap 3: Het embedden en opslaan van de data
De volgende stap is dat we de data gaan Embedden. In mijn voorbeeld heb ik Cohere Embeddings gebruikt, omdat ik daar goede dingen over had gelezen, maar gewoon OpenAI kan natuurlijk ook. Je kunt deze stap ook lokaal uitvoeren met een Open Source embeddingsmodel, als je een computer hebt die beschikt over voldoende GPU’s, het was mij te traag, maar als je data hebt die bijv. privacy gevoelig is dan is dit zeker te overwegen (al is het de vraag hoe handig het is om die data in de mogelijke contextwindow van een chatbot te plaatsen natuurlijk 🙂).
Meer grip op je data
Een groot voordeel van het werken met Postgres ten opzichte van bijvoorbeeld een in-memory vectorstore als Faiss of een dedicated vector embeddings database zoals Pinecone is vind ik dat je met je reguliere programma’s die je al kent, ook je database kunt verkennen, en kunt zien hoe de data is gestructureerd in je datagbase.
Na het creeeren van je database kun je met tools die je al kent je database inspecteren. Zo heb ik met pgAdmin4 de inhoud van mijn data kunnen bestuderen en dat helpt best wel goed om het begrip over de data structuur wat te vergroten:
Stap 4: Langchain
Nu we onze Vector Store hebben opgezet kunnen we onze langchain chatbot gaan bouwen. Dit is echt verrassend makkelijk en gedaan met een paar regels code.
de chatfunctie
Zoals je hierboven ziet hebben we een vrij rechttoe-rechtaan algoritme opgesteld voor de chatbot. In feite nemen we een bepaalde gespreksgeschiedenis, voeden we die gespreksgeschiedenis (bijv. de laatste 10 berichten) aan een context retrieval chain, die relevante text chunks uit onze vector database haalt (hoe dat gaat zoomen we zo verder op in).
Vervolgens voeden we die text chunks samen met de laatste input van de gebruiker aan een conversational RAG chain, die een antwoord voor de gebruiker formuleert. Daarop kan de gebruiker dan weer reageren en daarmee is het cirkeltje rond.
de retriever
In het vorige deel hebben we onder meer besproken hoe een retriever in de basis werkt: je creeert vector embeddings van de userquery en vergelijkt die embeddings op hun coseine similarity distance met alle tekst chunks, en geeft dan (in de meeste gevallen) die chunks terug met de dichtstbijzijnde afstand.
de queryeditor
Wij voegen daar 1 stap aan toe. wij vertalen de userquery eerst naar een “goede” search query; dat doen we bijvoorbeeld voor vervolg vragen, als een vervolgvraag bijvoorbeeld iets is als “oh, hoe zit dat dan?”, dan kan een retriever eigenlijk geen goede chunks teruggeven, omdat de context van het gesprek daarvoor ontbreekt. In de query editor stap, vragen we de LLM om de userquery op basis van de chat history te vertalen naar een search query, om zo de kwaliteit van de geretourneerde text chunks te verbeteren. Schematisch ziet dat er zo uit:
De conversational chain
in ons model moeten we die chunks dan vervolgens dus nog vertalen naar een antwoord, hiervoor hebben we een 2e chain ingericht, die de chunks en de userquery vertaalt naar een prompt:
Analyseren met Langsmith
Als laatste voor dit deel van de blog is het nog leuk om een ander handig tooltje van LangChain onder de aandacht te brengen: LangSmith. Bovenstaande flow kan zonder goede logging behoorlijk lastig te volgen zijn, of in het geval van lage kwaliteit antwoorden te debuggen. Om die reden is het super nuttig om LangSmith aan je code toe te voegen. Ik vond de handigste set up om te werken met een uniek project per “run”. Dit is hoe je dat doet: 1: vul je .env bestand met de volgende omgevingsvariabelen:
LANGCHAIN_TRACING_V2="true"
LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY="[YOUR API KEY]"
2: Voeg de volgende regels code toe aan het begin van je script:
from uuid import uuid4
from dotenv import load_dotenv
from langsmith import Client
load_dotenv()
# Set up LangSmith tracing
unique_id = uuid4().hex[:8]
os.environ["LANGCHAIN_PROJECT"] = f"Teslachatbot- {unique_id}"
client = Client(api_key=os.environ["LANGSMITH_API_KEY"])
Conclusie en next steps
Dit stelt je in staat om elke query van begin tot eind door al je chains te volgen.
Zie ter illustratie een paar screenshots, of bekijk de traces zelf hier: https://smith.langchain.com/public/d57c5f2b-af63-4b8c-aa99-79d9c10ed77e/r
Conclusie en next steps
Zoals je kunt zien is het echt vrij eenvoudig om gave applicaties te maken met tools als langchain, waarbij je gebruik kunt maken van een snel groeiend aanbod van verschillende LLMs en andere Ai toepassingen. De ontwikkelingen gaan zo snel dat er nu al discussies zijn over of RAG niet alweer over het hoogtepunt heen is, omdat de context windows van LLMs super snel groeien, zo heeft Gemini van Google al een contextwindow van 1.5 miljoen tokens. Ik zelf denk dat in de traceerbaarheid en de simpliciteit van de opzet met RAG voldoende voordelen zitten dat dat niet het geval is, daar komt nog bij dat het hebben van een kleinere context window ook andere voordelen met zich meebrengt, in de vorm van prijs en snelheid. Dit soort discussies tonen wel aan hoe snel innovaties elkaar opvolgen en het lijkt me voor een volgende blog leuk om een andere hoek in te duiken, bijvoorbeeld het gebruik van Agents. Net als bij de vorige delen ben ik super benieuwd naar jullie feedback dus mocht je die hebben hoor ik het graag.