Nível 3
Projetando uma ferramenta
- user
- assistant
- tool_result
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_matchupUm 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)?')
// 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]
# 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]
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.