mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
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:
1 parent
18a723a671
commit
d6023ef8a0
7 files changed
+514
-25
No files matched your search
@@ -728,6 +728,13 @@ type StatsEvent struct {
|
||||
// Taille TOTALE du prompt traité ce tour (préfixe caché compris), issue de
|
||||
// `usage.prompt_tokens`. 0 si le backend ne renvoie pas d'usage.
|
||||
PromptTokensTotal int `json:"prompt_tokens_total,omitempty"`
|
||||
// Morceaux de raisonnement reçus SÉPARÉS par le moteur (reasoning_content),
|
||||
// llama-server local seulement. Loki ne renvoie jamais ce raisonnement au
|
||||
// modèle (Message n'a pas de champ pour lui) : ces jetons font partie de
|
||||
// GenTokens mais pas de la requête suivante. Un morceau vaut au plus un
|
||||
// jeton, donc ce compte ne peut que sous-estimer — le sens sans danger : on
|
||||
// compacte au pire comme avant, jamais trop tard. Voir ctxAfter.
|
||||
ReasoningTokens int `json:"reasoning_tokens,omitempty"`
|
||||
// Conseil ponctuel quand le cache de prompts n'a pas tenu après un travail
|
||||
// annexe (voir noteEnginePrompt). Vide la plupart du temps.
|
||||
CacheHint string `json:"cache_hint,omitempty"`
|
||||
@@ -747,6 +754,15 @@ type StatsEvent struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
}
|
||||
|
||||
// ctxAfter : taille du contexte que la requête SUIVANTE renverra réellement au
|
||||
// moteur — le prompt de cette complétion plus ce qu'elle a généré, MOINS son
|
||||
// raisonnement, jamais renvoyé. Compter ce raisonnement gonflait le contexte de
|
||||
// 1 à 8 k jetons par étape, et la compaction (avec perte) partait trop tôt.
|
||||
// Borné par GenTokens : le compte de morceaux ne retire jamais plus que généré.
|
||||
func (s StatsEvent) ctxAfter() int {
|
||||
return s.PromptTokensTotal + s.GenTokens - min(s.GenTokens, s.ReasoningTokens)
|
||||
}
|
||||
|
||||
// ChatCallback receives stream events. Return false to abort the stream.
|
||||
type ChatCallback func(StreamEvent) bool
|
||||
|
||||
@@ -972,6 +988,18 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
// par rendre la main plutôt que faire attendre.
|
||||
const maxNudges = 2
|
||||
nudgeCount := 0
|
||||
// Génération la plus longue de ce tour (llama-server local), pour la garde
|
||||
// de marge de compactNeeded : le raisonnement n'étant plus compté dans le
|
||||
// contexte, il faut s'assurer AUTREMENT qu'une étape aussi longue tient
|
||||
// encore dans la fenêtre.
|
||||
peakGen := 0
|
||||
// Complétion coupée par la fenêtre pleine (finish_reason « length ») :
|
||||
// compactée puis rejouée une fois, au lieu du nudge « arrête de raisonner »
|
||||
// qui raccourcirait la réflexion du modèle.
|
||||
lengthReplays := 0
|
||||
// Contexte estimé pour la requête suivante (en-tour), comparé au compte
|
||||
// réel du moteur quand il arrive — journalisé s'il s'en écarte.
|
||||
lastEst := 0
|
||||
// Garde-fou « appel d'outil écrit en texte » (code_retry.go) : relances
|
||||
// consécutives bornées, compteur remis à zéro après chaque outil exécuté.
|
||||
patternRetries := 0
|
||||
@@ -1427,6 +1455,15 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
// Backend already separates reasoning — trust it, disable our split.
|
||||
sawReasoningField = true
|
||||
thinkOpen = false
|
||||
// Compté ICI seulement : le raisonnement découpé chez nous (<think>
|
||||
// en ligne, plus bas) reste dans assistantContent et repart dans
|
||||
// l'étape suivante — le retirer sous-estimerait le contexte. Pas
|
||||
// pour une API externe : son découpage en morceaux ne dit rien du
|
||||
// nombre de jetons, et son preset doit rester tel quel. stats est
|
||||
// propre à cette tentative : une relance repart de zéro.
|
||||
if !ep.External {
|
||||
stats.ReasoningTokens++
|
||||
}
|
||||
if !scb(StreamEvent{Reasoning: ch.Delta.ReasoningContent}) {
|
||||
aborted = true
|
||||
break
|
||||
@@ -1560,6 +1597,16 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
s.LostAlert = perfAlert(rec, perfLostAlertAt(chatCfg))
|
||||
}
|
||||
cb(StreamEvent{Stats: &s})
|
||||
if !ep.External {
|
||||
peakGen = max(peakGen, stats.GenTokens)
|
||||
if lastEst > 0 && stats.PromptTokensTotal > 0 {
|
||||
logCtxEstimate("en-tour", lastEst, stats.PromptTokensTotal)
|
||||
}
|
||||
}
|
||||
lastEst = 0
|
||||
}
|
||||
if finishReason == "length" {
|
||||
logCtx("finish=length prompt=%d généré=%d raisonnement=%d fenêtre=%d", stats.PromptTokensTotal, stats.GenTokens, stats.ReasoningTokens, ctxWindow())
|
||||
}
|
||||
if aborted {
|
||||
return extra, nil
|
||||
@@ -1977,12 +2024,14 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
// comptés. Sans eux, une étape qui lit plusieurs gros fichiers d'un coup
|
||||
// passait de 60 % à plus de 130 % de la fenêtre sans jamais compacter.
|
||||
// Serveur sans usage (base 0) : on laisse 0, compactWouldTrigger estime
|
||||
// alors TOUT l'historique — étape comprise.
|
||||
// alors TOUT l'historique — étape comprise. Le raisonnement de l'étape
|
||||
// est retiré (ctxAfter) : il ne repart pas dans la requête suivante.
|
||||
used := 0
|
||||
if base := stats.PromptTokensTotal + stats.GenTokens; base > 0 {
|
||||
if base := stats.ctxAfter(); base > 0 {
|
||||
used = base + estimateTokens(messages[stepStart:])
|
||||
lastEst = used
|
||||
}
|
||||
if compactWouldTrigger(messages, used) {
|
||||
if compactNeeded(messages, used, peakGen) {
|
||||
yes, no := true, false
|
||||
cb(StreamEvent{Compacting: &yes})
|
||||
c, changed := compactMessages(ctx, messages, caps)
|
||||
@@ -2020,8 +2069,31 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
// Filet de sécurité (le vrai fix est le prompt court, voir baseSystemPrompt) :
|
||||
// si un modèle « pense sans agir » malgré tout, on le relance avec une
|
||||
// consigne impérative au lieu d'afficher « pas de réponse ».
|
||||
//
|
||||
// Sauf si la complétion a été COUPÉE par une fenêtre pleine
|
||||
// (« length » au-delà du seuil de compaction) : le modèle n'a pas
|
||||
// « pensé sans agir », il n'a plus eu de place. Lui dire d'arrêter de
|
||||
// raisonner raccourcirait sa réflexion et alourdirait encore le
|
||||
// contexte ; on compacte et on rejoue l'étape, une fois.
|
||||
if finishReason == "length" && !ep.External && lengthReplays < 1 &&
|
||||
compactWouldTrigger(messages, stats.PromptTokensTotal+stats.GenTokens) {
|
||||
lengthReplays++
|
||||
yes, no := true, false
|
||||
cb(StreamEvent{Compacting: &yes})
|
||||
c, changed := compactMessages(ctx, messages, caps)
|
||||
cb(StreamEvent{Compacting: &no})
|
||||
logCompact("fenêtre-pleine", stats.PromptTokensTotal+stats.GenTokens, messages, c, changed)
|
||||
if changed {
|
||||
cb(StreamEvent{DropReasoning: true})
|
||||
messages = c
|
||||
extra = nil
|
||||
cb(StreamEvent{NewHistory: append([]Message(nil), messages...)})
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(tools) > 0 && !disableTools && nudgeCount < maxNudges {
|
||||
nudgeCount++
|
||||
logCtx("relance « pensé sans agir » n°%d (finish=%s)", nudgeCount, finishReason)
|
||||
// Le raisonnement de ce tour avorté ne mène à rien : on demande à
|
||||
// l'UI de l'effacer avant de relancer, pour ne pas afficher deux
|
||||
// blocs de réflexion successifs.
|
||||
|
||||
Reference in new issue
Block a user