TSM - Introducere în sistemele RAG

Darius Dima - Software System Engineer @ Centrul de Inginerie Bosch Cluj

Într-o eră digitală în care informația curge într-un ritm amețitor, tehnologiile care combină căutarea eficientă cu generarea de conținut devin esențiale. Aceste soluții inovatoare ne ajută să navigăm mai eficient prin complexitatea datelor și să găsim rapid răspunsuri relevante la întrebările noastre. Odată cu popularizarea modelelor de limbaj de mari dimensiuni (LLM), căutările semantice au evoluat semnificativ, devenind un instrument valoros în procesul de informare. Din păcate, antrenarea unui LLM necesită resurse greu accesibile, iar situațiile în care căutarea trebuie să extragă rezultate precise și de actualitate sunt tot mai frecvente. În acest context, sistemele care integrează extragerea informațiilor cu generarea de conținut vin să acopere acest gol punând în valoare puterea căutărilor semantice pe date relevante.

Ce este un sistem RAG ?

Un sistem RAG (Retriever-Augmented Generation) este o combinație a două elemente: un retriever și un generator.

  1. Retriever: Folosește baze de date vectoriale (precum Qdrant, Pinecone sau FAISS) pentru a căuta fragmentele relevante din documente.

  2. Generator: Un model LLM (Large Language Model), ce poate fi local (folosind instrumente precum Ollama sau LM Studio), sau accesat prin API-uri cloud cum sunt cele de la OpenAI (ChatGPT), Deepseek etc.

Retrieverul parcurge datele în căutarea fragmentelor contextuale relevante pentru întrebarea pusă, iar generatorul creează și oferă răspunsurile folosind acest context.

De ce sunt importante sistemele RAG ?

Sistemele RAG revoluționează modul în care interacționăm cu informația. Iată câteva exemple de aplicații ce pot fi implementate folosind un sistem RAG:

Sună interesant ? Haideți să ne construim propriul sistem RAG local

În acest articol propunem un setup local, ușor de utilizat, ce ne permite să păstrăm private datele sensibile.

Setup

1.Descărcați LM Studio pentru sistemul dumneavoastră de operare de pe site-ul oficial.

2.Mergeți la secțiunea Discover și alegeți un model pe care să îl descărcați. Voi folosi qwen2.5-7B-instruct-1M, deoarece este un model mic, însă suficient de capabil, dar vă sfătuiesc să experimentați cu diferite modele.

3.Încărcați modelul și mergeți la secțiunea Developer pentru a porni serverul (Ctrl + R). Acest lucru va face modelul disponibil local.

4.Creați un mediu virtual de Python în directorul proiectului RAG:

python -m venv .venv
source .venv/Scripts/activate  
# exemplu pentru terminalul Git Bash

5.Instalați dependințele:

pip install numpy faiss-cpu transformers
sentence-transformers openai

Structura proiectului:

Construirea retrieverului (data_retriever.py):

1.La început, documentele sunt împărțite în fragmente de o dimensiune aleasă (valoarea predefinită fiind de 512 caractere). Spre exemplu, câteva fișiere text pot vor deveni sute (sau mii) de fragmente text de 512 caractere fiecare. Acestea sunt apoi indexate pentru căutare rapidă și transformate în codificări numerice numite embedings.

class DataRetriever:
  def __init__(self, directory_path, 
  chunk_size=512, top_k=5):
    self.directory_path = directory_path
    if not os.path.isdir(self.directory_path):
       raise ValueError(f"Directory does not exist: 
       {self.directory_path}")
    self.model = SentenceTransformer(
    'all-MiniLM-L6-v2')
    self.chunk_size = chunk_size
    self.top_k = top_k
    self.index_path = 'faiss_index'
    self.embeddings_path = 'embeddings.pkl'
    self.document_chunks = self._chunk_documents()
    self.index, self.embeddings = 
    self._load_or_build_index()

2.Când utilizatorul abordează o cerință, aceasta este, de asemenea, codificată în reprezentări numerice (embedings). Retrieverul returnează primele top_k (în cazul nostru 5) fragmente de text din documente cu conținut similar cerinței utilizatorului. Folosind reprezentările numerice, căutarea este mult mai amplă decât o simplă căutare după cuvinte cheie, întrucât acum se iau în considerare și cuvintele apropiate ca sens (sinonime, familie de cuvinte etc.).

def retrieve(self, query):
  query_embedding = self.model.encode([query], 
convert_to_tensor=True).cpu().numpy()
    distances, indices = self.index.search(
    query_embedding, self.top_k)

    relevant_texts = [f"... {
    self.document_chunks[idx]} ..." for 
    idx in indices[0]]

return '\n'.join(relevant_texts)

Implementarea celorlalte metode ajutătoare se poate găsi în codul sursă.

Construirea generatorului de răspuns (response_generator.py):

1.LM Studio folosește API-ul openai. Dorim totuși, să fim flexibili, lăsând deschisă posibilitatea de a folosi sistemul RAG și cu alte API-uri (precum Ollama). Astfel, vom crea, mai întâi o interfață pe care să o moștenim:

from abc import ABC, abstractmethod

class IResponseGenerator(ABC):
  @abstractmethod
  def query(self, user_prompt):
    pass

class OpenaiResponseGenerator(IResponseGenerator):

2.Ca să folosim modelul local, vom specifica adresa serverului local: http://127.0.0.1:1234 pentru variabila base_url la inițializare.

   def __init__(self, base_url, api_key, model,
   history_length=20):

     self.client = OpenAI(base_url=base_url, 
     api_key=api_key)

3.Deoarece LLM-urile nu au capacitatea intrinsecă de a reține mai mult de un mesaj, dacă dorim să putem purta o conversație, va trebui să reținem mesajele anterioare într-o listă:

Într-o conversație cu un LLM există trei roluri posibile: cel de system, de user și de assistant. Rolul de system va fi întotdeauna primul și ne ajută să definim un comportament general pentru modelul de limbaj de-a lungul conversației. Vom adăuga posibilitatea de setare a unui prompt de sistem prin metoda set_system_prompt și vom folosi o metodă ajutătoare add_to_chat_history pentru a ne asigura că acesta este primul în conversație, chiar dacă se depășește mărimea istoricului.

def add_to_chat_history(self, message):
  sys_prompt_not_in_history = not any(
  msg['role'] == 'system' and msg['content'] == 
    self.system_prompt for msg in self.chat_history
  )
 if self.system_prompt and sys_prompt_not_in_history:
   self.chat_history.insert(0, {'role': 'system', 
                     'content': self.system_prompt})
   self.chat_history.append({'role': 'user', 
                     'content': message})
   if len(self.chat_history) > self.history_length:
     self.chat_history = self.chat_history[
                      -self.history_length:]

4.Metoda query este responsabilă pentru a extrage răspunsul LLM-ului.

def query(self, user_prompt):
  try:
    self.add_to_chat_history(user_prompt)
    response = self.client.chat.completions.create(
      messages=self.chat_history,
      model=self.model
    )
    assistant_response = {
      'role': 'assistant',
      'content': response.choices[0].message.content
    }
    self.chat_history.append(assistant_response)
    if len(self.chat_history) > self.history_length:
      self.chat_history = self.chat_history[
        -self.history_length:
      ]
    return response.choices[0].message.content
  except Exception as e:
    return f"S-a produs o excepție: {str(e)}"

Conectarea sistemului (rag.py):

După ce am construit retrieverul și generatorul, vom combina aceste două componente pentru a forma sistemul RAG. În esență, vom modifica promptul pentru a adăuga în context citatele relevante extrase din documente.

from data_retriever import DataRetriever
from response_generator import IResponseGenerator

class RAG:
  def __init__(
    self,
    retriever: DataRetriever,
    generator: IResponseGenerator
  ):
    self.retriever = retriever
    self.generator = generator

  def generate_response(self, user_query):
    retrieved_docs = self.retriever.retrieve(
      user_query
    )
    intro = (
      "Având în vedere anumite informații "
      "relevante din documente:"
    )
    pre_prompt = (
      "Răspunde la următoarea cerere "
      "a utilizatorului:"
    )
    full_prompt = (
      f"{intro}\n```\n{retrieved_docs}\n```\n"
      f"{pre_prompt}\n{user_query}"
    )
    response = self.generator.query(full_prompt)
    return response

Rulează sistemul RAG (main.py):

Definim funcționalitatea prin care utilizatorul interacționează cu sistemul.

def start_chat(rag_system):
  user_query = input("Utilizator: ")
  while user_query.lower() != 'quit':
    response = rag_system.generate_response(
      user_query
    )
    print(f"Asistent: {response}")
    user_query = input("Utilizator: ")

Implementăm metoda main, unde configurăm retrieverul și generatorul, iar apoi sistemul RAG.

def main():
  retriever = DataRetriever(
    'data',
    chunk_size=512,
    top_k=5
  )

  base_url, api_key = (
    "http://127.0.0.1:1234/v1",
    "api_key"
  )
  model = "qwen2.5-7b-instruct-1m"
  generator = OpenaiResponseGenerator(
    base_url,
    api_key,
    model,
    history_length=10
  )

  rag_system = RAG(retriever, generator)
  start_chat(rag_system)

if __name__ == "__main__":
  main()

Testare și îmbunătățiri

Concluzie

Marele beneficiu al sistemelor RAG (Retriever-Augmented Generation) este capacitatea acestora de a oferi răspunsuri mai precise și relevante prin integrarea informațiilor externe cu modele generative. Aceasta permite sistemelor de inteligență artificială să acceseze cunoștințe actualizate și să răspundă la întrebări complexe, îmbunătățind astfel acuratețea și utilitatea răspunsurilor. Această abordare ajută la depășirea limitărilor modelelor de limbaj bazate exclusiv pe datele cu care au fost antrenate.