> Niveau 3 de Agent Harness Patterns, un parcours de patterns sur le fonctionnement des agents d'IA. Version web : https://harnesspatterns.dev/fr/patterns/designing-a-tool · Tous les patterns (en anglais) : https://harnesspatterns.dev/llms.txt

# 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.

## 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.

**Avec astorlm**

```ts
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)?')
```

**TypeScript**

```ts
// A tool is two things: a description the model reads, and code the model never sees.

// ✗ Crooky's tool. The model has to guess what it does and what x means.
const doStuffSchema = {
  type: 'function',
  function: {
    name: 'do_stuff',
    description: 'Does stuff.',
    parameters: { type: 'object', properties: { x: { type: 'string' } } },
  },
}

// ✓ Named for what it does, described for someone who can't read the code.
const checkMatchupSchema = {
  type: 'function',
  function: {
    name: 'check_matchup',
    description:
      'How hard one type hits another. ' + // what it does
      'Use it to choose which partner to send into a battle. ' + // when to use it
      'Returns one line: the multiplier (2x, 1x or 0.5x) and what it means.', // what comes back
    parameters: {
      type: 'object',
      properties: {
        attack: { type: 'string', description: 'The attacking type, one lowercase word, e.g. "grass".' },
        defend: { type: 'string', description: 'The defending type, one lowercase word, e.g. "water".' },
      },
      required: ['attack', 'defend'],
      additionalProperties: false,
    },
  },
}

// ✓ A small helper tool, so the model never has to guess what it is facing.
const identifyWildSchema = {
  type: 'function',
  function: {
    name: 'identify_wild',
    description: 'Identify the wild creature you are facing. Returns its name and type.',
    parameters: { type: 'object', properties: {}, additionalProperties: false },
  },
}

// The code behind the names. In a real game, these read the battle state.
export function identifyWild(): string {
  const wild = battle.opponent()
  return `${wild.name} · type: ${wild.type}`
}

export function checkMatchup({ attack, defend }: { attack: string; defend: string }): string {
  // An error the model can fix on its next turn: what went wrong, and what to send instead.
  for (const value of [attack, defend]) {
    if (!/^[a-z]+$/.test(value)) {
      return `Error: a type is one lowercase word, like water. Got "${value}". Call identify_wild to get the wild one.`
    }
  }
  const times = typeChart[attack]?.[defend] ?? 1
  // Short, on-topic output: every character here goes into the history, and gets read every turn.
  return `${attack} vs ${defend}: ${times}x${times > 1 ? ', super effective' : ''}`
}

// Only the schemas travel to the model, in the `tools` field of each request.
export const toolSchemas = [checkMatchupSchema, identifyWildSchema]
```

**Python**

```python
# A tool is two things: a description the model reads, and code the model never sees.
import re

# ✗ Crooky's tool. The model has to guess what it does and what x means.
DO_STUFF_SCHEMA = {
    "type": "function",
    "function": {
        "name": "do_stuff",
        "description": "Does stuff.",
        "parameters": {"type": "object", "properties": {"x": {"type": "string"}}},
    },
}

# ✓ Named for what it does, described for someone who can't read the code.
CHECK_MATCHUP_SCHEMA = {
    "type": "function",
    "function": {
        "name": "check_matchup",
        "description": (
            "How hard one type hits another. "  # what it does
            "Use it to choose which partner to send into a battle. "  # when to use it
            "Returns one line: the multiplier (2x, 1x or 0.5x) and what it means."  # what comes back
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "attack": {"type": "string", "description": 'The attacking type, one lowercase word, e.g. "grass".'},
                "defend": {"type": "string", "description": 'The defending type, one lowercase word, e.g. "water".'},
            },
            "required": ["attack", "defend"],
            "additionalProperties": False,
        },
    },
}

# ✓ A small helper tool, so the model never has to guess what it is facing.
IDENTIFY_WILD_SCHEMA = {
    "type": "function",
    "function": {
        "name": "identify_wild",
        "description": "Identify the wild creature you are facing. Returns its name and type.",
        "parameters": {"type": "object", "properties": {}, "additionalProperties": False},
    },
}

# The code behind the names. In a real game, these read the battle state.
def identify_wild():
    wild = battle.opponent()
    return f"{wild['name']} · type: {wild['type']}"

def check_matchup(attack, defend):
    # An error the model can fix on its next turn: what went wrong, and what to send instead.
    for value in (attack, defend):
        if not re.fullmatch(r"[a-z]+", value):
            return f'Error: a type is one lowercase word, like water. Got "{value}". Call identify_wild to get the wild one.'
    times = TYPE_CHART.get(attack, {}).get(defend, 1)
    # Short, on-topic output: every character here goes into the history, and gets read every turn.
    return f"{attack} vs {defend}: {times}x" + (", super effective" if times > 1 else "")

# Only the schemas travel to the model, in the `tools` field of each request.
TOOL_SCHEMAS = [CHECK_MATCHUP_SCHEMA, IDENTIFY_WILD_SCHEMA]
```

## 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.

## Patterns liés

- [5 · Les erreurs dans la boucle](https://harnesspatterns.dev/fr/patterns/errors-in-the-loop.md)
- [8 · Des skills à la demande](https://harnesspatterns.dev/fr/patterns/skills.md)
- [14 · Sécurité et sandboxing](https://harnesspatterns.dev/fr/patterns/security.md)
