From 0f2f026baa08a57e3f7c4e7b256cf074d4e8612f Mon Sep 17 00:00:00 2001 From: Meleki Date: Thu, 14 May 2026 11:48:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(openai):=20Auto-Continue=20bei=20abgebroch?= =?UTF-8?q?enen=20Action-Ank=C3=BCndigungen=20+=20MiniMax-Sampling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MiniMax-M2.7 stoppt häufig nach Action-Ankündigung ohne Tool-Call (z.B. "Lass mich X machen:" → stop). Auto-Continue erkennt das per Heuristik und injiziert automatisch "Continue." als User-Message (max 3×). MiniMax-Sampling: temperature=1.0, top_p=0.95 (offizielle Empfehlung für agentic Performance). Co-Authored-By: Claude Opus 4.6 --- Services/OpenAiService.cs | 78 +++++++++++++++++++++++++++++++++++++-- wwwroot/js/chat.js | 4 ++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs index 97876ce..1aadba1 100644 --- a/Services/OpenAiService.cs +++ b/Services/OpenAiService.cs @@ -304,6 +304,8 @@ public class OpenAiService var sw = System.Diagnostics.Stopwatch.StartNew(); var usage = new TokenUsage(); var iteration = 0; + var autoContinueCount = 0; + const int MaxAutoContinues = 3; var chain = new ChainRecord { @@ -382,10 +384,15 @@ public class OpenAiService if (provider == "nvidia" && IsNvidiaThinkingModel(modelId)) payload["chat_template_kwargs"] = new { thinking = true }; - // MiniMax-Thinking-Modelle: reasoning_split → Thinking landet in delta.reasoning_details - // statt als ... inline in delta.content (siehe MiniMax-OpenAI-Compat-Docs) + // MiniMax-spezifisch: reasoning_split → Thinking landet in delta.reasoning_details + // statt als ... inline in delta.content (siehe MiniMax-OpenAI-Compat-Docs). + // Sampling-Parameter: temperature=1.0, top_p=0.95 (MiniMax-Empfehlung für Agentic Performance). if (provider == "minimax") + { payload["extra_body"] = new { reasoning_split = true }; + payload["temperature"] = 1.0; + payload["top_p"] = 0.95; + } var jsonPayload = JsonSerializer.Serialize(payload); var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); @@ -827,9 +834,43 @@ public class OpenAiService }); } - // Keine Tool-Calls → fertig (stopReason als Fallback, primär Count prüfen) + // Keine Tool-Calls → prüfen ob Auto-Continue sinnvoll ist if (toolCallAccumulators.Count == 0) { + // Auto-Continue: Wenn das Modell mit "stop" beendet hat, kein Tool-Call kam, + // aber der Text nach einer Action-Ankündigung aussieht, senden wir automatisch + // eine Continuation-Message. Bekanntes MiniMax-M2.7-Verhalten bei langen Kontexten. + // Auch bei anderen Modellen kann das vorkommen (DeepSeek-R1, QwQ). + // Max 3 Auto-Continues pro Chain um Endlos-Loops zu verhindern. + if (autoContinueCount < MaxAutoContinues + && !gotLengthFinish + && lastFinishReason == "stop" + && LooksLikeIncompleteAction(finalText)) + { + autoContinueCount++; + + // Thinking + Text als Assistant-Message in History einfügen + var continueContent = string.IsNullOrEmpty(finalText) ? "" : finalText; + if (!string.IsNullOrEmpty(finalThinking)) + continueContent = $"\n{finalThinking}\n\n\n{continueContent}"; + + if (!string.IsNullOrEmpty(continueContent)) + apiMessages.Add(new { role = "assistant", content = continueContent }); + + // Synthetische User-Message injizieren + apiMessages.Add(new { role = "user", content = "Continue." }); + + // SSE-Event an Frontend senden + await SseHelper.TryWriteSse(response, "auto_continue", new + { + reason = "Model stopped without tool call after action announcement", + attempt = autoContinueCount, + maxAttempts = MaxAutoContinues + }, sseCt); + + continue; // Nächste Iteration der while-Loop + } + await FinalizeChain(chain, usage, iteration, stopReason, sw); await SseHelper.TryWriteSse(response, "done", new { @@ -1409,6 +1450,37 @@ public class OpenAiService return (provider, modelId, providerConfig); } + /// + /// Prüft ob der Text eines Assistant-Turns wie eine unvollständige Action-Ankündigung aussieht. + /// Typisches MiniMax-M2.7-Verhalten: Modell schreibt "Lass mich X machen:" und stoppt ohne Tool-Call. + /// + private static bool LooksLikeIncompleteAction(string text) + { + if (string.IsNullOrWhiteSpace(text)) return false; + + var trimmed = text.TrimEnd(); + + // Heuristik 1: Text endet mit Doppelpunkt → Action-Ankündigung + if (trimmed.EndsWith(":")) return true; + + // Heuristik 2: Text endet mit typischen Action-Verben + unvollständiger Phrase + // z.B. "Lass mich die Datei anschauen" ohne nachfolgenden Tool-Call + var lower = trimmed.ToLowerInvariant(); + var actionPhrases = new[] + { + "lass mich", "let me", "i'll ", "i will ", "ich werde ", + "schauen wir", "let's ", "weiter mit", "jetzt ", "als nächstes", + "now i'll", "now let me", "proceeding to", "going to" + }; + + foreach (var phrase in actionPhrases) + { + if (lower.Contains(phrase)) return true; + } + + return false; + } + /// /// Prüft ob ein Nvidia-Modell Thinking-Mode unterstützt (chat_template_kwargs). /// diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index 2a6de08..05354d6 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -131,6 +131,10 @@ function handleStreamEvent(eventType, data) { addAutoContextBubble(data.context); break; + case 'auto_continue': + addWarningBubble(`Auto-Continue (${data.attempt}/${data.maxAttempts}): Modell wird fortgesetzt…`, true); + break; + case 'warning': case 'info': addWarningBubble(data.message, true);