Se mi aveste chiesto due anni fa se gli agenti IA sarebbero diventati infrastruttura di produzione, avrei risposto con un cauto "forse". Oggi, nel 2026, è un dato di fatto. Non stiamo più parlando di chatbot che rispondono a domande — stiamo parlando di sistemi autonomi capaci di ragionare, pianificare, usare strumenti esterni e coordinarsi con altri agenti. Un dato su tutti: secondo Gartner, le richieste relative a sistemi multi-agente sono cresciute del 1.445% tra il primo trimestre 2024 e il secondo trimestre 2025. Non è un segnale, è un'onda.
Ma cosa vuol dire, concretamente, costruire un agente IA? In questa guida esploreremo l'architettura degli agenti intelligenti, i design pattern fondamentali — dal classico ReAct alla pianificazione multi-step — e i tre framework che dominano il panorama oggi: LangGraph, CrewAI e AutoGen. Vedremo codice funzionante, pattern architetturali concreti e le best practice per portare questi sistemi in produzione, con un occhio di riguardo al Model Context Protocol (MCP), lo standard emergente per l'integrazione degli strumenti.
Anatomia di un Agente IA: Oltre il Semplice Chatbot
Prima di tuffarci nei framework e nel codice, facciamo un passo indietro. Cosa distingue davvero un agente IA da un semplice wrapper attorno a un LLM?
Un agente IA è un sistema software che percepisce il proprio ambiente, prende decisioni autonome e agisce per raggiungere obiettivi specifici. A differenza di una chiamata API a ChatGPT o Claude, un agente ha capacità fondamentali che lo rendono qualitativamente diverso. E qui sta il punto — non è solo una questione di "fa più cose", è proprio un salto concettuale.
I Quattro Pilastri dell'Agente
Ogni agente IA di produzione si fonda su quattro pilastri architetturali:
- Ragionamento (Reasoning) — La capacità di analizzare una situazione, scomporre problemi complessi in sotto-problemi e decidere il prossimo passo. È ciò che trasforma un LLM da generatore di testo a decisore autonomo. Pattern come ReAct e Chain-of-Thought strutturano questo processo in modo esplicito e tracciabile.
- Azione (Tool Use) — La capacità di interagire con il mondo esterno tramite strumenti: API, database, file system, browser web, terminali. Senza strumenti, un agente è confinato alla generazione di testo. Con gli strumenti, può effettivamente fare cose — interrogare un database, inviare un'email, eseguire codice.
- Memoria (Memory) — La capacità di mantenere contesto tra le interazioni e di apprendere dall'esperienza. Si divide in tre tipologie: memoria a breve termine (il contesto della conversazione corrente), memoria episodica (ricordi di interazioni passate), e memoria semantica (conoscenza strutturata accumulata nel tempo).
- Pianificazione (Planning) — La capacità di creare un piano d'azione multi-step prima di agire. Un agente senza pianificazione è reattivo; un agente con pianificazione è strategico. La differenza si nota eccome, soprattutto nei task complessi.
Agente Singolo vs Sistema Multi-Agente
L'evoluzione più significativa del 2026 è il passaggio da agenti monolitici — quelli che cercano di fare tutto da soli — a sistemi multi-agente dove agenti specializzati collaborano come un team coordinato.
Pensatela così: la differenza tra un tuttofare e un team di professionisti. Il tuttofare se la cava con compiti semplici, ma per progetti complessi servono specialisti che comunicano tra loro.
In un sistema multi-agente, un agente potrebbe specializzarsi nella ricerca di informazioni, un altro nell'analisi dei dati, un terzo nella generazione di report. Un orchestratore coordina il lavoro, gestisce le dipendenze tra i task e aggrega i risultati. I vantaggi sono concreti: ogni agente può avere un prompt specializzato e ottimizzato per il suo compito, la complessità viene distribuita, e il sistema diventa più facile da testare, debuggare e migliorare incrementalmente.
Design Pattern Fondamentali per Agenti IA
I design pattern sono la lingua franca dell'ingegneria degli agenti. Conoscerli vi permetterà di progettare sistemi robusti indipendentemente dal framework che sceglierete. Vediamoli nel dettaglio.
Pattern ReAct: Ragionare e Agire
Il pattern ReAct (Reason + Act) è, onestamente, il fondamento di quasi tutti gli agenti moderni. L'idea è semplice ma potente: strutturare il comportamento dell'agente in un ciclo esplicito dove ragiona sulla situazione corrente (Thought), decide quale azione intraprendere (Action), osserva il risultato (Observation), e ripete il ciclo fino al raggiungimento dell'obiettivo.
Perché funziona così bene? Perché risolve un problema critico dei primi approcci agentici: le allucinazioni negli strumenti. Separando i token di ragionamento dall'invocazione degli strumenti, il modello diventa molto più disciplinato. Ecco un esempio concreto del ciclo:
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain.tools import Tool
from langchain import hub
# Definizione degli strumenti disponibili
def cerca_database(query: str) -> str:
"""Cerca informazioni nel database aziendale."""
# Implementazione della ricerca
return f"Risultati per '{query}': 3 documenti trovati..."
def calcola_metriche(dati: str) -> str:
"""Calcola metriche statistiche sui dati forniti."""
return f"Media: 45.2, Mediana: 42.0, Dev. Std: 8.3"
tools = [
Tool(
name="CercaDatabase",
func=cerca_database,
description="Utile per cercare informazioni nel database aziendale. Input: la query di ricerca."
),
Tool(
name="CalcolaMetriche",
func=calcola_metriche,
description="Utile per calcolare metriche statistiche. Input: i dati su cui calcolare."
),
]
# Creazione dell'agente ReAct
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = hub.pull("hwchase17/react")
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=10,
handle_parsing_errors=True,
)
# Esecuzione: il ciclo Thought → Action → Observation è automatico
risultato = agent_executor.invoke({
"input": "Cerca i dati di vendita del Q4 2025 e calcolane le metriche"
})
print(risultato["output"])
Pattern di Pianificazione (Plan-and-Execute)
Il pattern Plan-and-Execute fa una cosa intelligente: separa la pianificazione dall'esecuzione. Prima, un agente pianificatore analizza l'obiettivo e genera un piano strutturato con passi sequenziali. Poi, un agente esecutore lavora su ogni passo individualmente.
Questo pattern è particolarmente utile per task complessi che richiedono molti passaggi. Un agente puramente ReAct potrebbe perdersi dopo 5-6 iterazioni, dimenticando l'obiettivo originale (vi è mai successo di perdere il filo in una conversazione lunga con un LLM? Ecco, stessa cosa). Con Plan-and-Execute, il piano funge da "mappa" che mantiene l'agente sulla rotta giusta.
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Fase 1: Pianificazione
planning_prompt = ChatPromptTemplate.from_messages([
("system", """Sei un pianificatore esperto. Dato un obiettivo,
crea un piano dettagliato con passi numerati. Ogni passo deve
essere atomico e chiaramente definito. Rispondi SOLO con il piano."""),
("human", "Obiettivo: {objective}")
])
# Fase 2: Esecuzione di ogni passo
execution_prompt = ChatPromptTemplate.from_messages([
("system", """Sei un esecutore preciso. Ti viene dato un passo
specifico di un piano più ampio. Esegui SOLO questo passo e
riporta il risultato. Contesto precedente: {context}"""),
("human", "Passo da eseguire: {step}")
])
async def plan_and_execute(objective: str):
# Genera il piano
plan_chain = planning_prompt | llm
plan = await plan_chain.ainvoke({"objective": objective})
steps = plan.content.strip().split("\n")
context = ""
# Esegui ogni passo sequenzialmente
exec_chain = execution_prompt | llm
for step in steps:
if step.strip():
result = await exec_chain.ainvoke({
"step": step,
"context": context
})
context += f"\n{step}: {result.content}"
print(f"✓ Completato: {step}")
return context
Pattern di Riflessione (Self-Reflection)
Il pattern di riflessione è forse il meno intuitivo ma uno dei più efficaci. Aggiunge un ciclo di auto-valutazione dopo ogni azione o sequenza di azioni: l'agente analizza criticamente il proprio output, identifica errori o lacune, e si auto-corregge prima di procedere.
In pratica, lo si implementa come un nodo aggiuntivo nel grafo dell'agente che valuta l'output secondo criteri predefiniti — correttezza, completezza, coerenza — e decide se accettare il risultato o richiedere una revisione. Migliora significativamente la qualità, specialmente nella generazione di codice e nell'analisi dati.
LangGraph: Grafi di Stato per Agenti Deterministici
LangGraph, sviluppato dal team LangChain, è il framework di riferimento per chi vuole controllo totale sul flusso di esecuzione. La sua filosofia è chiara: un workflow agentifico è un grafo orientato con stato, dove ogni nodo è un'unità di computazione e gli archi definiscono il flusso condizionale tra i nodi.
Perché LangGraph per la Produzione
LangGraph brilla in scenari dove servono:
- Determinismo — Sapete esattamente quali nodi vengono attraversati e in quale ordine. Niente black box.
- Durabilità — Lo stato dell'agente persiste attraverso failure e restart. Se un nodo fallisce, l'esecuzione riprende dal punto esatto di interruzione.
- Human-in-the-loop — Potete inserire punti di approvazione umana in qualsiasi punto del grafo. Per workflow critici, questo è imprescindibile.
- Tracciabilità — Ogni decisione e transizione di stato è registrata, rendendo il debugging e l'auditing decisamente più gestibili.
Costruire un Agente di Ricerca con LangGraph
Passiamo alla pratica. Costruiamo un agente che riceve una domanda, cerca informazioni sul web, valuta se sono sufficienti, e genera una risposta strutturata. Questo pattern si applica a innumerevoli scenari reali — dalla ricerca di mercato all'assistenza clienti avanzata.
from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_community.tools.tavily_search import TavilySearchResults
import operator
# 1. Definizione dello Stato del Grafo
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
search_results: str
is_sufficient: bool
final_answer: str
iteration_count: int
# 2. Inizializzazione dei componenti
llm = ChatOpenAI(model="gpt-4o", temperature=0)
search_tool = TavilySearchResults(max_results=5)
# 3. Definizione dei Nodi del Grafo
def analyze_query(state: AgentState) -> AgentState:
"""Nodo 1: Analizza la query e decide la strategia di ricerca."""
messages = state["messages"]
query = messages[-1].content
response = llm.invoke([
{"role": "system", "content": "Analizza la domanda e genera 2-3 query di ricerca ottimizzate."},
{"role": "user", "content": query}
])
return {
"messages": [AIMessage(content=f"Query di ricerca: {response.content}")],
"iteration_count": state.get("iteration_count", 0)
}
def search_information(state: AgentState) -> AgentState:
"""Nodo 2: Esegue la ricerca web."""
last_message = state["messages"][-1].content
results = search_tool.invoke(last_message)
formatted_results = "\n".join([
f"- {r['content'][:200]}" for r in results
])
return {
"messages": [AIMessage(content=f"Risultati trovati.")],
"search_results": formatted_results,
"iteration_count": state.get("iteration_count", 0) + 1
}
def evaluate_results(state: AgentState) -> AgentState:
"""Nodo 3: Valuta se i risultati sono sufficienti."""
response = llm.invoke([
{"role": "system", "content": """Valuta se i risultati di ricerca
sono sufficienti per rispondere alla domanda originale.
Rispondi SOLO con 'SUFFICIENTE' o 'INSUFFICIENTE'."""},
{"role": "user", "content": f"Domanda: {state['messages'][0].content}\n\nRisultati: {state['search_results']}"}
])
is_sufficient = "SUFFICIENTE" in response.content.upper()
return {
"messages": [AIMessage(content=response.content)],
"is_sufficient": is_sufficient
}
def generate_answer(state: AgentState) -> AgentState:
"""Nodo 4: Genera la risposta finale strutturata."""
response = llm.invoke([
{"role": "system", "content": """Genera una risposta dettagliata e
strutturata basata sui risultati di ricerca. Includi fonti quando possibile."""},
{"role": "user", "content": f"Domanda: {state['messages'][0].content}\n\nRisultati: {state['search_results']}"}
])
return {
"messages": [AIMessage(content=response.content)],
"final_answer": response.content
}
# 4. Funzione di Routing Condizionale
def should_continue_search(state: AgentState) -> str:
"""Decide se continuare a cercare o generare la risposta."""
if state.get("is_sufficient", False):
return "generate"
if state.get("iteration_count", 0) >= 3:
return "generate" # Max 3 iterazioni di ricerca
return "search"
# 5. Costruzione del Grafo
workflow = StateGraph(AgentState)
# Aggiunta dei nodi
workflow.add_node("analyze", analyze_query)
workflow.add_node("search", search_information)
workflow.add_node("evaluate", evaluate_results)
workflow.add_node("generate", generate_answer)
# Definizione degli archi
workflow.set_entry_point("analyze")
workflow.add_edge("analyze", "search")
workflow.add_edge("search", "evaluate")
# Arco condizionale: continua a cercare o genera la risposta
workflow.add_conditional_edges(
"evaluate",
should_continue_search,
{
"search": "search",
"generate": "generate",
}
)
workflow.add_edge("generate", END)
# 6. Compilazione e Esecuzione
app = workflow.compile()
# Esecuzione dell'agente
result = app.invoke({
"messages": [HumanMessage(content="Quali sono le tendenze dell'IA nel 2026?")],
"search_results": "",
"is_sufficient": False,
"final_answer": "",
"iteration_count": 0,
})
print(result["final_answer"])
Gestione dello Stato con Checkpoint
Una delle funzionalità più potenti di LangGraph (e secondo me una delle ragioni principali per sceglierlo) è il checkpointing. Ogni transizione di stato viene salvata in un backend persistente — SQLite, PostgreSQL, o un servizio custom — permettendo di riprendere l'esecuzione dopo un crash, implementare workflow asincroni che durano ore o giorni, e abilitare il time-travel debugging.
Sì, avete letto bene: time-travel debugging. La possibilità di ispezionare lo stato dell'agente in qualsiasi punto dell'esecuzione passata. Quando un agente fa qualcosa di inaspettato alle 3 di notte, poter "riavvolgere il nastro" è impagabile.
from langgraph.checkpoint.sqlite import SqliteSaver
# Checkpointing con SQLite per persistenza dello stato
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
app = workflow.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "sessione-utente-123"}}
# Prima esecuzione
result = app.invoke(
{"messages": [HumanMessage(content="Analizza il mercato IA")]},
config=config,
)
# Lo stato è persistito — potete riprendere la conversazione
# anche dopo un restart dell'applicazione
result2 = app.invoke(
{"messages": [HumanMessage(content="Approfondisci il punto 2")]},
config=config,
)
CrewAI: Team di Agenti Basati su Ruoli
Se LangGraph è il bisturi del chirurgo — preciso, controllato, ma che richiede competenza per essere usato — CrewAI è più simile a una squadra ben organizzata. La filosofia è radicalmente diversa: invece di modellare workflow come grafi, modellate team di agenti con ruoli, obiettivi e strumenti specifici, e lasciate che il framework gestisca l'orchestrazione.
Devo ammettere che la prima volta che ho provato CrewAI sono rimasto sorpreso da quanto fosse intuitivo. Ma andiamo con ordine.
La Filosofia Role-Based
In CrewAI, ogni agente ha un ruolo (chi è), un obiettivo (cosa vuole raggiungere), e una backstory (il contesto che guida il suo comportamento). I task vengono assegnati agli agenti in base alle loro competenze, e il framework gestisce comunicazione e coordinamento. È un approccio ispirato alla gestione di team umani — e funziona sorprendentemente bene.
Esempio Pratico: Team di Analisi di Mercato
Costruiamo un sistema multi-agente per l'analisi di mercato con tre specialisti: un ricercatore, un analista e un redattore.
from crewai import Agent, Task, Crew, Process
from crewai.tools import tool
from langchain_community.tools import DuckDuckGoSearchRun
# Definizione degli strumenti
search = DuckDuckGoSearchRun()
@tool("Ricerca Web")
def ricerca_web(query: str) -> str:
"""Cerca informazioni aggiornate sul web."""
return search.run(query)
@tool("Analisi Dati")
def analisi_dati(dati: str) -> str:
"""Analizza dati e genera insight strutturati."""
return f"Analisi completata per: {dati[:100]}..."
# Agente 1: Ricercatore Senior
ricercatore = Agent(
role="Ricercatore Senior di Mercato",
goal="Trovare dati aggiornati e affidabili sulle tendenze di mercato nell'IA",
backstory="""Sei un ricercatore con 15 anni di esperienza nell'analisi
dei mercati tecnologici. Hai un occhio acuto per identificare tendenze
emergenti e distinguere i segnali dal rumore. Ti concentri sempre su
dati verificabili e fonti autorevoli.""",
tools=[ricerca_web],
verbose=True,
allow_delegation=False,
max_iter=5,
)
# Agente 2: Analista Strategico
analista = Agent(
role="Analista Strategico",
goal="Trasformare dati grezzi in insight azionabili con analisi quantitativa",
backstory="""Sei un analista strategico specializzato in tecnologie IA.
Eccelli nel trovare pattern nei dati, calcolare metriche di crescita
e identificare opportunità di mercato. Presenti sempre i dati con
contesto e interpretazione critica.""",
tools=[analisi_dati],
verbose=True,
allow_delegation=False,
)
# Agente 3: Redattore Report
redattore = Agent(
role="Redattore di Report Esecutivi",
goal="Creare report chiari, concisi e convincenti per il management",
backstory="""Sei un comunicatore esperto che trasforma analisi tecniche
complesse in report esecutivi comprensibili. Usi un linguaggio chiaro,
strutturi le informazioni in modo logico e concludi sempre con
raccomandazioni concrete.""",
verbose=True,
allow_delegation=False,
)
# Definizione dei Task
task_ricerca = Task(
description="""Ricerca le 5 principali tendenze nel mercato dell'IA
per il 2026. Per ogni tendenza, trova: dimensione del mercato,
tasso di crescita, player principali e casi d'uso reali.
Concentrati su dati degli ultimi 6 mesi.""",
expected_output="Report di ricerca con 5 tendenze documentate con dati e fonti",
agent=ricercatore,
)
task_analisi = Task(
description="""Analizza i dati di ricerca e identifica:
1. Le 3 opportunità di investimento più promettenti
2. I rischi principali per ogni opportunità
3. Un punteggio di priorità (1-10) basato su potenziale e maturità
Usa analisi quantitativa dove possibile.""",
expected_output="Analisi strategica con scoring delle opportunità",
agent=analista,
context=[task_ricerca], # Dipende dal task di ricerca
)
task_report = Task(
description="""Crea un report esecutivo di massimo 2 pagine che includa:
- Executive Summary (3-4 frasi)
- Top 3 Opportunità con scoring
- Rischi e Mitigazioni
- Raccomandazioni Concrete (next steps)
Il tono deve essere professionale ma accessibile.""",
expected_output="Report esecutivo strutturato pronto per il management",
agent=redattore,
context=[task_ricerca, task_analisi],
)
# Creazione e esecuzione del Crew
crew = Crew(
agents=[ricercatore, analista, redattore],
tasks=[task_ricerca, task_analisi, task_report],
process=Process.sequential, # Esecuzione sequenziale
verbose=True,
)
# Avvio del workflow multi-agente
risultato = crew.kickoff()
print(risultato)
CrewAI vs LangGraph: Quando Usare Quale
La scelta tra CrewAI e LangGraph non è una questione di "migliore o peggiore" ma di adeguatezza al contesto.
Scegliete LangGraph quando vi serve controllo granulare sul flusso di esecuzione, quando tracciabilità e auditabilità sono requisiti non negoziabili (pensate al settore finanziario, sanitario, legale), o quando il workflow ha logica condizionale complessa con molti branch.
Scegliete CrewAI quando il problema si modella naturalmente come un team di specialisti, quando volete prototipare rapidamente, o quando la flessibilità nella comunicazione tra agenti conta più del determinismo. E in molti progetti reali — questo è un punto importante — i due framework coesistono: CrewAI per l'orchestrazione ad alto livello e LangGraph per i singoli agenti che necessitano di workflow interni complessi.
Model Context Protocol (MCP): Lo Standard per gli Strumenti
Una delle sfide più grosse nella costruzione di agenti IA è l'integrazione con strumenti esterni. Ogni API ha il suo formato, ogni servizio il suo protocollo di autenticazione, ogni strumento le sue convenzioni. È un caos, diciamolo.
Il Model Context Protocol (MCP), introdotto da Anthropic nel novembre 2024, affronta questo problema proponendo uno standard aperto e universale per la comunicazione tra agenti IA e fonti di dati esterne.
Come Funziona MCP
MCP segue un'architettura client-server dove l'agente IA (client) comunica con server MCP che espongono strumenti, risorse e prompt in un formato standardizzato. Pensate a MCP come a quello che USB è stato per le periferiche hardware: prima di USB, ogni dispositivo aveva il suo connettore proprietario. MCP fa la stessa cosa per gli strumenti IA.
L'adozione è stata impressionante. Dalla sua introduzione, la community ha costruito migliaia di server MCP, gli SDK sono disponibili per tutti i principali linguaggi, e nel 2026 OpenAI, Google DeepMind e numerosi tool vendor hanno adottato MCP come standard de facto. Nel 2026, MCP supporta anche contenuti multimediali — immagini, video e audio — ampliando significativamente le possibilità.
Implementare un Server MCP Custom
Ecco come creare un server MCP che espone l'accesso a un database aziendale come strumento per gli agenti:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import sqlite3
import json
# Creazione del server MCP
server = Server("database-aziendale")
@server.list_tools()
async def list_tools():
"""Dichiara gli strumenti disponibili."""
return [
Tool(
name="query_clienti",
description="Cerca clienti nel database aziendale per nome, settore o regione",
inputSchema={
"type": "object",
"properties": {
"filtro": {
"type": "string",
"description": "Criterio di ricerca (nome, settore o regione)"
},
"valore": {
"type": "string",
"description": "Valore da cercare"
},
"limite": {
"type": "integer",
"description": "Numero massimo di risultati",
"default": 10
}
},
"required": ["filtro", "valore"]
}
),
Tool(
name="report_vendite",
description="Genera un report sulle vendite per periodo e prodotto",
inputSchema={
"type": "object",
"properties": {
"data_inizio": {
"type": "string",
"description": "Data inizio periodo (YYYY-MM-DD)"
},
"data_fine": {
"type": "string",
"description": "Data fine periodo (YYYY-MM-DD)"
},
"prodotto": {
"type": "string",
"description": "Filtro opzionale per prodotto"
}
},
"required": ["data_inizio", "data_fine"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
"""Gestisce le chiamate agli strumenti."""
conn = sqlite3.connect("aziendale.db")
cursor = conn.cursor()
if name == "query_clienti":
filtro = arguments["filtro"]
valore = arguments["valore"]
limite = arguments.get("limite", 10)
query = f"SELECT * FROM clienti WHERE {filtro} LIKE ? LIMIT ?"
cursor.execute(query, (f"%{valore}%", limite))
risultati = cursor.fetchall()
return [TextContent(
type="text",
text=json.dumps(risultati, ensure_ascii=False, indent=2)
)]
elif name == "report_vendite":
query = """
SELECT prodotto, SUM(importo) as totale, COUNT(*) as transazioni
FROM vendite
WHERE data BETWEEN ? AND ?
GROUP BY prodotto
ORDER BY totale DESC
"""
params = [arguments["data_inizio"], arguments["data_fine"]]
if "prodotto" in arguments:
query = query.replace("GROUP BY", f"AND prodotto = ? GROUP BY")
params.insert(2, arguments["prodotto"])
cursor.execute(query, params)
risultati = cursor.fetchall()
return [TextContent(
type="text",
text=json.dumps(risultati, ensure_ascii=False, indent=2)
)]
conn.close()
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Considerazioni sulla Sicurezza di MCP
Un punto che va affrontato con trasparenza: MCP non è privo di preoccupazioni sulla sicurezza. Ricercatori hanno identificato vulnerabilità potenziali tra cui prompt injection attraverso i dati restituiti dagli strumenti, permessi eccessivi che potrebbero essere sfruttati per esfiltrare dati, e strumenti lookalike che possono sostituire silenziosamente strumenti fidati.
Per mitigare questi rischi, è essenziale implementare validazione rigorosa degli input, principio del minimo privilegio per ogni strumento, logging dettagliato di tutte le chiamate, e sandboxing dell'esecuzione. Non sono precauzioni opzionali — sono requisiti.
Function Calling: Il Meccanismo Fondamentale
Alla base di ogni interazione tra agente e strumento c'è il function calling — la capacità del modello di generare chiamate strutturate a funzioni predefinite. È il meccanismo primitivo su cui si costruiscono tutti i pattern agentici, incluso MCP.
Best Practice per il Function Calling nel 2026
L'esperienza accumulata nell'industria ha cristallizzato alcune best practice che vale la pena conoscere:
- Nomi descrittivi e inequivocabili — Usate nomi di funzione che comunicano chiaramente lo scopo.
get_customer_by_emailè infinitamente migliore diquery. Il modello seleziona la funzione giusta basandosi principalmente sul nome e sulla descrizione. - Descrizioni dettagliate — Ogni funzione dovrebbe avere una descrizione che spiega cosa fa, quando usarla, e cosa aspettarsi come output. Non lesinante sulla documentazione.
- Tipi espliciti per ogni parametro — Specificate tipi di dato espliciti (integer, string, enum) per ridurre errori nella generazione degli argomenti. Le enum sono particolarmente utili per parametri con valori predefiniti.
- Meno di 20 funzioni contemporaneamente — Oltre questa soglia, la precisione nella selezione degrada. Se avete bisogno di più strumenti, considerate strategie come il Tool Search di Anthropic, che filtra gli strumenti rilevanti prima di presentarli al modello.
- Output strutturati — Utilizzate Structured Outputs (disponibili sia con OpenAI che con Claude) per garantire che le risposte rispettino schemi JSON predefiniti. Questo colma il divario tra output probabilistici e requisiti deterministici del business.
from openai import OpenAI
import json
client = OpenAI()
# Definizione delle funzioni con best practice
tools = [
{
"type": "function",
"function": {
"name": "cerca_prodotto_per_categoria",
"description": """Cerca prodotti nel catalogo aziendale filtrandoli
per categoria. Restituisce nome, prezzo e disponibilità.
Usare quando l'utente chiede informazioni su prodotti specifici.""",
"parameters": {
"type": "object",
"properties": {
"categoria": {
"type": "string",
"enum": ["elettronica", "abbigliamento", "casa", "sport"],
"description": "La categoria di prodotto da cercare"
},
"prezzo_max": {
"type": "number",
"description": "Prezzo massimo in euro (opzionale)"
},
"solo_disponibili": {
"type": "boolean",
"description": "Se true, mostra solo prodotti in stock",
"default": True
}
},
"required": ["categoria"]
}
}
}
]
# Chiamata con function calling
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Mostrami i prodotti elettronici sotto i 500 euro"}
],
tools=tools,
tool_choice="auto",
)
# Gestione della risposta
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
arguments = json.loads(tool_call.function.arguments)
print(f"Funzione: {tool_call.function.name}")
print(f"Argomenti: {json.dumps(arguments, indent=2)}")
Memoria per Agenti: Da Transiente a Persistente
La memoria è ciò che distingue un agente persistente da un chatbot usa e getta. Un agente senza memoria dimentica tutto dopo ogni sessione, costringendo l'utente a ripetere contesto e preferenze ogni volta. Un agente con memoria ben progettata impara, si adatta e diventa progressivamente più efficace.
Suona familiare? È esattamente la frustrazione che proviamo quando dobbiamo rispiegare tutto da capo a un assistente virtuale.
Architettura della Memoria a Tre Livelli
L'architettura di memoria moderna per agenti IA si articola su tre livelli distinti:
- Memoria a breve termine (Working Memory) — Il contesto della conversazione corrente, gestito dalla finestra di contesto del modello. Limitata in dimensione ma immediatamente accessibile. Va gestita con attenzione, usando strategie di compressione e summarization per conversazioni lunghe.
- Memoria episodica — Ricordi di interazioni passate, salvati in un database vettoriale o relazionale. Quando l'agente affronta un problema simile a uno già incontrato, recupera l'esperienza precedente e applica le lezioni apprese. Si implementa tipicamente con un vector store come Qdrant o Pinecone, indicizzando le interazioni per similarità semantica.
- Memoria semantica (Knowledge Base) — Conoscenza strutturata e fatti permanenti. Non è legata a eventi specifici ma rappresenta conoscenza generalizzata — preferenze dell'utente, regole aziendali, definizioni di dominio. Si implementa tipicamente con grafi di conoscenza o database relazionali.
from datetime import datetime
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from langchain_openai import OpenAIEmbeddings
import uuid
class AgentMemory:
"""Sistema di memoria a tre livelli per agenti IA."""
def __init__(self):
self.short_term = [] # Memoria a breve termine
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
self.qdrant = QdrantClient(":memory:") # In produzione: URL del cluster
# Collezione per memoria episodica
self.qdrant.create_collection(
collection_name="episodic_memory",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
def add_to_short_term(self, role: str, content: str):
"""Aggiunge un messaggio alla memoria a breve termine."""
self.short_term.append({
"role": role,
"content": content,
"timestamp": datetime.utcnow().isoformat()
})
# Mantieni solo gli ultimi 20 messaggi
if len(self.short_term) > 20:
self.short_term = self.short_term[-20:]
def save_episode(self, summary: str, outcome: str, tags: list[str]):
"""Salva un'esperienza nella memoria episodica."""
text = f"{summary} | Risultato: {outcome}"
vector = self.embeddings.embed_query(text)
self.qdrant.upsert(
collection_name="episodic_memory",
points=[PointStruct(
id=str(uuid.uuid4()),
vector=vector,
payload={
"summary": summary,
"outcome": outcome,
"tags": tags,
"timestamp": datetime.utcnow().isoformat()
}
)]
)
def recall_similar(self, query: str, limit: int = 3) -> list[dict]:
"""Recupera esperienze simili dalla memoria episodica."""
vector = self.embeddings.embed_query(query)
results = self.qdrant.query_points(
collection_name="episodic_memory",
query=vector,
limit=limit,
with_payload=True
)
return [
{
"summary": p.payload["summary"],
"outcome": p.payload["outcome"],
"relevance": p.score
}
for p in results.points
]
# Utilizzo
memory = AgentMemory()
# L'agente completa un task e salva l'esperienza
memory.save_episode(
summary="Analisi del churn rate per il cliente Acme Corp usando dati CRM",
outcome="Identificati 3 segmenti a rischio con tasso di churn > 15%",
tags=["analisi_churn", "CRM", "segmentazione"]
)
# In una sessione futura, l'agente recupera esperienze rilevanti
esperienze = memory.recall_similar("Come analizzare il tasso di abbandono clienti?")
for exp in esperienze:
print(f"Esperienza simile (rilevanza: {exp['relevance']:.2f}): {exp['summary']}")
Portare gli Agenti in Produzione: Sfide e Best Practice
Costruire un agente che funziona in un notebook Jupyter è relativamente semplice. Portarlo in produzione? Tutta un'altra storia.
Le sfide si moltiplicano: affidabilità, latenza, costi, sicurezza, osservabilità. Ecco le lezioni chiave che l'industria ha imparato (spesso a proprie spese) nel 2026.
Osservabilità e Tracing
Ogni decisione e ogni chiamata a strumento dell'agente deve essere tracciata. Senza osservabilità, diagnosticare un comportamento inaspettato diventa praticamente impossibile. Utilizzate strumenti di tracing distribuito come LangSmith, OpenTelemetry, o il tracing integrato nell'OpenAI Agents SDK. Registrate tutto: il prompt completo inviato al modello, la risposta ricevuta, le chiamate a strumenti con input e output, la latenza di ogni operazione, e i token consumati con il relativo costo.
Gestione degli Errori e Retry
Gli agenti in produzione devono gestire gracefully una serie di problemi: rate limit delle API, timeout delle chiamate a strumenti, risposte malformate dal modello, failure dei servizi esterni. Implementate retry con backoff esponenziale, circuit breaker per servizi instabili, e fallback a modelli alternativi quando il modello primario non è disponibile.
Un consiglio che vorrei aver ricevuto prima: impostate sempre un limite massimo di iterazioni. Un agente che gira all'infinito consuma budget e non produce nulla di utile.
Controllo dei Costi
I costi degli agenti possono crescere rapidamente, soprattutto con modelli potenti come GPT-4o o Claude Opus. Strategie di contenimento includono: usare modelli più economici per task semplici (routing intelligente), caching delle risposte per query ripetute, limitare il numero di iterazioni e la dimensione del contesto, e monitorare il consumo per utente e per task.
Una regola empirica che trovo utile: se un agente consuma più di 5 dollari per singola esecuzione, probabilmente il workflow va ripensato.
Guardrail e Sicurezza
Gli agenti hanno accesso a strumenti che possono avere effetti reali — modificare database, inviare email, eseguire transazioni. I guardrail non sono un optional:
- Input validation — Validate ogni input dell'utente prima di passarlo all'agente. Prevenite prompt injection verificando che le istruzioni non contengano tentativi di manipolazione.
- Output validation — Verificate che le azioni proposte dall'agente siano coerenti con le policy aziendali prima di eseguirle.
- Principio del minimo privilegio — Ogni agente e ogni strumento dovrebbe avere solo i permessi strettamente necessari. Un agente di customer service non ha bisogno di accesso al database finanziario (sembra ovvio, eppure succede più spesso di quanto si pensi).
- Human-in-the-loop per azioni critiche — Per operazioni irreversibili — cancellazioni, transazioni, comunicazioni esterne — richiedete approvazione umana. LangGraph supporta nativamente punti di interruzione per questo.
Il Futuro degli Agenti IA: Tendenze 2026 e Oltre
Il panorama degli agenti IA si evolve a velocità vertiginosa. Ecco le tendenze che stanno plasmando il futuro prossimo.
Agentic Mesh — L'evoluzione da sistemi multi-agente chiusi a reti di agenti interoperabili che comunicano attraverso protocolli standardizzati come MCP. Immaginate agenti di aziende diverse che collaborano automaticamente — l'agente di un fornitore che negozia con l'agente di un cliente senza intervento umano. Fantascienza? No, sta già succedendo in alcuni settori.
Agenti con Ragionamento Avanzato — I nuovi modelli di ragionamento (come o3 di OpenAI e le capacità di extended thinking di Claude) permettono agli agenti di affrontare problemi che richiedono ragionamento multi-step profondo. Questo apre la strada ad agenti capaci di risolvere problemi scientifici, ingegneristici e di pianificazione strategica.
Specializzazione Verticale — Il trend si muove verso agenti altamente specializzati per domini specifici — legale, medico, finanziario, ingegneristico — con conoscenze e guardrail specifici integrati. Gartner prevede che il 40% delle applicazioni enterprise includerà agenti IA task-specific entro la fine del 2026, rispetto al 5% del 2025.
Governance e Standard — Con l'adozione crescente, emerge la necessità di standard per la governance degli agenti. Chi è responsabile delle azioni di un agente? Come si audita il processo decisionale? Sono domande che non possiamo più rimandare. MCP sta evolvendo verso una governance aperta, con processi trasparenti di standardizzazione e contribuzione dalla community.
Conclusione: Costruire Agenti con Criterio
Gli agenti IA rappresentano un cambio di paradigma nella costruzione di applicazioni intelligenti. Non si tratta più di chiamare un'API e formattare la risposta, ma di progettare sistemi autonomi che ragionano, pianificano, agiscono e imparano.
Il mio consiglio pratico? Partite semplici. Iniziate con un singolo agente ReAct che usa 2-3 strumenti ben definiti. Validate l'approccio con utenti reali. Solo quando avete un agente singolo che funziona bene, considerate l'evoluzione verso sistemi multi-agente.
LangGraph per il controllo, CrewAI per la velocità di prototipazione, MCP per l'integrazione standardizzata degli strumenti — avete tutti gli ingredienti necessari. Ora sta a voi costruire.
Se avete già familiarità con le pipeline RAG, noterete come agenti e RAG si complementino perfettamente: un agente può utilizzare una pipeline RAG come uno dei suoi strumenti, combinando la capacità di recupero informazioni con l'autonomia decisionale. È in questa convergenza che emergono le applicazioni più potenti dell'IA nel 2026.