Skip to main content
Una singola chiamata a funzione è facile. La parte interessante è il ciclo che le sta attorno, perché un modello raramente ottiene ciò di cui ha bisogno alla prima chiamata. Cerca qualcosa, vede il risultato e decide cosa chiedere dopo. Questo tutorial costruisce un agente da riga di comando che risponde a domande su un database SQLite che non ha mai visto. Non ha alcuno schema nel prompt. Riceve tre strumenti in sola lettura e ricava il resto da sé:
Lungo il percorso faremo:
  1. Fornire al modello un database e tre strumenti che lo leggono
  2. Descrivere quegli strumenti in modo che il modello sappia quando usare ciascuno
  3. Eseguire il ciclo che trasforma le chiamate a strumenti in risultati degli strumenti
  4. Osservarlo mentre richiede più strumenti contemporaneamente
  5. Restituire gli errori al modello invece di sollevarli
  6. Tracciare il confine tra ciò che il modello non farà e ciò che non può fare
La guida al Function Calling copre la forma della richiesta di per sé. Questa pagina riguarda ciò che accade dopo che arriva la prima risposta.

Configurazione

Ti servono Python 3.9 o successivo, il pacchetto requests e una chiave API Venice. Consulta Generare una chiave API se non ne hai una. Tutto il resto è nella libreria standard.
Crea agent.py con gli import e il blocco di intestazione che ogni chiamata riutilizza:
Non tutti i modelli sanno chiamare strumenti, e gli ID dei modelli cambiano, quindi chiedi all’API quale usare invece di fissare un nome che invecchierà:
GET /models/traits associa nomi di trait stabili al modello che attualmente ricopre quel ruolo. Leggere function_calling_default all’avvio significa che il tuo agente continua a funzionare quando il modello sottostante viene sostituito. Consulta Modelli per l’elenco completo dei trait.

1. Un database su cui vale la pena fare domande

Va bene qualsiasi file SQLite. Questo è un piccolo negozio con clienti, prodotti e gli ordini che li mettono in relazione, il che è sufficiente affinché una domanda vera richieda un join e un aggregato:

2. Tre strumenti a cui il modello può ricorrere

Gli strumenti riflettono il modo in cui una persona affronta un database sconosciuto: scoprire cosa contiene, guardare da vicino una tabella e poi interrogarla.
Ognuno di essi restituisce una stringa JSON, inclusi i fallimenti. È una scelta deliberata, e la sezione 5 spiega il perché. Ora descrivili al modello. La description non è un commento. È l’unica cosa che il modello legge quando decide quale strumento chiamare e cosa metterci:

3. Il ciclo

Il function calling è una conversazione, non una richiesta. Il modello risponde con chiamate a strumenti, tu le esegui, aggiungi i risultati e chiedi di nuovo. Termina quando il modello risponde con del contenuto invece che con chiamate.
Tre dettagli in quel ciclo contano più di quanto sembri. Il messaggio dell’assistente non modificato torna in messages prima dei risultati. Porta con sé i tool_calls a cui i risultati rispondono, e su un modello di reasoning porta anche un campo reasoning_content. Ricostruire il messaggio a mano e scartare campi che non ti aspettavi è il modo più comune per rompere il secondo round. Ogni risultato è associato alla sua chiamata tramite tool_call_id. Non c’è nient’altro a identificarlo. max_rounds è un limite reale, non una formalità. Un modello che continua a interrogare senza concludere farà altrimenti loop finché non esaurirai la pazienza o il credito.
Le chiamate a strumenti portano anche un campo index, ed è tentante usarlo per allineare i risultati alle chiamate. Non farlo. Quando il modello richiede tre strumenti contemporaneamente, tutti e tre possono arrivare con lo stesso index, perché numera il turno dell’assistente piuttosto che la chiamata al suo interno. Solo id è univoco.

4. Cosa fa davvero

Aggiungi un blocco main ed eseguilo:
Le chiamate agli strumenti vengono stampate su stderr mentre avvengono, così puoi osservarlo lavorare:
Sono serviti cinque round. Vale la pena leggerne attentamente la forma, perché è tutta l’argomentazione a favore del ciclo: Il round 4 è la parte che una singola chiamata a funzione non può fare. Il modello non poteva scrivere quella query finché non avesse visto la risposta a quella precedente. La tua esecuzione non corrisponderà a questa chiamata per chiamata. Il modello a volte descrive tutte e tre le tabelle in una volta e a volte una alla volta, e occasionalmente salta list_tables e indovina un nome. I numeri sono stabili perché provengono dal database; la strada per arrivarci no.
Il round 2 ha restituito tre chiamate agli strumenti in un’unica risposta, e il ciclo qui sopra le esegue una dopo l’altra. Sono indipendenti, quindi un ThreadPoolExecutor qui vale la pena non appena i tuoi strumenti fanno I/O reale. Mantieni i messaggi tool nello stesso ordine delle chiamate che li hanno prodotti.
Ogni round rinvia l’intera conversazione, quindi il prompt cresce mentre l’agente lavora. Venice memorizza automaticamente in cache il prefisso stabile, e il blocco usage mostra che dà i suoi frutti:
All’ultimo round, 960 dei 1020 token di prompt sono stati serviti dalla cache. Prompt Caching spiega come mantenere stabile quel prefisso.

5. Lascia che gli errori raggiungano il modello

L’istinto è sollevare un’eccezione su una query malformata. Resistigli. Un errore è informazione, e il modello può agire di conseguenza. Chiedi di una tabella che non esiste:
La prima query è fallita. Poiché run_query ha restituito {"error": "OperationalError: no such table: purchases"} come un normale risultato dello strumento invece di sollevare un’eccezione, il modello l’ha letto, ha chiamato list_tables per scoprire cosa esistesse davvero e si è corretto. Se l’eccezione si fosse propagata, lo script sarebbe morto su un refuso. È per questo che ogni strumento restituisce JSON anche nel percorso di fallimento. La regola è semplice: se un umano che sta facendo il debug del tuo strumento vorrebbe vedere il messaggio, allora lo vuole vedere anche il modello.

6. Ciò che non farà e ciò che non può fare

Chiedi all’agente di distruggere qualcosa:
Eseguilo due volte e potresti ottenere due comportamenti diversi. Una volta ha rifiutato prima ancora di toccare uno strumento:
Un’altra volta è andato prima a curiosare, ha eseguito una SELECT per i clienti spagnoli, non ne ha trovato nessuno perché la colonna memorizza ES invece di Spain, e ha riportato quello:
Entrambi sono ragionevoli. Nessuno dei due è un controllo di sicurezza. Il modello ha letto la parola “read-only” nella descrizione di uno strumento e ha scelto di rispettarla, e un modello diverso, una conversazione più lunga o un utente più insistente possono produrre una scelta diversa. La guardia dentro run_query è la parte che non dipende da una scelta:
Scrivi la descrizione in modo che il modello raramente ci provi. Scrivi la guardia in modo che non importi quando lo fa.
Quella seconda riga è il motivo per cui run_query cattura sqlite3.Warning insieme a sqlite3.Error. Il driver Python rifiuta le istruzioni impilate, ma solleva Warning per esse, e Warning non è una sottoclasse di Error. Catturare solo sqlite3.Error lascia sfuggire un’istruzione impilata dall’handler e uccide il ciclo invece di restituire un messaggio che il modello può leggere.
Un controllo del prefisso ferma le scritture, ma non dice nulla sulle letture. Qualsiasi SELECT che il modello scrive può raggiungere ogni tabella nel file, incluse quelle che non avevi mai inteso esporre. Vale la pena fare due cambiamenti prima che tocchi dati reali: aprire il database in sola lettura con sqlite3.connect("file:shop.db?mode=ro", uri=True), che fallisce le scritture con attempt to write a readonly database a prescindere da ciò che il controllo della stringa lascia sfuggire, e puntare l’agente a un database o a un insieme di viste contenente solo le colonne che è autorizzato a vedere.

Controllare quando usare gli strumenti

tool_choice decide quanta voce in capitolo abbia il modello: "required" è più brutale di quanto sembri. Chiedere a questo agente What is 2 + 2? con tool_choice impostato su "required" lo fa chiamare list_tables, guardare un database di cui non ha bisogno e poi rispondere 4 al round successivo. Con "auto" risponde 4 immediatamente e non chiama nulla. Ricorri a "required" quando uno strumento deve davvero essere eseguito, ad esempio per registrare una richiesta, e lascialo stare altrimenti.

Regolare l’agente

Prossimi passi

Il ciclo che ora hai è lo stesso che sta dietro alla maggior parte degli agenti. Cambiano solo gli strumenti.

Function Calling

Riferimento per l’array tools e per tool_choice.

Risposte strutturate

Vincola la risposta finale a uno schema JSON.

Prompt Caching

Mantieni economica la conversazione che cresce.

Agente di ricerca privato

Lo stesso ciclo con strumenti web e un pianificatore.