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 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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user