Vai al contenuto
Tutti gli insights
Tutorial12 min di lettura

Per una knowledge base aziendale si parte da RAG e da un test set, e si fa fine-tuning solo per il formato

Una tabella decisionale tra RAG e fine-tuning, e un test di due settimane che mostra se il retrieval sui vostri documenti risponde abbastanza bene alle vostre domande.

I team che vogliono un assistente sui propri documenti tendono a porsi la stessa prima domanda: addestriamo un modello sui nostri documenti o colleghiamo un modello ai documenti? Per una knowledge base la risposta di default è collegarlo, che è ciò che fa la retrieval-augmented generation (RAG), e costruire un test set di domande reali prima di scegliere qualsiasi altra cosa. Questa nota spiega entrambi gli approcci in termini semplici, propone una tabella decisionale e poi descrive un test di due settimane che si può svolgere con un ingegnere e una persona che conosce i documenti.

RAG e fine-tuning in termini semplici

Un modello linguistico sa ciò che c'era nei suoi dati di addestramento. Non conosce la vostra politica sui resi, il listino del mese scorso o la procedura interna per approvare un fornitore.

La RAG risolve il problema al momento della domanda. Quando qualcuno pone una domanda, il sistema cerca nei vostri documenti, prende i pochi passaggi che sembrano più pertinenti e li passa al modello con un'istruzione: rispondi a partire da questi passaggi e indica quali hai usato. I documenti restano dove sono. Si modifica una policy, la si reindicizza, e la risposta successiva riflette la modifica. La tecnica è stata descritta in un articolo di ricerca del 2020 che abbinava un generatore di testo a un indice di documenti interrogabile; l'idea di fondo da allora non è cambiata.

Il fine-tuning prosegue l'addestramento di un modello su esempi di input e degli output desiderati. Cambia le abitudini del modello: tono, struttura, lessico, il modo in cui gestisce un compito ristretto. È un modo debole per insegnare fatti che cambiano. Ogni modifica alla fonte richiede un nuovo addestramento, il modello non sa indicare da dove viene una risposta, e tutto ciò che ha imparato è disponibile a chiunque possa usarlo.

La maggior parte delle aziende lavora già così. Il rapporto di Menlo Ventures del dicembre 2025 sull'IA generativa in azienda ha rilevato che la progettazione dei prompt è il modo più comune in cui le aziende adattano i modelli, con la RAG al secondo posto; fine-tuning e reinforcement learning restavano di nicchia e usati soprattutto dai team di frontiera.

Quanto costa. Il test di due settimane descritto sotto costa il tempo di un ingegnere e qualche giorno di un esperto della materia, più un uso del modello che alla scala di un test set è ridotto. In produzione il costo di esercizio segue una formula: domande al mese × token per domanda × prezzo per token. A titolo illustrativo: 10.000 domande al mese, ognuna con cinque passaggi da circa 400 token più 500 token di istruzioni e domanda, e una risposta da 300 token, fanno 10.000 × 2.800 = 28 milioni di token. A un prezzo medio illustrativo di 5 $ per milione di token, sono 140 $ al mese. Usate i prezzi attuali del vostro provider; ciò che conta è la formula. Se volete il sistema costruito anziché testato, la nostra fascia pubblicata per un copilot RAG interno è 8k–18k $ in 4–8 settimane, e la pagina dei prezzi mostra il listino che c'è dietro.

La tabella decisionale

Cosa serve RAG Fine-tuning
Conoscenza che cambia ogni settimana o ogni giorno Le modifiche compaiono dopo la reindicizzazione successiva Ogni modifica richiede un nuovo addestramento e una nuova valutazione
Un link alla fonte per ogni risposta Integrato: la risposta cita i passaggi usati Nessun modo affidabile per dire da dove viene una risposta
Persone diverse autorizzate a vedere documenti diversi I passaggi vengono filtrati in base ai permessi dell'utente prima che il modello li veda Ciò che è stato addestrato è visibile a ogni utente del modello
Un formato di output fisso o un tono aziendale Possibile con istruzioni ed esempi nel prompt, a volte in modo incostante Adatto
Bassa latenza Aggiunge un passo di ricerca e un prompt più lungo Nessun passo in più; un modello addestrato più piccolo può anche essere più veloce
Costo di aggiornamento Reindicizzare i documenti modificati Preparare esempi, riaddestrare, rivalutare
Dati necessari per iniziare I documenti e un test set di domande reali Input di esempio con output ideali verificati da persone, più lo stesso test set

Contate quali righe contano nel vostro caso. Per una knowledge base di solito decidono le prime tre: il contenuto cambia, le persone vogliono verificare la fonte e non tutti possono leggere tutto.

Il fine-tuning si guadagna il posto in un insieme di casi più ristretto: le risposte devono seguire sempre una struttura rigida; un compito ad alto volume, come lo smistamento dei ticket in categorie, potrebbe girare su un modello più piccolo ed economico; oppure il tono deve rispettare una guida di stile che il prompting non riesce a mantenere. Anche allora di solito si aggiunge al retrieval invece di sostituirlo, come un modello addestrato che legge comunque i passaggi recuperati.

Quindi il default è prima RAG più un set di valutazione. Il fine-tuning viene dopo, per formato, tono o un compito ristretto, e solo se il test set mostra che gli errori rimanenti sono di quel tipo.

Il test di due settimane

Il test sostituisce le opinioni con un numero. Alla fine avete un punteggio sulle vostre domande e un elenco di errori ordinati per causa, che dice se costruire, cosa correggere e se il fine-tuning servirebbe a qualcosa.

Passo 1: raccogliere domande reali

Prendete domande che le persone hanno davvero posto: ticket dell'helpdesk, thread della chat interna, log di ricerca dell'intranet, le domande a cui il responsabile di una policy risponde via e-mail. Puntate a 150-300. Includetene un 10–15% a cui i vostri documenti non sanno rispondere, perché rifiutare correttamente fa parte del lavoro.

Perché così tante: con 200 domande e un'accuratezza reale intorno all'80%, il margine di errore al 95% è di circa ±5,5 punti percentuali (1.96 × √(0.8 × 0.2 / 200) ≈ 0.055). Un set più piccolo non riesce a distinguere due versioni del sistema.

Per ogni domanda, una persona che conosce la materia scrive la risposta attesa e indica il documento da cui dovrebbe provenire. Una riga JSON per domanda:

{"id": "q041", "question": "Who approves a new supplier above the purchasing threshold?", "expected": "Procurement lead and finance, both in writing.", "doc": "supplier-onboarding", "groups": ["procurement"], "answerable": true}

Ogni risposta riceve poi una di quattro etichette:

  • correct: giusta, completa e cita il documento giusto;
  • partly: giusta ma incompleta, oppure giusta con una citazione sbagliata o mancante;
  • wrong: afferma qualcosa di falso, o risponde a una domanda a cui i documenti non sanno rispondere;
  • declined: dichiara di non poter rispondere.

Declined è un'etichetta separata di proposito. Su una domanda che ha risposta è un mancato colpo, ma meno costoso di una risposta sbagliata, ed è il business a decidere quanti dell'uno e dell'altro può accettare. Concordate quella soglia prima della prima esecuzione, così come una metrica si concorda prima di addestrare un modello.

Un esperto del dominio valuta a mano le prime 50 risposte. Dopo, un secondo modello può valutare le restanti rispetto alle risposte attese, purché le sue etichette concordino con quelle dell'esperto su quelle 50. Entrambe le pratiche sono comuni: nel sondaggio di LangChain su 1.340 professionisti che costruiscono agenti, svolto a novembre e dicembre 2025, il 59,8% usava la revisione umana, il 53,3% usava un modello come giudice e il 52,4% eseguiva valutazioni offline su test set.

Passo 2: indicizzare i passaggi con i loro permessi

Dividete i documenti lungo i loro titoli in passaggi di qualche centinaio di parole. Conservate con ogni passaggio l'id del documento, il titolo, il link e i gruppi di accesso. I permessi appartengono al passaggio fin dall'inizio; aggiungerli dopo significa reindicizzare tutto.

Passo 3: retrieval ibrido con reciprocal rank fusion

La ricerca per parole chiave (BM25) trova termini esatti come codici prodotto, numeri di modulo e acronimi. La ricerca per embedding trova le parafrasi: "giorni liberi" quando la policy dice "ferie annuali". Ognuna perde ciò che l'altra trova, quindi si usano entrambe e si fondono le classifiche.

La reciprocal rank fusion (RRF) fonde per posizione anziché per punteggio, quindi le due scale di punteggio non vanno mai calibrate: ogni passaggio riceve 1 / (k + rank) da ciascuna lista, e i valori si sommano. L'articolo del 2009 che l'ha introdotta ha fissato k a 60 e ha trovato quel valore quasi ottimale ma non critico.

import numpy as np
from rank_bm25 import BM25Okapi

def tokenize(text):
    return text.lower().split()

class HybridIndex:
    def __init__(self, chunks, embed):
        # chunks: dicts with "text", "doc", "groups" (a set); embed: list[str] -> unit vectors
        self.chunks, self.embed = chunks, embed
        self.bm25 = BM25Okapi([tokenize(c["text"]) for c in chunks])
        self.vectors = np.asarray(embed([c["text"] for c in chunks]))

    def search(self, query, user_groups, k=5, depth=50, rrf_k=60):
        allowed = [i for i, c in enumerate(self.chunks) if c["groups"] & user_groups]
        lexical = self.bm25.get_scores(tokenize(query))
        semantic = self.vectors @ np.asarray(self.embed([query]))[0]
        fused = {}
        for scores in (lexical, semantic):
            ranked = sorted(allowed, key=lambda i: scores[i], reverse=True)[:depth]
            for rank, i in enumerate(ranked, start=1):
                fused[i] = fused.get(i, 0.0) + 1.0 / (rrf_k + rank)
        top = sorted(fused, key=fused.get, reverse=True)[:k]
        return [{**self.chunks[i], "similarity": float(semantic[i])} for i in top]

embed è una qualsiasi funzione di embedding, ospitata o open source; sostituirla cambia un solo argomento. Il filtro sui permessi viene eseguito prima del ranking, quindi un passaggio che l'utente non può aprire non arriva mai al modello.

Passo 4: rispondere con citazioni, oppure rifiutare

Due controlli impediscono al sistema di inventare. Prima di chiamare il modello: se il passaggio migliore non è abbastanza simile alla domanda, si rifiuta senza generare nulla. Dopo: ogni risposta deve citare dei passaggi, e ogni numero citato deve esistere. Tutto il resto conta come rifiuto.

import re

PROMPT = """Answer the question using only the numbered passages below.
After each sentence, cite the passages you used, for example [2].
If the passages do not contain the answer, reply with exactly: NOT_FOUND

{passages}

Question: {question}"""

def answer(question, index, user_groups, llm, min_similarity=0.35):
    hits = index.search(question, user_groups)
    if not hits or max(h["similarity"] for h in hits) < min_similarity:
        return {"status": "declined", "text": None, "docs": []}
    passages = "\n\n".join(f"[{n}] ({h['doc']}) {h['text']}" for n, h in enumerate(hits, 1))
    text = llm(PROMPT.format(passages=passages, question=question)).strip()
    cited = {int(n) for n in re.findall(r"\[(\d+)\]", text)}
    if text == "NOT_FOUND" or not cited or not cited <= set(range(1, len(hits) + 1)):
        return {"status": "declined", "text": None, "docs": []}
    return {"status": "answered", "text": text, "docs": [hits[n - 1]["doc"] for n in sorted(cited)]}

llm è una qualsiasi funzione che riceve un prompt e restituisce testo, quindi il modello si può sostituire senza toccare questo codice. La soglia 0.35 è un segnaposto: le scale di similarità differiscono tra modelli di embedding. Va fissata sul vostro test set in modo che le domande senza risposta vengano rifiutate senza perdere troppe domande che una risposta ce l'hanno.

Passo 5: il ciclo di valutazione

import json
from collections import Counter

def run_eval(path, index, llm, judge):
    items = [json.loads(line) for line in open(path)]
    labels, found = Counter(), 0
    for item in items:
        groups = set(item["groups"])
        if item["answerable"]:
            hits = index.search(item["question"], groups)
            found += item["doc"] in {h["doc"] for h in hits}
        result = answer(item["question"], index, groups, llm)
        if not item["answerable"]:
            label = "correct" if result["status"] == "declined" else "wrong"
        elif result["status"] == "declined":
            label = "declined"
        else:  # judge returns "correct", "partly" or "wrong"
            label = judge(item["question"], item["expected"], result["text"])
        labels[label] += 1
    answerable = sum(1 for item in items if item["answerable"])
    print({k: round(v / len(items), 3) for k, v in labels.items()})
    print("expected document in top 5:", round(found / answerable, 3))

Il secondo numero conta quanto il primo. Se il documento giusto non era tra i primi cinque, il modello non ha mai avuto una possibilità, e la correzione sta nel retrieval o nei documenti, non nel modello. Salvate le etichette di ogni esecuzione con la data e la modifica apportata, così ogni modifica viene confrontata con la stessa baseline.

Settimana 1 e settimana 2

Settimana 1: la baseline.

  • Giorni 1–2: raccogliere e scrivere le domande con i responsabili dei documenti; concordare la soglia di accettazione.
  • Giorno 3: indicizzare i documenti; eseguire prima la sola ricerca per parole chiave, poi quella ibrida, e registrare per entrambe il tasso di hit nei primi 5.
  • Giorni 4–5: prima esecuzione completa; l'esperto valuta 50 risposte; si verifica il giudice rispetto a quelle; si valutano le restanti.

Il risultato della settimana 1 è un punteggio e un elenco di errori ordinati in quattro cause: il documento manca o non è aggiornato; il retrieval ha mancato il passaggio giusto; il modello aveva il passaggio giusto e ha comunque risposto male; il sistema avrebbe dovuto rifiutare e non l'ha fatto.

Settimana 2: correggere in ordine di dimensione.

  • Si parte dalla causa più grande. I documenti mancanti o non aggiornati sono un compito di contenuto per i loro responsabili. Gli errori di retrieval di solito si risolvono con un chunking migliore, titoli dei documenti e intestazioni aggiunti a ogni passaggio, o sinonimi per il gergo interno. Le risposte sbagliate a partire da passaggi buoni si risolvono con modifiche al prompt e, a volte, con un modello diverso.
  • Si riesegue l'intero set dopo ogni modifica. Una modifica si tiene solo se il punteggio si sposta più del margine di errore.
  • La soglia di rifiuto si regola per ultima, sulla configurazione di retrieval finale.

Alla fine della settimana 2 la decisione di solito è chiara. Se il tasso di risposte sbagliate è sotto la soglia concordata, si costruisce la versione di produzione e si tiene il test set come gate di rilascio. Se gli errori rimanenti riguardano formato o tono mentre i fatti sono giusti, è il caso in cui vale la pena testare il fine-tuning, e il test set esiste già per mostrare se ha aiutato. Se la maggior parte degli errori sono documenti mancanti, il problema è la knowledge base, e nessuna scelta di modello lo risolverà.

È l'ordine che seguiamo quando costruiamo copilot RAG: prima le domande reali, poi il retrieval, poi un punteggio che ogni rilascio deve superare.

Fonti

Ha un processo che costa troppo?

Ce ne parli. La prima call è gratuita e, se l'AI non è la risposta, glielo diciamo.

Parliamo del suo caso