Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 7

Le sac à dos déborde

Chaque tour renvoie tout l'historique au modèle. Faites durer un chat assez longtemps, et il ne tient plus. La compaction réduit les parties les plus anciennes avant que cela n'arrive.
1/30 Messages :
  • user
  • assistant
  • tool_result
L'assistant support d'un magasin de vélos, sur un modèle qui lit 8 000 tokens au maximum. Le puits, c'est cette fenêtre. Chaque message tombe comme un bloc, et le system prompt est le sol. La ligne rouge est à 80 %.

EventBus

Le problème

Un modèle ne peut lire qu'une certaine quantité à la fois. Cette limite, c'est sa fenêtre de contexte, comptée en tokens (des morceaux de mots, d'environ quatre caractères chacun) : 8 000 pour beaucoup de petits modèles locaux, quelques centaines de milliers pour les gros modèles hébergés. Tout ce qu'il y a dans une requête doit y tenir : le system prompt, chaque message jusqu'ici, chaque résultat d'outil, et de la place pour la réponse.

Le modèle ne retient rien d'un appel à l'autre, donc la boucle envoie tout l'historique à chaque tour. Un chat d'assistance qui consulte une commande et cherche dans un catalogue accumule des milliers de tokens de résultats que le modèle a déjà utilisés. Chaque tour est plus lent et coûte plus cher que le précédent. Et un jour, la requête ne tient plus.

C'est Gulp, le débordement de contexte. Le fournisseur rejette alors la requête avec une erreur ou, pire, certains serveurs coupent discrètement la partie la plus ancienne pour qu'elle tienne. Or c'est dans la partie la plus ancienne que le client a dit de quelle commande il parlait.

La solution

Avant chaque appel au modèle, la boucle vérifie la taille de la requête. Au-delà d'une ligne, placée sous la vraie limite pour que la réponse ait encore de la place, elle réduit l'historique. Il y a trois façons courantes de le faire :

  • Tronquer les vieux résultats d'outils

    Remplacer un gros résultat que le modèle a déjà utilisé par une note d'une ligne et un court aperçu.

    Presque gratuit, et les résultats d'outils sont en général la partie la plus lourde de l'historique. Si le modèle a de nouveau besoin des détails, il rappelle l'outil.

  • Supprimer les vieux échanges

    Retirer les demandes les plus anciennes avec tout ce qui les a suivies, jusqu'à la demande suivante.

    Gratuit aussi, mais le modèle oublie complètement cette partie de la conversation. Gardez la toute première demande, qui dit souvent de quoi parle tout le chat.

  • Résumer avec le modèle

    Envoyer une fois la vieille partie au modèle, et mettre son résumé à la place de ces messages.

    Cela garde le sens, mais coûte un appel de plus, et un résumé peut laisser de côté, sans prévenir, le seul chiffre qui comptait.

Quelle que soit celle que vous choisissez, les mêmes règles s'appliquent : commencez par les messages les plus anciens, ne touchez pas aux dernières demandes, et arrêtez-vous dès que ça tient. astorlm fait les deux premières, dans cet ordre. Il tronque d'abord les vieux résultats d'outils, et ne supprime des messages que si cela n'a pas suffi.

Les personnages

Les mêmes personnages que d'habitude, cette fois dans un puits de blocs qui tombent.

Le puits fenêtre de contexte
Tout ce qu'une requête peut transporter. Si la pile atteint le haut, la requête ne tient pas.
Les blocs messages
Un par message, à la taille de ses tokens. Le sol, c'est le system prompt : il part avec chaque requête et n'est jamais compacté.
La ligne rouge seuil
80 % de la fenêtre. Au-delà, la boucle compacte avant d'appeler le modèle.
Le marteau le compacteur
Il réduit le plus ancien résultat d'outil à une note d'une ligne, et tout ce qui est au-dessus se tasse.
KEEP keepRecentTurns
Les deux dernières demandes et tout ce qui les suit. Le marteau n'y touche jamais.
SENT la facture
Les tokens envoyés jusqu'ici, sur tous les appels. Regardez de combien elle grimpe par tour avant et après le marteau.

Dans le panneau EventBus, la ligne compact signale l'optimiseur en action. astorlm n'émet pas d'événement pour cela ; il écrit « Context optimized » dans votre logger. Remarquez où elle tombe : après l'entrée de la demande dans l'historique, avant turn_start.

Le code

Avec astorlm : La compaction est activée par défaut, dimensionnée d'après le fournisseur. Passez contextOptimizer pour fixer la vraie fenêtre, la ligne et le nombre de demandes récentes à garder.

À partir de zéro : La boucle du niveau 2, avec un historique qui survit d'une demande à l'autre et un appel à compact() avant chaque appel au modèle. Le niveau 1 tronque les vieux résultats d'outils, le niveau 2 supprime de vieux échanges entiers.

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)

Points de vigilance

  • Indiquez la vraie fenêtre. L'OpenAIProvider d'astorlm suppose 128 000 tokens. Avec un modèle local de 8 000 tokens, l'optimiseur attendrait une ligne que le modèle n'atteint jamais.
  • Ne séparez jamais un appel d'outil de son résultat. Un résultat d'outil dont l'appel a été supprimé fait rejeter toute la requête par la plupart des API. Supprimez des échanges entiers, d'une demande à la suivante.
  • Tronquer n'est sûr que si l'outil peut être relancé. Si un résultat ne peut pas être obtenu deux fois, comme un reçu de paiement, gardez la partie importante dans la réponse, ou enregistrez-la hors de l'historique.
  • Dites au résumeur ce qui doit survivre. Numéros de commande, références de pièces, décisions. Un résumé qui se lit bien peut quand même perdre le seul fait dont le tour suivant a besoin.
  • La compaction ne compte que ce que vous envoyez. Estimer quatre caractères par token suffit pour décider quand agir. Laissez assez de place sous la ligne pour la réponse.