Co to jest Retrieval-Augmented Generation, czyli RAG?
Przykład kodu w Python który wykorzystuje RAG by przeszukiwać poemat Pan Tadeusz Adama Mickiewicza.
Aby zrozumieć najnowsze osiągnięcia w dziedzinie generatywnej sztucznej inteligencji, wyobraź sobie salę sądową.
Sędziowie rozpatrują i rozstrzygają sprawy w oparciu o swoje ogólne rozumienie prawa. Czasami sprawa – na przykład pozew o błąd w sztuce lub spór pracowniczy – wymaga specjalnej wiedzy specjalistycznej, dlatego sędziowie wysyłają urzędników sądowych do biblioteki prawniczej w poszukiwaniu precedensów i konkretnych spraw, które mogliby przytoczyć.
Środowisko programu
Skrypt który realizuje to zadanie jest napisany w Python. Wykorzystuje LangChain - framework do tworzenia aplikacji opartych na modelach językowych. Sam Lanchain rozbity został na trzy pakiety: langchain-core, langchain-community i langchain. Przeczytaj więcej o każdym z pakietów.
W programie wykorzystywana jest także baza Chroma. Odczytany z poematu tekst dzielimy na kawałki, obliczamy wektory ('embeddings') dla każdego 'kawałka' tekstu i zapisujemy sam fragment oraz wektor do właśnie tej bazy.
Następnym komponentem jest właśnie LLM. Używamy modelu do obliczania wektorów (do czego one są nam potrzebne niżej) oraz generowania treści z naszego poematu (zapisanego w Chroma DB). Środowiskiem modeli jest Ollama.
Jak działa program - loader?
- Odczytujemy plik, dzielimy na kawałki ("chunks"). Polecenie splittera 'CharacterTextSplitter' ma parametry 'chunk_size', 'chunk_overlap', 'separator'. Chunk_size mowi jakiej maksymalnej długości mają być kawałki tekstu. ALE jeśli tekst nie zmieści się w deklarowanej długości, chunk będzie dłuższy. Np. kiedy separator wypadnie dalej niż po 1000 znaków. Program wyświetli wtedy komunikat: 'Created a chunk .... longer than the specified'. Chunk_overlap to po prostu nakładanie się następujących po sobie fragmentów treści czasami wymagane by nie utracić kontekstu. Jeśli tekst konczyłby sie np słowami '... mały rowerek.' to przy 'chunk_overlap=8' to słowo 'rowerek' rozpocznie następne zdanie. Separator oznacza... separator który rozdziela kolejne fragmenty treści. Domyślnym separatorem jest nowa linia. Efektem zastosowania takiego separatora jest próba utrzymania wszystkich akapitów (a następnie zdań, a następnie słów) razem tak długo, jak to możliwe, ponieważ ogólnie wydają się one najsilniejszymi semantycznie powiązanymi fragmentami tekstu. Jednak Twój tekst może być inny, użyj więc innego separatora. Pana Tadeusza. Tekst możesz pobrać np. z https://wolnelektury.pl
- Obliczamy wektor dla tego fragmentu tekstu i zapisujemy sam tekst i wektor do bazy Chroma. Gdybyśmy nie użyli parametru 'persist_directory', fragmenty teksu i ich osadzenia ('embeddings') zapisane zostałyby w pamięci.
Skrypt loader.py
# import
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import CharacterTextSplitter
# mierzymy czas wykonania, start:
import time
start_time = time.time()
# ładujemy dokument
loader = TextLoader("F:/Ollama/RAG/pan_tadeusz.txt", encoding="utf8")
documents = loader.load()
# dzielimy go na kawałki (chunks)
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0, separator=".")
docs = text_splitter.split_documents(documents)
# model SBERT - embeddings
#embeddings = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
# model Ollama - embeddings
embeddings = OllamaEmbeddings(model="mistral")
# zapisujemy do Chroma ('vector store'), do pliku na dysku nie pamieci
db = Chroma.from_documents(docs, embeddings, persist_directory="F:/RAG/ChromaData")
#zapisujemy rezultat - baze z wektorami
db.persist()
# ile czasu zajelo wykonanie
end_time = time.time()
execution_time = end_time - start_time
print("Execution time:", execution_time, "seconds")
Jak działa program - reader?
- Odczytujemy z bazy Chroma treść która jest skojarzona z naszym zapytaniem. Zauważ że nasze połączenie z bazą danych Chroma używa modelu 'mistral' - tego samego którego użyliśmy do utworzenia wektorów w poprzednim kroku (parametr 'embedding_function'). W rezultacie tego zapytania możemy otrzymać kilka dokumentów - używamy ich jako treści której użyje model. Parametr 'return_source_documents=True' pokaże nam które dokumenty zostały użyte do wygenerowania odpowiedzi.
- Używamy modelu by wygenerował odpowiedź na nasze zapytaniu używając treści źródłowej jak wyżej i algorytmów modelu. Parametr 'search_kwargs' retrivera określa ile dokumentów ma być brane pod uwagę przy analizie materiału. Im wyższa liczba, tym dokładniejsza może być nasza odpowiedź (szczególnie jeśli zapytanie nie jest precyzyjne lub źródło 'rozproszone'). Ale będzie to mieć negatywny wpływ na szybkość odpowiedzi - trzeba przeanalizować wiekszą ilość informacji.
Skrypt reader.py
# import
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
# mierzymy czas wykonania, start:
import time
start_time = time.time()
# reset zmiennej
db=None
embeddings = OllamaEmbeddings(model="mistral")
# odczytaj z bazy Chroma
db = Chroma(persist_directory="F:/Ollama/RAG/data", embedding_function=embeddings)
results = db.get()
llm = Ollama(base_url='http://localhost:11434', model="mistral", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=db.as_retriever(search_type="similarity", search_kwargs={"k": 3}),
return_source_documents=True
)
question = "O czym mowa w utworze?"
result = qa_chain.invoke({"query": question})
result["result"]
print(result)
#skasuj wszystko
#db.delete_collection()
#print(results)
# ile czasu zajelo wykonanie
end_time = time.time()
execution_time = end_time - start_time
print("Execution time:", execution_time, "seconds")
Rezultat
Jeśli wykonasz ten skrypt zobaczysz odpowiedz podobną do tej poniżej:
{'query': 'O czym mowa w utworze?',
'result': " The poem appears to be a narrative about an event or scene involving various noblemen and their possessions.
There is mention of a judge, a seneschal (Rejent), an assessor, a bernardyn (a friar), Wilbik, Skoluba, Vojski, and Asesor.
They seem to be discussing the taking away of treasures from churches and the opposition to it.
There is also mention of Napoleon and his granting of titles and lands to his generals.
The poem contains descriptions of various objects such as a horse, a ring, golden armor, agun (a f), and a gun (fuzyjka).
There are mentions of Strapczyna, Wojski, Rejent, and Asesor. They seem to be opposing the taking away of treasures from churches.
The poem also contains descriptions of objects such as a horse, a judge, Spawnik, and a gun.
It is called Sagalas London à Bałabanów or Sagalas, which is a famous Polish English English English English poem.
It speaks of a sword, which was used to crush the animal's body. The poem also mentions a ring, a horse, and a judge.
They seem to be discussing the taking away of treasures from churches, opposition to it, and their opposition to it."}
Jeśli użyjesz parametru 'return_source_documents=True', w odpowiedzi otrzymasz wypisane 'source_documents' - dokumenty które zostały użyte do wygenerowania odpowiedzi.
Format odpowiedzi
W powyższym skrypcie 'result' ma format 'dictionary' (po polsku 'słownik'). Słowniki służą do przechowywania wartości danych w parach klucz : wartość. Słownik to zbiór uporządkowany*, podlegający zmianom i nie pozwalający na duplikowanie.