Contexte : le raisonnement jamais renvoyé ne compte plus, et la compaction ne part plus trop tôt

CtxUsed valait usage.prompt_tokens + généré, raisonnement compris. Or Loki
ne renvoie jamais ce raisonnement au modèle (Message n'a pas de champ pour
lui) : la requête suivante pesait 1 à 8 k jetons de moins que le compte, et
la compaction, qui perd de l'information, partait vers 44-47 k au lieu de
49 k sur une fenêtre de 65 k. Rien ne change dans les requêtes : seul le
compte qui décide de compacter.

- Comptage : seulement les morceaux reasoning_content séparés par le
  llama-server local, remis à zéro à chaque tentative. Le <think> découpé
  chez nous reste dans le message renvoyé pendant le tour, il n'est pas
  retiré ; l'API externe garde l'ancien calcul. Un morceau vaut au plus un
  jeton : on ne peut que sous-estimer, le sens sans danger.
- Début de tour : le message utilisateur (et la file) arrivé depuis la
  dernière mesure s'ajoute en estimation, comme en cours de tour.
- Vérificateur : sa trace isolée écrasait CtxUsed avec sa petite taille, et
  la fin de tour décidait sur ce chiffre-là. Plus maintenant ; la jauge de
  l'UI ne bouge pas non plus et suit le même calcul que le serveur.
- Marge de génération : le compte gonflé offrait une marge par accident.
  Elle est remplacée par une vraie garde — compacter si 1,2 × la plus
  longue génération récente ne tient plus dans la fenêtre. Plancher et
  marge seuls ne devancent jamais le seuil de 75 %.
- Fenêtre pleine en plein raisonnement (finish « length » au-delà du
  seuil) : compaction puis étape rejouée, au lieu du nudge « arrête de
  raisonner ».
- Journal [ctx] : estimation contre compte réel au-delà de 2 % d'écart,
  chaque « length » et chaque nudge, pour vérifier qu'ils n'augmentent pas.
- Le seuil reste à 75 % : la réserve fixe proposée (toolResultMax/3 + 6 k)
  laissait trop peu de place sans budget de raisonnement ni max_tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 00:45:55 +02:00
1 parent 18a723a671
commit d6023ef8a0
7 files changed
+514 -25

No files matched your search

+30 -9
View File
@@ -116,7 +116,7 @@ func (c *Conversation) runVerifyPass(ctx context.Context, caps Caps, temperature
// dans le moteur aussi : effacé à la fin de la passe (llm_slots.go).
defer engineSideJob()()
_, _ = runChat(withPerfKind(ctx, perfVerify), msgs, temperature, vcaps, func(ev StreamEvent) bool {
c.forwardStream(ev, epoch)
c.forwardStream(ev, epoch, true)
return true
})
}
@@ -165,7 +165,11 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
final = append([]Message{{Role: "system", Content: sp}}, final...)
}
var content strings.Builder
sawUsage := false
extra, _ := runChat(ctx, InjectSkills(final, caps), temperature, caps, func(ev StreamEvent) bool {
if ev.Stats != nil && ev.Stats.PromptTokensTotal > 0 {
sawUsage = true
}
if ev.Content != "" {
content.WriteString(ev.Content)
}
@@ -175,7 +179,7 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
if ev.Ask != nil {
asked = true
}
c.forwardStream(ev, epoch)
c.forwardStream(ev, epoch, false)
return true
})
c.mu.Lock()
@@ -184,6 +188,9 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
if s := content.String(); strings.TrimSpace(s) != "" {
c.Messages = append(c.Messages, Message{Role: "assistant", Content: s})
}
if sawUsage {
c.ctxUsedLen = len(c.Messages) // même règle que generate
}
}
c.mu.Unlock()
c.persist()
@@ -193,7 +200,13 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
// forwardStream relaie les événements d'un runChat secondaire (vérification,
// correction) vers le journal d'affichage — même mapping que generate(), sans
// la gestion de compaction (ces passes n'en déclenchent pas : contexte court).
func (c *Conversation) forwardStream(ev StreamEvent, epoch int) {
//
// isolated : la passe tourne sur sa propre trace (vérificateur), PAS sur
// l'historique de la discussion. Ses comptes ne disent rien du contexte de
// celle-ci : ils écrasaient CtxUsed avec la petite taille du vérificateur, et
// le contrôle de fin de tour — puis celui du tour suivant — décidaient sur ce
// chiffre-là. La jauge de l'UI ne doit pas bouger non plus (ctx_isolated).
func (c *Conversation) forwardStream(ev StreamEvent, epoch int, isolated bool) {
switch {
case ev.Err != nil:
c.appendDelta(epoch, map[string]any{"error": ev.Err.Error()})
@@ -223,13 +236,21 @@ func (c *Conversation) forwardStream(ev StreamEvent, epoch int) {
case ev.Ask != nil:
c.appendDelta(epoch, map[string]any{"ask": ev.Ask})
case ev.Stats != nil:
if ev.Stats.PromptTokensTotal > 0 {
c.mu.Lock()
if c.epoch == epoch {
c.CtxUsed = ev.Stats.PromptTokensTotal + ev.Stats.GenTokens
}
c.mu.Unlock()
if isolated {
c.appendDelta(epoch, map[string]any{"stats": ev.Stats, "ctx_isolated": true})
break
}
local := !externalActive()
c.mu.Lock()
if c.epoch == epoch {
if ev.Stats.PromptTokensTotal > 0 {
c.CtxUsed = ev.Stats.ctxAfter()
}
if local {
c.genPeak = max(c.genPeak, ev.Stats.GenTokens)
}
}
c.mu.Unlock()
c.appendDelta(epoch, map[string]any{"stats": ev.Stats})
case ev.DropReasoning:
c.appendDelta(epoch, map[string]any{"drop_reasoning": true})