Parliamo

project-cherry.dev

Avanzato18 min

Costruire AI Agents

Architettura e implementazione di agenti AI autonomi che pianificano, usano strumenti e completano compiti complessi. Dal pattern ReAct al tool use di Claude, con un agente legale funzionante.

Costruire AI Agents

L'Avv. Marco Rossi non vuole un chatbot che risponde a domande. Vuole un assistente legale che, dato un caso, cerchi autonomamente la giurisprudenza rilevante, analizzi i precedenti, e generi una bozza di memoria difensiva. Non un singolo prompt: un sistema che ragiona, pianifica e agisce.

Da chatbot ad agent

Un chatbot riceve una domanda e produce una risposta. Un agent riceve un obiettivo e decide autonomamente come raggiungerlo.

text
CHATBOT (flusso lineare):
Domanda → Risposta → Fine

AGENT (flusso ciclico):
Obiettivo → Osserva → Pensa → Agisci → Valuta → Ripeti

Esempio dell'Avv. Rossi:

Obiettivo: "Trova precedenti su responsabilità medica per
intervento ortopedico mal riuscito"

Ciclo 1: Osserva (nessun dato) → Pensa (devo cercare in
giurisprudenza) → Agisci (chiama tool ricerca) → Valuta
(trovate 12 sentenze, servono quelle della Cassazione)

Ciclo 2: Osserva (12 sentenze) → Pensa (filtro per Cassazione
e rilevanza) → Agisci (analizza le 4 più pertinenti) → Valuta
(ho abbastanza materiale per la bozza)

Ciclo 3: Osserva (4 sentenze analizzate) → Pensa (posso
generare la memoria) → Agisci (chiama tool genera documento)
→ Valuta (bozza pronta per revisione)

La differenza fondamentale: l'agent dell'Avv. Rossi non genera solo testo. Chiama strumenti: ricerca giurisprudenza, database clienti, template atti.

Il pattern ReAct

ReAct sta per Reasoning + Acting. È il pattern più diffuso per costruire agenti AI. Il ciclo è semplice: il modello alterna ragionamento e azione.

text
Schema del ciclo ReAct:

THOUGHT: "Devo trovare precedenti sulla responsabilità medica
per interventi ortopedici. Inizio cercando nella giurisprudenza
della Cassazione civile."

ACTION: cerca_giurisprudenza(
    query="responsabilità medica intervento ortopedico",
    corte="cassazione_civile",
    anni_recenti=5
)

OBSERVATION: "Trovate 8 sentenze. Le più rilevanti:
Cass. Civ. Sez. III, n. 12345/2023 — nesso causale,
Cass. Civ. Sez. III, n. 67890/2022 — onere della prova..."

THOUGHT: "Ho le sentenze principali. Ora devo verificare
se nel nostro archivio ci sono casi simili già trattati
dallo studio."

ACTION: consulta_archivio(
    tipo="casi",
    area="responsabilità medica",
    esito="favorevole"
)

OBSERVATION: "Trovati 2 casi simili: Caso Bianchi 2021
(transazione favorevole), Caso Verdi 2023 (sentenza vinta)..."

THOUGHT: "Ho giurisprudenza e precedenti interni. Posso
generare la bozza della memoria difensiva."

ACTION: genera_documento(
    tipo="memoria_difensiva",
    dati_giurisprudenza=[...],
    dati_archivio=[...]
)

Tool Use con Claude

L'API di Anthropic supporta il tool use nativo: definisci gli strumenti disponibili e il modello decide quando e come usarli.

python
# Definizione dei tool per l'agente legale dell'Avv. Rossi

strumenti_legali = [
    {
        "name": "cerca_giurisprudenza",
        "description": "Cerca sentenze e precedenti giurisprudenziali "
                       "in base a criteri di ricerca. Restituisce le "
                       "sentenze più rilevanti con riferimenti completi.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Termini di ricerca giuridica"
                },
                "corte": {
                    "type": "string",
                    "description": "Corte di riferimento: "
                                   "cassazione_civile, cassazione_penale, "
                                   "corte_appello, tribunale"
                },
                "anni_recenti": {
                    "type": "integer",
                    "description": "Limita la ricerca agli ultimi N anni"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "consulta_archivio",
        "description": "Consulta l'archivio interno dello studio legale "
                       "per trovare casi passati, documenti e template.",
        "input_schema": {
            "type": "object",
            "properties": {
                "tipo": {
                    "type": "string",
                    "description": "Tipo di documento: casi, contratti, "
                                   "memorie, template"
                },
                "area": {
                    "type": "string",
                    "description": "Area legale di riferimento"
                },
                "esito": {
                    "type": "string",
                    "description": "Filtro per esito: favorevole, "
                                   "sfavorevole, transazione"
                }
            },
            "required": ["tipo"]
        }
    },
    {
        "name": "genera_documento",
        "description": "Genera un documento legale (bozza) a partire "
                       "da dati strutturati: giurisprudenza, precedenti "
                       "interni e template dello studio.",
        "input_schema": {
            "type": "object",
            "properties": {
                "tipo": {
                    "type": "string",
                    "description": "Tipo: memoria_difensiva, comparsa, "
                                   "atto_citazione, parere"
                },
                "dati_giurisprudenza": {
                    "type": "array",
                    "description": "Lista di sentenze rilevanti"
                },
                "dati_archivio": {
                    "type": "array",
                    "description": "Lista di casi interni rilevanti"
                }
            },
            "required": ["tipo"]
        }
    }
]

Implementare l'agent

Ecco il loop ReAct completo: il modello riceve un obiettivo, sceglie quale tool usare, esegue, e continua finché il compito non è completato.

python
import anthropic
import json

# Inizializza il client
client = anthropic.Anthropic()

def esegui_tool(nome_tool: str, parametri: dict) -> str:
    """Esegue il tool richiesto e restituisce il risultato."""
    if nome_tool == "cerca_giurisprudenza":
        # Qui si collegherebbe al database giurisprudenziale
        return json.dumps({
            "risultati": [
                {"sentenza": "Cass. Civ. Sez. III, n. 12345/2023",
                 "massima": "Il medico risponde per colpa grave..."},
                {"sentenza": "Cass. Civ. Sez. III, n. 67890/2022",
                 "massima": "L'onere della prova del nesso causale..."}
            ],
            "totale": 8
        })
    elif nome_tool == "consulta_archivio":
        return json.dumps({
            "risultati": [
                {"caso": "Bianchi 2021", "esito": "transazione favorevole"},
                {"caso": "Verdi 2023", "esito": "sentenza vinta in appello"}
            ]
        })
    elif nome_tool == "genera_documento":
        return json.dumps({
            "documento": "BOZZA MEMORIA DIFENSIVA - [contenuto generato]",
            "stato": "bozza_pronta"
        })
    return json.dumps({"errore": "Tool non trovato"})

def loop_agent(obiettivo: str, max_iterazioni: int = 5) -> str:
    """Loop ReAct: il modello ragiona e agisce fino al completamento."""
    messaggi = [{"role": "user", "content": obiettivo}]

    for i in range(max_iterazioni):
        print(f"\n--- Iterazione {i + 1} ---")

        # Invio al modello con i tool disponibili
        risposta = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            tools=strumenti_legali,
            messages=messaggi
        )

        # Se il modello ha finito (nessun tool da chiamare)
        if risposta.stop_reason == "end_turn":
            # Estrai il testo finale
            testo_finale = ""
            for blocco in risposta.content:
                if blocco.type == "text":
                    testo_finale += blocco.text
            print(f"Agent completato in {i + 1} iterazioni.")
            return testo_finale

        # Processa ogni blocco della risposta
        messaggi.append({"role": "assistant", "content": risposta.content})
        risultati_tool = []

        for blocco in risposta.content:
            if blocco.type == "tool_use":
                print(f"Tool chiamato: {blocco.name}")
                print(f"Parametri: {json.dumps(blocco.input, indent=2)}")

                # Esegui il tool
                risultato = esegui_tool(blocco.name, blocco.input)
                risultati_tool.append({
                    "type": "tool_result",
                    "tool_use_id": blocco.id,
                    "content": risultato
                })

        # Rinvia i risultati al modello per la prossima iterazione
        messaggi.append({"role": "user", "content": risultati_tool})

    return "Raggiunto il limite massimo di iterazioni."

# Avvia l'agente legale
risultato = loop_agent(
    "Trova precedenti giurisprudenziali sulla responsabilità medica "
    "per intervento ortopedico mal riuscito. Consulta anche il nostro "
    "archivio per casi simili. Poi genera una bozza di memoria difensiva."
)
print("\nRisultato finale:", risultato[:300])

Gestione stato e memoria

Un agent deve ricordare cosa ha già fatto, quali risultati ha ottenuto e quali decisioni ha preso. Senza stato, ripeterebbe le stesse azioni.

python
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class PassoAgent:
    """Singolo passo eseguito dall'agent."""
    iterazione: int
    tipo: str              # "ragionamento" o "azione"
    tool_usato: str        # nome del tool (vuoto se ragionamento)
    parametri: dict        # parametri passati al tool
    risultato: str         # output del tool o del ragionamento
    timestamp: str = field(
        default_factory=lambda: datetime.now().isoformat()
    )

@dataclass
class StatoAgent:
    """Stato completo dell'agent durante l'esecuzione."""
    obiettivo: str
    passi: list = field(default_factory=list)
    risultati_raccolti: dict = field(default_factory=dict)
    completato: bool = False

    def aggiungi_passo(self, passo: PassoAgent):
        """Registra un nuovo passo nell'esecuzione."""
        self.passi.append(passo)
        print(f"  Passo {passo.iterazione}: {passo.tipo} "
              f"- {passo.tool_usato or 'ragionamento'}")

    def riepilogo(self) -> str:
        """Genera un riepilogo delle azioni eseguite."""
        righe = [f"Obiettivo: {self.obiettivo}"]
        righe.append(f"Passi totali: {len(self.passi)}")
        righe.append(f"Completato: {'Sì' if self.completato else 'No'}")
        for passo in self.passi:
            righe.append(
                f"  [{passo.iterazione}] {passo.tipo}: "
                f"{passo.tool_usato or 'pensiero'}"
            )
        return "\n".join(righe)

# Utilizzo
stato = StatoAgent(obiettivo="Memoria difensiva responsabilità medica")
stato.aggiungi_passo(PassoAgent(
    iterazione=1, tipo="azione",
    tool_usato="cerca_giurisprudenza",
    parametri={"query": "responsabilità medica ortopedia"},
    risultato="8 sentenze trovate"
))
print(stato.riepilogo())

Safety e loop control

Un agent senza controlli può entrare in loop infiniti, eseguire azioni non previste o consumare risorse senza limiti. Il safety wrapper è essenziale.

python
import time
import logging

# Configura il logging per tracciare ogni azione
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("agent_legale")

class SafetyWrapper:
    """Controlla e limita l'esecuzione dell'agent."""

    def __init__(self, max_iterazioni: int = 10,
                 timeout_secondi: int = 120,
                 azioni_critiche: list = None):
        self.max_iterazioni = max_iterazioni
        self.timeout_secondi = timeout_secondi
        self.azioni_critiche = azioni_critiche or [
            "genera_documento",    # richiede approvazione
            "invia_email",         # richiede approvazione
        ]
        self.iterazione_corrente = 0
        self.inizio = time.time()

    def controlla_limiti(self) -> bool:
        """Verifica che l'agent sia entro i limiti."""
        self.iterazione_corrente += 1

        # Controllo iterazioni
        if self.iterazione_corrente > self.max_iterazioni:
            logger.warning("Limite iterazioni raggiunto: "
                          f"{self.max_iterazioni}")
            return False

        # Controllo timeout
        tempo_trascorso = time.time() - self.inizio
        if tempo_trascorso > self.timeout_secondi:
            logger.warning(f"Timeout raggiunto: {tempo_trascorso:.0f}s")
            return False

        return True

    def richiedi_approvazione(self, nome_tool: str,
                               parametri: dict) -> bool:
        """Chiede approvazione umana per azioni critiche."""
        if nome_tool in self.azioni_critiche:
            logger.info(f"Azione critica: {nome_tool}")
            logger.info(f"Parametri: {parametri}")
            # In produzione: notifica via email o interfaccia web
            # Per ora: approvazione automatica in ambiente di test
            print(f"\n[APPROVAZIONE RICHIESTA] Tool: {nome_tool}")
            print(f"Parametri: {parametri}")
            print("In produzione, qui si attenderebbe "
                  "la conferma dell'Avv. Rossi.")
            return True  # In test, approva sempre
        return True

    def logga_azione(self, nome_tool: str, parametri: dict,
                     risultato: str):
        """Registra ogni azione per audit trail."""
        logger.info(
            f"Iterazione {self.iterazione_corrente} | "
            f"Tool: {nome_tool} | "
            f"Risultato: {risultato[:100]}..."
        )

Framework a confronto

Non esiste un solo modo per costruire agenti AI. Ecco i framework principali con vantaggi e limiti.

  • LangChain / LangGraph — Il framework più popolare. Ottimo per prototipi rapidi, ampia community e documentazione. Limite: astrazione pesante, debug complesso, aggiornamenti frequenti che rompono la retrocompatibilità
  • CrewAI — Specializzato in sistemi multi-agent dove più agenti collaborano (es. un agent cerca, un altro analizza, un terzo scrive). Limite: overhead di coordinamento, più complesso del necessario per agenti singoli
  • Anthropic tool use nativo — L'approccio mostrato in questa guida. Nessun framework aggiuntivo, pieno controllo sul loop. Limite: devi scrivere più codice infrastrutturale, ma sai esattamente cosa succede
  • Claude Agent SDK — SDK ufficiale di Anthropic per agenti production-ready. Gestione automatica del loop, guardrails integrati, logging strutturato. Limite: ecosistema più giovane rispetto a LangChain

Consiglio pratico: inizia con il tool use nativo per capire la meccanica, poi valuta un framework quando la complessità lo richiede.

Da ricordare

  • Un agent non è un chatbot: ragiona, pianifica e agisce in cicli autonomi fino al completamento dell'obiettivo
  • Il pattern ReAct (Reasoning + Acting) alterna pensiero e azione — è il fondamento di quasi tutti gli agent moderni
  • Il tool use nativo di Claude permette di definire strumenti che il modello chiama autonomamente durante il ragionamento
  • La gestione dello stato è essenziale: l'agent deve sapere cosa ha già fatto per non ripetere azioni inutili
  • Il safety wrapper con limiti di iterazione, timeout e approvazione umana non è opzionale — è indispensabile
  • Inizia semplice con il tool use nativo, poi valuta framework come LangChain o CrewAI quando la complessità cresce