🧙 Maestro Yoda Cap. 22 · Agenti, tool use, RAG

Puntata 236

Puntata 236 — Tool use / function calling

Livello: 🧙 Maestro Yoda · Capitolo 22 · Agenti, tool use, RAG

Senza tool, un LLM è un letterato chiuso in una stanza che ha letto internet fino a dicembre. Con i tool, è un letterato chiuso in una stanza che può chiamare il barista, controllare il meteo, eseguire codice. La differenza è qualitativa, non quantitativa: nasce qui l’agente.

un cervello LLM in una stanza con tante porte etichettate “search()”, “calculate()”, “send_email()”, “run_python()”, “read_db()”. Sopra ogni porta una piccola luce verde che si accende quando viene “aperta”. Sotto: “una funzione, una porta — l’LLM decide quale aprire e quando”

Cos’è il “tool use”

Tool use (o function calling, o action) è il pattern in cui un LLM:

  1. Riceve un prompt + una lista di funzioni disponibili (con nome, descrizione, schema parametri).
  2. Decide se rispondere direttamente o chiamare una funzione.
  3. Se chiama una funzione: emette in output un JSON strutturato {name, arguments}.
  4. Il runtime esegue la funzione e re-inietta il risultato nel context.
  5. L’LLM continua: rispondi all’utente o chiama altre funzioni.

Sembra banale. La rivoluzione è il passaggio 2-3: l’LLM emette output strutturato sufficientemente affidabile da poter essere passato a un parser → executor → sistema esterno.

OpenAI ha introdotto la function calling API a giugno 2023 (GPT-4-0613). Anthropic, Google, Mistral, tutti gli altri provider sono seguiti entro 6 mesi. Modelli open: Llama 3.1+, Qwen 2.5+, Mistral, DeepSeek tutti nativamente.


L’anatomia di una tool definition

{
  "name": "get_weather",
  "description": "Recupera le condizioni meteo correnti per una località. Usare quando l'utente chiede meteo, temperatura, pioggia, neve per una città specifica.",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "Nome della città (es. 'Roma', 'Milano')."
      },
      "units": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "default": "celsius"
      }
    },
    "required": ["city"]
  }
}

Lo schema usa JSON Schema. Tre pezzi critici:

  • name: identifier univoco, snake_case.
  • description: la cosa più importante. È prompt engineering: l’LLM la usa per decidere quando chiamare il tool. Bad description = tool mai usato o usato male.
  • parameters: schema dei parametri, validato lato runtime.

Il ciclo completo (esempio)

from openai import OpenAI

client = OpenAI()
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "...",
            "parameters": {...}
        }
    }
]

messages = [{"role": "user", "content": "Che tempo fa a Bologna?"}]

# Step 1: l'LLM decide
response = client.chat.completions.create(
    model="gpt-5",
    messages=messages,
    tools=tools,
)
msg = response.choices[0].message

# Step 2: vediamo se ha chiamato un tool
if msg.tool_calls:
    for tc in msg.tool_calls:
        name = tc.function.name
        args = json.loads(tc.function.arguments)
        # Step 3: il runtime esegue
        result = my_weather_api(args["city"], args.get("units", "celsius"))
        messages.append({"role": "assistant", "tool_calls": msg.tool_calls})
        messages.append({
            "role": "tool",
            "tool_call_id": tc.id,
            "content": json.dumps(result)
        })

    # Step 4: l'LLM continua con il risultato
    response2 = client.chat.completions.create(
        model="gpt-5",
        messages=messages,
        tools=tools,
    )
    print(response2.choices[0].message.content)
else:
    print(msg.content)

Stesso pattern in:

  • Anthropic Claude: parameter tools=[...], response con content_block.type == "tool_use".
  • Gemini: tools=[...] con FunctionDeclaration.
  • Open source via vLLM/Ollama: spesso OpenAI-compatible nativamente.

Tool calling sotto il cofano

L’LLM non “chiama davvero” la funzione. Quello che succede in concreto:

  1. Il system prompt include le tool definitions (formattate come testo, spesso XML o JSON).
  2. Il modello, addestrato su milioni di esempi di tool use, impara a:
    • Riconoscere quando un tool è utile.
    • Emettere un blocco strutturato (es. <tool_use><name>...</name><args>...</args></tool_use>).
  3. Il runtime parsa quel blocco, estrae name/args, valida lo schema.
  4. Esegue la funzione tu (codice tuo), passa il risultato.
  5. Re-inietta come messaggio “tool result”.
  6. Il modello prosegue.

Differenze tra provider:

  • OpenAI: emette JSON in un campo tool_calls separato.
  • Anthropic Claude: blocchi tool_use in content array.
  • Llama 3.1+: tag XML <|python_tag|> o JSON inline.

Più formati = più fragility. Ecco perché framework come LangChain / LangGraph / smolagents astrarono questa diversità.


Parallel tool calling

GPT-5, Claude 4.x, Gemini 2.5: tutti supportano parallel tool calls. In una singola risposta, il modello può emettere N chiamate tool contemporanee.

Es. “Compara il meteo di Roma e Milano”:

[
  {"name": "get_weather", "args": {"city": "Roma"}},
  {"name": "get_weather", "args": {"city": "Milano"}}
]

Il runtime esegue entrambe in parallelo, restituisce 2 risultati, l’LLM compone la risposta. Riduce latency e numero round-trip.


Structured output (gemello del tool use)

Pattern simile ma scopo diverso: vuoi forzare l’output a uno schema specifico (es. JSON conforme).

OpenAI ha response_format con JSON Schema:

response = client.chat.completions.create(
    model="gpt-5",
    messages=messages,
    response_format={"type": "json_schema", "json_schema": {...}}
)

Differenze dal tool use:

  • Tool use: il modello sceglie se chiamare e cosa chiamare.
  • Structured output: il modello DEVE rispondere con questo schema, e basta.

Use case: estrazione entità, classificazione, JSON pulito per pipeline downstream.


Tool design: l’arte invisibile

Il successo o fallimento di un agente dipende al 70% dal design dei tool, non dal modello.

Regole pratiche per buoni tool

  1. Granularità giusta: né troppo fine (read_byte, set_pixel), né troppo grossa (run_entire_workflow). Sweet spot: una azione naming-able in 2-4 parole.
  2. Naming chiaro: search_internal_kb > search. send_slack_message_to_user > notify.
  3. Descrizione che include il “quando”: “Use this when…” è più efficace di “This does…”.
  4. Schema strict: required parameters chiari, enum dove possibile, descrizioni anche dei singoli param.
  5. Idempotency dove possibile: chiamare due volte non fa danno. Riduce errori di retry.
  6. Output strutturato: il tool restituisce JSON o testo prevedibile, non HTML rumoroso.
  7. Error messages utili: se fallisce, dì perché — l’LLM può adattare la prossima call.
  8. Numero contenuto: 5-15 tool per agente. Sopra i 30 il modello si confonde.

Anti-pattern comuni

  • Tool con 15 parametri opzionali → confusione.
  • Due tool sovrapposti (search e query) → l’LLM ne sceglie uno a caso.
  • Output enorme (10k token) → context exhaustion.
  • Side effects nascosti → l’LLM non sa quando li sta innescando.
  • Naming generic (do_thing, process_data) → l’LLM non sa quando usare.

Sicurezza tool use

Ogni tool è una superficie di attacco:

  • Prompt injection (puntata 247) può manipolare il modello a chiamare tool malevoli.
  • Tool action irreversibili (delete, send) richiedono human-in-the-loop.
  • Tool con credenziali non devono mai esporre i secret nel context.
  • Network tools devono avere allowlist di domini, non “fai qualunque request”.

Pattern di mitigazione:

  • Permission scopes: ogni tool ha permission esplicita (read-only vs write).
  • Confirmation step: per azioni distruttive, richiedi conferma utente prima di eseguire.
  • Dry-run mode: l’agente prepara la chiamata, mostra cosa farebbe, l’utente approva.
  • Sandboxing: tool che eseguono codice (Python REPL) in container/firecracker, no rete, no filesystem host.

Tool use in agenti complessi

Quando un agente ha 10+ tool, emerge il tool selection problem. Il modello deve scegliere il giusto fra molti.

Pattern moderni:

  • Tool retrieval: prima di ogni step, fai retrieval (embedding-based) dei top-5 tool rilevanti al goal corrente. Inietta solo quelli, non tutti.
  • Hierarchical tools: tool di “scoperta” (list_available_tools(category)) che il modello chiama per espandere il toolset on-demand.
  • Tool grouping: organizza per dominio (file, web, db) e usa categorical selection.

Funzioni vs API vs MCP

Tre modi di esporre tool:

ApproccioProContro
Funzioni in-processVeloce, type-safeTied to runtime tuo
REST APILinguaggio-agnosticoDefinizione manuale OpenAPI ↔ JSON Schema
MCP servers (puntata 242)Standardizzato, riusabileSetup overhead, ecosystem giovane

Pattern 2026: tool interni all’app come funzioni; tool esterni/condivisi via MCP.


Glossario lampo

  • Function calling / tool use — pattern in cui l’LLM emette JSON strutturato che il runtime esegue come funzione.
  • JSON Schema — standard di descrizione di strutture dati, usato per definire parameters di tool.
  • Parallel tool calling — capacità del modello di emettere N tool call in una singola risposta, eseguibili in parallelo.
  • Structured output — pattern complementare: forza la risposta a uno schema, senza tool.
  • Tool selection problem — difficoltà del modello a scegliere il giusto tra molti tool simili. Mitigata da tool retrieval.

Take-away

Tool use è la singola feature che ha trasformato gli LLM da “macchine di testo” in “agenti software”. Il meccanismo è semplice: tu descrivi le funzioni, il modello le chiama. Il difficile è il design: granularità giusta, naming chiaro, descrizioni che spiegano il quando. Più tempo passi su questo, meno problemi avrai con l’agente. Più tempo passi sul framework, meno cambierai i risultati.


➡️ Prossima puntata: ReAct — il pattern reasoning + acting che fa funzionare i loop agentici.