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.
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.
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.
# 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.
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.
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.
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