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.
Cos’è il “tool use”
Tool use (o function calling, o action) è il pattern in cui un LLM:
- Riceve un prompt + una lista di funzioni disponibili (con nome, descrizione, schema parametri).
- Decide se rispondere direttamente o chiamare una funzione.
- Se chiama una funzione: emette in output un JSON strutturato
{name, arguments}. - Il runtime esegue la funzione e re-inietta il risultato nel context.
- 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 concontent_block.type == "tool_use". - Gemini:
tools=[...]conFunctionDeclaration. - 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:
- Il system prompt include le tool definitions (formattate come testo, spesso XML o JSON).
- 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>).
- Il runtime parsa quel blocco, estrae name/args, valida lo schema.
- Esegue la funzione tu (codice tuo), passa il risultato.
- Re-inietta come messaggio “tool result”.
- Il modello prosegue.
Differenze tra provider:
- OpenAI: emette JSON in un campo
tool_callsseparato. - Anthropic Claude: blocchi
tool_useincontentarray. - 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
- 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. - Naming chiaro:
search_internal_kb>search.send_slack_message_to_user>notify. - Descrizione che include il “quando”: “Use this when…” è più efficace di “This does…”.
- Schema strict: required parameters chiari, enum dove possibile, descrizioni anche dei singoli param.
- Idempotency dove possibile: chiamare due volte non fa danno. Riduce errori di retry.
- Output strutturato: il tool restituisce JSON o testo prevedibile, non HTML rumoroso.
- Error messages utili: se fallisce, dì perché — l’LLM può adattare la prossima call.
- 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 (
searchequery) → 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:
| Approccio | Pro | Contro |
|---|---|---|
| Funzioni in-process | Veloce, type-safe | Tied to runtime tuo |
| REST API | Linguaggio-agnostico | Definizione manuale OpenAPI ↔ JSON Schema |
| MCP servers (puntata 242) | Standardizzato, riusabile | Setup 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.