RAG w Delphi: oprzyj LLM na własnych dokumentach

· Komponenty

Szybka odpowiedź: Retrieval-Augmented Generation (RAG) to dwa pomysły sklejone razem. Najpierw raz osadzasz swoje dokumenty i przechowujesz powstałe wektory. Następnie, przy każdym pytaniu, osadzasz pytanie, pobierasz najbliższe znaczeniowo fragmenty i przekazujesz je do LLM jako kontekst, tak aby odpowiedź pochodziła z Twoich danych, a nie z pamięci treningowej modelu. W sgcWebSockets cały potok to trzy komponenty: TsgcAIOpenAIEmbeddings do zamiany tekstu na wektory, TsgcAIDatabaseVectorFile lub TsgcAIDatabaseVectorPinecone do ich przechowywania i przeszukiwania oraz TsgcHTTP_API_OpenAI do napisania ostatecznej, ugruntowanej odpowiedzi.

Ogólnego przeznaczenia model czatu wie wiele o świecie i nic o instrukcji Twojego produktu, Twoich zgłoszeniach serwisowych czy wewnętrznym raporcie z zeszłego kwartału. Zapytaj go o nie, a albo odmówi, albo, co gorsza, wymyśli coś prawdopodobnie brzmiącego. RAG rozwiązuje ten problem bez ponownego trenowania czegokolwiek: zostawiasz model bez zmian i podajesz mu odpowiednie fragmenty z Twojego własnego korpusu w momencie zadania pytania. Poniżej cała pętla w Delphi, od początku do końca, z prawdziwymi nazwami komponentów.

Co RAG właściwie robi

Osadzenie (embedding) to lista liczb, która oddaje znaczenie fragmentu tekstu. Dwa fragmenty na ten sam temat lądują blisko siebie w tej liczbowej przestrzeni, nawet gdy nie mają wspólnych słów kluczowych. Baza wektorowa przechowuje te liczby i dla zadanego wektora zapytania zwraca najbliższe wpisy uszeregowane według podobieństwa. RAG łączy to w jeden ciąg:

EtapCo się dziejeElement sgcWebSockets
1. Wczytanie (raz)Podziel dokumenty na fragmenty, osadź każdy fragment, zapisz wektoryTsgcAIOpenAIEmbeddings.CreateEmbeddingsFromFile
2. PrzechowywanieTrzymaj wektory w lokalnym pliku lub w indeksie w chmurzeTsgcAIDatabaseVectorFile · TsgcAIDatabaseVectorPinecone
3. Pobieranie (na pytanie)Osadź pytanie, znajdź najbliższe fragmentyGetEmbeddingQueryData
4. OdpowiedźUmieść fragmenty w prompcie, zapytaj modelTsgcHTTP_API_OpenAI._CreateChatCompletion

Kroki 1 i 2 uruchamiają się, gdy zmieniają się Twoje dane. Kroki 3 i 4 uruchamiają się przy każdym pytaniu użytkownika. Zbudujmy każdy z nich.

Krok 1 — osadź swoje dokumenty

Utwórz TsgcAIOpenAIEmbeddings, podaj mu klucz OpenAI, ustaw jego właściwość Database na magazyn wektorów i wywołaj CreateEmbeddingsFromFile. To jedno wywołanie odczytuje plik, dzieli go na fragmenty (sterowane przez EmbeddingsOptions.ChunkSize), osadza każdy fragment i zapisuje wektory do magazynu za pomocą sekwencji BeginAddData / AddData / EndAddData za Ciebie.

uses
  sgcAI, sgcAI_OpenAI_Embeddings,
  sgcAI_DB_Vector, sgcAI_DB_Vector_File, sgcAI_DB_Vector_Pinecone;

var
  Embeddings: TsgcAIOpenAIEmbeddings;
  DBFile: TsgcAIDatabaseVectorFile;
begin
  Embeddings := TsgcAIOpenAIEmbeddings.Create(nil);
  Embeddings.OpenAIOptions.ApiKey := 'sk-...';

  // local, file-based vector store
  DBFile := TsgcAIDatabaseVectorFile.Create(nil);
  DBFile.VectorFileOptions.InputFilename  := 'corpus.sgcif';
  DBFile.VectorFileOptions.VectorFilename := 'corpus.sgcvf';

  Embeddings.Database := DBFile;
  Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;

To cały krok wczytywania. Uruchom go raz lub za każdym razem, gdy zmieniają się Twoje dokumenty. Domyślny model osadzania to text-embedding-3-small; zmień go przez EmbeddingsOptions.Model, jeśli potrzebujesz innego. Więcej szczegółów znajdziesz na stronie komponentu Embeddings.

Krok 2 — wybierz, gdzie mają być wektory

Oba zaplecza dziedziczą po tym samym komponencie bazowym, TsgcAIDatabaseVector, więc są wymienne: zamień jedno na drugie, a Twój kod wczytywania i odpytywania się nie zmieni. Jedyna różnica to miejsce, w którym znajdują się dane.

Dla aplikacji desktopowej, narzędzia działającego offline lub mniejszego korpusu TsgcAIDatabaseVectorFile trzyma wszystko w lokalnym pliku, bez żadnej usługi zewnętrznej. Gdy indeks jest duży, musi być współdzielony między procesami lub użytkownikami albo musi skalować się poza jedną maszynę, przełącz się na TsgcAIDatabaseVectorPinecone, które wysyła (upsert) każdy fragment przez zarządzane REST API Pinecone:

var
  DBPinecone: TsgcAIDatabaseVectorPinecone;
begin
  DBPinecone := TsgcAIDatabaseVectorPinecone.Create(nil);
  DBPinecone.PineconeOptions.ApiKey         := 'pc-...';
  DBPinecone.PineconeIndexOptions.IndexName := 'sgc-embeddings';

  Embeddings.Database := DBPinecone;
  Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;

Zauważ, że linia wczytywania jest identyczna jak w kroku 1. O to właśnie chodzi we wspólnej klasie bazowej. Zobacz stronę baz wektorowych dla zaplecza plikowego oraz stronę Pinecone dla wersji chmurowej.

Krok 3 — pobierz i odpowiedz

Teraz ścieżka dla pojedynczego pytania. Osadź pytanie użytkownika i znajdź najbliższe przechowywane fragmenty w jednym wywołaniu: GetEmbedding osadza tekst i przepuszcza go przez metodę bazy QueryData, zwracając najbardziej trafne fragmenty z Twojego korpusu. Te fragmenty są Twoim kontekstem. Połącz je z pytaniem i wyślij całość do modelu czatu:

var
  Question, Context, Prompt, Answer: string;
  OpenAI: TsgcHTTP_API_OpenAI;
begin
  Question := 'How do I enable the WatchDog reconnect?';

  // retrieve the closest chunks from your own data
  Context := Embeddings.GetEmbedding(Question, '');

  // build a grounded prompt
  Prompt :=
    'Answer the question using only the context below.' + sLineBreak +
    'If the context does not contain the answer, say you do not know.' +
    sLineBreak + sLineBreak +
    'Context:' + sLineBreak + Context + sLineBreak + sLineBreak +
    'Question: ' + Question;

  // ask the model
  OpenAI := TsgcHTTP_API_OpenAI.Create(nil);
  OpenAI.OpenAIOptions.ApiKey := 'sk-...';
  Answer := OpenAI._CreateChatCompletion('gpt-4o-mini', Prompt);

  Memo1.Lines.Text := Answer;
end;

To jest RAG. Model nigdy nie widział Twoich dokumentów podczas treningu, a mimo to odpowiada na ich podstawie, ponieważ to Ty postawiłeś przed nim odpowiednie fragmenty w momencie żądania. Zmień korpus, a odpowiedzi zmienią się wraz z nim, bez żadnego dostrajania. Instrukcja, by odmówić, gdy kontekst jest pusty, jest tym, co utrzymuje model w szczerości zamiast zgadywania.

Lokalnie lub w chmurze, ten sam kod

Jeden szczegół warto powtórzyć: wybór między magazynem plikowym a Pinecone jest odwracalny. Ponieważ TsgcAIDatabaseVectorFile i TsgcAIDatabaseVectorPinecone dzielą bazę TsgcAIDatabaseVector, możesz prototypować na lokalnym pliku (zerowa infrastruktura, działa offline) i przejść na Pinecone później, podmieniając komponent przypisany do Embeddings.Database. Nic w Twoim kodzie wczytywania ani odpytywania się nie zmienia. To samo dotyczy LLM na końcu: _CreateChatCompletion na TsgcHTTP_API_OpenAI można podmienić na komponent Anthropic lub Gemini, jeśli wolisz, by ostateczną odpowiedź napisał inny model.

Uwaga o podziale na fragmenty i jakości

Jakość pobierania zależy od tego, jak podzielone są Twoje dokumenty. Mniejsze fragmenty czynią dopasowania precyzyjniejszymi, ale mogą tracić kontekst; większe fragmenty zachowują kontekst, lecz rozmywają dopasowanie. EmbeddingsOptions.ChunkSize steruje tym dla CreateEmbeddingsFromFile, więc warto go dostroić do Twojego materiału. Dla większej kontroli możesz też osadzać pojedyncze łańcuchy za pomocą CreateEmbeddings i samodzielnie kształtować fragmenty przed wczytaniem.

Jak zacząć

Wszystkie trzy komponenty są dostarczane w sgcWebSockets. Pobierz bezpłatną wersję próbną, upuść komponenty osadzania i magazynu wektorów na formularzu, wskaż im plik tekstowy, a będziesz mieć działającą pętlę RAG w znacznie mniej niż stu liniach. Przejrzyj pełen zestaw klocków AI w centrum komponentów AI i LLM.

Pytania o zastosowanie tego do własnego korpusu? Skontaktuj się z nami — odpowiedź otrzymasz od ludzi, którzy napisali ten kod.

Powiązane