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

Nível 8

Cache de prompts

A cada turno, o loop manda ao modelo a requisição inteira de novo. Quase tudo é igual à vez anterior, e o provedor consegue lembrar, desde que você não mude o jeito como ela começa.
1/35 Dobras do bandoneón:
  • user
  • assistant
  • tool_result
Noite de jogos. A cada turno, o loop manda ao Oráculo a requisição inteira de novo, então Astor a toca no Simon: uma luz por bloco, primeiro o system prompt e as ferramentas, depois cada mensagem.

EventBus

O problema

Um modelo não lembra de nada entre chamadas, então o loop reenvia tudo a cada turno: o system prompt, a lista de ferramentas e o histórico inteiro. Num agente que roda dez turnos, as instruções longas do alto viajam dez vezes, e são pagas dez vezes.

E deixam tudo mais lento: antes de escrever uma única palavra, o modelo precisa ler a requisição inteira de novo, desde o primeiro token.

A solução

Os provedores guardam uma memória de curta duração das requisições que acabaram de ler. Quando uma requisição nova começa exatamente como uma recente, eles reaproveitam a parte que coincide e só processam o resto. Essa parte é cobrada por uma fração do preço (muitas vezes um décimo, dependendo do provedor e do modelo), e a resposta começa antes.

A regra está na palavra começa: a coincidência é contada a partir do primeiro token e para no primeiro que é diferente. Um agente se encaixa naturalmente, porque cada requisição é a anterior mais algumas mensagens no final. Você só precisa não quebrar isso:

  • Algo que muda, lá no alto

    a hora, um id de requisição, o nome do usuário

    Mantenha o system prompt fixo. O que muda vai na última mensagem, no final.

  • Ferramentas que se mexem

    outra ordem, uma ferramenta adicionada no meio do caminho

    Monte a lista de ferramentas uma vez, numa ordem fixa, e mande a mesma a cada turno.

  • Reescrever o passado

    editar, cortar ou resumir mensagens antigas

    Só acrescente no final. Quando precisar reescrever, tudo o que vem depois desse ponto é pago de novo.

  • Trocar de modelo

    um modelo mais barato para um turno

    Cada modelo tem seu próprio cache. Uma troca o começa do zero.

Alguns provedores fazem cache sozinhos quando a requisição é longa o bastante: a OpenAI faz a partir de 1.024 tokens. Outros, como a Anthropic, só fazem cache onde você marca. De qualquer forma, o uso que eles devolvem diz quantos tokens de entrada vieram do cache, então dá para conferir.

O elenco

O mesmo elenco de sempre, numa noite de jogos.

O Simon a requisição
Cada rodada repete a sequência inteira e acrescenta algo no final, como cada turno reenvia a requisição.
As luzes seus blocos
Amarelas para o system prompt e as ferramentas, e depois uma por mensagem, nas cores do histórico.
Astor o loop
Toca a sequência inteira para o Oráculo a cada turno, e depois roda as ferramentas como sempre.
O Oráculo o modelo
Seu balão é o cache do provedor: as luzes que ele já conhece, a partir da primeira.
O placar o uso
Tokens de entrada enviados, lidos do cache e pagos, com os do cache a um décimo.
A sala as ferramentas
A prateleira de jogos e o telefone: game_shelf e order_pizza.

O código

Com astorlm: o agente monta seu system prompt uma vez e o reenvia sem mudanças a cada turno, com as mesmas ferramentas na mesma ordem, e o histórico só cresce no final. Sua parte é deixar o que muda fora de systemPrompt. Cada turn_end traz cacheReadTokens, que o OpenAIProvider lê da resposta.

Do zero: o loop do nível 2, com o começo fixo montado uma vez fora do loop, e uma linha que registra cached_tokens do uso de cada resposta.

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

const gameShelf = tool({
  name: 'game_shelf',
  description: 'List the board games on the living-room shelf.',
  schema: z.object({}),
  execute: async () => home.shelf(), // your code
})

const orderPizza = tool({
  name: 'order_pizza',
  description: 'Order pizza for delivery. Returns how long it will take.',
  schema: z.object({ size: z.enum(['medium', 'large']), count: z.number().int().min(1) }),
  execute: async ({ size, count }) => pizzeria.order(size, count), // your code
})

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
  }),
  // The start of every request. astorlm builds it once and resends it unchanged every turn.
  // Nothing that changes goes here: no clock, no request id, no user name.
  systemPrompt: HOUSE_RULES, // a long, fixed text: the more of it, the more the cache saves
  tools: [gameShelf, orderPizza], // same tools, same order, every turn
  maxTurns: 10,
})

// OpenAI caches long prompts on its own (from 1,024 tokens), and says how much it reused.
agent.on('event', (event) => {
  if (event.type !== 'turn_end' || !event.usage) return
  const { inputTokens, cacheReadTokens = 0 } = event.usage
  console.log(`turn ${event.turn}: ${cacheReadTokens} of ${inputTokens} input tokens from cache`)
})

// Need the time? Put it at the end, in the message, where it only changes what comes after it.
const now = new Date().toLocaleTimeString()
await agent.run(`Game night for four: see what games we have, and order pizza. (It is ${now}.)`)

O que observar

  • O cache não dura. Ele vive poucos minutos sem uso. Um agente que espera uma hora por uma pessoa, ou um heartbeat que bate a cada duas horas, paga de novo por inteiro a primeira requisição.
  • Compactação e cache puxam para lados opostos. Encolher as mensagens antigas, o próximo nível, reescreve o passado, então cada token depois da primeira mudança é pago por inteiro no turno seguinte. Compacte poucas vezes, e em passos grandes.
  • Prompts curtos não vão para o cache. Abaixo do mínimo do provedor, não há o que economizar. O cache compensa com system prompts longos, muitas ferramentas ou históricos longos: exatamente o que os agentes têm.
  • Meça. Se cacheReadTokens continua em zero a partir do segundo turno, algo no começo está mudando. Compare duas requisições lado a lado e ache a primeira diferença.
  • Só economiza na entrada. Os tokens que o modelo escreve custam o mesmo. Nos agentes, a entrada costuma ser a maior parte da conta, então continua sendo a maior economia possível.