Skip to main content
Un simple appel de fonction est facile. L’intéressant, c’est la boucle qui l’entoure, car un modèle obtient rarement ce dont il a besoin dès le premier appel. Il consulte quelque chose, examine le résultat, et décide ce qu’il va demander ensuite. Ce tutoriel construit un agent en ligne de commande qui répond à des questions sur une base SQLite qu’il n’a jamais vue. Il ne dispose d’aucun schéma dans son prompt. Il reçoit trois outils en lecture seule et se débrouille pour le reste :
Chemin faisant, nous allons :
  1. Donner au modèle une base de données et trois outils pour la lire
  2. Décrire ces outils afin que le modèle sache quand recourir à chacun
  3. Exécuter la boucle qui transforme les appels d’outils en résultats d’outils
  4. Le regarder demander plusieurs outils à la fois
  5. Renvoyer les erreurs au modèle plutôt que de les lever
  6. Tracer la frontière entre ce que le modèle refuse de faire et ce qu’il ne peut pas faire
Le guide Appel de fonctions couvre la forme de la requête isolément. Cette page traite de ce qui se passe après le retour de la première réponse.

Préparation

Vous avez besoin de Python 3.9 ou plus récent, du paquet requests, et d’une clé d’API Venice. Consultez Générer une clé d’API si vous n’en avez pas. Tout le reste se trouve dans la bibliothèque standard.
Créez agent.py avec les imports et l’en-tête que chaque appel réutilise :
Tous les modèles ne peuvent pas appeler des outils, et les identifiants de modèle changent, alors demandez à l’API lequel utiliser plutôt que de fixer un nom qui vieillira :
GET /models/traits associe des noms de traits stables au modèle qui remplit actuellement ce rôle. Lire function_calling_default au démarrage fait que votre agent continue de fonctionner lorsque le modèle sous-jacent est remplacé. Consultez Modèles pour la liste complète des traits.

1. Une base de données qui vaut la peine d’être interrogée

N’importe quel fichier SQLite fera l’affaire. Celui-ci est une petite boutique avec des clients, des produits, et les commandes qui les relient, ce qui suffit pour qu’une vraie question nécessite une jointure et une agrégation :

2. Trois outils à la portée du modèle

Les outils reflètent la façon dont une personne aborde une base de données inconnue : découvrir ce qu’elle contient, examiner de près une table, puis la requêter.
Chacun d’eux renvoie une chaîne JSON, y compris en cas d’échec. C’est délibéré, et la section 5 explique pourquoi. Décrivez-les maintenant pour le modèle. La description n’est pas un commentaire. C’est la seule chose que lit le modèle lorsqu’il décide quel outil appeler et ce qu’il faut y mettre :

3. La boucle

L’appel de fonctions est une conversation, pas une requête. Le modèle répond par des appels d’outils, vous les exécutez, vous ajoutez les résultats, puis vous redemandez. Cela se termine lorsque le modèle répond avec du contenu au lieu d’appels.
Trois détails de cette boucle comptent plus qu’il n’y paraît. Le message de l’assistant non modifié est réinjecté dans messages avant les résultats. Il porte les tool_calls auxquels les résultats répondent, et sur un modèle de raisonnement il porte aussi un champ reasoning_content. Reconstruire le message à la main en supprimant des champs auxquels vous ne vous attendiez pas est la façon la plus courante de casser le second tour. Chaque résultat est apparié à son appel via tool_call_id. Rien d’autre ne l’identifie. max_rounds est une vraie limite, pas une formalité. Un modèle qui continue de requêter sans conclure bouclera sinon jusqu’à épuisement de votre patience ou de vos crédits.
Les appels d’outils portent aussi un champ index, et il est tentant de s’en servir pour aligner les résultats sur les appels. Ne le faites pas. Lorsque le modèle demande trois outils en même temps, les trois peuvent arriver avec le même index, car il numérote le tour de l’assistant et non l’appel à l’intérieur de celui-ci. Seul id est unique.

4. Ce que cela fait réellement

Branchez un bloc principal et lancez-le :
Les appels d’outils s’affichent sur stderr au fil de leur exécution, afin que vous puissiez le regarder travailler :
Cela a pris cinq tours. Leur forme mérite d’être lue attentivement, car c’est tout l’argument en faveur de la boucle : Le tour 4 est la partie qu’un appel de fonction unique ne peut pas faire. Le modèle ne pouvait pas écrire cette requête tant qu’il n’avait pas vu la réponse à la précédente. Votre exécution ne correspondra pas appel pour appel à celle-ci. Le modèle décrit parfois les trois tables en une seule fois et parfois une par une, et il lui arrive de sauter list_tables et de deviner un nom. Les chiffres sont stables parce qu’ils proviennent de la base de données ; le chemin pour y arriver ne l’est pas.
Le tour 2 a renvoyé trois appels d’outils dans une seule réponse, et la boucle ci-dessus les exécute l’un après l’autre. Ils sont indépendants, donc un ThreadPoolExecutor ici vaut la peine dès que vos outils font de vraies E/S. Conservez les messages tool dans le même ordre que les appels qui les ont produits.
Chaque tour renvoie toute la conversation, donc le prompt grossit à mesure que l’agent travaille. Venice met en cache automatiquement le préfixe stable, et le bloc usage montre que cela paie :
Au dernier tour, 960 jetons de prompt sur 1020 étaient servis depuis le cache. Mise en cache des prompts explique comment garder ce préfixe stable.

5. Laissez les erreurs remonter jusqu’au modèle

L’instinct est de lever une exception sur une mauvaise requête. Résistez-y. Une erreur est une information, et le modèle peut agir dessus. Demandez une table qui n’existe pas :
La première requête a échoué. Parce que run_query a renvoyé {"error": "OperationalError: no such table: purchases"} comme un résultat d’outil ordinaire au lieu de lever une exception, le modèle l’a lu, a appelé list_tables pour découvrir ce qui existait bel et bien, et s’est corrigé. Si l’exception s’était propagée, le script serait mort sur une faute de frappe. C’est pourquoi chaque outil renvoie du JSON, y compris sur le chemin d’échec. La règle est simple : si une personne qui débogue votre outil voudrait voir le message, le modèle aussi.

6. Ce qu’il refuse de faire, et ce qu’il ne peut pas faire

Demandez à l’agent de détruire quelque chose :
Exécutez cela deux fois et vous pourriez obtenir deux comportements différents. Une fois, il a décliné avant même de toucher à un outil :
Une autre fois, il est allé regarder d’abord, a exécuté un SELECT pour les clients espagnols, n’en a trouvé aucun parce que la colonne stocke ES et non Spain, et a signalé cela à la place :
Les deux sont raisonnables. Aucun n’est un contrôle de sécurité. Le modèle a lu le mot « read-only » dans la description d’un outil et a choisi de le respecter, et un modèle différent, une conversation plus longue, ou un utilisateur plus insistant peut produire un choix différent. Le garde-fou à l’intérieur de run_query est la partie qui ne dépend pas d’un choix :
Rédigez la description pour que le modèle essaie rarement. Rédigez le garde-fou pour que cela n’ait pas d’importance quand il essaie.
Cette deuxième ligne est la raison pour laquelle run_query attrape sqlite3.Warning en plus de sqlite3.Error. Le pilote Python refuse les instructions empilées, mais il lève Warning pour cela, et Warning n’est pas une sous-classe de Error. N’attraper que sqlite3.Error laisse une instruction empilée s’échapper du gestionnaire et tuer la boucle au lieu de renvoyer un message que le modèle peut lire.
Une vérification de préfixe bloque les écritures, mais elle ne dit rien sur les lectures. Tout SELECT que le modèle écrit peut atteindre chaque table du fichier, y compris celles que vous n’aviez jamais l’intention d’exposer. Deux changements valent la peine d’être faits avant que cela ne touche à de vraies données : ouvrir la base en lecture seule avec sqlite3.connect("file:shop.db?mode=ro", uri=True), qui fait échouer les écritures avec attempt to write a readonly database quoi que la vérification de chaîne laisse passer, et pointer l’agent vers une base de données ou un ensemble de vues ne contenant que les colonnes qu’il est autorisé à voir.

Contrôler quand les outils sont utilisés

tool_choice décide de la latitude laissée au modèle : "required" est plus brutal qu’il n’y paraît. Demander à cet agent What is 2 + 2? avec tool_choice réglé sur "required" le fait appeler list_tables, examiner une base dont il n’a que faire, puis répondre 4 au tour suivant. Avec "auto", il répond 4 immédiatement et n’appelle rien. Recourez à "required" quand un outil doit véritablement s’exécuter, par exemple pour journaliser une requête, et laissez-le tranquille sinon.

Régler l’agent

Étapes suivantes

La boucle que vous avez maintenant est la même que celle derrière la plupart des agents. Seuls les outils changent.

Appel de fonctions

Référence pour le tableau tools et tool_choice.

Réponses structurées

Contraindre la réponse finale à un schéma JSON.

Mise en cache des prompts

Garder la conversation qui grossit peu coûteuse.

Agent de recherche privé

La même boucle avec des outils Web et un planificateur.