mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 10:48:35 +02:00
Projets : changer de projet ne recalcule plus tout le prompt
Repris d'AJEAN 0.15.8, adapté : chez nous le contexte projet n'est jamais persisté, il est injecté à chaque tour sous forme de messages système préfixés — ce qui rend le déplacement trivial et sans migration. LE PROBLÈME Sur un modèle hybride (Qwen3.5 et suivants, couches récurrentes), llama.cpp ne peut reprendre un prompt déjà calculé qu'à un point de sauvegarde, et il n'en pose qu'au DÉBUT d'un message utilisateur. La description du projet, l'index mémoire et l'index des trackers étaient fusionnés dans le bloc système : deux projets divergeaient AVANT le premier point de reprise, et le premier message après un changement de projet recalculait tout (l'amont mesure ~8 s pour 5 000 tokens sur un 27B). LE CORRECTIF normalizeSystemMessages sort ces messages (isProjectSystem, par leurs préfixes) du bloc système et les place, dans un bloc <project_context>, en tête du PREMIER message utilisateur de la séquence envoyée. Le système commun reste identique d'un projet à l'autre, donc en cache. L'historique persisté et l'affichage ne changent pas (copie, entrée non mutée — testé). InjectSkills ne fusionne plus son préambule DANS un message projet placé en tête (cas d'un preset sans prompt système) : il y aurait perdu son préfixe et serait resté dans le bloc commun. Le gain ne vaut qu'entre projets de même mode mémoire : la consigne mémoire fait partie du système commun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4602614be0
commit
626227f2f2
2 files changed
+117
-3
No files matched your search
@@ -244,7 +244,10 @@ func InjectSkills(msgs []Message, caps Caps) []Message {
|
||||
return msgs
|
||||
}
|
||||
prefix := strings.Join(parts, "\n\n")
|
||||
if len(msgs) > 0 && msgs[0].Role == "system" {
|
||||
// Un message PROJET en tête (pas de prompt de preset) ne doit pas absorber le
|
||||
// préambule : il perdrait son préfixe, et normalizeSystemMessages ne saurait
|
||||
// plus le sortir du bloc système commun (voir isProjectSystem).
|
||||
if len(msgs) > 0 && msgs[0].Role == "system" && !isProjectSystem(msgs[0]) {
|
||||
existing, _ := msgs[0].Content.(string)
|
||||
merged := append([]Message{{Role: "system", Content: prefix + "\n\n" + existing}}, msgs[1:]...)
|
||||
return merged
|
||||
@@ -287,15 +290,29 @@ func steerSystem(msgs []Message, hint string) []Message {
|
||||
// n'importe où n'est pas gêné de le recevoir en tête — la normalisation est donc
|
||||
// sans risque pour tous, et sans effet sur une conversation déjà normale (payload
|
||||
// identique, garanti par test).
|
||||
//
|
||||
// Ce qui dépend du PROJET (description, index mémoire, trackers) ne va PAS dans
|
||||
// ce bloc mais en tête du premier message utilisateur (AJEAN 0.15.8). Sur un
|
||||
// modèle hybride (Qwen3.5+, couches récurrentes), llama.cpp ne sait reprendre un
|
||||
// prompt qu'à un point de sauvegarde, et il n'en pose qu'au DÉBUT des messages
|
||||
// utilisateur. Laissé dans le système, le contexte projet faisait diverger le
|
||||
// prompt au milieu du bloc : changer de projet recalculait tout. Au début du 1er
|
||||
// message user, la divergence tombe pile sur un point de sauvegarde, après le
|
||||
// système commun, qui reste en cache.
|
||||
func normalizeSystemMessages(msgs []Message) []Message {
|
||||
var sys []string
|
||||
var sys, proj []string
|
||||
sawSystem := false
|
||||
rest := make([]Message, 0, len(msgs))
|
||||
for _, m := range msgs {
|
||||
if m.Role == "system" {
|
||||
if s, ok := m.Content.(string); ok {
|
||||
sawSystem = true
|
||||
if strings.TrimSpace(s) != "" {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
continue
|
||||
}
|
||||
if isProjectSystem(m) {
|
||||
proj = append(proj, s)
|
||||
} else {
|
||||
sys = append(sys, s)
|
||||
}
|
||||
continue
|
||||
@@ -310,6 +327,12 @@ func normalizeSystemMessages(msgs []Message) []Message {
|
||||
if !sawSystem {
|
||||
return msgs
|
||||
}
|
||||
if len(proj) > 0 {
|
||||
if !prependToFirstUser(rest, strings.Join(proj, "\n\n")) {
|
||||
// Pas encore de message user (cas théorique) : on les garde en système.
|
||||
sys = append(sys, proj...)
|
||||
}
|
||||
}
|
||||
// Des systèmes existaient mais tous vides : on les a retirés (un système vide
|
||||
// hors position 0 casserait tout autant), sans en réinsérer.
|
||||
if len(sys) == 0 {
|
||||
@@ -320,6 +343,44 @@ func normalizeSystemMessages(msgs []Message) []Message {
|
||||
return append(out, rest...)
|
||||
}
|
||||
|
||||
// isProjectSystem : message système propre au projet actif (description, index
|
||||
// mémoire, trackers), à sortir du bloc système commun (voir
|
||||
// normalizeSystemMessages).
|
||||
func isProjectSystem(m Message) bool {
|
||||
s, ok := m.Content.(string)
|
||||
if !ok || m.Role != "system" {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(s, projectContextPrefix) ||
|
||||
strings.HasPrefix(s, memIndexPrefix) ||
|
||||
strings.HasPrefix(s, trackerIndexPrefix)
|
||||
}
|
||||
|
||||
// prependToFirstUser place ctx en tête du premier message user de msgs (modifié
|
||||
// en place : msgs est déjà une copie, mais ses Content ne sont PAS recopiés —
|
||||
// on remplace donc la valeur, jamais on ne mute une tranche partagée).
|
||||
// Renvoie false s'il n'y a aucun message user.
|
||||
func prependToFirstUser(msgs []Message, ctx string) bool {
|
||||
block := "<project_context>\n" + ctx + "\n</project_context>\n\n"
|
||||
for i, m := range msgs {
|
||||
if m.Role != "user" {
|
||||
continue
|
||||
}
|
||||
switch c := m.Content.(type) {
|
||||
case string:
|
||||
msgs[i].Content = block + c
|
||||
case []any:
|
||||
msgs[i].Content = append([]any{map[string]any{"type": "text", "text": block}}, c...)
|
||||
case []map[string]any:
|
||||
msgs[i].Content = append([]map[string]any{{"type": "text", "text": block}}, c...)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// EnabledTools returns the tools to advertise on the next inference call.
|
||||
func EnabledTools(caps Caps) []Tool {
|
||||
tools := []Tool{}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Le contexte PROJET quitte le bloc système pour la tête du 1er message user :
|
||||
// le système commun reste identique d'un projet à l'autre (cache de prompt), et
|
||||
// l'historique d'origine n'est pas muté.
|
||||
func TestNormalizeMovesProjectContextToFirstUser(t *testing.T) {
|
||||
in := []Message{
|
||||
{Role: "system", Content: "Préambule commun"},
|
||||
{Role: "system", Content: projectContextPrefix + " — projet A"},
|
||||
{Role: "system", Content: memIndexPrefix + " for project A"},
|
||||
{Role: "user", Content: "bonjour"},
|
||||
{Role: "assistant", Content: "salut"},
|
||||
{Role: "user", Content: "suite"},
|
||||
}
|
||||
ref := append([]Message(nil), in...)
|
||||
out := normalizeSystemMessages(in)
|
||||
if len(out) != 4 || out[0].Content != "Préambule commun" {
|
||||
t.Fatalf("système commun attendu seul en tête : %#v", out)
|
||||
}
|
||||
first, _ := out[1].Content.(string)
|
||||
if !strings.HasPrefix(first, "<project_context>\n"+projectContextPrefix) ||
|
||||
!strings.Contains(first, memIndexPrefix) || !strings.HasSuffix(first, "bonjour") {
|
||||
t.Fatalf("contexte projet absent du 1er message user : %q", first)
|
||||
}
|
||||
if out[3].Content != "suite" {
|
||||
t.Fatal("seul le PREMIER message user reçoit le contexte")
|
||||
}
|
||||
if !reflect.DeepEqual(in, ref) {
|
||||
t.Fatal("l'entrée a été mutée (interdit)")
|
||||
}
|
||||
}
|
||||
|
||||
// Sans prompt de preset, le premier système est un message projet : le préambule
|
||||
// ne doit pas l'absorber (il perdrait son préfixe et resterait dans le système).
|
||||
func TestInjectSkillsKeepsProjectMessageSeparate(t *testing.T) {
|
||||
in := []Message{
|
||||
{Role: "system", Content: projectContextPrefix + " — projet A"},
|
||||
{Role: "user", Content: "bonjour"},
|
||||
}
|
||||
out := normalizeSystemMessages(InjectSkills(in, Caps{Agent: true}))
|
||||
if s, _ := out[0].Content.(string); out[0].Role != "system" || strings.Contains(s, projectContextPrefix) {
|
||||
t.Fatalf("le contexte projet ne doit pas rester dans le système : %#v", out[0])
|
||||
}
|
||||
if u, _ := out[1].Content.(string); !strings.Contains(u, projectContextPrefix) {
|
||||
t.Fatalf("contexte projet attendu dans le 1er message user : %#v", out[1])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user