Lesson 3 — Coder la boucle soi-même

call_llm et les schémas d'outils

5 min read

This content is not translated yet. Showing the original version.

call_llm et les schémas d'outils

Le principe : un seul point de contact avec le fournisseur

Tout le reste de l'agent ne doit connaître qu'une fonction : call_llm(messages, tools). Si demain vous changez de modèle ou de fournisseur, vous ne modifiez qu'elle.

flowchart LR
  B["Boucle de l'agent"] --> C["call_llm(messages, tools)"]
  C --> G["Gemini (endpoint compatible OpenAI)"]
  C -.-> O["OpenAI"]
  C -.-> L["Modèle local (Ollama, vLLM...)"]

Nous utilisons le format chat completions popularisé par OpenAI, parce que beaucoup de fournisseurs l'acceptent : Gemini propose un endpoint compatible, de même que la plupart des serveurs de modèles locaux. On le parle avec la bibliothèque standard (urllib), donc zéro dépendance.

agent.py (partie 1)
import json
import os
import urllib.request

BASE_URL = os.environ.get(
    "LLM_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/openai")
MODEL = os.environ.get("LLM_MODEL", "gemini-2.5-flash")


def call_llm(messages, tools):
    """Envoie la conversation + les outils, renvoie le message de l'assistant."""
    payload = {"model": MODEL, "messages": messages}
    if tools:                       # certains fournisseurs refusent "tools": []
        payload["tools"] = tools
    body = json.dumps(payload)
    req = urllib.request.Request(
        f"{BASE_URL}/chat/completions",
        data=body.encode(),
        headers={"Content-Type": "application/json",
                 "Authorization": f"Bearer {os.environ['LLM_API_KEY']}"},
    )
    with urllib.request.urlopen(req, timeout=60) as resp:
        return json.load(resp)["choices"][0]["message"]

Configuration, uniquement par variables d'environnement :

terminal
export LLM_API_KEY="votre-cle"          # jamais dans le code, jamais dans git
# Optionnel, pour changer de fournisseur :
# export LLM_BASE_URL="https://api.openai.com/v1"
# export LLM_MODEL="nom-du-modele"

Les outils : du Python ordinaire

Un outil est une fonction normale. Ici, le catalogue et le stock sont de simples dictionnaires pour que tout tienne dans un fichier ; en vrai, ce serait une base de données ou une API.

agent.py (partie 2)
CATALOGUE = {"P1": ("Café en grains 1 kg", 18.0), "P2": ("Thé vert 200 g", 9.5)}
STOCK = {"P1": 12, "P2": 0}


def chercher_produit(texte: str) -> list:
    t = texte.lower()
    return [{"ref": r, "nom": n, "prix": p}
            for r, (n, p) in CATALOGUE.items() if t in n.lower()]


def consulter_stock(ref: str) -> dict:
    if ref not in STOCK:
        raise ValueError(f"Référence inconnue : {ref}")
    return {"ref": ref, "quantite": STOCK[ref]}

Les schémas : ce que le modèle voit

Le modèle ne lit pas votre code. Il lit uniquement le nom, la description et le schéma des paramètres. Puis on garde une table nom → fonction pour exécuter ce qu'il demande.

agent.py (partie 3)
TOOLS_PY = {"chercher_produit": chercher_produit,
            "consulter_stock": consulter_stock}

TOOLS = [
    {"type": "function", "function": {
        "name": "chercher_produit",
        "description": "Cherche des produits du catalogue par mot-clé.",
        "parameters": {"type": "object",
                       "properties": {"texte": {"type": "string"}},
                       "required": ["texte"]}}},
    {"type": "function", "function": {
        "name": "consulter_stock",
        "description": "Donne la quantité en stock d'une référence (ex. P1).",
        "parameters": {"type": "object",
                       "properties": {"ref": {"type": "string"}},
                       "required": ["ref"]}}},
]
Élément du schémaRôleBonne pratique
nameidentifiant que le modèle renverraverbe + objet, en snake_case : consulter_stock
descriptionle prompt de l'outildire quand l'utiliser, donner un exemple de valeur
propertiesparamètres et leurs typestypes simples (string, integer, boolean), enum quand la liste est fermée
requiredparamètres obligatoirestout ce sans quoi la fonction ne marche pas

Vous renommez le paramètre Python ref en reference dans consulter_stock, sans toucher au schéma. Que se passe-t-il ?

Quelle description d'outil aidera le plus le modèle à bien l'utiliser ?

Exercice : ajouter un outil prix_total

Écrivez la fonction prix_total(ref: str, quantite: int) -> dict qui renvoie {"ref", "quantite", "total"} à partir du catalogue, puis son schéma, et ajoutez-la à TOOLS_PY et TOOLS. La quantité doit être un entier strictement positif.

Voir la solution
agent.py (ajout)
def prix_total(ref: str, quantite: int) -> dict:
    if ref not in CATALOGUE:
        raise ValueError(f"Référence inconnue : {ref}")
    if quantite < 1:
        raise ValueError("La quantité doit être au moins 1")
    return {"ref": ref, "quantite": quantite,
            "total": round(CATALOGUE[ref][1] * quantite, 2)}


TOOLS_PY["prix_total"] = prix_total
TOOLS.append({"type": "function", "function": {
    "name": "prix_total",
    "description": "Calcule le prix total en euros pour une référence et une quantité. "
                   "Toujours l'utiliser plutôt que de calculer soi-même.",
    "parameters": {"type": "object",
                   "properties": {"ref": {"type": "string"},
                                  "quantite": {"type": "integer", "minimum": 1}},
                   "required": ["ref", "quantite"]}}})

Deux points importants :

  • la validation (quantite < 1) est dans le code, même si le schéma dit minimum: 1 : le schéma guide, le code garantit ;
  • la description dit explicitement de ne pas calculer soi-même. Un calcul fait par un LLM peut être faux ; un calcul fait par Python ne l'est pas.

Je sais faire

  • Écrire une fonction call_llm qui lit sa clé dans une variable d'environnement
  • Changer de fournisseur sans toucher au reste du code
  • Décrire un outil en JSON Schema avec une description utile
  • Valider les arguments dans le code, pas seulement dans le schéma
Next