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>
114 lines
4.4 KiB
C#
114 lines
4.4 KiB
C#
using ClawdDotNet.Core.Api.Models;
|
|
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Core.Engine;
|
|
using ClawdDotNet.Core.Tests.Infrastructure;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Engine;
|
|
|
|
/// <summary>
|
|
/// #605: Im Dauerbetrieb lief die Kompaktierung bei jedem Schritt an und blieb wirkungslos -
|
|
/// Stufe 1 pruefte mit der rohen statt der kalibrierten Schaetzung, kompaktierte nur knapp
|
|
/// unter die Schwelle, und der Zusammenfassungs-Aufruf wurde nicht verbucht.
|
|
/// </summary>
|
|
public sealed class KompaktierungAbstandTests
|
|
{
|
|
private const string Model = "test/model";
|
|
|
|
private static (ContextCompactor Compactor, FakeChatClient Client) Erzeuge()
|
|
{
|
|
var client = new FakeChatClient().AlwaysRespondsWithText("- Der Agent hat Daten geprueft.");
|
|
return (new ContextCompactor(client, TestLogging.Factory), client);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Die rohe Schaetzung, wie sie nach Stufe 1 aussaehe - nachgebaut, um Schwellen zu
|
|
/// waehlen, an denen alte und neue Pruefung verschieden entscheiden. Werte wie im
|
|
/// ContextCompactor: die letzten 6 Nachrichten geschuetzt, Tool-Ergebnisse auf 2.000 Zeichen.
|
|
/// </summary>
|
|
private static int RohNachStufe1(List<ChatMessage> messages)
|
|
{
|
|
var kopie = messages
|
|
.Select(m => new ChatMessage { Role = m.Role, Content = m.Content, ToolCallId = m.ToolCallId, ToolCalls = m.ToolCalls })
|
|
.ToList();
|
|
|
|
var schutz = Math.Max(0, kopie.Count - 6);
|
|
for (var i = 0; i < schutz; i++)
|
|
{
|
|
if (kopie[i].Role == "tool" && kopie[i].Content is { Length: > 2_000 } inhalt)
|
|
kopie[i].Content = inhalt[..2_000] + "\n\n[... Ergebnis gekürzt ...]";
|
|
}
|
|
|
|
return ContextCompactor.EstimateTokens(kopie);
|
|
}
|
|
|
|
private static List<ChatMessage> LangerVerlauf()
|
|
=> Conversation.Start().Repeat(times: 6, toolCallsPerCycle: 1, resultLength: 8_000).Build();
|
|
|
|
[Fact]
|
|
public async Task Nach_Stufe_1_entscheidet_die_kalibrierte_Schaetzung()
|
|
{
|
|
var (compactor, client) = Erzeuge();
|
|
|
|
// Dieses Modell zaehlt rund dreimal so viele Tokens, wie Zeichen/4 vermuten laesst.
|
|
for (var i = 0; i < 12; i++)
|
|
compactor.Calibrate(Model, 1_000, 3_000);
|
|
|
|
var messages = LangerVerlauf();
|
|
var rohDanach = RohNachStufe1(messages);
|
|
|
|
// Roh liegt Stufe 1 unter der Schwelle, kalibriert deutlich darueber.
|
|
var schwelle = (int)(rohDanach * 1.5);
|
|
var guard = new LoopGuardConfig { MaxContextTokens = schwelle * 2, CompactionThreshold = 0.5 };
|
|
|
|
await compactor.CompactIfNeededAsync(messages, guard, Model, default);
|
|
|
|
client.CallCount.ShouldBe(1, "die rohe Zahl meldete faelschlich 'unter der Schwelle' und Stufe 2 lief nie");
|
|
ContextInvariants.AssertValid(messages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Knapp_unter_der_Schwelle_reicht_nicht_es_wird_mit_Abstand_kompaktiert()
|
|
{
|
|
var (compactor, client) = Erzeuge();
|
|
var messages = LangerVerlauf();
|
|
var rohDanach = RohNachStufe1(messages);
|
|
|
|
// Stufe 1 bringt knapp unter die Schwelle (90 %), aber nicht unter das Ziel (55 %).
|
|
var schwelle = (int)(rohDanach * 1.2);
|
|
var guard = new LoopGuardConfig { MaxContextTokens = (int)(schwelle / 0.9), CompactionThreshold = 0.9 };
|
|
|
|
await compactor.CompactIfNeededAsync(messages, guard, Model, default);
|
|
|
|
client.CallCount.ShouldBe(1, "knapp unter der Schwelle waere beim naechsten Schritt wieder darueber");
|
|
ContextInvariants.AssertValid(messages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Der_Zusammenfassungs_Aufruf_wird_verbucht()
|
|
{
|
|
var (compactor, _) = Erzeuge();
|
|
var gebucht = new List<Usage>();
|
|
var messages = Conversation.Start().Repeat(15).Build();
|
|
|
|
await compactor.CompactIfNeededAsync(
|
|
messages, new LoopGuardConfig { MaxContextTokens = 1_000, CompactionThreshold = 0.5 }, Model, default, gebucht.Add);
|
|
|
|
gebucht.ShouldHaveSingleItem().PromptTokens.ShouldBe(100);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unter_der_Schwelle_wird_nichts_verbucht()
|
|
{
|
|
var (compactor, client) = Erzeuge();
|
|
var gebucht = new List<Usage>();
|
|
|
|
await compactor.CompactIfNeededAsync(
|
|
Conversation.Start().Repeat(2).Build(),
|
|
new LoopGuardConfig { MaxContextTokens = 10_000_000, CompactionThreshold = 0.9 }, Model, default, gebucht.Add);
|
|
|
|
gebucht.ShouldBeEmpty();
|
|
client.CallCount.ShouldBe(0);
|
|
}
|
|
}
|