Valider les appels d’outils sélectionnés par le modèle

ShellBeginner
Pratiquer maintenant

Introduction

Un modèle d’IA peut répondre en texte libre, mais une application a parfois besoin d’informations structurées avant de pouvoir effectuer une tâche utile. Les appels d’outils permettent à l’application de décrire une opération, par exemple rechercher un article dans un catalogue, puis au modèle de proposer un nom d’outil et des arguments. Le modèle n’est pas autorisé à exécuter du code arbitraire. Il produit des données que votre Worker doit traiter comme des entrées non fiables.

Dans ce laboratoire, vous allez créer POST /catalog-help. Un modèle Llama hébergé par Cloudflare reçoit une question courte telle que « Le SKU KB-101 est-il en stock ? » et peut proposer l’outil en lecture seule lookup_catalog_item. Votre Worker accepte exactement un outil connu, valide un objet d’arguments exactement conforme à { sku }, puis lit un petit catalogue synthétique. Les outils inconnus, les champs manquants ou supplémentaires, les SKU mal formés et les appels multiples n’atteignent jamais l’exécuteur.

Vous utiliserez les appels de fonction traditionnels afin que la limite de sécurité reste visible : l’inférence propose, la validation décide et le code de l’application exécute. Le résultat renvoyé est limité à quelques champs publics de la fixture. Cet exercice n’accorde aucun accès en écriture, n’appelle aucun service externe et ne permet pas au modèle de choisir du code exécutable.

Il s’agit du cinquième laboratoire du cours. Si vous êtes arrivé directement ici, terminez d’abord Connect LabEx to Your Cloudflare Account afin d’apprendre à utiliser le terminal de la VM, à autoriser Wrangler, à confirmer votre compte d’apprentissage et à configurer son ID de compte.

Le modèle sélectionné, @cf/meta/llama-3.3-70b-instruct-fp8-fast, prend en charge les appels de fonction et est disponible via l’allocation Workers AI standard. Workers Free inclut actuellement 10 000 Neurons par jour. Ce laboratoire n’envoie qu’une courte requête réelle en local et une autre après le déploiement ; Workers Paid n’est donc pas nécessaire tant que l’allocation gratuite reste disponible. L’inférence locale atteint tout de même Cloudflare et consomme l’usage du compte ; arrêtez-vous au lieu de réessayer constamment si le modèle ou l’allocation est indisponible.

La configuration installe Node.js 22.22.0 et Wrangler 4.132.0, installé localement dans le projet, dans /home/labex/project/tool-call-guard. Elle fournit également des fixtures de modèle déterministes et des vérifications indépendantes. La configuration n’autorise pas Wrangler, ne crée pas le code source du Worker, n’appelle pas de modèle, ne déploie rien et ne crée aucune ressource cloud.

Autoriser la VM et configurer le Worker d’appels d’outils

Dans cette étape, vous allez autoriser cette nouvelle VM et configurer un Worker temporaire. Votre navigateur est peut-être déjà connecté au Cloudflare Dashboard, mais Wrangler installé dans une nouvelle VM doit disposer de sa propre autorisation limitée.

Accédez au projet préparé et vérifiez la version figée de l’interface en ligne de commande :

cd /home/labex/project/tool-call-guard
npx wrangler --version

Vous devez obtenir 4.132.0. Demandez uniquement les autorisations nécessaires à un Worker utilisant l’IA. Wrangler 4.132.0 vérifie également les dépendances KV lors de la suppression ; le nettoyage nécessite donc l’autorisation KV, même si ce laboratoire ne crée aucune donnée KV.

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write ai:write

Ouvrez le lien affiché, saisissez le code actuel, vérifiez le compte et les autorisations, puis autorisez votre compte d’apprentissage. N’envoyez jamais le code, le mot de passe ou le jeton à une autre personne. Inspectez ensuite les informations d’identité structurées :

npx wrangler whoami --json

Vérifiez la présence de loggedIn: true, puis générez un nom unique afin que le nettoyage puisse cibler uniquement le Worker de ce laboratoire :

RUN="labex-c07-a05-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

Le document ci-dessous écrit une configuration JSON ordinaire. Remplacez YOUR_ACCOUNT_ID par l’ID réel du compte d’apprentissage visé :

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-16",
  "workers_dev": true,
  "preview_urls": false,
  "observability": { "enabled": true, "head_sampling_rate": 1 },
  "ai": { "binding": "AI", "remote": true }
}
JSON

La liaison AI fournit au code une référence sûre env.AI, sans placer de clé d’API de modèle dans le code source. remote: true signifie que le développement local appelle toujours le modèle associé au compte au lieu de simuler l’inférence hors ligne.

Comprendre la limite des outils

Dans cette étape, vous allez relier la liaison de la plateforme à la limite que votre application doit faire respecter.

Générez les types d’environnement à partir de wrangler.jsonc, puis inspectez l’interface générée :

npx wrangler types
grep -A4 'interface __BaseEnv_Env' worker-configuration.d.ts

Recherchez AI: Ai. Une description d’outil est une donnée structurée envoyée au modèle : un nom, une finalité en langage courant et un schéma décrivant les arguments possibles. Elle aide le modèle à proposer un appel, mais ne constitue ni une autorisation ni du code exécutable.

Ce laboratoire autorise un seul outil en lecture seule, lookup_catalog_item, avec un argument tel que { "sku": "KB-101" }. Après l’inférence, l’application exige exactement un appel proposé et le nom exact autorisé. Elle exige ensuite que arguments soit un objet contenant uniquement sku, vérifie le format court des SKU publics de ce laboratoire et transmet la valeur validée uniquement à la fonction fixe de l’application, qui effectue une lecture seule.

Inspectez les fixtures de rejet fournies :

grep -nE 'unknown tools|missing, extra|zero or multiple' test/worker.test.mjs

Ces fixtures sont des réponses fictives délibérées du modèle. Elles prouvent que la limite de sécurité fonctionne sans consommer de Neurons et sans dépendre de la production en direct d’un appel mal formé par un modèle.

Créer l’outil de catalogue validé

Dans cette étape, vous allez décrire l’outil au modèle, valider sa proposition et exécuter uniquement la fonction de catalogue en lecture seule de l’application.

Créez le point d’entrée du Worker :

cat > src/index.js <<'JS'
const MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
const TOOL_NAME = "lookup_catalog_item";
const MAX_QUESTION = 240;
const SKU_PATTERN = /^[A-Z]{2}-[0-9]{3}$/;
const CATALOG = [
  { sku: "KB-101", name: "Compact Keyboard", priceUsd: 49, inStock: true },
  { sku: "MS-205", name: "Wireless Mouse", priceUsd: 29, inStock: false }
];

const TOOLS = [{
  name: TOOL_NAME,
  description: "Read one public catalog item by the exact SKU stated in the user's question.",
  parameters: {
    type: "object",
    properties: { sku: { type: "string", description: "An exact catalog SKU such as KB-101" } },
    required: ["sku"]
  }
}];

function json(data, status = 200) { return Response.json(data, { status }); }

async function readQuestion(request) {
  if (!(request.headers.get("content-type") || "").toLowerCase().includes("application/json")) {
    return { error: json({ error: "json_required" }, 415) };
  }
  let body;
  try { body = await request.json(); } catch { return { error: json({ error: "invalid_json" }, 400) }; }
  const question = typeof body?.question === "string" ? body.question.trim() : "";
  if (!question) return { error: json({ error: "invalid_question" }, 400) };
  if (question.length > MAX_QUESTION) return { error: json({ error: "question_too_large" }, 413) };
  return { question };
}

export function validateToolSelection(toolCalls) {
  if (!Array.isArray(toolCalls) || toolCalls.length !== 1) throw new Error("exactly one tool call is required");
  const call = toolCalls[0];
  if (!call || call.name !== TOOL_NAME) throw new Error("unknown tool");
  const args = call.arguments;
  if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("arguments must be an object");
  if (Object.keys(args).length !== 1 || !Object.hasOwn(args, "sku")) throw new Error("unexpected arguments");
  if (typeof args.sku !== "string" || !SKU_PATTERN.test(args.sku)) throw new Error("invalid sku");
  return { name: TOOL_NAME, arguments: { sku: args.sku } };
}

export function executeCatalogTool(argumentsValue) {
  const item = CATALOG.find((candidate) => candidate.sku === argumentsValue.sku);
  return item ? { ...item, found: true } : { sku: argumentsValue.sku, found: false };
}

export async function handleCatalogHelp(request, env, execute = executeCatalogTool) {
  const parsed = await readQuestion(request);
  if (parsed.error) return parsed.error;
  const requestId = crypto.randomUUID();
  let inference;
  try {
    inference = await env.AI.run(MODEL, {
      messages: [
        { role: "system", content: "Use exactly one provided read-only tool. Copy only the exact SKU from the user. Do not answer from memory." },
        { role: "user", content: parsed.question }
      ],
      tools: TOOLS,
      max_tokens: 128,
      temperature: 0
    });
  } catch {
    console.error(JSON.stringify({ event: "tool_inference_failed", requestId, model: MODEL }));
    return json({ error: "model_unavailable", requestId }, 502);
  }
  let selected;
  try { selected = validateToolSelection(inference?.tool_calls); }
  catch {
    console.error(JSON.stringify({ event: "tool_call_rejected", requestId, model: MODEL }));
    return json({ error: "invalid_tool_call", requestId }, 502);
  }
  const result = execute(selected.arguments);
  console.log(JSON.stringify({ event: "tool_call_executed", requestId, model: MODEL, tool: selected.name, found: result.found }));
  return json({ model: MODEL, tool: selected.name, arguments: selected.arguments, result, requestId });
}

export default { async fetch(request, env) {
  const url = new URL(request.url);
  if (request.method === "GET" && url.pathname === "/health") return json({ status: "ok" });
  if (request.method === "POST" && url.pathname === "/catalog-help") return handleCatalogHelp(request, env);
  return json({ error: "not_found" }, 404);
} };
JS

Observez l’ordre des opérations : env.AI.run() renvoie des données, validateToolSelection() les réduit à une seule structure autorisée, puis seulement executeCatalogTool() s’exécute. Le modèle ne fournit jamais de JavaScript, ne choisit aucune URL et n’obtient aucun accès à une opération d’écriture. Les journaux enregistrent les métadonnées du cycle de vie, mais omettent la question de l’utilisateur et le résultat du catalogue.

Exécutez les cinq tests déterministes, puis demandez à Wrangler de créer le bundle sans déployer :

node --test test/worker.test.mjs
npx wrangler deploy --dry-run

Les tests doivent signaler cinq réussites. La simulation de déploiement doit afficher env.AI comme liaison AI. Ensemble, ces résultats prouvent que le code de validation et la configuration du Worker sont compatibles avant qu’un appel réel au modèle ne consomme de l’usage.

Effectuer une sélection d’outil en direct

Dans cette étape, vous allez exécuter le Worker localement pendant que sa liaison AI effectue une inférence distante réelle. Seule la recherche dans le catalogue s’exécute localement ; le modèle s’exécute toujours sur Cloudflare.

Démarrez Wrangler en arrière-plan et attendez que la route de santé, qui n’utilise pas l’IA, réponde. & crée une tâche en arrière-plan, $! contient son identifiant de processus et la boucle limitée cesse d’attendre dès que /health répond correctement :

npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then break; fi
  sleep 1
done

Envoyez une question courte contenant un SKU synthétique exact :

curl --silent --show-error http://127.0.0.1:8787/catalog-help \
  --header 'Content-Type: application/json' \
  --data '{"question":"Is SKU KB-101 in stock and what does it cost?"}'

Vous devez obtenir le modèle Llama exact, tool: "lookup_catalog_item", des arguments contenant uniquement KB-101 et la fixture limitée Compact Keyboard. La formulation générée n’est pas évaluée, car l’application utilise la proposition d’outil structurée plutôt qu’une réponse en texte libre.

Rejetez une question vide avant l’inférence :

curl --silent --show-error --write-out '\nHTTP %{http_code}\n' http://127.0.0.1:8787/catalog-help \
  --header 'Content-Type: application/json' --data '{"question":""}'

Vous devez obtenir {"error":"invalid_question"} et HTTP 400. Cela prouve que la validation ordinaire de la requête a lieu avant l’utilisation du modèle.

Déployer et inspecter les preuves d’utilisation de l’outil

Dans cette étape, vous allez déployer le même point de terminaison et relier son comportement à des éléments visibles dans Cloudflare.

Arrêtez uniquement le processus de développement enregistré, attendez sa fin, puis déployez :

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
npx wrangler deploy

Enregistrez l’URL exacte affichée par Wrangler et envoyez une question publique :

WORKER_URL="https://YOUR_WORKER_URL"
curl --silent --show-error "$WORKER_URL/catalog-help" \
  --header 'Content-Type: application/json' \
  --data '{"question":"Is SKU KB-101 in stock and what does it cost?"}'

Vérifiez que le résultat public utilise le modèle exact et l’outil autorisé, renvoie uniquement l’argument SKU validé et contient les mêmes champs limités de la fixture en lecture seule.

Ouvrez Workers & Pages → votre Worker labex-c07-a05-... → Bindings. Une liaison est une connexion nommée qui rend un service Cloudflare disponible dans le code du Worker. Vérifiez la présence d’une connexion Workers AI portant le nom AI ; c’est ce nom qui permet au programme d’appeler env.AI.run(...).

Liaison Workers AI nommée AI

Ouvrez ensuite Observability. Cette page rassemble les enregistrements d’invocation et les journaux de l’application. L’exemple ci-dessous affiche quatre événements réussis et aucune erreur après la requête publique et les vérifications indépendantes. Votre nombre peut différer, car chaque requête peut produire un enregistrement d’invocation et un événement d’application, et l’affichage dans le Dashboard peut être retardé.

Événements Worker réussis dans Observability

La notification bleue concernant le forfait Free sur cette page concerne l’allocation d’événements Workers Logs, et non l’inférence AI. Dans le champ de recherche, saisissez tool_call_executed, puis développez une ligne correspondante. L’exemple ciblé affiche deux correspondances réussies et les champs volontairement limités au début de l’événement : lookup_catalog_item, le modèle Llama exact et un ID de requête. L’événement complet contient également event: "tool_call_executed" et found: true, mais n’enregistre ni la question de l’utilisateur, ni la réponse brute du modèle, ni l’enregistrement de catalogue renvoyé.

Journal d’exécution d’outil limité pour protéger la confidentialité

Ouvrez enfin AI → Workers AI et laissez l’onglet Neurons sélectionné. Un Neuron est l’unité utilisée par Cloudflare pour mesurer les calculs Workers AI. Le compte de l’exemple a utilisé 342.34/10k Neurons ce jour-là ; sa ligne Llama affiche 341.57, tandis qu’un exercice d’embeddings précédent apparaît séparément. Il s’agit d’exemples partagés au niveau du compte, et non d’un coût garanti pour une seule requête. Recherchez la ligne Llama exacte dans votre compte et vérifiez que le total du jour reste dans l’allocation 10k de Workers Free.

Utilisation quotidienne des Neurons Workers AI

Les pages du Dashboard vous aident à relier la configuration, le trafic et l’utilisation au résultat de la ligne de commande. Ne relancez pas l’inférence uniquement pour forcer la mise à jour d’un graphique. La réponse JSON et la vérification indépendante restent les références, car les graphiques et les journaux peuvent apparaître plus tard.

Supprimer le Worker et se déconnecter

Dans cette étape, vous allez supprimer le point de terminaison public temporaire, puis retirer l’autorisation de cette VM. L’utilisation de Workers AI reste enregistrée dans l’historique du compte ; la suppression du Worker n’efface pas cet enregistrement.

Supprimez exactement le Worker indiqué dans wrangler.jsonc :

npx wrangler delete

Confirmez uniquement lorsque Wrangler affiche le nom unique labex-c07-a05-... de ce laboratoire. Attendez le message Successfully deleted, puis exécutez la vérification indépendante de l’absence dans le cloud tant que l’autorisation est encore disponible :

python3 .labex/verify.py deleted

Ce n’est qu’après l’affichage de PASS: deleted que vous devez vous déconnecter et inspecter l’état structuré :

npx wrangler logout
npx wrangler whoami --json

Vous devez obtenir loggedIn: false. Fermer un onglet du navigateur ou supprimer le code source local ne prouverait pas que le Worker public a disparu.

Résumé

Vous avez séparé la sélection effectuée par le modèle de l’autorité de l’application. Workers AI a proposé une recherche structurée dans le catalogue, votre Worker a validé le nom exact de l’outil et l’objet d’arguments, puis le code fixe en lecture seule s’est exécuté. Les fixtures déterministes ont prouvé que les outils inconnus, les arguments mal formés et les appels multiples ne peuvent pas agir, tandis que l’inférence en direct a démontré l’échange avec le modèle réel. Vous avez également inspecté des éléments de preuve limités pour protéger la confidentialité, puis supprimé le Worker temporaire et l’autorisation de la VM.