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

Nível 7

A mochila enche

Cada turno envia o histórico inteiro ao modelo de novo. Se um chat continuar por tempo suficiente, ele para de caber. A compactação encolhe as partes mais antigas antes que isso aconteça.
1/30 Mensagens:
  • user
  • assistant
  • tool_result
O assistente de suporte de uma loja de bicicletas, num modelo que lê no máximo 8.000 tokens. O poço é essa janela. Cada mensagem cai como um bloco, e o system prompt é o chão. A linha vermelha fica em 80%.

EventBus

O problema

Um modelo só consegue ler uma certa quantidade de uma vez. Esse limite é a janela de contexto dele, contada em tokens (pedaços de palavras, de uns quatro caracteres cada): 8.000 em muitos modelos locais pequenos, algumas centenas de milhares nos grandes modelos hospedados. Tudo numa requisição precisa caber nela: o system prompt, cada mensagem até agora, cada resultado de ferramenta e espaço para a resposta.

O modelo não lembra nada entre chamadas, então o loop envia o histórico inteiro a cada turno. Um chat de suporte que consulta um pedido e busca num catálogo acumula milhares de tokens de resultados que o modelo já usou. Cada turno é mais lento e custa mais que o anterior. E um dia a requisição não cabe.

Esse é o Gulp, o estouro de contexto. Aí o provedor rejeita a requisição com um erro ou, pior, alguns servidores cortam em silêncio a parte mais antiga para caber. A parte mais antiga é onde o cliente disse de qual pedido estava falando.

A solução

Antes de cada chamada ao modelo, o loop confere o tamanho da requisição. Passada uma linha, colocada abaixo do limite real para que a resposta ainda tenha espaço, ele encolhe o histórico. Há três jeitos comuns de fazer isso:

  • Truncar resultados de ferramentas antigos

    Trocar um resultado grande que o modelo já usou por uma nota de uma linha e uma prévia curta.

    Quase de graça, e os resultados de ferramentas costumam ser a parte mais pesada do histórico. Se o modelo precisar dos detalhes de novo, ele chama a ferramenta outra vez.

  • Descartar trocas antigas

    Remover os pedidos mais antigos com tudo o que veio depois deles, até o pedido seguinte.

    Também é de graça, mas o modelo esquece completamente essa parte da conversa. Mantenha o primeiríssimo pedido, porque ele muitas vezes diz do que se trata o chat inteiro.

  • Resumir com o modelo

    Enviar a parte antiga ao modelo uma vez e colocar o resumo dele no lugar dessas mensagens.

    Mantém o sentido, mas custa uma chamada a mais, e um resumo pode deixar de fora, sem avisar, o único número que importava.

Seja qual for a escolha, valem as mesmas regras: comece pelas mensagens mais antigas, não mexa nos últimos pedidos e pare assim que couber. O astorlm faz as duas primeiras, nessa ordem. Primeiro ele trunca os resultados de ferramentas antigos, e só descarta mensagens se isso não bastou.

O elenco

O mesmo elenco de sempre, desta vez num poço de blocos que caem.

O poço janela de contexto
Tudo o que uma requisição consegue carregar. Se a pilha chegar ao topo, a requisição não cabe.
Os blocos mensagens
Um por mensagem, do tamanho dos seus tokens. O chão é o system prompt: ele vai em toda requisição e nunca é compactado.
A linha vermelha limiar
80% da janela. Passou dela, o loop compacta antes de chamar o modelo.
O martelo o compactador
Reduz o resultado de ferramenta mais antigo a uma nota de uma linha, e tudo o que está acima assenta.
KEEP keepRecentTurns
Os dois últimos pedidos e tudo o que veio depois deles. O martelo nunca toca neles.
SENT a conta
Os tokens enviados até agora, somando todas as chamadas. Veja quanto cresce por turno antes e depois do martelo.

No painel EventBus, a linha compact marca o otimizador em ação. O astorlm não emite um evento para isso; ele escreve “Context optimized” no seu logger. Repare onde ela cai: depois que o pedido entra no histórico, antes do turn_start.

O código

Com astorlm: A compactação vem ligada por padrão, dimensionada a partir do provedor. Passe contextOptimizer para definir a janela real, a linha e quantos pedidos recentes manter.

Do zero: O loop do nível 2, com um histórico que vive entre pedidos e uma chamada a compact() antes de cada chamada ao modelo. O nível 1 trunca resultados de ferramentas antigos, o nível 2 descarta trocas antigas inteiras.

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

const getOrder = tool({
  name: 'get_order',
  description: 'Everything about one order: items, shipping, invoice.',
  schema: z.object({ order: z.number().int() }),
  execute: async ({ order }) => loadOrder(order), // your code
})

const searchParts = tool({
  name: 'search_parts',
  description: 'Search the parts catalog, with stock and price for each match.',
  schema: z.object({ query: z.string() }),
  execute: async ({ query }) => searchCatalog(query), // your code
})

const createReturn = tool({
  name: 'create_return',
  description: 'Open a return for an order and ship a replacement part.',
  schema: z.object({ order: z.number().int(), part: z.string() }),
  execute: async ({ order, part }) => openReturn(order, part), // 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
  }),
  tools: [getOrder, searchParts, createReturn],
  maxTurns: 10,
  // Compaction is on by default, sized from the provider. OpenAIProvider assumes a
  // 128,000-token window, so on a small local model, say how big it really is.
  contextOptimizer: {
    maxTokens: 8000,
    compressThreshold: 0.8, // compact once the request passes 80% of the window
    keepRecentTurns: 2, // never touch the last two requests, or anything after them
  },
  // There's no event for compaction: astorlm logs "Context optimized…" when it happens.
  logger: console,
})

// One agent, one history: every run() adds to it, and the optimizer checks it before each model call.
await agent.run('Hi! My order #4471 came with a bent front wheel. Can you help?')
await agent.run('Is that same wheel in stock?')
const last = await agent.run('Great. Open a return for my order and ship me the new wheel.')
console.log(last.content)

O que observar

  • Informe a janela real. O OpenAIProvider do astorlm assume 128.000 tokens. Num modelo local de 8.000 tokens, o otimizador esperaria por uma linha que o modelo nunca alcança.
  • Nunca separe uma chamada de ferramenta do seu resultado. Um resultado de ferramenta cuja chamada foi descartada faz a maioria das APIs rejeitar a requisição inteira. Descarte trocas inteiras, de um pedido até o seguinte.
  • Truncar só é seguro se a ferramenta puder rodar de novo. Se um resultado não pode ser obtido duas vezes, como um comprovante de pagamento, guarde a parte que importa na resposta, ou salve fora do histórico.
  • Diga a quem resume o que precisa sobreviver. Números de pedido, números de peça, decisões. Um resumo que se lê bem ainda pode perder o único fato de que o próximo turno precisa.
  • A compactação só conta o que você envia. Estimar quatro caracteres por token é suficiente para decidir quando agir. Deixe espaço suficiente abaixo da linha para a resposta.