mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 05:28:15 +02:00
Raisonnement : REASONING_ECHO renvoie au moteur local la réflexion du modèle, en opt-in
Les gabarits Qwen3.5/3.6 rendent <think>…</think> pour chaque message assistant après la dernière question : c'est leur format entraîné. Loki ne gardait que le texte et les appels, donc à chaque étape d'une boucle d'outils le modèle relisait ses étapes précédentes avec des blocs vides, et le moteur recalculait le dernier message. Nouvelle clé REASONING_ECHO (off par défaut) : avec on, le raisonnement séparé par le serveur est gardé avec chaque message et renvoyé au même modèle. Sans la clé, rien n'est capturé ni envoyé : la requête est identique à l'octet près (testé). - Message.ReasoningContent (reasoning_content) + ReasoningModel, persistés ; seulement depuis reasoning_content du flux, jamais depuis le découpage « </think> » fait chez nous (rendu en double) - un seul point de sortie (echoMessages) : étiquette toujours retirée, raisonnement retiré pour une API externe, un autre modèle, un message sans texte ni appel, ou si la sonde dit que le gabarit ne le rend jamais - comptage : estimateTokens/compactBounds comptent ce qui part et que le gabarit rend (passé seulement si la sonde dit qu'il le garde, ou l'ignore) ; ctxAfter ne retire plus un raisonnement renvoyé - débordement : relance d'abord sans raisonnement, avant toute compaction ; shrinkToFit retire le raisonnement le plus ancien avant tout le reste ; le torse compacté le perd comme le résumé - erreur de gabarit (raise_exception, thinking, Failed to parse messages) : relance une fois sans raisonnement avant tool_choice none / outils coupés ; retenu pour le modèle si la relance passe - runBuilderTurn applique enfin NewHistory comme generate (compaction perdue pendant une passe de correction) ; forwardStream affiche la bannière - sonde de gabarit : nouveau verdict renders_reasoning - REASONING_PRESERVE (lot 1) inchangée, son interaction documentée Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3af481ff05
commit
7c9aa04fd9
12 files changed
+1121
-37
No files matched your search
@@ -166,11 +166,29 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
|
||||
}
|
||||
var content strings.Builder
|
||||
sawUsage := false
|
||||
// newBase / echo : comme generate — vue modèle publiée par une compaction
|
||||
// survenue pendant la passe (elle REMPLACE l'historique, tour compris), et
|
||||
// raisonnement de la réponse finale (REASONING_ECHO).
|
||||
var newBase []Message
|
||||
var echo *ReasoningEcho
|
||||
sent, tools := prepareTurn(final, caps)
|
||||
extra, _ := runChatTools(ctx, sent, tools, temperature, caps, func(ev StreamEvent) bool {
|
||||
if ev.Stats != nil && ev.Stats.PromptTokensTotal > 0 {
|
||||
sawUsage = true
|
||||
}
|
||||
if ev.NewHistory != nil {
|
||||
// Préfixe système injecté retiré, comme dans generate : il n'appartient
|
||||
// pas à l'historique persisté. Ignorer cet événement laissait la
|
||||
// compaction perdue et c.Messages incohérent (fil complet + tour).
|
||||
base := ev.NewHistory
|
||||
for len(base) > 0 && base[0].Role == "system" {
|
||||
base = base[1:]
|
||||
}
|
||||
newBase = append([]Message(nil), base...)
|
||||
}
|
||||
if ev.Echo != nil {
|
||||
echo = ev.Echo
|
||||
}
|
||||
if ev.Content != "" {
|
||||
content.WriteString(ev.Content)
|
||||
}
|
||||
@@ -185,9 +203,12 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
|
||||
})
|
||||
c.mu.Lock()
|
||||
if c.epoch == epoch {
|
||||
if newBase != nil {
|
||||
c.Messages = newBase
|
||||
}
|
||||
c.Messages = append(c.Messages, extra...)
|
||||
if s := content.String(); strings.TrimSpace(s) != "" {
|
||||
c.Messages = append(c.Messages, Message{Role: "assistant", Content: s})
|
||||
c.Messages = append(c.Messages, withEcho(Message{Role: "assistant", Content: s}, echo))
|
||||
}
|
||||
c.Messages = dropStrayNudges(c.Messages) // même règle que generate
|
||||
if sawUsage {
|
||||
@@ -202,8 +223,10 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
|
||||
}
|
||||
|
||||
// forwardStream relaie les événements d'un runChat secondaire (vérification,
|
||||
// correction) vers le journal d'affichage — même mapping que generate(), sans
|
||||
// la gestion de compaction (ces passes n'en déclenchent pas : contexte court).
|
||||
// correction) vers le journal d'affichage — même mapping que generate(). Ce
|
||||
// qui touche à l'HISTORIQUE (NewHistory, Echo) n'a rien à afficher : c'est
|
||||
// l'appelant qui le range (runBuilderTurn), ou l'ignore pour une passe isolée
|
||||
// sur sa propre trace (vérificateur).
|
||||
//
|
||||
// isolated : la passe tourne sur sa propre trace (vérificateur), PAS sur
|
||||
// l'historique de la discussion. Ses comptes ne disent rien du contexte de
|
||||
@@ -212,6 +235,14 @@ func (c *Conversation) runBuilderTurn(ctx context.Context, caps Caps, temperatur
|
||||
// chiffre-là. La jauge de l'UI ne doit pas bouger non plus (ctx_isolated).
|
||||
func (c *Conversation) forwardStream(ev StreamEvent, epoch int, isolated bool) {
|
||||
switch {
|
||||
case ev.NewHistory != nil, ev.Echo != nil:
|
||||
// Rangés par l'appelant, voir plus haut.
|
||||
case ev.Compacting != nil:
|
||||
// Même bannière que generate. Pas pour une passe isolée : ce n'est pas
|
||||
// la discussion qu'elle compacte.
|
||||
if !isolated {
|
||||
c.appendDelta(epoch, map[string]any{"compacting": *ev.Compacting})
|
||||
}
|
||||
case ev.Err != nil:
|
||||
c.appendDelta(epoch, map[string]any{"error": ev.Err.Error()})
|
||||
case ev.ToolUsed != nil:
|
||||
|
||||
Reference in new issue
Block a user