Salta al contenuto
Torna alla home
2024-11-158 min read
AI AgentsResearchLLMProduction

>-

>-

Perché conta

Un research agent autonomo che allucina può essere peggio di nessun agent. Produce output che sembrano autorevoli, includono citazioni plausibili e possono essere parzialmente o completamente sbagliati. Il ricercatore che riceve l'output non ha un modo efficiente per verificarlo: è proprio per questo che ha delegato a un agent.

Ho costruito research agent che hanno fallito silenziosamente esattamente in questo modo. Il planner generava buone sotto-domande. L'executor recuperava documenti reali. Poi lo step di synthesis, sotto pressione di contesto, cercava di produrre un report coerente da risultati parziali. In quel passaggio riempiva i vuoti con affermazioni plausibili che non avevano alcuna fonte. L'affermazione non era in nessun documento recuperato. Era nei training data del model, riciclata attraverso l'apparenza della ricerca.

Nel mio design io considero questo un fallimento architetturale, non un problema da risolvere solo con prompt-engineering. Una coppia planner-executor senza uno step di verification non distingue in modo affidabile "l'ho trovato in una fonte" da "il model ne è sicuro". Per questo, nel mio setup, io aggiungo un critic con accesso indipendente alle fonti. È la correzione strutturale che tendo a usare per rendere verificabile il passaggio dalla ricerca al report. Ecco perché, e come appare in pratica.

1. Il pattern planner-executor e i suoi limiti

Io uso la coppia planner-executor come blueprint di base per la ricerca autonoma: il planner scompone un tema di alto livello in sotto-domande, l'executor recupera e riassume le risposte a ciascuna sotto-domanda, e il planner sintetizza gli output dell'executor in un report finale.

Questo pattern funziona bene quando il corpus è ben definito, le sotto-domande possono ricevere risposta in modo indipendente, e i documenti recuperati contengono abbastanza informazioni rilevanti. Nel mio caso d'uso diventa più fragile quando il corpus è scarso, le sotto-domande sono ambigue, oppure lo step di synthesis prova a riconciliare fonti parziali e contraddittorie.

La failure mode più pericolosa è nella synthesis. Il planner riceve output dell'executor come "La fonte A dice X" e "La fonte B è ambigua su Y" e deve produrre un report coerente. Con token limitati e molte fonti, il synthesis model può riempire le parti ambigue con ciò che si aspetta di vedere, in base ai training data. L'output suona coerente. Le affermazioni però non sono verificabili.

Io non uso il critic loop per eliminare le cause radice, come corpora scarsi o domande ambigue. Io lo uso per rendere visibile il fallimento. Il critic segnala le affermazioni che non riesce a mappare alle fonti recuperate, distingue "trovato nei documenti" da "inferito dal training", e dà al planner abbastanza segnale per rilanciare la query o marcare esplicitamente l'incertezza.

2. Cosa fa davvero un critic agent

Io tratto il critic come un verification agent, non come un secondo synthesizer. Gli do due capacità specifiche: accesso agli stessi documenti fonte recuperati dall'executor, e un vincolo rigoroso di grounding.

Il task del critic è concreto: dati il summary dell'executor e i documenti fonte, marca ogni claim come grounded (trovato testualmente o parafrasato in una fonte), inferred (segue logicamente dalle fonti ma non è dichiarato direttamente), oppure unsupported (non trovato in nessuna fonte recuperata).

from pydantic import BaseModel
from typing import Literal

class ClaimVerification(BaseModel):
    claim: str
    status: Literal["grounded", "inferred", "unsupported"]
    source_url: str | None  # required when status == "grounded"
    confidence: float       # 0.0 to 1.0

class CriticOutput(BaseModel):
    verified_claims: list[ClaimVerification]
    overall_groundedness: float  # fraction of claims that are "grounded"
    flags: list[str]             # specific issues for the planner to act on

Nel mio schema io faccio restituire questo output strutturato al planner, non come free text e non all'executor. Il planner legge overall_groundedness e la lista flags per decidere cosa succede dopo. Se la groundedness è sopra la soglia, per esempio 0.8, io faccio approvare il report. Se è sotto, io faccio rimettere in coda le sotto-domande segnalate con istruzioni esplicite per trovare fonti per le affermazioni unsupported.

Io aggiungo sempre una guardia di max-iterations a questo loop. Se la groundedness non migliora dopo due cicli di re-query, io faccio marcare esplicitamente al planner le claim a bassa confidence nel report finale, invece di lasciarle passare silenziosamente.

3. Accesso indipendente alle fonti per il critic

Nel mio design io faccio vedere al critic gli stessi documenti fonte recuperati dall'executor, non solo il summary che l'executor ha prodotto da quei documenti. Questo è il requisito strutturale chiave nel mio setup.

Se il critic vede solo il summary dell'executor, può controllare la coerenza interna, per esempio se alcune claim si contraddicono. Non può però verificare il grounding esterno: non può sapere se una claim è davvero nella fonte. Un summary può essere internamente coerente e completamente sbagliato.

# Wrong: critic sees only the summary
critic_input = {
    "summary": executor_output.summary,
    "task": "Verify the claims in this summary."
}
# Critic can check consistency but not source grounding

# Right: critic sees summary and original documents
critic_input = {
    "summary": executor_output.summary,
    "source_documents": executor_output.retrieved_docs,  # original text, not summaries
    "task": "For each claim in the summary, verify it against the source documents."
}
# Critic can map claims to specific passages

La conseguenza pratica, nel mio setup, è che la context window del critic deve contenere il summary e gli estratti rilevanti delle fonti. Per task di ricerca long-form con molte fonti, io scelgo di eseguire il critic su una sotto-domanda alla volta, oppure uso un large-context model solo per il critic pass.

Eseguire il critic per sotto-domanda tende a costare meno nel mio setup, ma può perdere le claim multi-source, come "Le fonti A e B confermano entrambe che...", se non passo esplicitamente l'intero contesto della claim. Eseguire il critic sull'intero corpus in una volta sola tende a costare di più, ma intercetta meglio le contraddizioni cross-source: il critic può vedere se due fonti dicono cose in conflitto sulla stessa claim e segnalarle entrambe.

4. Recursive summarization e tracing delle citazioni

I research agent autonomi spesso recuperano più contenuto di quanto entri in una singola context window. La risposta standard è la recursive summarization: riassumere il documento A, riassumere il documento B, sintetizzare i summary.

Io uso la recursive summarization per comprimere il contesto, ma non la considero sufficiente per il tracing delle citazioni. Quando riassumi il documento A, produci una versione condensata che perde passaggi specifici. A quel punto il critic non può più mappare le claim al testo esatto nel documento A. La catena probatoria si interrompe.

Il design che io uso per preservarla è semplice: conservo il documento originale insieme al summary, e passo entrambi al critic.

class ExecutorResult(BaseModel):
    sub_question: str
    summary: str             # compressed, used for planning
    source_docs: list[str]   # original text, used for critic verification
    source_urls: list[str]

Il critic legge il summary per capire il contesto della claim, poi cerca nell'originale il passaggio specifico. Questo raddoppia i dati archiviati per ogni risultato dell'executor. Nel mio caso è accettabile quando il corpus si misura in migliaia di token. Se la dimensione del corpus lo rende impraticabile, io uso un'alternativa: faccio embedding degli originali ed eseguo citation search al momento del critic invece di passarli inline.

5. Map-reduce per sotto-domande indipendenti

Quando il research task si scompone in sotto-domande indipendenti — market trends, competitive landscape, regulatory environment — io faccio dispatchare al planner l'executor in parallelo (map) e poi aggrego i risultati verificati (reduce).

Nello step map, il planner dispatcha tutte le sotto-domande simultaneamente. Nello step reduce, sintetizza tutti gli output verificati dell'executor in un report finale. Io faccio girare il critic sulla synthesis finale rispetto all'intero corpus, non su ciascun singolo risultato dell'executor.

Nel mio setup questo può ridurre la latenza rispetto all'esecuzione sequenziale e mantiene la verificabilità: nello step reduce io passo tutti i documenti fonte al critic, che può verificare la synthesis finale rispetto a tutto ciò che è stato recuperato.

La failure mode da evitare nello step reduce è concreta: il planner sintetizza tra sotto-domande senza controllare se la stessa claim è apparsa in più risultati dell'executor con valori in conflitto. "La market size è $5B" da una sotto-domanda e "La market size è $3B" da un'altra non vanno mediate. Vanno segnalate. La lista flags del critic serve a far emergere questi conflitti.

6. Gestire esplicitamente i coverage gap

Un research agent che funziona solo quando esistono buone fonti è costruito a metà. Nel mio design, io faccio gestire al critic loop anche il caso in cui le fonti recuperate siano insufficienti per la domanda.

Il segnale è questo: se il critic marca costantemente claim come unsupported attraverso due cicli di re-query con search term diverse, io considero probabile un problema di corpus coverage, non solo di qualità della query. In quel caso io faccio riportare esplicitamente il coverage gap invece di produrre una risposta plausibile.

class ResearchReport(BaseModel):
    findings: list[ClaimVerification]
    coverage_gaps: list[str]   # topics where sources were insufficient
    confidence: float          # overall groundedness across all findings
    generated_at: str          # ISO timestamp

Io rendo obbligatorio il campo coverage_gaps: il planner deve compilarlo, anche con una lista vuota. Se lo schema del report non include un campo coverage gap obbligatorio, il sistema può ometterli silenziosamente.

Per il mio caso d'uso, io tendo a preferire coverage gap espliciti a risposte sicure ma sbagliate. Un utente che vede "Coverage gap: regulatory landscape in EU after 2023 — sources available only through Q2 2023" sa quale ricerca aggiuntiva serve. Un utente che riceve una risposta allucinata non ha alcun segnale che qualcosa non va finché la validazione downstream non fallisce.

Il category frame

Quando progetto research agent autonomi che devono produrre output verificabili, io uso tre componenti: un planner che scompone e sintetizza, un executor che recupera e riassume preservando le fonti, e un critic che verifica il grounding con accesso ai documenti originali.

Il critic loop non elimina l'allucinazione: i model mantengono priors dai training data. Io lo uso per renderla visibile. Il critic distingue "trovato nelle fonti" da "inferred" da "unsupported", e il planner riporta quella distinzione invece di appiattirla in falsa confidence.

Nel mio schema, la garanzia strutturale è limitata ma utile: ogni claim nel report finale dovrebbe essere tracciata a un source URL, marcata esplicitamente come inferred, oppure esposta come coverage gap. In pratica, io riduco le categorie operative a tre e rendo più difficile far passare una claim non verificata come se fosse fondata.

Se stai costruendo research automation che deve rispettare uno standard di verificabilità — compliance, legal, financial — scrivimi. È qui che, nel mio setup, questo pattern può giustificare il suo costo di complessità.

Condividi: