fix(openai): Tool-Calling-Reliability verbessern (maxIterations, forced tool_choice, Thinking-Strip)
- maxIterations aus Server-Settings an OpenAI-StreamChat durchreichen (war hardcoded 10) - Auto-Continue: tool_choice="required" erzwingt Tool-Call statt erneuter Ankündigung, wird nach erfolgreichem Tool-Call wieder auf "auto" zurückgesetzt - Direktive Continuation-Message statt einfaches "Continue." - StripOldThinking: <think>-Blöcke aus älteren Assistant-Messages entfernen (nur letzte 4 behalten), reduziert Kontext-Inflation die ab ~20k Tokens zu Tool-Call-Degradation führt - reasoning_split=true entfernt (wird von MiniMax-API ignoriert) - parallel_tool_calls=false für MiniMax (Streaming, Non-Streaming, Payload-Preview) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f2f026baa
commit
358d9328be
3 files changed
+94
-11
No files matched your search
@@ -370,6 +370,9 @@ public class OpenAiService
|
||||
var client = CreateClient(providerConfig.ApiKey);
|
||||
// MiniMax-spezifisch: höheres Output-Budget (Thinking-Modelle brauchen viel)
|
||||
var maxTokens = provider == "minimax" ? 32768 : 16384;
|
||||
// Bei Auto-Continue: tool_choice = "required" erzwingt einen Tool-Call,
|
||||
// statt dass das Modell erneut nur eine Ankündigung als Text schreibt.
|
||||
var toolChoice = autoContinueCount > 0 ? "required" : "auto";
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = modelId,
|
||||
@@ -378,20 +381,23 @@ public class OpenAiService
|
||||
["max_tokens"] = maxTokens,
|
||||
["stream_options"] = new { include_usage = true },
|
||||
["tools"] = _openAiToolDefs,
|
||||
["tool_choice"] = "auto"
|
||||
["tool_choice"] = toolChoice
|
||||
};
|
||||
|
||||
if (provider == "nvidia" && IsNvidiaThinkingModel(modelId))
|
||||
payload["chat_template_kwargs"] = new { thinking = true };
|
||||
|
||||
// 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).
|
||||
// MiniMax-spezifisch:
|
||||
// - temperature=1.0, top_p=0.95: MiniMax-Empfehlung für agentic Performance
|
||||
// - parallel_tool_calls=false: Modell muss nur einen Tool-Call planen statt mehrere parallel,
|
||||
// was bei komplexen Multi-Step-Tasks die Zuverlässigkeit erhöht
|
||||
// Hinweis: reasoning_split=true wurde entfernt – wird von der gehosteten MiniMax-API ignoriert
|
||||
// (Thinking kommt trotzdem als inline <think>...</think> in delta.content)
|
||||
if (provider == "minimax")
|
||||
{
|
||||
payload["extra_body"] = new { reasoning_split = true };
|
||||
payload["temperature"] = 1.0;
|
||||
payload["top_p"] = 0.95;
|
||||
payload["parallel_tool_calls"] = false;
|
||||
}
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||
@@ -857,8 +863,9 @@ public class OpenAiService
|
||||
if (!string.IsNullOrEmpty(continueContent))
|
||||
apiMessages.Add(new { role = "assistant", content = continueContent });
|
||||
|
||||
// Synthetische User-Message injizieren
|
||||
apiMessages.Add(new { role = "user", content = "Continue." });
|
||||
// Synthetische User-Message injizieren – direktiv, nicht nur "Continue."
|
||||
// Damit das Modell den Tool-Call tatsächlich ausführt statt nur erneut anzukündigen.
|
||||
apiMessages.Add(new { role = "user", content = "You announced an action but didn't execute it. Call the appropriate tool now to carry out what you described. Do not repeat the announcement." });
|
||||
|
||||
// SSE-Event an Frontend senden
|
||||
await SseHelper.TryWriteSse(response, "auto_continue", new
|
||||
@@ -885,7 +892,10 @@ public class OpenAiService
|
||||
return;
|
||||
}
|
||||
|
||||
// Tool-Calls ausführen
|
||||
// Tool-Calls ausführen → autoContinueCount zurücksetzen, damit tool_choice
|
||||
// wieder "auto" wird (Modell darf wieder frei zwischen Text und Tool-Call wählen)
|
||||
autoContinueCount = 0;
|
||||
|
||||
// Assistant-Message mit tool_calls in die History einfügen
|
||||
var toolCallsForHistory = new List<object>();
|
||||
foreach (var (_, (tcId, tcName, tcArgs, tcTs)) in toolCallAccumulators)
|
||||
@@ -1150,7 +1160,11 @@ public class OpenAiService
|
||||
payload["chat_template_kwargs"] = new { thinking = true };
|
||||
|
||||
if (provider == "minimax")
|
||||
payload["extra_body"] = new { reasoning_split = true };
|
||||
{
|
||||
payload["temperature"] = 1.0;
|
||||
payload["top_p"] = 0.95;
|
||||
payload["parallel_tool_calls"] = false;
|
||||
}
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
@@ -1513,9 +1527,74 @@ public class OpenAiService
|
||||
}
|
||||
apiMessages.Add(converted);
|
||||
}
|
||||
|
||||
// Thinking aus älteren Turns strippen (Kontext-Spar-Maßnahme):
|
||||
// MiniMax/DeepSeek-R1/QwQ brauchen ihr eigenes Thinking in der History,
|
||||
// aber nur die letzten paar Turns sind relevant – ältere Thinking-Blöcke
|
||||
// blähen den Kontext auf und verursachen Tool-Call-Degradation ab ~20k Tokens.
|
||||
StripOldThinking(apiMessages, keepRecentCount: 4);
|
||||
|
||||
return apiMessages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entfernt <think>...</think>-Blöcke aus älteren Assistant-Messages,
|
||||
/// behält sie aber in den letzten <paramref name="keepRecentCount"/> Turns.
|
||||
/// Reduziert Kontext-Inflation bei Thinking-Modellen (MiniMax-M2, DeepSeek-R1, etc.).
|
||||
/// </summary>
|
||||
private static void StripOldThinking(List<object> apiMessages, int keepRecentCount)
|
||||
{
|
||||
// Finde alle Indizes von Assistant-Messages mit <think>-Blöcken
|
||||
var assistantIndices = new List<int>();
|
||||
for (int i = 0; i < apiMessages.Count; i++)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(apiMessages[i]);
|
||||
if (json.Contains("\"role\":\"assistant\"") && json.Contains("<think>"))
|
||||
assistantIndices.Add(i);
|
||||
}
|
||||
|
||||
// Nur die ältesten strippen, die neuesten keepRecentCount behalten
|
||||
var stripCount = assistantIndices.Count - keepRecentCount;
|
||||
if (stripCount <= 0) return;
|
||||
|
||||
for (int j = 0; j < stripCount; j++)
|
||||
{
|
||||
var idx = assistantIndices[j];
|
||||
var json = JsonSerializer.Serialize(apiMessages[idx]);
|
||||
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||||
|
||||
if (!el.TryGetProperty("content", out var contentProp) || contentProp.ValueKind != JsonValueKind.String)
|
||||
continue;
|
||||
|
||||
var content = contentProp.GetString() ?? "";
|
||||
// <think>...\n</think>\n\n entfernen (greedy innerhalb eines Blocks)
|
||||
var stripped = System.Text.RegularExpressions.Regex.Replace(
|
||||
content, @"<think>[\s\S]*?</think>\s*", "", System.Text.RegularExpressions.RegexOptions.None);
|
||||
stripped = stripped.TrimStart();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stripped))
|
||||
{
|
||||
// Kein sichtbarer Text übrig nach Strip → Message trotzdem behalten falls tool_calls dran sind
|
||||
if (json.Contains("\"tool_calls\""))
|
||||
stripped = "";
|
||||
else
|
||||
continue; // Komplett leer → überspringen
|
||||
}
|
||||
|
||||
// Neue Message mit gestripptem Content erstellen
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
|
||||
var newDict = new Dictionary<string, object>();
|
||||
foreach (var kv in dict)
|
||||
{
|
||||
if (kv.Key == "content")
|
||||
newDict["content"] = stripped;
|
||||
else
|
||||
newDict[kv.Key] = kv.Value;
|
||||
}
|
||||
apiMessages[idx] = newDict;
|
||||
}
|
||||
}
|
||||
|
||||
private Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw)
|
||||
=> SseHelper.FinalizeChain(_db, chain, usage, iterations, stopReason, sw);
|
||||
|
||||
|
||||
Reference in new issue
Block a user