mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Deux défauts révélés par un tour d'agent d'une heure (~50 appels d'outils) sur un modèle très quantifié. 1. La vitesse affichée sous les réponses tombait de 17 tok/s à 0,9 au fil du tour, ce qui donnait à croire que le moteur s'effondrait. Il n'en était rien : un tour d'agent ouvre une bulle NEUVE après chaque appel d'outil (le flux repasse contentEl à null), mais les compteurs n'étaient jamais remis à zéro. Chaque bulle affichait donc le CUMUL de tout le tour divisé par le temps écoulé depuis le tout premier token — exécution des outils, pages web et prefill compris. La vitesse convergeait mécaniquement vers « tokens générés ÷ durée totale du tour ». Les compteurs sont maintenant remis à zéro à la CRÉATION de la bulle, ce qui couvre tout chemin qui en ouvre une neuve, aujourd'hui comme demain. Rejoué sur un tour synthétique où le moteur décode à 20 tok/s constants entre deux outils de deux minutes : 20,5 / 0,6 / 0,5 tok/s avant, 20,5 sur les trois bulles après. La durée « travail », elle, reste bien celle du tour entier — c'est sa définition. 2. Rien n'exerçait de pression sur un tour qui tourne en rond. Le plafond d'itérations avait été retiré en v0.6.3 (il coupait des recherches légitimes) et la déduplication d'appels ne rattrape pas ce cas : sa clé est « nom + arguments bruts », or relire le même fichier par tranches (`sed -n '1,80p'` puis `sed -n '80,160p'`) produit des clés différentes. D'où un budget SOUPLE : au-delà de 24 appels d'outils sur un tour, on rappelle au modèle combien il en a déjà faits et on lui demande de conclure. Le rappel revient à chaque palier en durcissant le ton, et ne coupe jamais le tour. Il est ajouté EN FIN d'historique, ce qui laisse intact le préfixe déjà en cache côté llama-server, et n'est pas persisté. `AGENT_BUDGET` dans config.env règle le palier, `off` le désactive. Élargir plutôt la clé de déduplication à la CIBLE de l'appel a été écarté : deux tranches d'un même fichier renvoient un contenu différent, les confondre casserait toute lecture paginée légitime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5UndZ9DedRXPuAoRXbmDb
92 lines
4.1 KiB
Go
92 lines
4.1 KiB
Go
package loki
|
||
|
||
// llm_budget.go — budget SOUPLE d'appels d'outils par tour.
|
||
//
|
||
// Le plafond dur (TOOL_LIMIT) et l'anti-boucle ont été retirés en v0.6.3 : ils
|
||
// coupaient des tours parfaitement légitimes, une recherche enchaînant sans
|
||
// problème des dizaines d'appels. Mais il ne restait plus RIEN entre « le
|
||
// modèle converge » et « le modèle tourne en rond pendant une heure » — vu en
|
||
// production : ~50 appels d'outil, 55 minutes, le modèle relisant cinq fois les
|
||
// mêmes fonctions pour re-dériver une conclusion qu'il avait déjà écrite.
|
||
//
|
||
// La déduplication d'appels identiques (repeatedCallResult) ne rattrape pas ce
|
||
// cas : sa clé est « nom + arguments bruts », et relire le même fichier par
|
||
// tranches (`sed -n '1,80p'` puis `sed -n '80,160p'`) produit des clés
|
||
// différentes. Élargir cette clé à la CIBLE serait pire que le mal — deux
|
||
// tranches d'un même fichier renvoient un contenu différent, les confondre
|
||
// casserait toute lecture paginée légitime.
|
||
//
|
||
// D'où un budget qui ne coupe rien : au-delà d'un palier, on RAPPELLE au modèle
|
||
// combien d'appels il a déjà faits et on lui demande de conclure. Le tour reste
|
||
// entièrement sous son contrôle — c'est de la pression, pas une barrière.
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// agentBudgetDefault : nombre d'appels d'outils au-delà duquel un tour cesse
|
||
// d'être une recherche et commence à ressembler à du sur-place. Assez haut pour
|
||
// qu'une vraie enquête (lire dix fichiers, croiser trois pages web) passe sans
|
||
// jamais voir le rappel.
|
||
const agentBudgetDefault = 24
|
||
|
||
// agentBudget lit le palier dans config.env (AGENT_BUDGET). 0 — ou une valeur
|
||
// off/false/no/non — désactive complètement le mécanisme.
|
||
func agentBudget() int {
|
||
v := strings.ToLower(strings.TrimSpace(ReadConfig()["AGENT_BUDGET"]))
|
||
switch v {
|
||
case "":
|
||
return agentBudgetDefault
|
||
case "off", "false", "no", "non", "disable", "disabled":
|
||
return 0
|
||
}
|
||
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
|
||
return n
|
||
}
|
||
return agentBudgetDefault
|
||
}
|
||
|
||
// budgetNudge renvoie le rappel à injecter quand le tour vient de franchir un
|
||
// palier, ou "" s'il n'y a rien à dire.
|
||
//
|
||
// calls : appels d'outils exécutés depuis le début du tour
|
||
// budget : taille d'un palier (0 = mécanisme désactivé)
|
||
// sent : rappels déjà envoyés dans ce tour
|
||
//
|
||
// Les paliers sont à budget, 2×budget, 3×budget… : la pression revient donc
|
||
// régulièrement au lieu de s'éteindre après un avertissement ignoré. Le coût en
|
||
// contexte est négligeable (une phrase toutes les 24 requêtes) et le message est
|
||
// AJOUTÉ EN FIN d'historique, ce qui laisse intact le préfixe déjà en cache côté
|
||
// llama-server.
|
||
//
|
||
// Le texte est en anglais comme le reste du protocole d'outils : c'est la langue
|
||
// dans laquelle les modèles suivent le mieux une consigne impérative, quelle que
|
||
// soit celle de la conversation.
|
||
func budgetNudge(calls, budget, sent int) string {
|
||
if budget <= 0 || calls < budget*(sent+1) {
|
||
return ""
|
||
}
|
||
n := fmt.Sprintf("%d tool calls", calls)
|
||
switch sent {
|
||
case 0:
|
||
return "[system] You have made " + n + " in this single turn. That is a lot: you almost certainly have enough to answer already. " +
|
||
"Stop exploring and write your final answer now, unless exactly one specific call is genuinely still missing."
|
||
case 1:
|
||
return "[system] " + n + " now, and you are going in circles. Re-reading a file you have already read, or re-deriving a conclusion you have already written, adds nothing. " +
|
||
"Answer NOW with what you have. If something remains uncertain, say so in the answer instead of investigating further."
|
||
default:
|
||
return "[system] " + n + ". Stop. Write your final answer in this message. Do not call another tool."
|
||
}
|
||
}
|
||
|
||
// logBudget trace UNE ligne par rappel sur la sortie d'erreur (donc dans
|
||
// `journalctl -u loki-ui`), comme logCompact. Sans ça, un tour qui part en
|
||
// vrille est invisible côté serveur : on ne voit qu'une génération qui dure.
|
||
func logBudget(calls, budget, sent int) {
|
||
fmt.Fprintf(os.Stderr, "[agent] %d appels d'outils sur ce tour (palier=%d) — rappel n°%d envoyé au modèle\n",
|
||
calls, budget, sent)
|
||
}
|