Saltar al contenido
astorlm
Idioma: Español
← Mapa

Nivel 3

Diseñar una herramienta

El modelo nunca ve el código de tu herramienta. Ve un nombre, una descripción y un esquema, y solo con eso decide si usarla y qué pasarle. Para el modelo, eso es toda la herramienta.
1/16 Pliegues del bandoneón:
  • user
  • assistant
  • tool_result
Una criatura salvaje bloquea el camino. Su tipo todavía es ???. El Oráculo tiene cuatro herramientas, y todo lo que va a saber de ellas es la lista de movimientos: nombres, parámetros y descripciones.

EventBus

El problema

Cuando escribes una función, sabes qué hace. El modelo no: lo único que recibe es el cartel en la puerta del Taller. Si el cartel es vago, el modelo adivina. Una mala suposición cuesta un turno, un error y más tokens en el historial. Peor aún, una herramienta vaga puede "tener éxito" con un resultado inútil y que nadie lo note.

En la batalla, una criatura salvaje bloquea el camino y el entrenador pregunta a qué compañero mandar. El Oráculo tiene cuatro herramientas, y el menú de movimientos muestra exactamente lo que recibe el modelo por cada una: un nombre, sus parámetros y una descripción. Eso alcanza para descartar do_stuff, para descartar heal_party porque su descripción dice "nunca en batalla", para saber que primero hay que averiguar el tipo de la criatura, y para pasarle a check_matchup sus datos en el formato correcto al primer intento.

Anatomía de una buena herramienta

  • Nombre

    En lugar de do_stuff, usa identify_wild, check_matchup

    Un verbo y un sustantivo concretos le dicen al modelo qué hace la herramienta antes de que lea cualquier otra cosa.

  • Descripción

    En lugar de "Does stuff.", usa Qué hace, cuándo usarla y qué devuelve.

    Es la única documentación que recibe el modelo. Escríbela para alguien que no puede leer tu código.

  • Parámetros

    En lugar de x: string, usa attack y defend, cada uno con su descripción, y el formato explícito: un tipo en minúsculas, como water.

    Marca qué es obligatorio y prohíbe los extras, así una entrada incorrecta falla rápido en lugar de hacer algo raro.

  • Salida

    En lugar de La tabla de tipos completa, 18 tipos por 18, usa Una línea: grass vs water: 2x, super effective.

    Todo lo que devuelve una herramienta entra en el historial, y el modelo lo vuelve a leer en cada turno.

  • Errores

    En lugar de "Invalid input", usa "defend must be one lowercase type, like water. Call identify_wild to get it."

    Un error que el modelo puede entender es un error que puede corregir en su siguiente turno.

Una regla más: menos herramientas, y más precisas. Cada herramienta que agregas es un cartel más que el modelo lee en cada turno, y una forma más de elegir la equivocada.

El código

Con astorlm: tool() recibe un esquema de Zod, lo convierte en el JSON Schema que lee el modelo y valida contra él los datos de entrada del modelo antes de que se ejecute tu código. Si la entrada no coincide, el modelo recibe un error claro en lugar de que tu función explote.

Desde cero: Una herramienta es un JSON Schema que lee el modelo más una función que el modelo nunca ve. Aquí están el do_stuff de Crooky y las dos herramientas que usó el Oráculo en la batalla, lado a lado. Solo los esquemas viajan al modelo, en el campo tools de cada pedido del nivel 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)?')

Revisa tus herramientas como lo haría el modelo

  • Lee solo el esquema. Oculta el código y pregúntate: ¿sabría cuándo llamar a esto y qué pasarle?
  • Mira las primeras llamadas. Si el modelo sigue mandando datos incorrectos, el arreglo casi siempre está en la descripción, no en el prompt.
  • Mide la salida. Una herramienta que devuelve toda la tabla de tipos cuando bastaba una línea llena el historial muy rápido.