This commit is contained in:
2026-08-22 00:41:17 +02:00
parent d06a93f759
commit 9250528442
18 changed files with 48210 additions and 53 deletions
+86 -19
View File
@@ -295,8 +295,8 @@ namespace AnotherReplayReader
var budget = AiContextBudget.GetContextBudget(model);
var hardCap = model.ContextLength > 0
? (int)(model.ContextLength * AiContextBudget.HardUsageRatio)
: 48_000;
var effectiveBudget = budget > 0 ? budget : hardCap;
: 128_000; // 上下文未知时的保守默认;仅用于“预算为 0 时的单 slice”判断。
var effectiveBudget = budget > 0 ? budget : 0;
var headroom = AiContextBudget.GetOutputHeadroom(requestContext.Provider, model);
var fixedOverhead = AiContextBudget.EstimateTokens(_systemPrompt)
+ 4_000 // digest
@@ -304,7 +304,25 @@ namespace AnotherReplayReader
+ 1_500 // instruction
+ 10_000 // findings reserve
+ headroom;
var sliceBudget = Math.Max(effectiveBudget - fixedOverhead, MechanicalSegmenter.MinSliceTokens);
var logTokens = AiContextBudget.EstimateTokens(replayData);
int sliceBudget;
if (effectiveBudget <= 0)
{
// 没有上下文预算(128K 及以下或未知):只允许短录像整局作为一个 slice。
if (logTokens + fixedOverhead > hardCap)
{
throw new InvalidOperationException(
$"模型没有足够的上下文预算({FormatNumber(budget, false)} token),日志约 {FormatNumber(logTokens, false)} token"
+ $"加上固定开销后超过单 slice 上限 {FormatNumber(hardCap, false)} token。"
+ "请改用更长上下文的模型,或缩短操作记录。");
}
sliceBudget = Math.Max(logTokens, MechanicalSegmenter.MinSliceTokens);
AppendLog("上下文预算提示", "模型上下文较短,仅按单 slice 处理整局;长录像质量不保证。", false);
}
else
{
sliceBudget = Math.Max(effectiveBudget - fixedOverhead, MechanicalSegmenter.MinSliceTokens);
}
var (slices, sliceWarnings) = MechanicalSegmenter.Slice(
replayData, eventSpans, sliceBudget, sliceBudget / 12);
_slices = slices;
@@ -321,7 +339,7 @@ namespace AnotherReplayReader
"v2 管线准备",
$"上下文预算: {FormatNumber(budget, false)} token0 = 不支持长录像)\r\n"
+ $"分段预算: {FormatNumber(sliceBudget, false)} token,实际切分 {_slices.Length} 段\r\n"
+ $"日志总量: {FormatNumber(AiContextBudget.EstimateTokens(replayData), false)} token\r\n"
+ $"日志总量: {FormatNumber(logTokens, false)} token\r\n"
+ $"[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
false);
@@ -334,15 +352,25 @@ namespace AnotherReplayReader
var overviewMessages = BuildSystemMessages(_systemPrompt, _digest, null)
.Add(new AIAnalyze.ChatMessage("user", AIAnalyze.BuildOverviewUserPrompt(_slices)));
CheckAndLogContextUsage(overviewMessages, requestContext);
var overviewResult = await _analyzer.CompleteAsync(
overviewMessages,
requestContext,
OnChunk,
_linkedCts.Token);
UpdateTokenDisplay(overviewResult);
_overview = OverviewParser.Parse(overviewResult.Response);
_overviewNarrative = _overview.Narrative;
AppendLog("整局总览", overviewResult.Response, false);
AIAnalyze.Result overviewResult = default;
var overviewAttempt = 0;
do
{
overviewResult = await _analyzer.CompleteAsync(
overviewMessages,
requestContext,
OnChunk,
_linkedCts.Token);
UpdateTokenDisplay(overviewResult);
_overview = OverviewParser.Parse(overviewResult.Response);
_overviewNarrative = _overview.Narrative;
AppendLog(
overviewAttempt == 0 ? "整局总览" : "整局总览(重试)",
overviewResult.Response,
false);
overviewAttempt++;
}
while (_overview.Segments.IsEmpty && overviewAttempt < 2);
if (_overview.Segments.IsEmpty)
{
AppendLog("总览解析警告", "未解析到 [分段概述] 块,将直接使用机械分段。", false);
@@ -413,11 +441,22 @@ namespace AnotherReplayReader
_lastContentParagraph = null;
var eventCount = _eventCounts?.Query(slice.Start, slice.End) ?? slice.EventCount;
var title = _overview.Segments.FirstOrDefault(s => s.Index == segmentIndex)?.Title;
var overviewEntry = _overview.Segments.FirstOrDefault(s => s.Index == segmentIndex);
var title = overviewEntry?.Title;
var sliceText = slice.GetText(_replayData);
var findingsText = string.Join("\n\n", _findings);
var modelBudget = AiContextBudget.GetContextBudget(requestContext.Model);
var maxFindingsTokens = modelBudget > 0
? Math.Min(10_000, Math.Max(2_000, modelBudget / 10))
: 4_000;
var findingsText = LimitFindings(_findings, maxFindingsTokens);
var instruction = AIAnalyze.BuildSegmentUserPromptV2(
_currentSegmentIndex, totalSegments, slice, eventCount, title);
_currentSegmentIndex,
totalSegments,
slice,
eventCount,
title,
overviewEntry?.Description,
overviewEntry?.BackqueryHints);
var messages = BuildSystemMessages(_systemPrompt, _digest, _overviewNarrative)
.Add(new AIAnalyze.ChatMessage("user", sliceText));
@@ -441,6 +480,8 @@ namespace AnotherReplayReader
string segmentResponse;
while (true)
{
// 回查会增长同一会话的消息列表,每次请求前都重新做预算检查。
CheckAndLogContextUsage(segmentMessages, requestContext);
var segmentResult = await _analyzer!.CompleteAsync(
segmentMessages,
requestContext,
@@ -506,10 +547,13 @@ namespace AnotherReplayReader
if (validationResult.RequiresRevision)
{
var issueCount = validationResult.Issues.Count(
i => i.Severity == AIValidationSeverity.Contradiction);
i => i.Severity is AIValidationSeverity.Contradiction
or AIValidationSeverity.Fatal
or AIValidationSeverity.Warning
or AIValidationSeverity.WeakEvidence);
AppendLog(
$"第{segmentIndex}段修订",
$"验证器发现 {issueCount} 个矛盾问题,正在请求 AI 修正...",
$"验证器发现 {issueCount} 个需要修正/降级的问题,正在请求 AI 修正...",
false);
var relevantFacts = RelevantFactsFormatter.Format(validationResult.Claims, _factIndex!);
var revisionPrompt = AIAnalyze.BuildRevisionUserPrompt(
@@ -655,7 +699,7 @@ namespace AnotherReplayReader
{
total += AiContextBudget.EstimateTokens(message.Content);
}
var check = AiContextBudget.CheckPromptUsage(
var check = AiContextBudget.CheckRequestUsage(
total, requestContext.Provider, requestContext.Model);
if (check.Block)
{
@@ -678,6 +722,29 @@ namespace AnotherReplayReader
return text.Substring(0, chars) + "\n…(已截断)";
}
/// <summary>已发现事实按 token 预算保留最近若干条,避免跨段事实无限增长。</summary>
private static string LimitFindings(IReadOnlyList<string> findings, int maxTokens)
{
if (findings.Count == 0)
{
return string.Empty;
}
var selected = new List<string>();
var totalTokens = 0;
for (var i = findings.Count - 1; i >= 0; --i)
{
var entry = findings[i];
var tokens = AiContextBudget.EstimateTokens(entry);
if (selected.Count > 0 && totalTokens + tokens > maxTokens)
{
break;
}
selected.Insert(0, entry);
totalTokens += tokens;
}
return string.Join("\n\n", selected);
}
// ---- 块追加与折叠 ----
private void OnChunk(AIAnalyze.AIChunk chunk)
{