Files
Loki/internal/loki/chat_prompt_size_test.go
T
MichaelandClaude Opus 5.5 eaf5e313d9 Cache : corrections de relecture — la ligne MCP et les descriptions d'outils partagent un seul format
mcpPromptLine relisait le préfixe « [MCP: <serveur>] » des descriptions en le
réécrivant à la main : changer le format dans mcpTools aurait fait disparaître
la ligne en silence. Un nom de serveur contenant « ] » y était aussi coupé.

- mcpTagDescription pose le préfixe, mcpToolServer le relit : un seul endroit.
- mcpToolServer retient la coupure dont la forme assainie est celle du nom
  d'outil — « a] b » reste « a] b ».
- Les tests qui appellent EnabledTools isolent $LOKI_HOME : la configuration
  MCP de la machine de test (et ses connexions) ne fausse plus le cas « sans
  outil MCP » ni le budget du préambule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:34:36 +02:00

99 lines
4.6 KiB
Go

package loki
import (
"encoding/json"
"strings"
"testing"
)
// Budget du préambule envoyé À CHAQUE TOUR : prompt système + schémas des outils.
// Il est payé sur toute la conversation et sort du contexte utile.
//
// Ce test existe pour empêcher le regonflement. Le prompt a déjà été raccourci
// une fois pour une raison de comportement (un préambule verbeux fait
// sur-raisonner les modèles à reasoning, qui finissent leur tour sans appeler
// d'outil), puis une seconde fois pour le coût. Les deux fois, il avait
// regrossi ligne par ligne, chacune paraissant anodine.
//
// Approximation 1 tok ≈ 4 caractères : suffisante pour une alerte, et sans
// dépendance à un tokenizer.
//
// Le budget n'a été relevé que pour de VRAIS outils nouveaux, jamais pour des
// lignes de prompt — et chaque palier dit lequel :
//
// 7500 → 7900 : recall + recall_search (chat_recall.go), ~720 car. Le compactage
// cesse d'être destructif : le modèle ne relance plus les lectures
// et recherches déjà faites. Il récupère bien plus qu'il ne coûte.
// 7900 → 8600 : tracker (tracker.go), ~620 car. Un seul schéma pour les quatre
// actions — quatre outils auraient coûté quatre fois ça.
// 8600 → 11000 : les quatre outils task_* (tasks_tools.go), ~1290 car. L'IA se
// donne elle-même ses rappels et ses veilles ; sans eux il fallait
// passer par l'interface. Descriptions déjà au plus court, et un
// seul schéma par action (create/update/delete ne partagent pas
// assez d'arguments pour fusionner sans rendre chacun ambigu).
// Les ~500 car restants couvrent web_screenshot, déclaré dès que
// Playwright est présent — c'est le cas dans l'image, pas sur le
// runner de CI : le budget doit tenir pour l'image, la vraie.
// (Le pilotage de navigateur, lui, est HORS de ce décompte : ses
// schémas n'existent que si on arme son interrupteur.)
//
// Les ~200 car de prompt qui les accompagnent sont le seul écart
// assumé à la règle « pas de relèvement pour du prompt » : un outil
// qu'aucune ligne ne mentionne n'est jamais appelé de lui-même, et
// la ligne dit ce que les schémas ne disent pas (quand se planifier,
// et qu'un script planifié ne charge aucun modèle). Elle a été
// ramenée de trois lignes à une.
//
// Descriptions déjà réduites au strict nécessaire. La consigne d'origine tient
// pour la suite : ce budget ne se relève pas pour du prompt.
const promptCharBudget = 11000 // ~2750 tokens, tout allumé
func TestSystemPromptStaysLean(t *testing.T) {
// $LOKI_HOME isolé : les outils MCP configurés sur la machine de test (et
// leur ligne au préambule) ne comptent pas dans le budget de Loki.
testHome(t)
caps := Caps{Agent: true, Internet: true, Mem: MemAlways}
tools := EnabledTools(caps)
sp := baseSystemPrompt(caps, tools)
tb, err := json.Marshal(tools)
if err != nil {
t.Fatal(err)
}
total := len(sp) + len(tb)
t.Logf("prompt %d car (~%d tok) + outils %d car (~%d tok) = ~%d tok",
len(sp), len(sp)/4, len(tb), len(tb)/4, total/4)
if total > promptCharBudget {
t.Fatalf("préambule à %d car (~%d tok), budget %d car (~%d tok).\n"+
"Avant de relever le budget : les schémas d'outils pèsent le double du prompt,\n"+
"et une consigne écrite dans un schéma n'a pas à être répétée dans le prompt.",
total, total/4, promptCharBudget, promptCharBudget/4)
}
}
// Le prompt ne doit pas redevenir un catalogue d'outils : leurs schémas partent
// dans la MÊME requête et les décrivent déjà un par un.
func TestSystemPromptDoesNotRelistTools(t *testing.T) {
sp := baseSystemPrompt(Caps{Agent: true, Internet: true, Mem: MemAlways}, nil)
for _, line := range strings.Split(sp, "\n") {
l := strings.TrimSpace(line)
if !strings.HasPrefix(l, "- ") {
continue
}
// Une puce qui commence par un nom d'outil = un catalogue qui revient.
for _, tool := range []string{"bash ", "write ", "edit ", "mem_", "web_"} {
if strings.HasPrefix(l[2:], tool) {
t.Fatalf("le prompt réénumère un outil : %q", l)
}
}
}
}
// Sans agent ni mémoire, aucun préambule : un modèle de chat simple à qui on
// ordonne d'appeler des outils invente des appels textuels qui fuient dans la
// réponse.
func TestSystemPromptEmptyWithoutTools(t *testing.T) {
if sp := baseSystemPrompt(Caps{Mem: MemOff}, nil); sp != "" {
t.Fatalf("préambule non vide sans outils : %q", sp)
}
}