feat(openai): Auto-Continue bei abgebrochenen Action-Ankündigungen + MiniMax-Sampling

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 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-05-14 11:48:33 +02:00
1 parent 1fe04025d3
commit 0f2f026baa
2 files changed
+79 -3

No files matched your search

+75 -3
View File
@@ -304,6 +304,8 @@ public class OpenAiService
var sw = System.Diagnostics.Stopwatch.StartNew(); var sw = System.Diagnostics.Stopwatch.StartNew();
var usage = new TokenUsage(); var usage = new TokenUsage();
var iteration = 0; var iteration = 0;
var autoContinueCount = 0;
const int MaxAutoContinues = 3;
var chain = new ChainRecord var chain = new ChainRecord
{ {
@@ -382,10 +384,15 @@ public class OpenAiService
if (provider == "nvidia" && IsNvidiaThinkingModel(modelId)) if (provider == "nvidia" && IsNvidiaThinkingModel(modelId))
payload["chat_template_kwargs"] = new { thinking = true }; payload["chat_template_kwargs"] = new { thinking = true };
// MiniMax-Thinking-Modelle: reasoning_split → Thinking landet in delta.reasoning_details // MiniMax-spezifisch: reasoning_split → Thinking landet in delta.reasoning_details
// statt als <think>...</think> inline in delta.content (siehe MiniMax-OpenAI-Compat-Docs) // statt als <think>...</think> 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") if (provider == "minimax")
{
payload["extra_body"] = new { reasoning_split = true }; payload["extra_body"] = new { reasoning_split = true };
payload["temperature"] = 1.0;
payload["top_p"] = 0.95;
}
var jsonPayload = JsonSerializer.Serialize(payload); var jsonPayload = JsonSerializer.Serialize(payload);
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); 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) 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 = $"<think>\n{finalThinking}\n</think>\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 FinalizeChain(chain, usage, iteration, stopReason, sw);
await SseHelper.TryWriteSse(response, "done", new await SseHelper.TryWriteSse(response, "done", new
{ {
@@ -1409,6 +1450,37 @@ public class OpenAiService
return (provider, modelId, providerConfig); return (provider, modelId, providerConfig);
} }
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Prüft ob ein Nvidia-Modell Thinking-Mode unterstützt (chat_template_kwargs). /// Prüft ob ein Nvidia-Modell Thinking-Mode unterstützt (chat_template_kwargs).
/// </summary> /// </summary>
+4
View File
@@ -131,6 +131,10 @@ function handleStreamEvent(eventType, data) {
addAutoContextBubble(data.context); addAutoContextBubble(data.context);
break; break;
case 'auto_continue':
addWarningBubble(`Auto-Continue (${data.attempt}/${data.maxAttempts}): Modell wird fortgesetzt…`, true);
break;
case 'warning': case 'warning':
case 'info': case 'info':
addWarningBubble(data.message, true); addWarningBubble(data.message, true);