Realizzare RAG con LangChain
Indice dell'articolo
LangChain, che abbiamo introdotto in questo articolo, è un tool software per creare applicazioni basate su Intelligenza Artificiale. Una delle tipologie di applicazioni più richieste in questo ambito è la cosiddetta RAG (Retireval Augmented Generation), che permette di abbinare la versatilità di interazione di un modello linguistico con la necessità di fornire dati certi basati su informazioni proprietarie.
Per questo motivo, la RAG assume una specifica importanza nel business moderno, in quanto rende l’applicazione in grado di risolvere problemi ampliando la conoscenza a disposizione dell’LLM. Essenzialmente, quindi, non cercheremo di rendere il motore più potente come faremmo con il fine tuning ma gli forniremo del materiale sotto forma di documenti o delle banche dati da consultare con interrogazioni e API per recuperare le informazioni rilevanti e comporre la risposta.
LangChain offre un completo set di strumenti che include tutto ciò di cui abbiamo bisogno, e soprattutto offre un set di funzioni per l’interazione con gli LLM. In questo articolo percorreremo le fasi principali della costruzione di una RAG con LangChain e lo faremo seguendo i passi di un esempio pratico formalizzato in linguaggio Python (sebbene non sia l’unica opzione a disposizione).
Come si costruisce un sistema RAG
Uno dei tipi di applicazione, nel paradigma RAG, che possiamo implementare è sicuramente il Question & Answer, generalmente indicato con la sigla Q&A, ovvero un meccanismo di domanda e risposta in cui possiamo mettere una base di conoscenza a disposizione di un’applicazione, che potrà gestire quindi un flusso di domande sui temi trattati dai documenti e rispondere in maniera corretta. Il tutto lasciando fare il lavoro “difficile” all’infrastruttura che LangChain e gli LLM offrono.
Questo tipo di applicazione è molto utile in quanto può costituire una sorta di motore di ricerca aziendale, tirato su in poco tempo, sfruttando documenti a nostra disposizione, tipicamente non conosciuti dagli LLM. Inoltre, in fase di studio o assunzione a livello aziendale di una nuova tecnologia è un tipo di sperimentazione non difficile, rapida, efficace e sicuramente in grado di dare grande soddisfazione e motivazione.
La creazione di una struttura di questo tipo si articola per lo più in tre fasi:
- Indexing
- Retrieval
- Generation
Indicizzazione dei dati per la RAG
L’indexing consiste nel raccogliere documenti (nostri o forniti da varie fonti) ed inserirli in un database vettoriale.
L’indexing a sua volta si articola in una serie di sottofasi tra cui distinguiamo:
- loading, il documento viene caricato e ciò può avvenire in vari modi ad esempio direttamente dal web, da file di vari formati o da servizi on line;
- splitting, ogni documento acquisito viene splittato ovvero sminuzzato in una serie di componenti. Questa è la fase iniziale della sua comprensione in quanto un documento è tipicamente scritto per essere leggibile da esseri umani pertanto contiene delle strutture al suo interno che il nostro cervello ricostruisce velocemente ma nelle quali una macchina deve essere guidata per individuarne i collegamenti costituenti;
- embedding, uno dei momenti centrali dell’Intelligenza Artificiale Generativa. Si tratta di trasformare parole e concetti in numeri e vettori, la forma di dato che la macchina riesce maggiormente a comprendere;
- storing, immagazzinamento in un database vettoriale di quello che abbiamo prodotto con la fase di embedding.
Al termine di questo possiamo dire che i documenti sono stati archiviati in un database quindi sono pronti per essere messi a disposizione di un LLM. Quest’ultimo, a questo punto del processo, sarà entrato già in gioco ma non come capacità intellettiva, risolutiva e generativa bensì a livello di embedding. La penultima fase, infatti, della sequenza che abbiamo enunciato poco fa, l’embedding, è stata svolta proprio da un modulo detto embedder tipicamente offerto dalla stessa libreria che utilizzeremo anche per avere il nostro LLM a lavoro.
Se, ad esempio, abbiamo scelto di utilizzare OpenAI, lavorando in linguaggio Python, dovremo:
- includere la libreria per OpenAI:
pip install -qU langchain-openai
- inserire una API key per interagire con questo sistema (spesso con la variabile d’ambiente
OPENAI_API_KEY
)
Un flusso di lavoro ideale di quello che abbiamo detto sinora, finalizzato al caricamento di dati testuali, potrebbe essere:
from langchain_community.document_loaders import TextLoader
# caricamento di dati testuali
# inserire il nome del file di interesse
loader = TextLoader(....)
doc=loader.load()
# preparazione di uno splitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
# splitting dei documenti caricati
splits = text_splitter.split_documents(doc)
# immagazzinamento in un database vettoriale
store= Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
Notare che all’ultimo passaggio fa la sua comparsa OpenAI (il motore di AI che avremmo scelto per l’esempio) per offrire il suo embedder.
A questo punto abbiamo svolto una parte di preparazione dei dati: sembra qualcosa di semplicemente preliminare ma è un processo fondamentale in quanto abbiamo messo a disposizione tutto ciò che servirà sapere alla nostra RAG ovvero la sua base di conoscenza.
Recupero dei dati
A questo punto dovremo assemblare questa componente con le altre che serviranno a creare la nostra applicazione. In questo caso subentra l’utilità del concetto di chain di Langchain ovvero la sua abilità di connettere componenti tra loro in un flusso in cui l’output dell’una diventa l’input dell’altra.
Prepariamo gli strumenti mettendo a disposizione un retriever
retriever = vectorstore.as_retriever()
ed un prompt a nostra scelta:
template = """
Context: {context}
Question: {question}
Answer: """
prompt = PromptTemplate(
template=template
)
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
)
ora che la RAG sarà pronta potremo provarla al volo passando una domanda mediante invoke
.
rag_chain.invoke(....)
Testare la catena per la RAG
Una volta messo a disposizione un semplice esempio di questo tipo e averlo collegato con le apposite chiavi all’LLM da utilizzare possiamo svolgere la sperimentazione. Un modo veloce e pratico potrebbe essere la creazione di documenti con contenuti da noi inventati di cui ovviamente l’LLM non sarà a conoscenza. Nel nostro caso, ad esempio abbiamo scritto un testo riguardante dei nostri eventuali amici di questo tipo:
“I miei migliori amici sono Gabriele, Silvia, Enzo e Giorgia.
Giorgia lavora in un ufficio come segretaria mentre Silvia è la più sportiva infatti è un’istruttrice di yoga…”
e l’abbiamo fornito alla RAG per la sua fase di immagazzinamento nel database.
Abbiamo poi posto una serie di domande all’applicazione mediante metodo invoke
e quella che segue è la nostra sessione di Question & Answer:
Q: Chi si interessa di yoga?
A: Silvia
Q: Chi fa la segretaria?
A: Giorgia
Q: Di quali amici non conosco il lavoro?
A: Non conosco il lavoro di Gabriele e Enzo tra i miei amici.
Le risposte sono tutte corrette ed in particolare l’ultima ha richiesto un’elaborazione leggermente più complessa delle prime in quanto non ha recuperato informazioni esistenti bensì, al contrario, ha dedotto la risposta considerando ciò che mancava.
Sviluppi ed implementazioni della RAG
Nell’esempio, abbiamo scritto il testo della domanda direttamente nel codice ma, come si può facilmente supporre, non è la vera modalità di utilizzo. Il testo del quesito potrebbe provenire da un form web, via API per non parlare di comunicazioni mail, messaggistica o trascrizione di una richiesta vocale: tutte modalità che permetterebbero di implementare subito chatbot, servizi remoti di assistenza, siti web intelligenti e tanto altro sia per l’utenza interna all’azienda sia per la clientela.
In ognuno dei casi citati, si avrà comunque un utente che potrà inviare una domanda ed ottenere una risposta rapida, corretta e completa, in base ai documenti di alimentazione.
Qual è quindi la parte che potremmo considerare più critica da parte nostra?
Beh, sicuramente se il ragionamento lo mette l’LLM, tutte le componenti dell’indexing e di comunicazione sono fornite da LangChain, quello che dobbiamo davvero fare bene è selezionare, raccogliere e fornire i documenti. Questa è una parte in cui la cosiddetta Data Engineering può giocare un ruolo fondamentale permettendo il recupero dei documenti o la formazione di questi, ad esempio, raccogliendo dati da varie fonti ed inserendoli all’interno di testi e strutture dati.
L’aspetto modulare di LangChain, tra l’altro, mostra ancora come possiamo non solo scegliere le componenti che desideriamo ed unirle ma possiamo anche inserirci in qualsiasi punto fornendo ad esempio nostre componenti o modellandole in un sistema che è assolutamente aperto.