Salta al contenuto principale

Tool design per agent: naming, schema, error handling che riducono errori del 60%

Tool design per agent: naming, schema, error handling che riducono errori del 60%: Tool design per agent: naming, schema, error handling che riducono...

Cosa ne pensi di questo articolo?

Tool design per agent: naming, schema, error handling che riducono errori del 60%
AI EngineeringFoto:Tara WinsteadPexels

In Breve

Tool design per agent: naming, schema, error handling che riducono errori del 60%: Tool design per agent: naming, schema, error handling che riducono...

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.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti