Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 3

Concevoir un outil

Le modèle ne voit jamais le code de votre outil. Il voit un nom, une description et un schéma, et à partir de cela seulement, il décide s'il utilise l'outil et ce qu'il lui passe. Pour le modèle, c'est l'outil tout entier.
1/16 Plis du bandonéon :
  • user
  • assistant
  • tool_result
Une créature sauvage bloque le chemin. Son type est encore ???. L'Oracle a quatre outils, et tout ce qu'il en saura jamais, c'est la liste des attaques : des noms, des paramètres et des descriptions.

EventBus

Le problème

Quand vous écrivez une fonction, vous savez ce qu'elle fait. Le modèle, non : tout ce qu'il reçoit, c'est l'enseigne sur la porte de l'Atelier. Si l'enseigne est vague, le modèle devine. Une mauvaise supposition coûte un tour, une erreur et plus de tokens dans l'historique. Pire, un outil vague peut « réussir » avec un résultat inutile sans que personne ne s'en aperçoive.

Dans le combat, une créature sauvage bloque le chemin et le dresseur demande quel partenaire envoyer. L'Oracle a quatre outils, et le menu des attaques montre exactement ce que le modèle reçoit pour chacun : un nom, ses paramètres et une description. Cela suffit pour écarter do_stuff, pour écarter heal_party parce que sa description dit « jamais en combat », pour savoir qu'il faut d'abord découvrir le type de la créature, et pour passer à check_matchup son entrée au bon format du premier coup.

Anatomie d'un bon outil

  • Nom

    Au lieu de do_stuff, utilisez identify_wild, check_matchup

    Un verbe et un nom précis disent au modèle ce que fait l'outil avant qu'il ne lise quoi que ce soit d'autre.

  • Description

    Au lieu de "Does stuff.", utilisez Ce qu'il fait, quand l'utiliser et ce qu'il renvoie.

    C'est la seule documentation que reçoit le modèle. Écrivez-la pour quelqu'un qui ne peut pas lire votre code.

  • Paramètres

    Au lieu de x: string, utilisez attack et defend, chacun avec sa description, et le format écrit noir sur blanc : un type en minuscules, comme water.

    Indiquez ce qui est obligatoire et interdisez le reste, pour qu'une mauvaise entrée échoue vite au lieu de faire quelque chose de bizarre.

  • Sortie

    Au lieu de Toute la table des types, 18 types sur 18, utilisez Une ligne : grass vs water: 2x, super effective.

    Tout ce qu'un outil renvoie entre dans l'historique, et le modèle le relit à chaque tour.

  • Erreurs

    Au lieu de "Invalid input", utilisez "defend must be one lowercase type, like water. Call identify_wild to get it."

    Une erreur que le modèle peut comprendre est une erreur qu'il peut corriger au tour suivant.

Une règle de plus : moins d'outils, mais plus précis. Chaque outil que vous ajoutez, c'est une enseigne de plus que le modèle lit à chaque tour, et une façon de plus de choisir le mauvais.

Le code

Avec astorlm : tool() prend un schéma Zod, le transforme en JSON Schema que lit le modèle, et vérifie l'entrée du modèle avec lui avant que votre code ne s'exécute. Si l'entrée ne correspond pas, le modèle reçoit une erreur claire au lieu que votre fonction plante.

À partir de zéro : Un outil, c'est un JSON Schema que le modèle lit plus une fonction qu'il ne voit jamais. Voici le do_stuff de Crooky et les deux outils que l'Oracle a utilisés pendant le combat, côte à côte. Seuls les schémas voyagent jusqu'au modèle, dans le champ tools de chaque requête du niveau 2.

import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

const type = z
  .string()
  .regex(/^[a-z]+$/, 'a type is one lowercase word, like water; call identify_wild to get the wild one')

// Describe the input once with Zod: astorlm turns it into the JSON Schema the model
// reads, and checks the model's input against it before running your code.
const checkMatchup = tool({
  name: 'check_matchup',
  description:
    'How hard one type hits another. Use it to choose which partner to send into a battle. ' +
    'Returns one line: the multiplier (2x, 1x or 0.5x) and what it means.',
  schema: z.object({
    attack: type.describe('The attacking type, one lowercase word, e.g. "grass".'),
    defend: type.describe('The defending type, one lowercase word, e.g. "water".'),
  }),
  execute: async ({ attack, defend }) => {
    const times = typeChart[attack]?.[defend] ?? 1 // your code; the model never sees it
    return `${attack} vs ${defend}: ${times}x${times > 1 ? ', super effective' : ''}`
  },
})

const identifyWild = tool({
  name: 'identify_wild',
  description: 'Identify the wild creature you are facing. Returns its name and type.',
  schema: z.object({}),
  execute: async () => {
    const wild = battle.opponent() // your code
    return `${wild.name} · type: ${wild.type}`
  },
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  tools: [checkMatchup, identifyWild],
})

await agent.run('A wild creature appeared! Should I send Emberpup (fire) or Sproutle (grass)?')

Vérifiez vos outils comme le ferait le modèle

  • Ne lisez que le schéma. Cachez le code et demandez-vous : saurais-je quand appeler ceci, et quoi lui passer ?
  • Observez les premiers appels. Si le modèle continue d'envoyer une mauvaise entrée, la solution est presque toujours dans la description, pas dans le prompt.
  • Mesurez la sortie. Un outil qui renvoie toute la table des types alors qu'une ligne suffirait remplit l'historique à toute vitesse.