mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 09:47:20 +02:00
v0.2.1 : anti-boucle + system prompt facon pi
- Nouveau system prompt structure facon pi (identite -> methode/guidelines -> date -> outils + conseils -> contexte machine/skills), injecte via InjectSkills. Corrige les boucles de raisonnement infinies. - Plafond de reflexion par defaut (REASONING_BUDGET=2048 au lieu de -1) : coupe une reflexion qui derape au lieu de tourner a l'infini. - Garde-fou anti-boucle dans runChat : stop si le modele relance exactement le meme appel d'outil plusieurs tours de suite. - Descriptions d'outils enrichies (run_shell, skill). - Bump version 0.2.0 -> 0.2.1.
This commit is contained in:
1 parent
e606206d90
commit
ca5d3f9197
6 files changed
+78
-10
No files matched your search
@@ -160,6 +160,7 @@ Tout vit sous **`$JEAN_HOME`** (défaut `/etc/jean` sur Linux/macOS, `%ProgramDa
|
||||
| `CUDA_VISIBLE_DEVICES` | GPU à utiliser (réglé par `jean gpu`) | tous |
|
||||
| `KV_TYPE` (`_K`/`_V`) | quantization du cache KV | — |
|
||||
| `REASONING` | passthrough du mode raisonnement | — |
|
||||
| `REASONING_BUDGET` | plafond de tokens de réflexion (anti-boucle) ; `-1` = illimité | `2048` |
|
||||
| `EXTRA_ARGS` | ajouté tel quel à `llama-server` | — |
|
||||
|
||||
La clé API (quand elle est définie avec `jean set-api-key`) est stockée dans `$JEAN_HOME/.api_key`, séparément de `config.env`.
|
||||
|
||||
@@ -86,6 +86,8 @@ func runShellTool() Tool {
|
||||
// skills are enabled). Merges with an existing system message if present.
|
||||
func InjectSkills(msgs []Message) []Message {
|
||||
var parts []string
|
||||
// Always-on decisive-agent preamble (anti-loop). See baseSystemPrompt.
|
||||
parts = append(parts, baseSystemPrompt())
|
||||
if mp := machineSystemPrompt(); mp != "" {
|
||||
parts = append(parts, mp)
|
||||
}
|
||||
@@ -234,6 +236,10 @@ func runChat(messages []Message, temperature float64, cb ChatCallback) error {
|
||||
// retry the same turn once with tools removed so the model answers in plain
|
||||
// text from the tool results already gathered, instead of dying mid-chat.
|
||||
disableTools := false
|
||||
// Anti-loop net: if the model re-emits the exact same tool call (name+args)
|
||||
// several times, it's stuck — break instead of spinning to the iteration cap.
|
||||
lastSig := ""
|
||||
repeatSig := 0
|
||||
for iter := 0; iter < 8; iter++ {
|
||||
payload := map[string]any{
|
||||
"model": "jean",
|
||||
@@ -447,6 +453,25 @@ func runChat(messages []Message, temperature float64, cb ChatCallback) error {
|
||||
assistant.Content = s
|
||||
}
|
||||
messages = append(messages, assistant)
|
||||
// Loop guard: same exact call(s) as last turn? Count it; on the 3rd
|
||||
// identical turn, stop so we don't churn the same command forever.
|
||||
sig := strings.Builder{}
|
||||
for _, tc := range tcs {
|
||||
sig.WriteString(tc.Function.Name)
|
||||
sig.WriteByte('\x00')
|
||||
sig.WriteString(tc.Function.Arguments)
|
||||
sig.WriteByte('\n')
|
||||
}
|
||||
if s := sig.String(); s == lastSig {
|
||||
repeatSig++
|
||||
if repeatSig >= 2 {
|
||||
cb(StreamEvent{Content: "\n\n[stop: appel d'outil répété en boucle — " + tcs[0].Function.Name + "]"})
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
lastSig = s
|
||||
repeatSig = 0
|
||||
}
|
||||
// 2. Execute each tool locally and append a "tool" reply.
|
||||
for _, tc := range tcs {
|
||||
var args map[string]any
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.2.0"
|
||||
const Version = "0.2.1"
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
|
||||
@@ -61,11 +61,12 @@ func cmdServe(args []string) error {
|
||||
llmArgs = append(llmArgs, "-ctv", vtv)
|
||||
}
|
||||
if r := cfg["REASONING"]; r != "" {
|
||||
// budget -1 = thinking illimité (défaut). NE PAS forcer 0 : sur llama.cpp
|
||||
// vanilla, 0 = "immediate end" → coupe le raisonnement net (le fork
|
||||
// ik_llama.cpp, lui, l'ignore, d'où l'ancien comportement trompeur).
|
||||
// Configurable via REASONING_BUDGET (ex: 2048 pour plafonner).
|
||||
llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "-1"))
|
||||
// budget plafonné par défaut (2048) : une réflexion qui part en boucle est
|
||||
// coupée net au lieu de tourner à l'infini — c'est le comportement de pi.
|
||||
// NE PAS forcer 0 : sur llama.cpp vanilla, 0 = "immediate end" → coupe tout
|
||||
// le raisonnement (le fork ik_llama.cpp l'ignore). Mettre -1 pour illimité.
|
||||
// Configurable via REASONING_BUDGET.
|
||||
llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "2048"))
|
||||
}
|
||||
// API_KEY protège le serveur quand il est exposé sur internet : llama-server
|
||||
// exige alors l'en-tête "Authorization: Bearer <clé>". La clé est lue depuis
|
||||
|
||||
@@ -162,12 +162,20 @@ func skillsSystemPrompt() string {
|
||||
}
|
||||
list := ListSkills()
|
||||
var b strings.Builder
|
||||
b.WriteString(`Skills = guides Markdown que tu gères toi-même via le tool skill(action, name, content) : read=lire, write=créer/remplacer, append=ajouter à la fin sans réécrire, delete=supprimer. De ta propre initiative, crée ou enrichis un skill quand tu découvres une solution réutilisable (problème enfin résolu, procédure, astuce) ; sa 1re ligne est un titre court (#) servant de description. N'utilise read que si la question concerne un skill listé.`)
|
||||
b.WriteString("Outil disponible — skill(action, name, content) : gère tes guides Markdown réutilisables. action: read=lire, write=créer/remplacer, append=ajouter à la fin, delete=supprimer.\n")
|
||||
b.WriteString("Conseils skill :\n")
|
||||
for _, l := range []string{
|
||||
"De ta propre initiative, crée ou enrichis un skill quand tu découvres une solution réutilisable (problème résolu, procédure, astuce).",
|
||||
"La 1re ligne du contenu est un titre court (#) qui sert de description.",
|
||||
"N'utilise read que si la question concerne un skill listé ci-dessous — sinon n'appelle pas l'outil.",
|
||||
} {
|
||||
b.WriteString("- " + l + "\n")
|
||||
}
|
||||
if len(list) == 0 {
|
||||
b.WriteString("\nAucun skill pour l'instant.")
|
||||
b.WriteString("Skills : aucun pour l'instant.")
|
||||
return b.String()
|
||||
}
|
||||
b.WriteString("\nSkills :\n")
|
||||
b.WriteString("Skills :\n")
|
||||
for _, s := range list {
|
||||
fmt.Fprintf(&b, "- %s: %s\n", s.Name, s.Desc)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,30 @@ func setToolsEnabled(on bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// baseSystemPrompt is the always-on system preamble. Structured like pi's
|
||||
// proven prompt (identity → method → guidelines → concision → date): a concrete,
|
||||
// procedural prompt gives the model rails so it stops deliberating forever
|
||||
// ("Wait, let me check… Wait, I'll just run it…") and commits to an action.
|
||||
// The per-tool "Outil disponible" sections live in machine/skills prompts so
|
||||
// they only appear when the matching feature is on.
|
||||
func baseSystemPrompt() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("Tu es un assistant expert opérant dans jean, un agent qui agit directement sur cette machine. Tu aides l'utilisateur en inspectant le système, en exécutant des commandes et en gérant des skills réutilisables.\n\n")
|
||||
b.WriteString("Méthode :\n")
|
||||
for _, l := range []string{
|
||||
"Raisonne brièvement (1 à 2 phrases) puis AGIS. Dès qu'une action est possible, appelle l'outil immédiatement — ne l'annonce pas, fais-le.",
|
||||
"Ne répète JAMAIS deux fois la même étape de réflexion ni le même appel d'outil. Si tu hésites (« je devrais peut-être vérifier… »), arrête de réfléchir et appelle l'outil maintenant.",
|
||||
"Pour toute question système (fichiers, process, réseau, OS), utilise tes outils au lieu de supposer — tu as un accès réel, ne demande pas la permission pour inspecter.",
|
||||
"Quand la tâche est faite, donne une réponse finale courte et arrête-toi. N'enchaîne pas d'outils inutiles.",
|
||||
"Sois concis dans tes réponses.",
|
||||
"Affiche clairement les chemins de fichiers quand tu travailles dessus.",
|
||||
} {
|
||||
b.WriteString("- " + l + "\n")
|
||||
}
|
||||
b.WriteString("\nDate du jour : " + time.Now().Format("2006-01-02"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// machineSystemPrompt returns a short briefing about the host the model is
|
||||
// running on, so that when machine access is enabled it knows *which* machine
|
||||
// run_shell acts upon (and doesn't claim it has no access to "your PC").
|
||||
@@ -56,7 +80,16 @@ func machineSystemPrompt() string {
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("Accès machine actif : le tool run_shell exécute des commandes sur cette machine — c'est ton accès réel, ne dis jamais que tu n'y as pas accès. Pour toute question système (OS, disque, process, fichiers, réseau), utilise-le au lieu de supposer.\n")
|
||||
b.WriteString("Outil disponible — run_shell(command, timeout) : exécute une commande shell sur CETTE machine et renvoie stdout, stderr et le code de sortie. C'est ton accès réel ; ne dis jamais que tu n'y as pas accès.\n")
|
||||
b.WriteString("Conseils run_shell :\n")
|
||||
for _, l := range []string{
|
||||
"Sers-t'en pour toute question système (OS, disque, process, fichiers, réseau, lire un script) plutôt que de supposer.",
|
||||
"Préfère des commandes ciblées et non-interactives ; chaîne-les avec && quand elles sont dépendantes.",
|
||||
"Évite les commandes destructrices (rm -rf, mkfs, dd…) sauf demande explicite de l'utilisateur.",
|
||||
"Si une commande échoue, lis stderr et le code de sortie, puis corrige — ne relance pas la même commande à l'identique.",
|
||||
} {
|
||||
b.WriteString("- " + l + "\n")
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Machine : hôte=%s, %s/%s", host, runtime.GOOS, runtime.GOARCH))
|
||||
if who != "" {
|
||||
b.WriteString(", user=" + who)
|
||||
|
||||
Reference in new issue
Block a user