Kompaktierung: kalibriert pruefen, mit Abstand kompaktieren, Zusammenfassung verbuchen (#605)
Im Dauerbetrieb lief die Kompaktierung bei jedem Schritt an und blieb wirkungslos: Stufe 1 pruefte mit der rohen Schaetzung, kompaktierte nur knapp unter die Schwelle und brach dabei jedes Mal den Prompt-Cache; der Zusammenfassungs-Aufruf fehlte im Verbrauch. Ziel jetzt min(Schwelle, 55 % des Limits), Usage des Aufrufs geht in den Lauf. Version 0.1.8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,17 @@ public sealed class ContextCompactor
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private const int ProtectedTailMessages = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Wohin kompaktiert wird, als Anteil von <see cref="LoopGuardConfig.MaxContextTokens"/> -
|
||||
/// nie hoeher als die Schwelle selbst (#605).
|
||||
///
|
||||
/// <para>Vorher reichte "knapp unter der Schwelle". Im Dauerbetrieb kuerzte Stufe 1 dann
|
||||
/// bei jedem Schritt ein wenig, der Kontext lag sofort wieder darueber, und weil jede
|
||||
/// Kuerzung alte Nachrichten aendert, brach dabei jedes Mal der Prompt-Cache. Mit Abstand
|
||||
/// wird selten und kraeftig kompaktiert, und der Cache haelt danach mehrere Laeufe.</para>
|
||||
/// </summary>
|
||||
internal const double ZielAnteil = 0.55;
|
||||
private const int MaxToolResultChars = 2000;
|
||||
private const string TruncatedMarker = "\n\n[... Ergebnis gekürzt ...]";
|
||||
|
||||
@@ -95,11 +106,16 @@ public sealed class ContextCompactor
|
||||
(_, bisher) => Naechster(bisher, gemessen));
|
||||
}
|
||||
|
||||
/// <param name="verbrauch">
|
||||
/// Erhaelt den Verbrauch des Zusammenfassungs-Aufrufs (#605). Die Engine zaehlt ihn zum
|
||||
/// Lauf - sonst waere er in Verbrauch, Beleg und Tagesbudget unsichtbar.
|
||||
/// </param>
|
||||
public async Task<bool> CompactIfNeededAsync(
|
||||
List<ChatMessage> messages,
|
||||
LoopGuardConfig guard,
|
||||
string model,
|
||||
CancellationToken ct)
|
||||
CancellationToken ct,
|
||||
Action<Usage>? verbrauch = null)
|
||||
{
|
||||
var contextTokens = EstimateFor(model, messages);
|
||||
|
||||
@@ -108,19 +124,24 @@ public sealed class ContextCompactor
|
||||
if (contextTokens < threshold)
|
||||
return false;
|
||||
|
||||
var ziel = Math.Min(threshold, (int)(guard.MaxContextTokens * ZielAnteil));
|
||||
|
||||
_logger.LogInformation(
|
||||
"Context kompaktierung gestartet: {Tokens} Tokens (Schwelle: {Threshold})",
|
||||
contextTokens, threshold);
|
||||
"Context kompaktierung gestartet: {Tokens} Tokens (Schwelle: {Threshold}, Ziel: {Ziel})",
|
||||
contextTokens, threshold, ziel);
|
||||
|
||||
// Stufe 1: Tool-Results kürzen
|
||||
var pruned = PruneToolResults(messages);
|
||||
if (pruned)
|
||||
{
|
||||
var afterPrune = EstimateTokens(messages);
|
||||
_logger.LogInformation("Stufe 1 (Tool-Pruning): {Before} → {After} geschätzte Tokens",
|
||||
contextTokens, afterPrune);
|
||||
// #605: Mit derselben, kalibrierten Schaetzung pruefen, die ausgeloest hat. Die
|
||||
// rohe Zahl (Zeichen/4) lag bei deutschem Text und JSON deutlich zu niedrig,
|
||||
// meldete "unter der Schwelle", und Stufe 2 lief nie.
|
||||
var afterPrune = EstimateFor(model, messages);
|
||||
_logger.LogInformation("Stufe 1 (Tool-Pruning): {Before} → {After} geschätzte Tokens (Ziel: {Ziel})",
|
||||
contextTokens, afterPrune, ziel);
|
||||
|
||||
if (afterPrune < threshold)
|
||||
if (afterPrune < ziel)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -129,7 +150,22 @@ public sealed class ContextCompactor
|
||||
? model
|
||||
: guard.SummaryModel;
|
||||
|
||||
await CompactViaLlmAsync(messages, summaryModel, ct);
|
||||
await CompactViaLlmAsync(messages, summaryModel, ct, verbrauch);
|
||||
|
||||
var danach = EstimateFor(model, messages);
|
||||
if (danach >= threshold)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Kompaktierung bleibt über der Schwelle: {After} geschätzte Tokens (Schwelle: {Threshold}). "
|
||||
+ "Das geschützte Ende der letzten {Tail} Nachrichten ist allein zu groß.",
|
||||
danach, threshold, ProtectedTailMessages);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Kompaktierung abgeschlossen: {Before} → {After} geschätzte Tokens (Ziel: {Ziel})",
|
||||
contextTokens, danach, ziel);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -175,7 +211,7 @@ public sealed class ContextCompactor
|
||||
}
|
||||
|
||||
private async Task CompactViaLlmAsync(
|
||||
List<ChatMessage> messages, string model, CancellationToken ct)
|
||||
List<ChatMessage> messages, string model, CancellationToken ct, Action<Usage>? verbrauch)
|
||||
{
|
||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||
|
||||
@@ -230,6 +266,11 @@ public sealed class ContextCompactor
|
||||
try
|
||||
{
|
||||
var response = await _client.CompleteAsync(summaryRequest, ct);
|
||||
|
||||
// Bezahlt ist der Aufruf auch dann, wenn die Zusammenfassung unbrauchbar ist.
|
||||
if (response.Usage is { } usage)
|
||||
verbrauch?.Invoke(usage);
|
||||
|
||||
var summary = response.Choices.FirstOrDefault()?.Message?.Content;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(summary))
|
||||
|
||||
Reference in New Issue
Block a user