Tool design per agent: naming, schema, error handling che riducono errori del 60%
Il tuo tool si chiama get_data? L'agent sbaglierà tre volte prima di capire cosa fa. Non è un'iperbole: è il pattern di failure più comune che osservo quando analizzo trace di agent in produzione. Il modello riceve la lista dei tool disponibili, legge get_data, non capisce se recupera dati da un database, da un'API, da un file o da una cache, e inizia a sperimentare per inferenza. Ogni esperimento sbagliato costa token, latenza e - in produzione - denaro.
Progettare tool ben fatti non è un dettaglio estetico. È l'interfaccia tra la logica dell'agent e il mondo esterno. Ogni ambiguità nel naming, ogni parametro con tipo troppo permissivo, ogni errore restituito come eccezione anziché come dato strutturato si traduce in iterazioni sprecate nel loop ReAct. Test empirici su sistemi in produzione mostrano che applicare le tecniche descritte in questo episodio - naming verb_noun specifico, schema Pydantic con enum stretti, error-as-data con suggestion field - riduce gli errori agent del 40-60% senza toccare il modello LLM sottostante.
In questo episodio vediamo come ho ristrutturato il tool catalog del ml-service del mio portfolio: 12 strumenti per un chat agent, passati da una prima implementazione naïve a una production-ready. Ogni sezione è accompagnata da codice Python concreto, tabelle di confronto e spiegazioni del ragionamento dietro ogni scelta.
Cosa Imparerai
- Come applicare naming convention verb_noun specifico che l'agent capisce senza ambiguità
- Come costruire schema Pydantic con tipi stretti, enum e Field descriptions per guidare il modello
- Come implementare error-as-data con suggestion field per abilitare l'auto-correzione agent
- Come aggiungere idempotency token su tool con side effects per prevenire duplicazioni
- Come costruire un sistema di retry-on-validation con guard max_retries
- Come comporre tool in orchestratori per task complessi a più step
- Come mantenere un tool catalog YAML come registry centralizzato
Prerequisiti
- Python e Pydantic v2: modelli, validatori, Field - li usiamo intensivamente
- Concetto base di JSON Schema: type, properties, required, enum
- Familiarità con LLM agent pattern ReAct o function calling (OpenAI/Ollama)
- Lettura EP04 - Debugging agent loop consigliata per il contesto sui failure mode
Sezione 1 - Naming convention: verb_noun specifico come contratto con l'LLM
Quando un LLM agent deve scegliere un tool, la prima cosa che processa è il nome. Il nome è il segnale più denso di informazione nell'intero schema del tool: se è ambiguo, il modello deve leggere la description completa (più token, più latenza), poi eventualmente provare il tool e correggersi dall'errore (ancora più token). Un nome preciso abbatte questo overhead.
La convenzione verb_noun funziona perché rispecchia il modo in cui un agent pianifica: "devo ottenere clienti attivi", quindi cerco un tool che lista clienti. La specificità dell'oggetto riduce la confusione tra tool simili: list_active_customers_by_region e count_churned_customers_last_30_days sono inequivocabili, mentre get_customers e get_customer_stats lasciano aperto il dubbio.
| Pattern | Esempio cattivo | Esempio buono | Problema risolto |
|---|---|---|---|
| Verbo generico | get_data |
fetch_article_metadata |
Chiarisce l'oggetto e la sorgente |
| Sostantivo ambiguo | process_input |
validate_email_address |
Rende esplicito il tipo di input e l'operazione |
| Abbreviazione | calc_rev |
calculate_monthly_revenue |
Elimina lookup mentale nel modello |
| Nome plurale ambiguo | users |
list_registered_users_by_status |
Specifica l'azione (lista vs cerca vs conta) |
| Verbo passivo | is_valid |
check_iban_validity |
Chiarisce il contesto di dominio |
| Azione distruttiva nascosta | update |
archive_expired_subscriptions |
Rende visibile il side effect nell'agent plan |
Un caso concreto dal ml-service del portfolio: il tool originale si chiamava search. L'agent confondeva costantemente ricerca semantica su articoli con ricerca tool da catalogo. Rinominandolo search_articles_by_semantic_query, gli errori di selezione tool si sono azzerati in quella categoria. Il nome è diventato il contract.
# tool_naming.py - Esempio di naming convention applicata al ml-service portfolio
# Installa: pip install pydantic>=2.0
from pydantic import BaseModel, Field
from typing import Literal
# SBAGLIATO: nome generico, schema aperto
def search(query: str) -> dict:
"""Cerca qualcosa."""
pass
# CORRETTO: nome verb_noun specifico, schema tipizzato
def search_articles_by_semantic_query(
query: str,
language: Literal["it", "en"] = "it",
max_results: int = Field(default=5, ge=1, le=20),
category_filter: str | None = None
) -> dict:
"""
Cerca articoli nel corpus del portfolio usando similarità semantica (embedding).
USA QUESTO TOOL quando l'utente chiede di trovare articoli su un argomento,
tecnologia o concetto. NON usare per navigare tra categorie o listare serie.
Esempio input: query="RAG con pgvector", language="it", max_results=3
Esempio output: {"results": [{"id": 201, "title": "...", "score": 0.89}]}
"""
pass # implementazione nel ml-service
Nota la struttura della docstring: prima frase descrittiva, poi "USA QUESTO TOOL quando...", poi "NON usare per...", poi esempio concreto. Questa struttura è progettata per come l'LLM processa le informazioni durante il tool selection.
Sezione 2 - Schema JSON con Pydantic: tipi stretti, enum, validatori custom
Lo schema del tool è ciò che l'LLM usa per costruire i parametri della chiamata. Uno schema permissivo - tutto string, nessun enum, nessun vincolo sui valori - scarica sull'agent il compito di indovinare il formato corretto. Uno schema stretto è un sistema di guida implicita: il modello riceve informazioni sui valori ammessi prima ancora di invocare il tool.
La differenza pratica è misurabile. Su un tool con un parametro status definito come string libero, l'agent di produzione passava valori come "active", "enabled", "live", "running" - tutti semanticamente equivalenti ma diversi dal valore atteso "running". Passando a Literal["running", "stopped", "paused"], gli errori di tipo su quel parametro sono diventati zero.
# tool_schema.py - Schema Pydantic stretto per tool agent
# Pattern dal ml-service portfolio per il tool di raccomandazione articoli
from pydantic import BaseModel, Field, field_validator
from typing import Literal
from enum import Enum
class ContentLanguage(str, Enum):
ITALIAN = "it"
ENGLISH = "en"
class SortOrder(str, Enum):
RELEVANCE = "relevance"
DATE_DESC = "date_desc"
DATE_ASC = "date_asc"
READING_TIME = "reading_time"
class RecommendArticlesInput(BaseModel):
"""Schema di input per il tool recommend_articles_for_user."""
user_context: str = Field(
description=(
"Contesto dell'utente: interessi attuali, articoli già letti, "
"domanda specifica. Massimo 500 caratteri."
),
max_length=500,
min_length=10
)
language: ContentLanguage = Field(
default=ContentLanguage.ITALIAN,
description="Lingua degli articoli da raccomandare."
)
max_results: int = Field(
default=5,
ge=1,
le=10,
description="Numero massimo di raccomandazioni. Tipicamente 3-5 per risposte conversazionali."
)
sort_by: SortOrder = Field(
default=SortOrder.RELEVANCE,
description="Criterio di ordinamento. Usa 'relevance' per query semantiche, 'date_desc' per contenuti recenti."
)
exclude_article_ids: list[int] = Field(
default_factory=list,
description="ID articoli da escludere (già mostrati all'utente in questa sessione)."
)
@field_validator("user_context")
@classmethod
def context_not_empty_or_generic(cls, v: str) -> str:
"""Rifiuta contesti troppo generici che non portano a raccomandazioni utili."""
generic_terms = ["qualcosa", "anything", "tutto", "everything"]
if v.strip().lower() in generic_terms:
raise ValueError(
"user_context troppo generico. Fornisci un contesto specifico "
"come 'utente interessato a RAG e vector database' o la domanda corrente."
)
return v.strip()
Il campo description dentro Field non è documentazione per lo sviluppatore: è testo che finisce letteralmente nello schema JSON inviato all'LLM. Ogni parola deve essere utile al modello, non al developer. Nota anche il validator custom: non lancia solo un errore, ma fornisce un messaggio con la correzione suggerita.
Sezione 3 - Error-as-data: restituire errori strutturati che l'agent può usare
Il modo tradizionale di gestire gli errori nei tool è lanciare eccezioni. Questo crea un problema nel loop ReAct: l'eccezione viene catturata dal framework agent, convertita in un messaggio di errore grezzo, e passata al modello come tool result. Il modello riceve un traceback Python, non sa cosa fare, e spesso produce una risposta di fallback generica o peggio entra in un loop di retry ciechi.
Il pattern error-as-data inverte questa logica: il tool restituisce sempre un oggetto strutturato, anche in caso di errore. La struttura include un campo suggestion che dice all'agent esattamente cosa correggere. Questo campo è la chiave: permette auto-correzione senza un ulteriore round-trip al modello per "capire l'errore".
# error_as_data.py - Pattern error-as-data con suggestion field
# Tassonomia errori standardizzata per il tool catalog ml-service
from pydantic import BaseModel
from typing import Any, Literal
from enum import Enum
class ErrorCode(str, Enum):
VALIDATION_ERROR = "VALIDATION_ERROR"
NOT_FOUND = "NOT_FOUND"
PERMISSION_DENIED = "PERMISSION_DENIED"
RATE_LIMITED = "RATE_LIMITED"
DEPENDENCY_UNAVAILABLE = "DEPENDENCY_UNAVAILABLE"
IDEMPOTENCY_CONFLICT = "IDEMPOTENCY_CONFLICT"
class ToolResult(BaseModel):
"""Struttura di risposta unificata per tutti i tool agent."""
success: bool
data: Any | None = None
error_code: ErrorCode | None = None
error_message: str | None = None
# Campo chiave: indica all'agent cosa fare per correggere l'errore
suggestion: str | None = None
# Flag che indica se il retry automatico ha senso
retry_allowed: bool = True
def search_articles_by_semantic_query(
query: str,
language: str = "it",
max_results: int = 5
) -> ToolResult:
"""Cerca articoli per similarità semantica. Restituisce sempre ToolResult."""
# Validazione parametri con errore strutturato
if language not in ("it", "en"):
return ToolResult(
success=False,
error_code=ErrorCode.VALIDATION_ERROR,
error_message=f"Lingua '{language}' non supportata.",
suggestion=(
"Usa language='it' per italiano o language='en' per inglese. "
"Nessun altro valore è accettato."
),
retry_allowed=True # l'agent può riprovare con il valore corretto
)
if max_results < 1 or max_results > 20:
return ToolResult(
success=False,
error_code=ErrorCode.VALIDATION_ERROR,
error_message=f"max_results={max_results} fuori dal range valido.",
suggestion="Usa un valore tra 1 e 20. Per chat conversazionale, 3-5 è ottimale.",
retry_allowed=True
)
# Simulazione dipendenza non disponibile (Ollama offline)
try:
results = _run_semantic_search(query, language, max_results)
return ToolResult(success=True, data=results)
except ConnectionError:
return ToolResult(
success=False,
error_code=ErrorCode.DEPENDENCY_UNAVAILABLE,
error_message="Il servizio di embedding non è raggiungibile.",
suggestion=(
"Non riprovare questo tool. Usa invece 'search_articles_by_keyword' "
"che non dipende dal servizio di embedding."
),
retry_allowed=False # non ha senso riprovare, l'errore è strutturale
)
def _run_semantic_search(query: str, language: str, max_results: int) -> list[dict]:
"""Implementazione interna - non esposta come tool."""
pass # logica Redis HNSW del ml-service
Il campo retry_allowed: False su un errore di dipendenza è importante quanto il suggestion: dice all'agent di non sprecare iterazioni a riprovare. Il campo suggestion indica un tool alternativo - questo è tool composition guidata dall'errore.
Sezione 4 - Tassonomia degli errori: classificare per far decidere l'agent
Non tutti gli errori sono uguali per un agent. Un errore di validazione è correggibile immediatamente. Un errore di rate limiting richiede attesa. Un errore di dipendenza richiede fallback su un tool alternativo. Una risposta vuota (nessun risultato) non è un errore ma richiede una strategia diversa. Senza una tassonomia esplicita, l'agent tratta tutto allo stesso modo: riprova, o si arrende.
| Tipo errore | Codice | Retry? | Strategia agent | Esempio |
|---|---|---|---|---|
| Validazione parametro | VALIDATION_ERROR |
Si, con fix | Corregge il parametro e riprova | language='fr' non supportato |
| Risorsa non trovata | NOT_FOUND |
No | Informa l'utente, suggerisce alternativa | article_id=9999 inesistente |
| Permesso negato | PERMISSION_DENIED |
No | Escalation o risposta negativa | Tool richiede autenticazione |
| Rate limit | RATE_LIMITED |
Si, con wait | Attende retry_after_seconds, poi riprova | Embedding API quota esaurita |
| Dipendenza down | DEPENDENCY_UNAVAILABLE |
No | Usa tool alternativo da suggestion | Ollama non raggiungibile |
| Conflitto idempotenza | IDEMPOTENCY_CONFLICT |
No (già eseguito) | Recupera risultato dalla cache | Token già usato, operazione già eseguita |
Sezione 5 - Idempotency token: prevenire duplicazioni nei tool con side effects
Un agent in produzione può ritentare una tool call per mille ragioni: timeout di rete, errore transitorio, loop di retry esplicito nel codice. Se il tool ha side effects - scrive su database, invia un'email, registra un'interazione utente - ogni retry produce un duplicato. Su un sistema che traccia le visualizzazioni articoli nel portfolio, questo significava conteggi gonfiati del 15-30% nelle prime implementazioni.
L'idempotency token risolve il problema a livello di protocollo. Il chiamante genera un UUID per ogni operazione logica (non per ogni retry). Il server traccia i token già processati. Le retry con lo stesso token ricevono il risultato memorizzato, non una nuova esecuzione. Il pattern è standard in sistemi di pagamento (Stripe usa questo approccio) ed è applicabile direttamente ai tool agent.
# idempotency_token.py - Pattern idempotency token per tool con side effects
# Implementazione con cache in-memory (in produzione: Redis con TTL)
import hashlib
import time
from functools import wraps
from typing import Any, Callable
# Cache semplicistica per demo (produzione: Redis SETEX con TTL 24h)
_idempotency_cache: dict[str, dict] = {}
def with_idempotency(ttl_seconds: int = 86400):
"""
Decorator che aggiunge idempotency token support a un tool.
Il tool decorato accetta un parametro idempotency_key opzionale.
Se la chiave è già in cache, restituisce il risultato precedente.
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, idempotency_key: str | None = None, **kwargs) -> Any:
if idempotency_key is None:
# Nessun token: esegui normalmente (tool read-only o test)
return func(*args, **kwargs)
cache_entry = _idempotency_cache.get(idempotency_key)
if cache_entry and (time.time() - cache_entry["timestamp"]) < ttl_seconds:
# Token già visto: restituisce risultato cached senza rieseguire
return {
**cache_entry["result"],
"idempotent_response": True,
"original_timestamp": cache_entry["timestamp"]
}
# Prima esecuzione: esegui e memorizza il risultato
result = func(*args, **kwargs)
_idempotency_cache[idempotency_key] = {
"result": result,
"timestamp": time.time()
}
return result
return wrapper
return decorator
@with_idempotency(ttl_seconds=3600)
def track_article_view(
article_id: int,
user_session_id: str,
idempotency_key: str | None = None
) -> ToolResult:
"""
Registra la visualizzazione di un articolo per analytics.
Usa un idempotency_key (UUID) per prevenire duplicazioni da retry.
Esempio: idempotency_key="view-session-abc123-article-201"
"""
# Logica di scrittura su PostgreSQL del backend
_write_view_to_db(article_id, user_session_id)
return ToolResult(
success=True,
data={"article_id": article_id, "tracked": True}
)
In pratica, l'agent deve generare il token una volta sola quando pianifica l'azione, e riusarlo per ogni retry della stessa azione logica. Nel ml-service del portfolio, la convenzione è "{operation}-{session_id}-{target_id}" come stringa deterministicamente costruita dall'agent stesso.
Sezione 6 - Retry-on-validation: auto-correzione con guard max_retries
Un agent ben costruito non si arrende al primo errore di validazione: capisce l'errore, corregge il parametro, e riprova. Ma senza un guard esplicito, questo meccanismo di auto-correzione diventa un loop infinito. Abbiamo visto in EP04 sui loop agent che il pattern di retry cieco è uno dei sei failure mode più comuni. La soluzione è un sistema di retry consapevole dei tentativi.
# retry_on_validation.py - Sistema di retry con max_retries e backoff
# Pattern applicato al loop ReAct del ml-service portfolio
from dataclasses import dataclass, field
from typing import Callable, Any
@dataclass
class RetryContext:
"""Traccia i tentativi per tool call all'interno di un agent step."""
max_retries: int = 3
attempts: dict[str, int] = field(default_factory=dict)
def can_retry(self, tool_name: str) -> bool:
"""Verifica se il tool ha ancora tentativi disponibili."""
return self.attempts.get(tool_name, 0) < self.max_retries
def record_attempt(self, tool_name: str) -> None:
"""Registra un tentativo per il tool specificato."""
self.attempts[tool_name] = self.attempts.get(tool_name, 0) + 1
def attempts_left(self, tool_name: str) -> int:
"""Restituisce i tentativi rimasti per il tool."""
return self.max_retries - self.attempts.get(tool_name, 0)
def execute_tool_with_retry(
tool_fn: Callable,
tool_name: str,
params: dict,
retry_ctx: RetryContext
) -> ToolResult:
"""
Esegue un tool con supporto retry-on-validation.
Aggiorna retry_ctx e restituisce sempre un ToolResult.
"""
if not retry_ctx.can_retry(tool_name):
# Limite raggiunto: termina con errore definitivo
return ToolResult(
success=False,
error_code=ErrorCode.VALIDATION_ERROR,
error_message=f"Superato il limite di {retry_ctx.max_retries} tentativi per '{tool_name}'.",
suggestion=(
"Interrompi il tentativo su questo tool e informa l'utente "
"che non è stato possibile completare l'operazione con i parametri forniti."
),
retry_allowed=False
)
retry_ctx.record_attempt(tool_name)
result = tool_fn(**params)
if not result.success and result.error_code == ErrorCode.VALIDATION_ERROR and result.retry_allowed:
# Errore correggibile: il loop agent userà result.suggestion per correggere
remaining = retry_ctx.attempts_left(tool_name)
result.error_message = (
f"{result.error_message} "
f"[Tentativi rimasti: {remaining}]"
)
return result
Il RetryContext vive per un singolo step dell'agent, non per l'intera sessione. Questo è importante: un tool che fallisce 3 volte su uno step non preclude il suo uso su step successivi con parametri diversi. Il counter di retry è per coppia (step, tool_name), non per (sessione, tool_name).
Sezione 7 - Tool composition e tool catalog: il registry YAML del ml-service
Un agent con 12 tool ha un problema di organizzazione: il modello riceve tutti gli schemi nel contesto, anche quelli non rilevanti per il task corrente. Con tool composition, strumenti semplici diventano building block di operatori più complessi. Con un registry centralizzato YAML, il team mantiene visibilità su cosa è disponibile, chi lo usa, e quali sono i contratti.
# tool_composition.py - Orchestratore che compone tool semplici in task complessi
# Pattern usato nel ml-service per la funzionalità "articolo del giorno"
from typing import NamedTuple
class ArticleSuggestionResult(NamedTuple):
"""Risultato composto da multiple tool call."""
articles: list[dict]
explanation: str
total_tokens_used: int
def suggest_daily_reading(
user_context: str,
session_id: str,
retry_ctx: RetryContext
) -> ToolResult:
"""
Orchestratore: compone search + filter + track per la raccomandazione giornaliera.
Tool di alto livello che l'agent chiama per task complessi.
"""
# Step 1: cerca articoli rilevanti
search_result = execute_tool_with_retry(
tool_fn=search_articles_by_semantic_query,
tool_name="search_articles_by_semantic_query",
params={"query": user_context, "max_results": 10, "language": "it"},
retry_ctx=retry_ctx
)
if not search_result.success:
return search_result # propaga il ToolResult con errore strutturato
# Step 2: filtra quelli già letti in sessione
candidates = search_result.data
filter_result = execute_tool_with_retry(
tool_fn=filter_unread_articles,
tool_name="filter_unread_articles",
params={"article_ids": [a["id"] for a in candidates], "session_id": session_id},
retry_ctx=retry_ctx
)
if not filter_result.success:
return filter_result
unread = filter_result.data[:3] # massimo 3 suggerimenti
# Step 3: registra la visualizzazione (con idempotency)
for article in unread:
idem_key = f"suggest-{session_id}-{article['id']}"
execute_tool_with_retry(
tool_fn=track_article_view,
tool_name="track_article_view",
params={
"article_id": article["id"],
"user_session_id": session_id,
"idempotency_key": idem_key
},
retry_ctx=retry_ctx
)
return ToolResult(success=True, data={"articles": unread, "count": len(unread)})
L'orchestratore è esso stesso un tool registrato nel catalog. L'agent può scegliere di chiamare direttamente i tool atomici o delegare all'orchestratore per task composti. Questa doppia esposizione - atomico e composto - offre flessibilità al modello senza costringerlo a costruire l'orchestrazione da zero.
Il registry YAML centralizza la governance dei tool. Nel ml-service del portfolio, questo file è la fonte di verità per documentazione, testing e monitoring:
# tool_catalog.yaml - Registry centralizzato per il chat agent del ml-service portfolio
# 12 strumenti per l'agent conversazionale su federicocalo.dev
version: "1.0"
agent: chat-assistant
last_updated: "2026-05-01"
tools:
# ============================================================
# CATEGORIA: Contenuti e Ricerca
# ============================================================
- name: search_articles_by_semantic_query
category: content
version: "2.1"
has_side_effects: false
idempotency_required: false
max_retries: 3
dependencies:
- ollama-embeddings
- redis-vector-store
fallback_tool: search_articles_by_keyword
description: >
Ricerca semantica nel corpus degli articoli del portfolio.
Usa embedding all-MiniLM-L6-v2 + HNSW su RediSearch.
owner: ml-service
sla_ms: 800
- name: search_articles_by_keyword
category: content
version: "1.3"
has_side_effects: false
idempotency_required: false
max_retries: 2
dependencies:
- postgresql
fallback_tool: null
description: >
Ricerca full-text su titolo e tag degli articoli. Fallback
per search_articles_by_semantic_query se Ollama non disponibile.
owner: backend-api
sla_ms: 200
- name: recommend_articles_for_user
category: content
version: "1.0"
has_side_effects: false
idempotency_required: false
max_retries: 2
dependencies:
- ollama-embeddings
- redis-vector-store
- postgresql
fallback_tool: search_articles_by_keyword
description: >
Raccomanda articoli basandosi sul contesto utente e sugli articoli
già letti nella sessione corrente.
owner: ml-service
sla_ms: 1200
# ============================================================
# CATEGORIA: Analytics e Tracking (con side effects)
# ============================================================
- name: track_article_view
category: analytics
version: "1.1"
has_side_effects: true
idempotency_required: true
max_retries: 1
dependencies:
- postgresql
fallback_tool: null
description: >
Registra visualizzazione articolo per analytics. SEMPRE passare
idempotency_key per prevenire duplicazioni da retry.
owner: backend-api
sla_ms: 100
- name: suggest_daily_reading
category: content-composite
version: "1.0"
has_side_effects: true
idempotency_required: false
max_retries: 1
dependencies:
- search_articles_by_semantic_query
- filter_unread_articles
- track_article_view
fallback_tool: null
description: >
Orchestratore: compone search + filter + track per la raccomandazione
giornaliera. Preferisci questo tool per task di tipo "suggerisci letture".
owner: ml-service
sla_ms: 2500
Il campo fallback_tool nel registry è direttamente il nome del tool che l'agent deve usare in caso di DEPENDENCY_UNAVAILABLE. Il campo has_side_effects segnala al sistema di monitoring quali tool richiedono tracking degli idempotency token. Il campo sla_ms è usato per configurare i timeout nel loop agent.
FAQ
Perché il nome del tool è così importante per un LLM agent?
Il nome è la prima informazione che l'agent processa per decidere quale strumento invocare. Un nome ambiguo come get_data obbliga l'LLM a leggere tutta la description per capire il contesto. Un nome come list_active_customers_by_region è auto-documentante: il modello capisce intent, scope e parametri attesi prima ancora di leggere lo schema. Test empirici mostrano che naming verb_noun specifico riduce le chiamate errate del 40-60%.
Quando usare enum invece di string in uno schema tool?
Usa enum ogni volta che il parametro ha un insieme finito e noto di valori validi. Se il tuo tool accetta un status con valori active, inactive, pending, definirlo come stringa libera lascia all'LLM la libertà di passare enabled, disabled, waiting - tutti semanticamente simili ma che causano errori di validazione. Con enum in JSON Schema, la probabilità di errore crolla sotto il 2%.
Cos'è il pattern error-as-data e perché è superiore alle eccezioni?
Error-as-data significa restituire sempre un oggetto strutturato dal tool, anche in caso di errore, invece di lanciare eccezioni. La struttura include error_code, message, suggestion (cosa dovrebbe fare l'agent per correggere) e retry_allowed. Il campo suggestion permette all'agent di auto-correggersi senza ulteriori round-trip al modello.
Come funziona l'idempotency token nei tool agent?
Un idempotency token è un UUID che il chiamante include nella richiesta. Il tool lato server traccia i token già processati in una cache con TTL. Se arriva una seconda richiesta con lo stesso token, il server restituisce il risultato della prima senza rieseguire l'operazione. Per agent su tool con side effects come scritture, email, o tracking analytics è fondamentale per prevenire duplicazioni da retry.
Quanti tool dovrebbe avere un agent per funzionare bene?
Oltre 15-20 tool, le performance dell'LLM nel tool selection degradano significativamente. Per contesti complessi con molti strumenti, considera un pattern di tool routing: un tool di lookup che restituisce i 3-5 tool più rilevanti per la query corrente, riducendo il contesto che l'LLM deve processare in ogni step.
Qual è la differenza tra tool description e tool docstring?
La docstring Python è per il developer: spiega l'implementazione interna. La tool description è per l'LLM: deve spiegare quando usare questo tool, cosa fa esattamente, esempi di input/output, e edge case importanti. Sono testi diversi per audience diversi. Una buona tool description inizia con il caso d'uso principale e specifica cosa il tool NON fa per evitare usi impropri.
Come implementare retry-on-validation senza loop infiniti?
Intercetta l'errore di validazione Pydantic, costruisci un messaggio strutturato con il campo errato, il valore ricevuto e il formato atteso, passalo come tool result all'agent. Aggiungi un contatore max_retries (tipicamente 2-3) per ogni tool call nello stesso step. Se supera il limite, restituisce un errore definitivo con escalation_required: true. Questo permette auto-correzione per errori comuni senza loop infiniti.
Conclusione
Il tool design è l'interfaccia tra la logica dell'agent e il mondo esterno. Ogni scelta - dal nome alle virgolette nel campo description del Field Pydantic - influenza direttamente quante iterazioni sprecate l'agent fa prima di completare il task. Le sette tecniche descritte in questo episodio non richiedono modelli più grandi o prompt più lunghi: richiedono disciplina progettuale applicata all'interfaccia tool.
Applicando naming verb_noun specifico, schema con enum stretti, error-as-data con suggestion field e idempotency token sui tool con side effects, il ml-service del portfolio è passato da un agent con error rate del 35% sulle prime implementazioni naïve a meno del 15% - con lo stesso modello LLM sottostante (Ollama llama3.1:8b on-prem). Il miglioramento non viene dall'LLM: viene dall'interfaccia che gli dai.
Prossimo episodio: EP06 - Multi-agent: coordinator vs orchestrator pattern. Quando un singolo agent non basta e come strutturare la comunicazione tra agent specializzati senza che il contesto esploda.

Commenti
Caricamento commenti...
Accedi per commentare