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:
1 parent
1fe04025d3
commit
0f2f026baa
2 files changed
+79
-3
No files matched your search
@@ -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 <think>...</think> inline in delta.content (siehe MiniMax-OpenAI-Compat-Docs)
|
||||
// MiniMax-spezifisch: reasoning_split → Thinking landet in delta.reasoning_details
|
||||
// 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")
|
||||
{
|
||||
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 = $"<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 SseHelper.TryWriteSse(response, "done", new
|
||||
{
|
||||
@@ -1409,6 +1450,37 @@ public class OpenAiService
|
||||
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>
|
||||
/// Prüft ob ein Nvidia-Modell Thinking-Mode unterstützt (chat_template_kwargs).
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user