Saltar al contenido
astorlm
Idioma: Español
← Mapa

Nivel 7

La mochila se llena

Cada turno vuelve a enviar todo el historial al modelo. Si un chat sigue lo suficiente, deja de entrar. La compactación achica las partes más viejas antes de que eso pase.
1/30 Mensajes:
  • user
  • assistant
  • tool_result
El asistente de soporte de una bicicletería, sobre un modelo que lee como máximo 8.000 tokens. El pozo es esa ventana. Cada mensaje cae como un bloque, y el system prompt es el piso. La línea roja está en el 80%.

EventBus

El problema

Un modelo solo puede leer cierta cantidad de una vez. Ese límite es su ventana de contexto, contada en tokens (pedazos de palabras, de unos cuatro caracteres cada uno): 8.000 en muchos modelos locales chicos, unos cientos de miles en los grandes modelos alojados. Todo lo de un pedido tiene que entrar ahí: el system prompt, cada mensaje hasta ahora, cada resultado de herramienta y lugar para la respuesta.

El modelo no recuerda nada entre llamadas, así que el bucle envía todo el historial en cada turno. Un chat de soporte que consulta un pedido y busca en un catálogo acumula miles de tokens de resultados que el modelo ya usó. Cada turno es más lento y cuesta más que el anterior. Y un día el pedido no entra.

Ese es Gulp, el desborde de contexto. Entonces el proveedor rechaza el pedido con un error o, peor, algunos servidores cortan en silencio la parte más vieja para que entre. La parte más vieja es donde el cliente dijo de qué pedido hablaba.

La solución

Antes de cada llamada al modelo, el bucle revisa qué tan grande es el pedido. Pasada una línea, puesta por debajo del límite real para que la respuesta todavía tenga lugar, achica el historial. Hay tres formas comunes de hacerlo:

  • Truncar resultados de herramientas viejos

    Reemplazar un resultado grande que el modelo ya usó por una nota de una línea y una vista previa corta.

    Casi gratis, y los resultados de herramientas suelen ser la parte más pesada del historial. Si el modelo vuelve a necesitar los detalles, llama otra vez a la herramienta.

  • Descartar intercambios viejos

    Quitar los pedidos más antiguos con todo lo que vino después de ellos, hasta el siguiente pedido.

    También es gratis, pero el modelo olvida por completo esa parte de la conversación. Conserva el primerísimo pedido, porque a menudo dice de qué trata todo el chat.

  • Resumir con el modelo

    Enviar la parte vieja al modelo una vez, y poner su resumen en lugar de esos mensajes.

    Conserva el sentido, pero cuesta una llamada extra, y un resumen puede dejar afuera en silencio el único número que importaba.

Elijas la que elijas, se aplican las mismas reglas: empieza por los mensajes más viejos, no toques los últimos pedidos y detente apenas entre. astorlm hace las dos primeras, en ese orden. Primero trunca los resultados de herramientas viejos, y solo descarta mensajes si eso no alcanzó.

El elenco

El mismo elenco de siempre, esta vez en un pozo de bloques que caen.

El pozo ventana de contexto
Todo lo que puede llevar un pedido. Si la pila llega arriba, el pedido no entra.
Los bloques mensajes
Uno por mensaje, del tamaño de sus tokens. El piso es el system prompt: va con cada pedido y nunca se compacta.
La línea roja umbral
El 80% de la ventana. Pasada esa línea, el bucle compacta antes de llamar al modelo.
El martillo el compactador
Reduce el resultado de herramienta más viejo a una nota de una línea, y todo lo de arriba se asienta.
KEEP keepRecentTurns
Los dos últimos pedidos y todo lo que vino después. El martillo nunca los toca.
SENT la cuenta
Los tokens enviados hasta ahora, sumando todas las llamadas. Mira cuánto crece por turno antes y después del martillo.

En el panel EventBus, la línea compact marca al optimizador en acción. astorlm no emite un evento para eso; escribe “Context optimized” en tu logger. Fíjate dónde cae: después de que el pedido entra en el historial, antes de turn_start.

El código

Con astorlm: La compactación viene activada por defecto, dimensionada según el proveedor. Pasa contextOptimizer para fijar la ventana real, la línea y cuántos pedidos recientes conservar.

Desde cero: El bucle del nivel 2, con un historial que vive entre pedidos y una llamada a compact() antes de cada llamada al modelo. El nivel 1 trunca resultados de herramientas viejos, el nivel 2 descarta intercambios viejos enteros.

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)

Qué vigilar

  • Dile la ventana real. El OpenAIProvider de astorlm asume 128.000 tokens. Con un modelo local de 8.000 tokens, el optimizador esperaría una línea a la que el modelo nunca llega.
  • Nunca separes una llamada a herramienta de su resultado. Un resultado de herramienta cuya llamada se descartó hace que la mayoría de las APIs rechacen todo el pedido. Descarta intercambios enteros, desde un pedido hasta el siguiente.
  • Truncar solo es seguro si la herramienta puede volver a ejecutarse. Si un resultado no se puede obtener dos veces, como un comprobante de pago, conserva la parte que importa en la respuesta, o guárdalo fuera del historial.
  • Dile al que resume qué tiene que sobrevivir. Números de pedido, números de pieza, decisiones. Un resumen que se lee bien igual puede perder el único dato que necesita el turno siguiente.
  • La compactación solo cuenta lo que envías. Estimar cuatro caracteres por token alcanza para decidir cuándo actuar. Deja suficiente lugar debajo de la línea para la respuesta.