Pular para o conteúdo
astorlm
Idioma: Português
← Mapa

Nível 3

Projetando uma ferramenta

O modelo nunca vê o código da sua ferramenta. Ele vê um nome, uma descrição e um schema, e só com isso decide se usa a ferramenta e o que passar para ela. Para o modelo, isso é a ferramenta inteira.
1/16 Dobras do bandoneón:
  • user
  • assistant
  • tool_result
Uma criatura selvagem bloqueia o caminho. O tipo dela ainda é ???. O Oráculo tem quatro ferramentas, e tudo o que ele vai saber sobre elas é a lista de golpes: nomes, parâmetros e descrições.

EventBus

O problema

Quando você escreve uma função, sabe o que ela faz. O modelo não: tudo o que ele recebe é a placa na porta da Oficina. Se a placa é vaga, o modelo chuta. Um chute errado custa um turno, um erro e mais tokens no histórico. Pior: uma ferramenta vaga pode "dar certo" com um resultado inútil sem que ninguém perceba.

Na batalha, uma criatura selvagem bloqueia o caminho e o treinador pergunta qual parceiro mandar. O Oráculo tem quatro ferramentas, e o menu de golpes mostra exatamente o que o modelo recebe de cada uma: um nome, os parâmetros e uma descrição. Isso basta para pular do_stuff, para pular heal_party porque a descrição diz "nunca em batalha", para saber que primeiro é preciso descobrir o tipo da criatura, e para mandar a check_matchup a entrada no formato certo na primeira tentativa.

Anatomia de uma boa ferramenta

  • Nome

    Em vez de do_stuff, use identify_wild, check_matchup

    Um verbo e um substantivo específicos dizem ao modelo o que a ferramenta faz antes que ele leia qualquer outra coisa.

  • Descrição

    Em vez de "Does stuff.", use O que ela faz, quando usar e o que ela devolve.

    É a única documentação que o modelo recebe. Escreva para alguém que não consegue ler o seu código.

  • Parâmetros

    Em vez de x: string, use attack e defend, cada um com uma descrição, e o formato explícito: um tipo em minúsculas, como water.

    Marque o que é obrigatório e proíba extras, para que uma entrada errada falhe rápido em vez de fazer algo estranho.

  • Saída

    Em vez de A tabela de tipos inteira, 18 tipos por 18, use Uma linha: grass vs water: 2x, super effective.

    Tudo o que uma ferramenta devolve vai para o histórico, e o modelo lê de novo a cada turno.

  • Erros

    Em vez de "Invalid input", use "defend must be one lowercase type, like water. Call identify_wild to get it."

    Um erro que o modelo consegue entender é um erro que ele consegue corrigir no turno seguinte.

Mais uma regra: menos ferramentas, e mais afiadas. Cada ferramenta que você adiciona é mais uma placa que o modelo lê a cada turno, e mais um jeito de escolher a errada.

O código

Com astorlm: tool() recebe um schema Zod, transforma-o no JSON Schema que o modelo lê e valida a entrada do modelo contra ele antes de o seu código rodar. Se a entrada não bate, o modelo recebe um erro claro em vez de a sua função quebrar.

Do zero: Uma ferramenta é um JSON Schema que o modelo lê mais uma função que o modelo nunca vê. Aqui estão o do_stuff do Crooky e as duas ferramentas que o Oráculo usou na batalha, lado a lado. Só os schemas viajam até o modelo, no campo tools de cada requisição do nível 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)?')

Confira suas ferramentas como o modelo faria

  • Leia só o schema. Esconda o código e se pergunte: eu saberia quando chamar isto, e o que passar?
  • Observe as primeiras chamadas. Se o modelo continua mandando a entrada errada, a correção quase sempre está na descrição, não no prompt.
  • Meça a saída. Uma ferramenta que devolve a tabela de tipos inteira quando uma linha bastaria enche o histórico rapidinho.