ai plan v2
This commit is contained in:
+382
-78
@@ -8,6 +8,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -64,6 +65,20 @@ namespace AnotherReplayReader
|
||||
private TimeIndexedPrefixSums? _eventCounts;
|
||||
private ReplayFactIndex? _factIndex;
|
||||
|
||||
// v2 管线状态
|
||||
private Replay? _replay;
|
||||
private ImmutableSortedDictionary<int, Player> _players = ImmutableSortedDictionary<int, Player>.Empty;
|
||||
private string? _systemPrompt;
|
||||
private string? _replayData;
|
||||
private ImmutableArray<EventSpan> _eventSpans = ImmutableArray<EventSpan>.Empty;
|
||||
private ImmutableArray<ReplaySlice> _slices = ImmutableArray<ReplaySlice>.Empty;
|
||||
private string? _digest;
|
||||
private OverviewResult _overview = new(string.Empty, ImmutableArray<SegmentOverview>.Empty);
|
||||
private string? _overviewNarrative;
|
||||
private readonly List<string> _findings = new();
|
||||
private readonly List<string> _segmentResponses = new();
|
||||
private int _currentSegmentIndex;
|
||||
|
||||
// 分析状态
|
||||
private bool _isRunning;
|
||||
private bool _isFailed;
|
||||
@@ -79,6 +94,8 @@ namespace AnotherReplayReader
|
||||
private bool _thinkBlockWasExpanded = false;
|
||||
// 输出段落
|
||||
private (Paragraph? Think, Paragraph? Content)? _currentContent;
|
||||
private Paragraph? _lastContentParagraph;
|
||||
private bool _suppressDisplay;
|
||||
|
||||
// 进度计时
|
||||
private TimeSpan _previousTotalTime;
|
||||
@@ -167,11 +184,25 @@ namespace AnotherReplayReader
|
||||
_analyzer = null;
|
||||
_eventCounts = null;
|
||||
_factIndex = null;
|
||||
_replay = null;
|
||||
_players = ImmutableSortedDictionary<int, Player>.Empty;
|
||||
_systemPrompt = null;
|
||||
_replayData = null;
|
||||
_eventSpans = ImmutableArray<EventSpan>.Empty;
|
||||
_slices = ImmutableArray<ReplaySlice>.Empty;
|
||||
_digest = null;
|
||||
_overview = new(string.Empty, ImmutableArray<SegmentOverview>.Empty);
|
||||
_overviewNarrative = null;
|
||||
_findings.Clear();
|
||||
_segmentResponses.Clear();
|
||||
_currentSegmentIndex = 0;
|
||||
|
||||
_document.Blocks.Clear();
|
||||
_updateCurrentThinkingSection = null;
|
||||
_thinkBlockWasExpanded = false;
|
||||
_currentContent = null;
|
||||
_lastContentParagraph = null;
|
||||
_suppressDisplay = false;
|
||||
|
||||
_previousTotalTime = TimeSpan.Zero;
|
||||
_previousTotalChars = 0;
|
||||
@@ -201,6 +232,7 @@ namespace AnotherReplayReader
|
||||
string replayData,
|
||||
TimeIndexedPrefixSums eventCounts,
|
||||
ReplayFactIndex factIndex,
|
||||
ImmutableArray<EventSpan> eventSpans,
|
||||
CancellationToken externalToken)
|
||||
{
|
||||
if (_isRunning)
|
||||
@@ -249,54 +281,79 @@ namespace AnotherReplayReader
|
||||
_analyzer = new AIAnalyze();
|
||||
_eventCounts = eventCounts;
|
||||
_factIndex = factIndex;
|
||||
_replay = replay;
|
||||
_players = players;
|
||||
_replayData = replayData;
|
||||
_eventSpans = eventSpans;
|
||||
|
||||
FinishCurrentContent();
|
||||
var requestContext = GetRequestContext();
|
||||
_systemPrompt = AIAnalyze.GetSystemPrompt(replay, players, GetPromptSettings?.Invoke());
|
||||
|
||||
var systemPrompt = AIAnalyze.GetSystemPrompt(replay, players, GetPromptSettings?.Invoke());
|
||||
var userPrompt = AIAnalyze.BuildUserPrompt(replay.Mod, players, replayData, out var userPromptPrefix);
|
||||
AppendLog($"让 AI 了解录像...",
|
||||
userPromptPrefix
|
||||
+ "[输入:操作信息流水账]\r\n"
|
||||
// ---- 机械分段(M2) ----
|
||||
var model = requestContext.Model;
|
||||
var budget = AiContextBudget.GetContextBudget(model);
|
||||
var hardCap = model.ContextLength > 0
|
||||
? (int)(model.ContextLength * AiContextBudget.HardUsageRatio)
|
||||
: 48_000;
|
||||
var effectiveBudget = budget > 0 ? budget : hardCap;
|
||||
var headroom = AiContextBudget.GetOutputHeadroom(requestContext.Provider, model);
|
||||
var fixedOverhead = AiContextBudget.EstimateTokens(_systemPrompt)
|
||||
+ 4_000 // digest
|
||||
+ 2_000 // overview
|
||||
+ 1_500 // instruction
|
||||
+ 10_000 // findings reserve
|
||||
+ headroom;
|
||||
var sliceBudget = Math.Max(effectiveBudget - fixedOverhead, MechanicalSegmenter.MinSliceTokens);
|
||||
var (slices, sliceWarnings) = MechanicalSegmenter.Slice(
|
||||
replayData, eventSpans, sliceBudget, sliceBudget / 12);
|
||||
_slices = slices;
|
||||
foreach (var warning in sliceWarnings)
|
||||
{
|
||||
AppendLog("分段警告", warning, false);
|
||||
}
|
||||
if (_slices.IsEmpty)
|
||||
{
|
||||
throw new InvalidOperationException("操作记录为空,无法分析。");
|
||||
}
|
||||
|
||||
AppendLog(
|
||||
"v2 管线准备",
|
||||
$"上下文预算: {FormatNumber(budget, false)} token(0 = 不支持长录像)\r\n"
|
||||
+ $"分段预算: {FormatNumber(sliceBudget, false)} token,实际切分 {_slices.Length} 段\r\n"
|
||||
+ $"日志总量: {FormatNumber(AiContextBudget.EstimateTokens(replayData), false)} token\r\n"
|
||||
+ $"[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
|
||||
true);
|
||||
false);
|
||||
|
||||
#region info
|
||||
var (systemPromptSize, systemPromptTokenCount) = AIAnalyze.EstimateTokenCount(systemPrompt);
|
||||
var (userPromptSize, userPromptTokenCount) = AIAnalyze.EstimateTokenCount(userPrompt);
|
||||
var totalPromptSize = systemPromptSize + userPromptSize;
|
||||
var totalPromptTokenCount = systemPromptTokenCount + userPromptTokenCount;
|
||||
var estimateText = $"系统提示词: {FormatNumber(systemPromptSize, true, "B")},约 {FormatNumber(systemPromptTokenCount, false)} Token\r\n" +
|
||||
$"初始用户提示词: {FormatNumber(userPromptSize, true, "B")},约 {FormatNumber(userPromptTokenCount, false)} Token\r\n" +
|
||||
$"总大小: {FormatNumber(totalPromptSize, true, "B")},约 {FormatNumber(totalPromptTokenCount, false)} Token";
|
||||
AppendLog("初始阶段 Token 用量预测", estimateText, false);
|
||||
#endregion
|
||||
// ---- 对局摘要 ----
|
||||
_digest = MatchDigestBuilder.Build(factIndex, players, replay.Mod, _slices, replayData);
|
||||
AppendLog("对局摘要", _digest, true);
|
||||
|
||||
var result = await _analyzer.AnalyzeAsync(
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
// ---- 总览轮 ----
|
||||
_phaseText.Text = "正在生成总览...";
|
||||
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(result);
|
||||
|
||||
var eventCountsBuilder = new StringBuilder();
|
||||
foreach (var (Start, End, Description) in result.State.Segments)
|
||||
UpdateTokenDisplay(overviewResult);
|
||||
_overview = OverviewParser.Parse(overviewResult.Response);
|
||||
_overviewNarrative = _overview.Narrative;
|
||||
AppendLog("整局总览", overviewResult.Response, false);
|
||||
if (_overview.Segments.IsEmpty)
|
||||
{
|
||||
var count = eventCounts.Query(Start, End);
|
||||
eventCountsBuilder.AppendLine($"[{Start:mm\\:ss\\.ff} - {End:mm\\:ss\\.ff}],事件数: {count}");
|
||||
AppendLog("总览解析警告", "未解析到 [分段概述] 块,将直接使用机械分段。", false);
|
||||
}
|
||||
AppendLog("各分段事件数量", eventCountsBuilder.ToString(), false);
|
||||
|
||||
// 分析完成,分段列表就绪
|
||||
_lastSuccessfulSegment = 0;
|
||||
// ---- 分段分析循环 ----
|
||||
_currentSegmentIndex = 0;
|
||||
await ProcessSegmentsAsync(requestContext);
|
||||
|
||||
// 分段分析循环
|
||||
await ProcessSegmentsAsync(result.State, eventCounts);
|
||||
|
||||
// 总结
|
||||
await ProcessSummaryAsync(eventCounts);
|
||||
// ---- 总结 ----
|
||||
await ProcessSummaryAsync(requestContext);
|
||||
|
||||
// 成功结束
|
||||
_phaseText.Text = "分析完成";
|
||||
@@ -328,42 +385,113 @@ namespace AnotherReplayReader
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 分析阶段 ----
|
||||
private async Task ProcessSegmentsAsync(AIAnalyze.State lastState,
|
||||
TimeIndexedPrefixSums eventCounts)
|
||||
// ---- 分析阶段(v2) ----
|
||||
private async Task ProcessSegmentsAsync(AiRequestContext requestContext)
|
||||
{
|
||||
_analyzer!.SetState(lastState);
|
||||
while (lastState.CurrentSegment < lastState.Segments.Count)
|
||||
if (_linkedCts is null || _replayData is null || _systemPrompt is null || _digest is null || _replay is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playerNameToIndex = AIAnalyze.PlayerNamesForAI(_replay.Mod, _players)
|
||||
.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
var structuredKnowledge = StructuredKnowledge.GetForMod(
|
||||
AIAnalyze.GetKnowledgeModName(_replay));
|
||||
|
||||
while (_currentSegmentIndex < _slices.Length)
|
||||
{
|
||||
if (_linkedCts is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
_linkedCts.Token.ThrowIfCancellationRequested();
|
||||
var seg = lastState.Segments[lastState.CurrentSegment];
|
||||
var segmentIndex = lastState.CurrentSegment + 1;
|
||||
var totalSegments = lastState.Segments.Count;
|
||||
var slice = _slices[_currentSegmentIndex];
|
||||
var segmentIndex = _currentSegmentIndex + 1;
|
||||
var totalSegments = _slices.Length;
|
||||
|
||||
_phaseText.Text = $"正在分析第 {segmentIndex}/{totalSegments} 段";
|
||||
|
||||
FinishCurrentContent();
|
||||
// 记录回滚点
|
||||
_blockCountBeforeSegment = _document.Blocks.Count;
|
||||
_lastContentParagraph = null;
|
||||
|
||||
// 计算事件数
|
||||
var eventCount = eventCounts.Query(seg.Start, seg.End);
|
||||
var segmentPrompt = AIAnalyze.BuildSegmentUserPrompt(lastState.Segments, lastState.CurrentSegment, eventCount);
|
||||
var requestContext = GetRequestContext!();
|
||||
AppendLog($"让 AI 分析第{segmentIndex}段...",
|
||||
segmentPrompt
|
||||
+ $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
|
||||
var eventCount = _eventCounts?.Query(slice.Start, slice.End) ?? slice.EventCount;
|
||||
var title = _overview.Segments.FirstOrDefault(s => s.Index == segmentIndex)?.Title;
|
||||
var sliceText = slice.GetText(_replayData);
|
||||
var findingsText = string.Join("\n\n", _findings);
|
||||
var instruction = AIAnalyze.BuildSegmentUserPromptV2(
|
||||
_currentSegmentIndex, totalSegments, slice, eventCount, title);
|
||||
|
||||
var messages = BuildSystemMessages(_systemPrompt, _digest, _overviewNarrative)
|
||||
.Add(new AIAnalyze.ChatMessage("user", sliceText));
|
||||
if (!string.IsNullOrWhiteSpace(findingsText))
|
||||
{
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage(
|
||||
"user", "之前各段的已发现事实:\n" + findingsText));
|
||||
}
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage("user", instruction));
|
||||
|
||||
AppendLog(
|
||||
$"让 AI 分析第{segmentIndex}段...",
|
||||
instruction + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
|
||||
true);
|
||||
var segmentResult = await _analyzer.ContinueAnalyzeAsync(
|
||||
segmentPrompt,
|
||||
requestContext,
|
||||
OnChunk,
|
||||
_linkedCts.Token);
|
||||
var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims(segmentResult.Response, _factIndex);
|
||||
CheckAndLogContextUsage(messages, requestContext);
|
||||
|
||||
// 回查循环(M3):同一段会话内,模型可多次请求远处原始区间
|
||||
const int maxBackqueriesPerSegment = 3;
|
||||
var segmentMessages = messages;
|
||||
var backqueryCount = 0;
|
||||
string segmentResponse;
|
||||
while (true)
|
||||
{
|
||||
var segmentResult = await _analyzer!.CompleteAsync(
|
||||
segmentMessages,
|
||||
requestContext,
|
||||
OnChunk,
|
||||
_linkedCts.Token);
|
||||
UpdateTokenDisplay(segmentResult);
|
||||
segmentResponse = segmentResult.Response;
|
||||
|
||||
if (backqueryCount >= maxBackqueriesPerSegment)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var backqueries = BackqueryParser.Parse(segmentResponse);
|
||||
var pendingTexts = new List<string>();
|
||||
foreach (var (start, end) in backqueries)
|
||||
{
|
||||
if (backqueryCount + pendingTexts.Count >= maxBackqueriesPerSegment)
|
||||
{
|
||||
AppendLog("回查限制", "本段回查次数已达上限,剩余区间已忽略。", false);
|
||||
break;
|
||||
}
|
||||
var (text, reason) = BackquerySliceExtractor.Extract(
|
||||
_replayData, _eventSpans, start, end);
|
||||
if (text is null)
|
||||
{
|
||||
AppendLog("回查失败", reason ?? "未知原因", false);
|
||||
continue;
|
||||
}
|
||||
pendingTexts.Add(
|
||||
$"[回查 {MatchDigestBuilder.FormatTime(start)}~{MatchDigestBuilder.FormatTime(end)}]\n" + text);
|
||||
}
|
||||
|
||||
if (pendingTexts.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
backqueryCount += pendingTexts.Count;
|
||||
segmentMessages = segmentMessages
|
||||
.Add(new AIAnalyze.ChatMessage("assistant", segmentResponse))
|
||||
.Add(new AIAnalyze.ChatMessage(
|
||||
"user", AIAnalyze.BuildBackqueryUserPrompt(string.Join("\n\n", pendingTexts))));
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段回查(第 {backqueryCount} 次)",
|
||||
$"已提供 {pendingTexts.Count} 个区间,继续分析。",
|
||||
true);
|
||||
}
|
||||
|
||||
FinishCurrentContent();
|
||||
var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims(
|
||||
segmentResponse, _factIndex, playerNameToIndex, structuredKnowledge);
|
||||
if (validationResult.HasIssues)
|
||||
{
|
||||
AppendLog(
|
||||
@@ -371,16 +499,91 @@ namespace AnotherReplayReader
|
||||
AIAnalysisValidation.FormatIssues(validationResult.Issues),
|
||||
false);
|
||||
}
|
||||
_lastSuccessfulSegment = lastState.CurrentSegment;
|
||||
lastState = segmentResult.State;
|
||||
UpdateTokenDisplay(segmentResult);
|
||||
|
||||
// 隐藏修订 pass(M6):Contradiction 触发,最多 1 次;修订期间不流式显示
|
||||
var finalResponse = segmentResponse;
|
||||
var finalValidation = validationResult;
|
||||
if (validationResult.RequiresRevision)
|
||||
{
|
||||
var issueCount = validationResult.Issues.Count(
|
||||
i => i.Severity == AIValidationSeverity.Contradiction);
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段修订",
|
||||
$"验证器发现 {issueCount} 个矛盾问题,正在请求 AI 修正...",
|
||||
false);
|
||||
var relevantFacts = RelevantFactsFormatter.Format(validationResult.Claims, _factIndex!);
|
||||
var revisionPrompt = AIAnalyze.BuildRevisionUserPrompt(
|
||||
segmentResponse,
|
||||
AIAnalysisValidation.FormatIssues(validationResult.Issues),
|
||||
string.IsNullOrWhiteSpace(relevantFacts) ? "(无额外事实)" : relevantFacts);
|
||||
var revisionMessages = segmentMessages
|
||||
.Add(new AIAnalyze.ChatMessage("assistant", segmentResponse))
|
||||
.Add(new AIAnalyze.ChatMessage("user", revisionPrompt));
|
||||
CheckAndLogContextUsage(revisionMessages, requestContext);
|
||||
|
||||
_suppressDisplay = true;
|
||||
AIAnalyze.Result revisionResult;
|
||||
try
|
||||
{
|
||||
revisionResult = await _analyzer!.CompleteAsync(
|
||||
revisionMessages,
|
||||
requestContext,
|
||||
OnChunk,
|
||||
_linkedCts.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressDisplay = false;
|
||||
}
|
||||
UpdateTokenDisplay(revisionResult);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(revisionResult.Response))
|
||||
{
|
||||
AppendLog("修订失败", "修订输出为空,保留原分析。", false);
|
||||
}
|
||||
else
|
||||
{
|
||||
finalResponse = revisionResult.Response;
|
||||
finalValidation = AIAnalysisValidation.ValidateMachineReadableClaims(
|
||||
revisionResult.Response, _factIndex, playerNameToIndex, structuredKnowledge);
|
||||
ReplaceSegmentContent(finalResponse);
|
||||
AppendLog(
|
||||
"修订完成",
|
||||
"已采用修正后的分析。" + (finalValidation.HasIssues
|
||||
? "\n仍存在的问题:\n" + AIAnalysisValidation.FormatIssues(finalValidation.Issues)
|
||||
: string.Empty),
|
||||
false);
|
||||
}
|
||||
}
|
||||
FinishCurrentContent();
|
||||
|
||||
// 追加已发现事实
|
||||
var entry = new StringBuilder();
|
||||
var summary = ClaimFindingsFormatter.ExtractSummary(finalResponse);
|
||||
if (!string.IsNullOrWhiteSpace(summary))
|
||||
{
|
||||
entry.AppendLine($"[小结] {summary}");
|
||||
}
|
||||
var claimsText = ClaimFindingsFormatter.Format(finalValidation.Claims);
|
||||
if (!string.IsNullOrWhiteSpace(claimsText))
|
||||
{
|
||||
entry.AppendLine(claimsText);
|
||||
}
|
||||
if (entry.Length > 0)
|
||||
{
|
||||
_findings.Add(entry.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
_segmentResponses.Add(finalResponse);
|
||||
_lastSuccessfulSegment = _currentSegmentIndex;
|
||||
_currentSegmentIndex++;
|
||||
AppendLog($"第{segmentIndex}段完成。", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessSummaryAsync(TimeIndexedPrefixSums eventCounts)
|
||||
private async Task ProcessSummaryAsync(AiRequestContext requestContext)
|
||||
{
|
||||
if (_linkedCts is null)
|
||||
if (_linkedCts is null || _systemPrompt is null || _digest is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -390,16 +593,37 @@ namespace AnotherReplayReader
|
||||
FinishCurrentContent();
|
||||
_blockCountBeforeSegment = _document.Blocks.Count;
|
||||
|
||||
var totalEvents = eventCounts.GetTotal();
|
||||
var finalPrompt = AIAnalyze.BuildFinalUserPrompt(totalEvents);
|
||||
var requestContext = GetRequestContext!();
|
||||
AppendLog($"让 AI 生成总结...",
|
||||
finalPrompt
|
||||
+ $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
|
||||
var totalEvents = _eventCounts?.GetTotal() ?? 0;
|
||||
var analyses = string.Join("\n\n", _segmentResponses);
|
||||
var budget = AiContextBudget.GetContextBudget(requestContext.Model);
|
||||
var maxAnalysesTokens = budget > 0 ? Math.Max(20_000, budget / 3) : 40_000;
|
||||
var truncated = TruncateByTokens(analyses, maxAnalysesTokens);
|
||||
if (!ReferenceEquals(truncated, analyses))
|
||||
{
|
||||
AppendLog(
|
||||
"总结输入警告",
|
||||
$"各段分析过长,已截断到约 {FormatNumber(maxAnalysesTokens, false)} token。",
|
||||
false);
|
||||
}
|
||||
|
||||
var finalPrompt = AIAnalyze.BuildSummaryUserPromptV2(totalEvents);
|
||||
var findingsText = string.Join("\n\n", _findings);
|
||||
var messages = BuildSystemMessages(_systemPrompt, _digest, _overviewNarrative)
|
||||
.Add(new AIAnalyze.ChatMessage("user", "各分段的推理分析:\n" + truncated));
|
||||
if (!string.IsNullOrWhiteSpace(findingsText))
|
||||
{
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage(
|
||||
"user", "各分段的已发现事实:\n" + findingsText));
|
||||
}
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage("user", finalPrompt));
|
||||
|
||||
AppendLog(
|
||||
"让 AI 生成总结...",
|
||||
finalPrompt + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
|
||||
true);
|
||||
|
||||
var result = await _analyzer!.FinishAnalyzeAsync(
|
||||
finalPrompt,
|
||||
CheckAndLogContextUsage(messages, requestContext);
|
||||
var result = await _analyzer!.CompleteAsync(
|
||||
messages,
|
||||
requestContext,
|
||||
OnChunk,
|
||||
_linkedCts.Token);
|
||||
@@ -407,6 +631,53 @@ namespace AnotherReplayReader
|
||||
UpdateTokenDisplay(result);
|
||||
}
|
||||
|
||||
// ---- v2 管线辅助 ----
|
||||
private static ImmutableList<AIAnalyze.ChatMessage> BuildSystemMessages(
|
||||
string systemPrompt,
|
||||
string digest,
|
||||
string? overviewNarrative)
|
||||
{
|
||||
var content = systemPrompt + "\n\n# 对局摘要\n" + digest;
|
||||
if (!string.IsNullOrWhiteSpace(overviewNarrative))
|
||||
{
|
||||
content += "\n\n# 整局总览\n" + overviewNarrative;
|
||||
}
|
||||
return ImmutableList<AIAnalyze.ChatMessage>.Empty
|
||||
.Add(new AIAnalyze.ChatMessage("system", content));
|
||||
}
|
||||
|
||||
private void CheckAndLogContextUsage(
|
||||
ImmutableList<AIAnalyze.ChatMessage> messages,
|
||||
AiRequestContext requestContext)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var message in messages)
|
||||
{
|
||||
total += AiContextBudget.EstimateTokens(message.Content);
|
||||
}
|
||||
var check = AiContextBudget.CheckPromptUsage(
|
||||
total, requestContext.Provider, requestContext.Model);
|
||||
if (check.Block)
|
||||
{
|
||||
throw new InvalidOperationException(check.Message);
|
||||
}
|
||||
if (!check.IsOk)
|
||||
{
|
||||
AppendLog("上下文预算警告", check.Message, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string TruncateByTokens(string text, int maxTokens)
|
||||
{
|
||||
if (AiContextBudget.EstimateTokens(text) <= maxTokens)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
// 中文约 1 token/字,取保守系数 1.1 字符/token
|
||||
var chars = Math.Min(text.Length, (int)(maxTokens * 1.1));
|
||||
return text.Substring(0, chars) + "\n…(已截断)";
|
||||
}
|
||||
|
||||
// ---- 块追加与折叠 ----
|
||||
private void OnChunk(AIAnalyze.AIChunk chunk)
|
||||
{
|
||||
@@ -444,9 +715,28 @@ namespace AnotherReplayReader
|
||||
private void StartContentParagraph()
|
||||
{
|
||||
_currentContent = (_currentContent?.Think, Content: new Paragraph());
|
||||
_lastContentParagraph = _currentContent.Value.Content;
|
||||
_document.Blocks.Add(_currentContent.Value.Content);
|
||||
}
|
||||
|
||||
private void ReplaceSegmentContent(string text)
|
||||
{
|
||||
FinishCurrentContent();
|
||||
// 移除本段已显示的全部块(草稿/回查中间内容),只保留最终修正版
|
||||
while (_document.Blocks.Count > _blockCountBeforeSegment)
|
||||
{
|
||||
_document.Blocks.Remove(_document.Blocks.LastBlock);
|
||||
}
|
||||
_currentContent = null;
|
||||
_lastContentParagraph = null;
|
||||
StartContentParagraph();
|
||||
_lastContentParagraph!.Inlines.Add(new Run(text)
|
||||
{
|
||||
Foreground = ContentStyle.Foreground,
|
||||
FontSize = ContentStyle.FontSize
|
||||
});
|
||||
}
|
||||
|
||||
private void AppendToParagraph(Paragraph? paragraph, string text, ParagraphStyle style)
|
||||
{
|
||||
if (paragraph is null)
|
||||
@@ -610,6 +900,16 @@ namespace AnotherReplayReader
|
||||
{
|
||||
AutoScroll();
|
||||
|
||||
// 修订 pass 期间:不显示中间内容,只统计字符数
|
||||
if (_suppressDisplay)
|
||||
{
|
||||
while (_chunkQueue.TryDequeue(out var data))
|
||||
{
|
||||
_currentOutputChars += data.Chunk.Text.Length;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var thinkSb = new StringBuilder();
|
||||
var contentSb = new StringBuilder();
|
||||
var errorSb = new StringBuilder();
|
||||
@@ -738,7 +1038,7 @@ namespace AnotherReplayReader
|
||||
MessageBox.Show("无法重试:外部取消令牌不可用。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
if (_analyzer.LastSuccessfulState.CurrentSegment < 0)
|
||||
if (_overviewNarrative is null)
|
||||
{
|
||||
_canRetry = false;
|
||||
UpdateButtons();
|
||||
@@ -778,12 +1078,16 @@ namespace AnotherReplayReader
|
||||
_internalCts = new CancellationTokenSource();
|
||||
_linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalCancelToken, _internalCts.Token);
|
||||
|
||||
// 注意:此时 _analyzer.CurrentSegment 停留在失败的那一段
|
||||
// 需要继续 ProcessSegmentsAsync,但需要将现有的 result.State 作为起点
|
||||
await ProcessSegmentsAsync(_analyzer.LastSuccessfulState, _eventCounts);
|
||||
// 注意:此时 _currentSegmentIndex 停留在失败的那一段
|
||||
// _slices/_findings/_overview 保留,从失败段继续
|
||||
if (GetRequestContext() is not { } retryContext)
|
||||
{
|
||||
throw new InvalidOperationException("未配置 AI 请求上下文,无法重试。");
|
||||
}
|
||||
await ProcessSegmentsAsync(retryContext);
|
||||
|
||||
// 总结
|
||||
await ProcessSummaryAsync(_eventCounts);
|
||||
await ProcessSummaryAsync(retryContext);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user