RAG e pgvector

Dialogo con Gemini.

Ciao, puoi portare qualche esempio per introdurre pgvector?

pgvector è un’estensione open source per PostgreSQL che permette di memorizzare, indicizzare e cercare embedding vettoriali direttamente all’interno del database relazionale, combinando la potenza delle query SQL tradizionali con la ricerca semantica.

1. Attivazione e creazione della tabella

Abilita l’estensione nel database e definisci una colonna di tipo vector(N), dove N è il numero di dimensioni del vettore (ad esempio 1536 per i modelli OpenAI o 384 per modelli locali come Sentence-Transformers).

SQL

-- Abilita l'estensione pgvector
CREATE EXTENSION IF NOT EXISTS vector;

-- Crea una tabella per salvare articoli ed i relativi vettori (es. a 3 dimensioni)
CREATE TABLE articoli (
    id SERIAL PRIMARY KEY,
    titolo TEXT,
    contenuto TEXT,
    embedding vector(3)
);

2. Inserimento dei vettori

I vettori si inseriscono come stringhe formattate tra parentesi quadre:

SQL

INSERT INTO articoli (titolo, contenuto, embedding) VALUES
    ('Guida a Python', 'Impara la programmazione con Python', '[0.12, 0.85, 0.23]'),
    ('Introduzione a SQL', 'Query e database relazionali', '[0.18, 0.78, 0.15]'),
    ('Ricetta per la Pizza', 'Impasto e lievitazione naturale', '[0.91, 0.05, 0.82]');

3. Query di ricerca per similarità

pgvector mette a disposizione operatori specifici per misurare la distanza tra i vettori:

  • <=> : Distanza Coseno (la più usata per il recupero del testo/ricerca semantica)
  • <-> : Distanza Euclidea (L2)
  • <#> : Prodotto interno negativo (Inner product)

Per recuperare i 2 articoli più simili a un vettore di query (es. [0.15, 0.80, 0.20]):

SQL

-- Ricerca semantica basata sulla distanza coseno
SELECT 
    id, 
    titolo, 
    1 - (embedding <=> '[0.15, 0.80, 0.20]') AS similarita_coseno
FROM articoli
ORDER BY embedding <=> '[0.15, 0.80, 0.20]'
LIMIT 2;

4. Query Ibride (SQL Tradizionale + Similarità)

Un grande vantaggio di pgvector è la possibilità di unire filtri SQL classici con la ricerca vettoriale in un’unica istruzione:

SQL

-- Cerca solo tra gli articoli con id > 1 ordinando per similarità
SELECT titolo 
FROM articoli 
WHERE id > 1
ORDER BY embedding <=> '[0.15, 0.80, 0.20]'
LIMIT 1;

5. Indicizzazione per grandi volumi di dati

Senza un indice, PostgreSQL esegue una ricerca esatta (scansione completa). Per tabelle di grandi dimensioni è consigliabile creare un indice ANN (Approximate Nearest Neighbor):

SQL

-- Indice HNSW (Hierarchical Navigable Small World) - consigliato per alte prestazioni
CREATE INDEX ON articoli USING hnsw (embedding vector_cosine_ops);

-- In alternativa, indice IVFFlat (Inverted File Flat)
CREATE INDEX ON articoli USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

Vuoi vedere un esempio pratico in Python con SQLAlchemy o Psycopg3 per generare e cercare embedding?Sì

Python, grazie…

Ecco un esempio completo in Python che usa psycopg (v3) e il client ufficiale di pgvector per connettersi al database, registrare il tipo vettoriale, inserire dati ed eseguire ricerche per similarità.

Prerequisiti

Assicurati di installare le librerie necessarie:

Bash

pip install psycopg[binary] pgvector

Esempio completo in Python

Python

import psycopg
from pgvector.psycopg import register_vector

# 1. Connessione al database PostgreSQL
conn = psycopg.connect(
    dbname="tuo_db",
    user="postgres",
    password="password",
    host="localhost",
    port="5432"
)

with conn:
    # Registra l'estensione pgvector per psycopg su questa connessione
    register_vector(conn)

    with conn.cursor() as cur:
        # 2. Crea l'estensione se non esiste e la tabella
        cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
        
        cur.execute("DROP TABLE IF EXISTS prodotti;")
        cur.execute("""
            CREATE TABLE prodotti (
                id SERIAL PRIMARY KEY,
                nome TEXT,
                embedding vector(3)
            );
        """)

        # 3. Inserimento di dati (puoi passare liste/numpy array nativi di Python)
        prodotti = [
            ("Laptop da Gaming", [0.15, 0.82, 0.10]),
            ("Ultrabook Portatile", [0.12, 0.88, 0.15]),
            ("Caffettiere Espresso", [0.90, 0.05, 0.85]),
        ]

        for nome, emb in prodotti:
            cur.execute(
                "INSERT INTO prodotti (nome, embedding) VALUES (%s, %s);",
                (nome, emb)
            )

        print("--- Dati inseriti con successo ---\n")

        # 4. Ricerca per similarità Coseno
        # Vettore di ricerca (es. rappresenta un'ipotesi di query per un computer)
        query_vector = [0.14, 0.85, 0.12]

        cur.execute("""
            SELECT 
                nome, 
                1 - (embedding <=> %s) AS similarita
            FROM prodotti
            ORDER BY embedding <=> %s
            LIMIT 2;
        """, (query_vector, query_vector))

        risultati = cur.fetchall()

        print("Risultati della ricerca vettoriale:")
        for nome, sim in risultati:
            print(f"- {nome} (Similarità: {sim:.4f})")

Integrazione con SQLAlchemy / LangChain

Se in futuro lavorerai con ORM o framework AI:

  • SQLAlchemy: È sufficiente installare pgvector e usare il tipo Vector(dim) fornito da pgvector.sqlalchemy.
  • LangChain / LlamaIndex: Entrambi includono già moduli dedicati (PGVector vector store) che gestiscono la creazione delle tabelle, il calcolo automatico degli embedding tramite OpenAI/Ollama e la ricerca semantica in poche righe di codice.

Vuoi vedere l’integrazione con SQLAlchemy ORM o con un modello di Embedding reale (es. OpenAI / Sentence-Transformers)?Sì

Potresti introdurre ORM e SQLAlchemy ORM?

Un ORM (Object-Relational Mapper) è uno strumento di programmazione che agisce da ponte tra il paradigma della programmazione orientata agli oggetti (OOP) e i database relazionali (RDBMS).

Invece di scrivere query SQL manuali sotto forma di stringhe, un ORM ti permette di manipolare i dati del database utilizzando le classi e gli oggetti del linguaggio di programmazione che stai usando (come Python).

Perché usare un ORM?

  • Astrazione dell’SQL: Crei, leggi, aggiorni ed elimini dati usando metodi nativi Python (session.add(), session.delete()).
  • Mappatura automatica: Le tabelle del database diventano classi, le colonne diventano attributi e le righe diventano istanze di quelle classi.
  • Sicurezza: Gestisce automaticamente la parametrizzazione delle query, prevenendo attacchi di tipo SQL Injection.
  • Indipendenza dal DB: Puoi cambiare il database sottostante (es. da SQLite in fase di sviluppo a PostgreSQL in produzione) senza dover riscrivere il codice della logica applicativa.

Che cos’è SQLAlchemy?

SQLAlchemy è l’ORM standard de facto nell’ecosistema Python. È noto per la sua architettura flessibile divisa in due livelli principali:

  1. SQLAlchemy Core: Un livello a basso livello che fornisce un’astrazione dell’SQL guidata dagli eventi. Gestisce la connessione, il connection pooling e la costruzione di query SQL tramite sintassi Python.
  2. SQLAlchemy ORM: Un livello ad alto livello costruito sopra il Core che implementa il pattern Data Mapper, permettendo di associare oggetti Python a tabelle del database in modo trasparente.

Esempio pratico con SQLAlchemy (versione 2.0+)

Ecco come definire un modello, creare la tabella, inserire dati e fare query usando la sintassi moderna di SQLAlchemy 2.0:

1. Definizione dei Modelli (Schema del DB)

Python

from typing import Optional
from sqlalchemy import String, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

# Base comune per tutti i modelli
class Base(DeclarativeBase):
    pass

# Definizione della tabella 'utenti' come classe Python
class Utente(Base):
    __tablename__ = "utenti"

    id: Mapped[int] = mapped_column(primary_key=True)
    nome: Mapped[str] = mapped_column(String(50))
    email: Mapped[str] = mapped_column(String(100), unique=True)
    eta: Mapped[Optional[int]]

    def __repr__(self) -> str:
        return f"Utente(id={self.id!r}, nome={self.nome!r}, email={self.email!r})"

2. Connessione al Database e Creazione Tabelle

Python

# Connessione a un DB SQLite in memoria (o file local.db)
engine = create_engine("sqlite:///:memory:", echo=True)

# Genera fisicamente le tabelle definite nelle classi ereditate da Base
Base.metadata.create_all(engine)

3. Operazioni CRUD (Create, Read, Update, Delete)

Python

# Apertura di una sessione di lavoro
with Session(engine) as session:
    
    # --- CREATE (Inserimento) ---
    nuovo_utente = Utente(nome="Marco", email="marco@example.com", eta=28)
    session.add(nuovo_utente)
    
    # Salva le modifiche nel database
    session.commit()

    # --- READ (Query) ---
    # Costruzione della query con la sintassi select() di SQLAlchemy 2.0
    stmt = select(Utente).where(Utente.nome == "Marco")
    utente = session.scalars(stmt).first()
    print(f"Utente trovato: {utente}")

    # --- UPDATE (Modifica) ---
    if utente:
        utente.eta = 29  # SQLAlchemy traccia la modifica sull'oggetto
        session.commit()

    # --- DELETE (Cancellazione) ---
    # session.delete(utente)
    # session.commit()

Come SQLAlchemy si integra con pgvector

Un grande vantaggio di SQLAlchemy è l’estensibilità. Con la libreria pgvector, puoi aggiungere colonne di tipo vettoriale ai tuoi modelli ORM in poche righe:

Python

from pgvector.sqlalchemy import Vector
from sqlalchemy.orm import Mapped, mapped_column

class Documento(Base):
    __tablename__ = "documenti"

    id: Mapped[int] = mapped_column(primary_key=True)
    testo: Mapped[str]
    # Colonna vettoriale a 1536 dimensioni (es. OpenAI embeddings)
    embedding: Mapped[list[float]] = mapped_column(Vector(1536))

Vuoi approfondire la gestione delle relazioni (1-a-molti, molti-a-molti) in SQLAlchemy?Sì

SI grazie

In SQLAlchemy 2.0, le relazioni tra tabelle si definiscono utilizzando le funzioni relationship() e mapped_column() combinati con il tipo ForeignKey.

Vediamo un esempio classico 1-a-Molti (One-to-Many): un Autore può aver scritto molti Libri, ma un Libro appartiene a un solo Autore.

1. Definizione dei Modelli con Relazione

Python

from typing import List, Optional
from sqlalchemy import ForeignKey, String, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase):
    pass

class Autore(Base):
    __tablename__ = "autori"

    id: Mapped[int] = mapped_column(primary_key=True)
    nome: Mapped[str] = mapped_column(String(50))

    # Relazione 1-a-Molti: un autore ha una lista di libri
    # 'back_populates' collega questa relazione al campo 'autore' nella classe Libro
    libri: Mapped[List["Libro"]] = relationship(
        back_populates="autore", 
        cascade="all, delete-orphan"  # Se cancelli l'autore, cancella anche i suoi libri
    )

    def __repr__(self) -> str:
        return f"Autore(id={self.id}, nome={self.nome!r})"


class Libro(Base):
    __tablename__ = "libri"

    id: Mapped[int] = mapped_column(primary_key=True)
    titolo: Mapped[str] = mapped_column(String(100))
    
    # Chiave Esterna (Foreign Key) che punta alla colonna 'id' della tabella 'autori'
    autore_id: Mapped[int] = mapped_column(ForeignKey("autori.id"))

    # Relazione inversa: consente di accedere all'oggetto Autore da un oggetto Libro
    autore: Mapped["Autore"] = relationship(back_populates="libri")

    def __repr__(self) -> str:
        return f"Libro(id={self.id}, titolo={self.titolo!r})"

2. Uso Pratico: Inserimento e Query

Creiamo il database e vediamo come interagire con i dati senza dover gestire manualmente le chiavi esterne.

Python

# Connessione a un database SQLite in memoria per il test
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(engine)

with Session(engine) as session:
    # --- INSERIMENTO DATI ---
    # Invece di impostare gli ID manualmente, colleghiamo direttamente gli oggetti Python
    autore_1 = Autore(
        nome="Italo Calvino",
        libri=[
            Libro(titolo="Il barone rampante"),
            Libro(titolo="Le città invisibili")
        ]
    )
    
    # Basta aggiungere l'autore principale: SQLAlchemy salverà in automatico anche i libri!
    session.add(autore_1)
    session.commit()

    print("--- Dati Inseriti ---")

    # --- QUERY: Navigare le relazioni ---
    
    # 1. Recupera l'autore e accedi ai suoi libri
    stmt_autore = select(Autore).where(Autore.nome == "Italo Calvino")
    calvino = session.scalars(stmt_autore).first()
    
    print(f"Autore: {calvino.nome}")
    for libro in calvino.libri:
        print(f"  - Libro scritto: {libro.titolo}")

    # 2. Recupera un libro e risali all'autore
    stmt_libro = select(Libro).where(Libro.titolo == "Il barone rampante")
    libro_trovato = session.scalars(stmt_libro).first()
    
    print(f"\nIl libro '{libro_trovato.titolo}' è stato scritto da: {libro_trovato.autore.nome}")

Concetti Chiave da ricordare:

  1. ForeignKey("autori.id"): Definisce il vincolo di integrità a livello di database (usa il nome della tabella autori).
  2. relationship(...): Gestisce l’associazione a livello di Python/ORM (usa il nome delle classi "Libro" o "Autore").
  3. back_populates: Mantiene sincronizzati i due lati della relazione. Se aggiungi un libro a autore.libri, SQLAlchemy aggiornerà automaticamente la variabile libro.autore.
  4. cascade="all, delete-orphan": Definisce cosa succede ai figli quando il genitore viene eliminato. In questo caso, se rimuovi un Autore, tutti i suoi Libri associati verranno cancellati automaticamente.

Vuoi vedere come strutturare una relazione Molti-a-Molti (Many-to-Many) o come funziona il caricamento ‘Lazy’ vs ‘Eager’?Sì

Potresti spiegare perchè pgvector è strutturale nella programmazione AI?

pgvector è diventato un componente fondamentale e “strutturale” nell’architettura delle applicazioni basate su intelligenza artificiale — in particolare nei sistemi RAG (Retrieval-Augmented Generation), negli agenti AI e nella ricerca semantica — per una ragione precisa: risolve il problema della frammentazione dell’infrastruttura.

Invece di costringere gli sviluppatori ad aggiungere un Vector Database dedicato (come Pinecone, Qdrant, Weaviate o Milvus), pgvector porta la ricerca vettoriale all’interno del database relazionale più diffuso al mondo: PostgreSQL.

Le ragioni principali per cui pgvector ha un ruolo strategico e strutturale nella programmazione AI si riassumono in 5 pilastri:

1. Elimina la duplicazione dei dati e la sincronizzazione

Nelle applicazioni tradizionali basate su intelligenza artificiale, i dati aziendali risiedono in un database relazionale (es. PostgreSQL per utenti, transazioni, metadati), mentre i relativi vettori (gli embedding generati dai LLM) vengono inviati a un Vector DB esterno.

Questo approccio presenta svantaggi critici:

  • Incoerenza dei dati: Se un record viene modificato o eliminato dal DB relazionale, occorre sincronizzare la modifica sul Vector DB in tempo reale.
  • Overhead di rete: Due chiamate di rete separate per ogni operazione (leggere i vettori, poi recuperare i metadati dal DB).
  • Complessità architetturale: Più risorse infrastrutturali da gestire, monitorare e pagare.

Con pgvector, il dato grezzo, i suoi metadati e il suo embedding vettoriale risiedono nella stessa riga della stessa tabella.

2. Consente le “Query Ibride” (SQL + Vettori) in una sola transazione

I modelli generativi producono spesso risposte troppo generiche se non vengono filtrati i dati di contesto. Nella ricerca reale, raramente serve solo la “ricerca per similarità semantica pura”: serve quasi sempre applicare prima o dopo dei filtri aziendali.

Grazie a pgvector, è possibile combinare la potenza espressiva di SQL con la ricerca vettoriale in un’unica istruzione SQL atomicamente sicura:

SQL

-- Cerca articoli simili A LIVELLO SEMANTICO, ma solo di una specifica categoria e pubblicati quest'anno
SELECT titolo, contenuto
FROM documenti
WHERE categoria = 'Tech' 
  AND data_pubblicazione >= '2026-01-01'
ORDER BY embedding <=> '[0.12, 0.45, ...]' -- Distanza coseno
LIMIT 5;

Eseguire la stessa query con un Vector DB dedicato e un DB relazionale separato richiede query multiple e join applicativi complessi.

3. Garanzie ACID anche per i vettori

PostgreSQL è celebre per le sue garanzie ACID (Atomicità, Coerenza, Isolamento, Durabilità).

Quando usi pgvector:

  • Se una transazione fallisce, l’inserimento o l’aggiornamento del vettore viene annullato automaticamente.
  • Puoi sfruttare backup, ripristino point-in-time (PITR), replicazione e failover già esistenti nell’infrastruttura PostgreSQL senza dover implementare strategie di backup ad hoc per i dati vettoriali.

4. Integrazione nativa nei Framework AI (LangChain, LlamaIndex)

I principali framework usati per sviluppare applicazioni AI (come LangChain, LlamaIndex, Kernel Memory, Semantic Kernel) supportano pgvector fin dal primo giorno come componente di prima classe (VectorStore).

Questo significa che per uno sviluppatore passare da una prototipazione rapida a un sistema pronto per la produzione richiede solo di modificare la stringa di connessione al database, senza stravolgere il codice dell’applicazione.

5. Riduzione drastica dei costi operativi (TCO)

Gestire Vector DB nativi distribuiti in cloud può diventare estremamente oneroso man mano che il volume dei vettori e le chiamate di lettura/scrittura crescono.

pgvector sfrutta le risorse hardware e i database PostgreSQL già esistenti nelle aziende (come AWS RDS, Google Cloud SQL, Azure Database for PostgreSQL, o cluster PostgreSQL su Kubernetes). Ciò si traduce in:

  • Nessun nuovo vendor cloud da autorizzare o pagare.
  • Minori costi di hosting.
  • Competenze del team DB / DevOps già applicabili senza bisogno di riqualificazione.

In sintesi

AspettoVector DB SeparatoPostgreSQL + pgvector
Architettura2 Database distinti1 Database Unificato
Filtri sui datiFiltraggio applicativo o metadati limitatiQuery SQL complete (JOIN, WHERE, HAVING)
Consistenza datiSincronizzazione manuale via pipeline/eventiTransazionale (ACID nativo)
InfrastrutturaNuova tecnologia da gestireIntegrazione nell’ecosistema PG esistente

pgvector è strutturale perché ha trasformato la ricerca vettoriale da “tecnologia esotica che richiede un database a sé” a una “funzionalità standard di un database relazionale”, consentendo la transizione dei sistemi di intelligenza artificiale dal mondo dei prototipi a quello delle applicazioni aziendali di livello enterprise.

Per approfondire l’uso di pgvector nei sistemi AI:

Vedi un esempio di RAG con LangChain e pgvector

Confronta gli indici HNSW e IVFFlat in pgvector

Mostrami un esempio in Python di come implementare una pipeline RAG con LangChain, OpenAI e pgvector.

Ecco un esempio completo in Python che illustra come costruire una pipeline RAG (Retrieval-Augmented Generation) utilizzando LangChain, il modello di embedding e completamento di OpenAI e pgvector su PostgreSQL.

Prerequisiti

Installa le librerie necessarie:

Bash

pip install langchain langchain-openai langchain-postgres psycopg[binary] pgvector

Assicurati inoltre di avere:

  1. Un’istanza di PostgreSQL attiva con l’estensione pgvector installata (CREATE EXTENSION IF NOT EXISTS vector;).
  2. Una chiave API OpenAI impostata nella variabile d’ambiente OPENAI_API_KEY.

Codice Completo Python

Il codice si divide nei due passaggi chiave di una pipeline RAG:

  1. Ingestion: Caricamento, suddivisione dei documenti (chunking), calcolo degli embedding e salvataggio su pgvector.
  2. Retrieval & Generation: Recupero dei contesti pertinenti e generazione della risposta con un LLM.

Python

import os
from langchain_community.document_loaders import TextLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_postgres import PGVector
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1. Configurazione Parametri e Connessione
os.environ["OPENAI_API_KEY"] = "sk-..."  # Sostituisci con la tua chiave OpenAI

# Stringa di connessione a PostgreSQL (formato psycopg3)
DB_URL = "postgresql+psycopg://postgres:password@localhost:5432/tuo_db"
COLLECTION_NAME = "documenti_aziendali"

# Inizializzazione del modello di Embedding
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# ------------------------------------------------------------------
# FASE 1: INGESTION (Caricamento e Inserimento Dati)
# ------------------------------------------------------------------
def ingesta_documenti(file_path: str):
    print("--- 1. Caricamento e suddivisione del documento ---")
    loader = TextLoader(file_path, encoding="utf-8")
    docs = loader.load()

    # Suddivide il testo in frammenti (chunk) gestibili
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
    chunks = text_splitter.split_documents(docs)

    print(f"Creati {len(chunks)} chunk. Salvataggio su PostgreSQL (pgvector)...")

    # Memorizza i vettori nel DB creando/popolando la tabella
    vector_store = PGVector.from_documents(
        embedding=embeddings,
        documents=chunks,
        collection_name=COLLECTION_NAME,
        connection=DB_URL,
        use_jsonb=True,
    )
    print("--- Ingestion completata con successo! ---\n")
    return vector_store


# ------------------------------------------------------------------
# FASE 2: RETRIEVAL & GENERATION (Pipeline RAG)
# ------------------------------------------------------------------
def esegui_rag_chain(query: str):
    # Connessione al VectorStore esistente su pgvector
    vector_store = PGVector(
        embeddings=embeddings,
        collection_name=COLLECTION_NAME,
        connection=DB_URL,
        use_jsonb=True,
    )

    # Configura il Retriever (Recupera i 3 chunk più simili usando la distanza coseno)
    retriever = vector_store.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 3}
    )

    # Definizione del Prompt Template
    template = """Rispondi alla domanda dell'utente basandoti ESCLUSIVAMENTE sul seguente contesto fornito.
Se la risposta non è presente nel contesto, rispondi "Non ho informazioni sufficienti per rispondere".

Contesto:
{context}

Domanda: 
{question}

Risposta:"""

    prompt = ChatPromptTemplate.from_template(template)
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

    # Funzione di supporto per formattare i documenti recuperati in una singola stringa
    def format_docs(docs):
        return "\n\n".join(doc.page_content for doc in docs)

    # Costruzione della Chain RAG con LCEL (LangChain Expression Language)
    rag_chain = (
        {
            "context": retriever | format_docs,
            "question": RunnablePassthrough()
        }
        | prompt
        | llm
        | StrOutputParser()
    )

    print(f"Domanda: {query}")
    print("Elaborazione risposta...")
    risposta = rag_chain.invoke(query)
    print(f"\nRisposta RAG:\n{risposta}")


# ------------------------------------------------------------------
# ESECUZIONE
# ------------------------------------------------------------------
if __name__ == "__main__":
    # Esempio: supponiamo di avere un file "policy.txt"
    # ingesta_documenti("policy.txt")  # Decommenta al primo avvio per caricare i dati

    domanda = "Quali sono le regole per lo smart working secondo la policy aziendale?"
    esegui_rag_chain(domanda)

Come funziona sotto il cofano?

  1. PGVector.from_documents(...): LangChain prende i frammenti di testo, invia il testo a OpenAI per calcolare l’array di numeri (embedding), e poi esegue automaticamente le query SQL necessarie per creare la tabella e inserire testo + vettori.
  2. vector_store.as_retriever(...): Trasforma il vector store in un componente di ricerca. Quando riceve una domanda, la converte in un vettore ed esegue l’operatore <=> (distanza coseno) su PostgreSQL.
  3. rag_chain (LCEL):
    • Prende la domanda e la invia al retriever.
    • Formattta i documenti trovati e li inserisce nel parametro {context} del prompt.
    • Invia il prompt arricchito al modello LLM (gpt-4o-mini), che genera la risposta finale basandosi solo sui dati reali del DB.

Vuoi vedere come aggiungere la memoria storico-conversazionale (Chat History) a questa pipeline RAG?Sì

Si grazie.

Per integrare la memoria delle conversazioni (Chat History) in una pipeline RAG, la catena di esecuzione deve svolgere due passaggi fondamentali:

  1. Riformulazione della domanda (History-Aware Retriever): Se l’utente fa una domanda di follow-up come “Quali sono i requisiti?” (riferendosi alla domanda precedente sullo smart working), l’LLM deve prima trasformarla in una domanda indipendente (es. “Quali sono i requisiti per lo smart working?”) prima di interrogare pgvector.
  2. Generazione orientata al contesto e alla storia: L’LLM genera la risposta considerando i documenti recuperati da pgvector e lo storico della chat.

Inoltre, per rendere il sistema completo, è opportuno memorizzare la Chat History direttamente in PostgreSQL, sfruttando così lo stesso database per documenti, vettori e conversazioni.

Prerequisiti

Installa/aggiorna le librerie necessarie:

Bash

pip install langchain langchain-openai langchain-postgres psycopg[binary] pgvector

Codice Completo Python con Chat History in Postgres

Python

import os
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_community.chat_message_histories import PostgresChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_postgres import PGVector

# 1. Configurazione Parametri e Connessione
os.environ["OPENAI_API_KEY"] = "sk-..."  # Sostituisci con la tua chiave OpenAI

# Stringa di connessione a PostgreSQL (formato psycopg3)
DB_URL = "postgresql+psycopg://postgres:password@localhost:5432/tuo_db"
COLLECTION_NAME = "documenti_aziendali"

# Inizializzazione dei modelli
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 2. Connessione al VectorStore pgvector
vector_store = PGVector(
    embeddings=embeddings,
    collection_name=COLLECTION_NAME,
    connection=DB_URL,
    use_jsonb=True,
)
retriever = vector_store.as_retriever(search_kwargs={"k": 3})


# 3. PROMPT 1: Riformulazione della Domanda in base allo Storico
contextualize_q_system_prompt = (
    "Dato uno storico di chat e l'ultima domanda dell'utente "
    "che potrebbe fare riferimento al contesto nella cronologia della chat, "
    "formula una domanda autonoma che possa essere compresa senza la cronologia. "
    "NON rispondere alla domanda, limitati a riformularla se necessario, "
    "altrimenti restituiscila così com'è."
)

contextualize_q_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", contextualize_q_system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

# Retriever consapevole della storia conversazionale
history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)


# 4. PROMPT 2: Generazione della Risposta RAG
system_prompt = (
    "Sei un assistente virtuale aziendale. Rispondi alla domanda usando ESCLUSIVAMENTE "
    "il contesto fornito. Se non conosci la risposta, rispondi 'Non possiedo questa informazione'.\n\n"
    "Contesto recuperato:\n{context}"
)

qa_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

# Catena per combinare i documenti recuperati con la risposta dell'LLM
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)

# Catena RAG finale (Retriever + QA)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)


# 5. Gestione della Memoria su PostgreSQL
def get_session_history(session_id: str):
    """
    Recupera o crea uno storico messaggi salvato nella tabella 'chat_history' di PostgreSQL.
    """
    return PostgresChatMessageHistory(
        session_id=session_id,
        connection_string=DB_URL,
        table_name="chat_history"
    )

# Avvolge la catena RAG gestendo automaticamente lettura e salvataggio della Chat History
conversational_rag_chain = RunnableWithMessageHistory(
    rag_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    output_messages_key="answer",
)


# ------------------------------------------------------------------
# SIMULAZIONE DI UNA CONVERSAZIONE A PIÙ TURNI
# ------------------------------------------------------------------
if __name__ == "__main__":
    # ID unico della sessione (es. id utente o id chat)
    session_config = {"configurable": {"session_id": "utente_sessione_123"}}

    print("--- TURNO 1 ---")
    domanda_1 = "Quali sono le regole per lo smart working?"
    print(f"User: {domanda_1}")
    
    res1 = conversational_rag_chain.invoke({"input": domanda_1}, config=session_config)
    print(f"AI: {res1['answer']}\n")

    print("--- TURNO 2 (Domanda con riferimento implicito) ---")
    # Nota: la domanda usa il pronome "essi" riferito allo smart working della domanda 1
    domanda_2 = "Quanti giorni al mese sono concessi per farlo?"
    print(f"User: {domanda_2}")
    
    res2 = conversational_rag_chain.invoke({"input": domanda_2}, config=session_config)
    print(f"AI: {res2['answer']}\n")

Perché questo approccio è ideale in produzione?

  1. PostgreSQL come “Single Source of Truth”:
    • La tabella creata da PGVector contiene i documenti e i loro vettori.
    • La tabella chat_history gestisce l’elenco dei messaggi (human/ai) inviati nella sessione.
    • Non è necessario alcun database aggiuntivo (come Redis) per gestire la sessione.
  2. Soluzione del problema dei riferimenti anaforici: Se l’utente scrive “Chi è il responsabile di cui mi avevi parlato?”, la prima sotto-catena riformula la richiesta in “Chi è il responsabile del progetto X?” usando la cronologia, garantendo che la ricerca vettoriale su pgvector trovi i chunk corretti.