focus on intervals, and fix provider
This commit is contained in:
+195
-179
@@ -1,4 +1,4 @@
|
||||
using AnotherReplayReader.Apm;
|
||||
using AnotherReplayReader.Apm;
|
||||
using AnotherReplayReader.ReplayFile;
|
||||
using AnotherReplayReader.Utils;
|
||||
using System;
|
||||
@@ -97,7 +97,6 @@ namespace AnotherReplayReader
|
||||
// 输出段落
|
||||
private (Paragraph? Think, Paragraph? Content)? _currentContent;
|
||||
private Paragraph? _lastContentParagraph;
|
||||
private bool _suppressDisplay;
|
||||
private bool _nextThinkingIsContinuation;
|
||||
|
||||
// 进度计时
|
||||
@@ -228,7 +227,6 @@ namespace AnotherReplayReader
|
||||
_thinkBlockWasExpanded = false;
|
||||
_currentContent = null;
|
||||
_lastContentParagraph = null;
|
||||
_suppressDisplay = false;
|
||||
_nextThinkingIsContinuation = false;
|
||||
|
||||
_previousTotalTime = TimeSpan.Zero;
|
||||
@@ -458,6 +456,8 @@ namespace AnotherReplayReader
|
||||
var structuredKnowledge = StructuredKnowledge.GetForMod(
|
||||
AIAnalyze.GetKnowledgeModName(_replay));
|
||||
|
||||
const int maxBackqueriesPerSegment = 3;
|
||||
|
||||
while (_currentSegmentIndex < _slices.Length)
|
||||
{
|
||||
_linkedCts.Token.ThrowIfCancellationRequested();
|
||||
@@ -481,187 +481,229 @@ namespace AnotherReplayReader
|
||||
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,
|
||||
overviewEntry?.Description,
|
||||
overviewEntry?.BackqueryHints);
|
||||
|
||||
var messages = BuildSystemMessages(_systemPrompt, _digest, _overviewNarrative)
|
||||
.Add(new AIAnalyze.ChatMessage("user", sliceText));
|
||||
if (!string.IsNullOrWhiteSpace(findingsText))
|
||||
// 段内焦点窗口:数据保持整段切片(尽量长),分析重点落在窗口上。
|
||||
var windows = FocusPlanner.Plan(slice, _eventSpans);
|
||||
if (windows.IsEmpty)
|
||||
{
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage(
|
||||
"user", "之前各段的已发现事实:\n" + findingsText));
|
||||
windows = ImmutableArray.Create(
|
||||
new FocusWindow(0, slice.Start, slice.End, slice.EventCount, slice.EstimatedTokens));
|
||||
}
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage("user", instruction));
|
||||
|
||||
AppendLog(
|
||||
$"让 AI 分析第{segmentIndex}段...",
|
||||
instruction + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
|
||||
true);
|
||||
CheckAndLogContextUsage(messages, requestContext);
|
||||
StartVersion("版本 1:正文", collapsedByDefault: false);
|
||||
// 本段已发现事实:段内窗口之间的累积摘要(窗口内也会看到之前的段/窗口发现)。
|
||||
var segmentFindings = new List<string>();
|
||||
|
||||
// 回查循环(M3):同一段会话内,模型可多次请求远处原始区间
|
||||
const int maxBackqueriesPerSegment = 3;
|
||||
var segmentMessages = messages;
|
||||
var backqueryCount = 0;
|
||||
string segmentResponse;
|
||||
while (true)
|
||||
for (var windowIndex = 0; windowIndex < windows.Length; ++windowIndex)
|
||||
{
|
||||
// 回查会增长同一会话的消息列表,每次请求前都重新做预算检查。
|
||||
CheckAndLogContextUsage(segmentMessages, requestContext);
|
||||
var segmentResult = await _analyzer!.CompleteAsync(
|
||||
segmentMessages,
|
||||
requestContext,
|
||||
OnChunk,
|
||||
_linkedCts.Token);
|
||||
UpdateTokenDisplay(segmentResult);
|
||||
ReportReasoningGuardResult(segmentResult);
|
||||
segmentResponse = segmentResult.Response;
|
||||
_linkedCts.Token.ThrowIfCancellationRequested();
|
||||
var window = windows[windowIndex];
|
||||
_phaseText.Text =
|
||||
$"正在分析第 {segmentIndex}/{totalSegments} 段(窗口 {windowIndex + 1}/{windows.Length})";
|
||||
|
||||
if (backqueryCount >= maxBackqueriesPerSegment)
|
||||
FinishCurrentContent();
|
||||
_lastContentParagraph = null;
|
||||
|
||||
// 段内已发现事实:窗口之间共享“已发现事实”,但只保留本段窗口已发现的。
|
||||
var combinedFindings = new List<string>(_findings);
|
||||
combinedFindings.AddRange(segmentFindings);
|
||||
var findingsText = LimitFindings(combinedFindings, maxFindingsTokens);
|
||||
var windowEventCount =
|
||||
_eventCounts?.Query(window.Start, window.End) ?? window.EventCount;
|
||||
var instruction = AIAnalyze.BuildFocusWindowUserPromptV2(
|
||||
_currentSegmentIndex,
|
||||
totalSegments,
|
||||
slice,
|
||||
windowIndex,
|
||||
windows.Length,
|
||||
window,
|
||||
eventCount,
|
||||
windowEventCount,
|
||||
title,
|
||||
overviewEntry?.Description,
|
||||
overviewEntry?.BackqueryHints);
|
||||
|
||||
// 数据层 = 整段切片(唯一的数据消息),只发一次;窗口指令作为“重点”指令。
|
||||
var messages = BuildSystemMessages(_systemPrompt, _digest, _overviewNarrative)
|
||||
.Add(new AIAnalyze.ChatMessage("user", sliceText));
|
||||
if (!string.IsNullOrWhiteSpace(findingsText))
|
||||
{
|
||||
break;
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage(
|
||||
"user", "之前的已发现事实:\n" + findingsText));
|
||||
}
|
||||
messages = messages.Add(new AIAnalyze.ChatMessage("user", instruction));
|
||||
|
||||
var backqueries = BackqueryParser.Parse(segmentResponse);
|
||||
var pendingTexts = new List<string>();
|
||||
foreach (var (start, end) in backqueries)
|
||||
AppendLog(
|
||||
$"让 AI 分析第{segmentIndex}段窗口 {windowIndex + 1}/{windows.Length}...",
|
||||
instruction + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]",
|
||||
true);
|
||||
CheckAndLogContextUsage(messages, requestContext);
|
||||
StartVersion(
|
||||
$"窗口 {windowIndex + 1}/{windows.Length}:正文",
|
||||
collapsedByDefault: false);
|
||||
|
||||
// 回查循环(M3):同一窗口会话内,模型可多次请求远处原始区间
|
||||
var windowMessages = messages;
|
||||
var backqueryCount = 0;
|
||||
string windowResponse;
|
||||
while (true)
|
||||
{
|
||||
if (backqueryCount + pendingTexts.Count >= maxBackqueriesPerSegment)
|
||||
// 回查会增长同一会话的消息列表,每次请求前都重新做预算检查。
|
||||
CheckAndLogContextUsage(windowMessages, requestContext);
|
||||
var windowResult = await _analyzer!.CompleteAsync(
|
||||
windowMessages,
|
||||
requestContext,
|
||||
OnChunk,
|
||||
_linkedCts.Token);
|
||||
UpdateTokenDisplay(windowResult);
|
||||
ReportReasoningGuardResult(windowResult);
|
||||
windowResponse = windowResult.Response;
|
||||
|
||||
if (backqueryCount >= maxBackqueriesPerSegment)
|
||||
{
|
||||
AppendLog("回查限制", "本段回查次数已达上限,剩余区间已忽略。", false);
|
||||
break;
|
||||
}
|
||||
var (text, reason) = BackquerySliceExtractor.Extract(
|
||||
_replayData, _eventSpans, start, end);
|
||||
if (text is null)
|
||||
|
||||
var backqueries = BackqueryParser.Parse(windowResponse);
|
||||
var pendingTexts = new List<string>();
|
||||
foreach (var (start, end) in backqueries)
|
||||
{
|
||||
AppendLog("回查失败", reason ?? "未知原因", false);
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
pendingTexts.Add(
|
||||
$"[回查 {MatchDigestBuilder.FormatTime(start)}~{MatchDigestBuilder.FormatTime(end)}]\n" + text);
|
||||
|
||||
if (pendingTexts.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
backqueryCount += pendingTexts.Count;
|
||||
var backqueryPrompt = AIAnalyze.BuildBackqueryUserPrompt(
|
||||
string.Join("\n\n", pendingTexts));
|
||||
windowMessages = windowMessages
|
||||
.Add(new AIAnalyze.ChatMessage("assistant", windowResponse))
|
||||
.Add(new AIAnalyze.ChatMessage(
|
||||
"user", backqueryPrompt));
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段窗口 {windowIndex + 1} 回查(第 {backqueryCount} 次)",
|
||||
$"已提供 {pendingTexts.Count} 个区间,继续分析。\n\n用户消息:\n" + backqueryPrompt,
|
||||
true);
|
||||
}
|
||||
|
||||
if (pendingTexts.Count == 0)
|
||||
FinishCurrentContent();
|
||||
var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims(
|
||||
windowResponse, _factIndex, playerNameToIndex, structuredKnowledge);
|
||||
AppendMachineJsonAndValidation(windowResponse, validationResult);
|
||||
if (validationResult.HasIssues)
|
||||
{
|
||||
break;
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段窗口 {windowIndex + 1} 机器可读声明检查",
|
||||
AIAnalysisValidation.FormatIssues(validationResult.Issues),
|
||||
false);
|
||||
}
|
||||
backqueryCount += pendingTexts.Count;
|
||||
var backqueryPrompt = AIAnalyze.BuildBackqueryUserPrompt(
|
||||
string.Join("\n\n", pendingTexts));
|
||||
segmentMessages = segmentMessages
|
||||
.Add(new AIAnalyze.ChatMessage("assistant", segmentResponse))
|
||||
.Add(new AIAnalyze.ChatMessage(
|
||||
"user", backqueryPrompt));
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段回查(第 {backqueryCount} 次)",
|
||||
$"已提供 {pendingTexts.Count} 个区间,继续分析。\n\n用户消息:\n" + backqueryPrompt,
|
||||
true);
|
||||
}
|
||||
|
||||
FinishCurrentContent();
|
||||
var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims(
|
||||
segmentResponse, _factIndex, playerNameToIndex, structuredKnowledge);
|
||||
AppendMachineJsonAndValidation(segmentResponse, validationResult);
|
||||
if (validationResult.HasIssues)
|
||||
{
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段机器可读声明检查",
|
||||
AIAnalysisValidation.FormatIssues(validationResult.Issues),
|
||||
false);
|
||||
}
|
||||
|
||||
// 隐藏修订 pass(M6):Contradiction 触发,最多 1 次;修订期间不流式显示
|
||||
var finalResponse = segmentResponse;
|
||||
var finalValidation = validationResult;
|
||||
if (validationResult.RequiresRevision)
|
||||
{
|
||||
var issueCount = validationResult.Issues.Count(
|
||||
i => i.Severity is AIValidationSeverity.Contradiction
|
||||
or AIValidationSeverity.Fatal
|
||||
or AIValidationSeverity.Warning
|
||||
or AIValidationSeverity.WeakEvidence);
|
||||
var relevantFacts = RelevantFactsFormatter.Format(validationResult.Claims, _factIndex!);
|
||||
var revisionPrompt = AIAnalyze.BuildRevisionUserPrompt(
|
||||
segmentResponse,
|
||||
AIAnalysisValidation.FormatIssues(validationResult.Issues),
|
||||
string.IsNullOrWhiteSpace(relevantFacts) ? "(无额外事实)" : relevantFacts);
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段修订",
|
||||
$"验证器发现 {issueCount} 个需要修正/降级的问题,正在请求 AI 修正...\n\n用户消息:\n"
|
||||
+ revisionPrompt,
|
||||
true);
|
||||
var revisionMessages = segmentMessages
|
||||
.Add(new AIAnalyze.ChatMessage("assistant", segmentResponse))
|
||||
.Add(new AIAnalyze.ChatMessage("user", revisionPrompt));
|
||||
CheckAndLogContextUsage(revisionMessages, requestContext);
|
||||
|
||||
_suppressDisplay = true;
|
||||
AIAnalyze.Result revisionResult;
|
||||
try
|
||||
// 隐藏修订 pass(M6):Contradiction 触发,最多 1 次;
|
||||
// 修订草稿按时间顺序流式显示,完成后折叠草稿并默认展开最终正文。
|
||||
var finalResponse = windowResponse;
|
||||
var finalValidation = validationResult;
|
||||
if (validationResult.RequiresRevision)
|
||||
{
|
||||
revisionResult = await _analyzer!.CompleteAsync(
|
||||
var issueCount = validationResult.Issues.Count(
|
||||
i => i.Severity is AIValidationSeverity.Contradiction
|
||||
or AIValidationSeverity.Fatal
|
||||
or AIValidationSeverity.Warning
|
||||
or AIValidationSeverity.WeakEvidence);
|
||||
var relevantFacts = RelevantFactsFormatter.Format(validationResult.Claims, _factIndex!);
|
||||
var revisionPrompt = AIAnalyze.BuildRevisionUserPrompt(
|
||||
windowResponse,
|
||||
AIAnalysisValidation.FormatIssues(validationResult.Issues),
|
||||
string.IsNullOrWhiteSpace(relevantFacts) ? "(无额外事实)" : relevantFacts);
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段窗口 {windowIndex + 1} 修订",
|
||||
$"验证器发现 {issueCount} 个需要修正/降级的问题,正在请求 AI 修正...\n\n用户消息:\n"
|
||||
+ revisionPrompt,
|
||||
true);
|
||||
var revisionMessages = windowMessages
|
||||
.Add(new AIAnalyze.ChatMessage("assistant", windowResponse))
|
||||
.Add(new AIAnalyze.ChatMessage("user", revisionPrompt));
|
||||
CheckAndLogContextUsage(revisionMessages, requestContext);
|
||||
|
||||
FinishCurrentContent();
|
||||
var previousVersionTitle = _currentVersionTitle;
|
||||
var previousVersionUpdate = _currentVersionUpdate;
|
||||
StartVersion("版本 2:修订草稿", collapsedByDefault: false);
|
||||
|
||||
var revisionResult = await _analyzer!.CompleteAsync(
|
||||
revisionMessages,
|
||||
requestContext,
|
||||
OnChunk,
|
||||
_linkedCts.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressDisplay = false;
|
||||
}
|
||||
UpdateTokenDisplay(revisionResult);
|
||||
ReportReasoningGuardResult(revisionResult);
|
||||
UpdateTokenDisplay(revisionResult);
|
||||
ReportReasoningGuardResult(revisionResult);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(revisionResult.Response))
|
||||
{
|
||||
AppendLog("修订失败", "修订输出为空,保留原分析。", false);
|
||||
if (string.IsNullOrWhiteSpace(revisionResult.Response))
|
||||
{
|
||||
CollapseCurrentVersion();
|
||||
AppendLog("修订失败", "修订输出为空,保留原分析。", false);
|
||||
}
|
||||
else
|
||||
{
|
||||
finalResponse = revisionResult.Response;
|
||||
finalValidation = AIAnalysisValidation.ValidateMachineReadableClaims(
|
||||
revisionResult.Response, _factIndex, playerNameToIndex, structuredKnowledge);
|
||||
CollapseCurrentVersion();
|
||||
previousVersionUpdate?.Invoke(
|
||||
previousVersionTitle ?? "版本 1:正文",
|
||||
true);
|
||||
StartVersion("版本 3:修订后正文", collapsedByDefault: false);
|
||||
AppendDirectText(finalResponse);
|
||||
AppendMachineJsonAndValidation(finalResponse, finalValidation);
|
||||
AppendLog(
|
||||
"修订完成",
|
||||
"已采用修正后的分析。" + (finalValidation.HasIssues
|
||||
? "\n仍存在的问题:\n" + AIAnalysisValidation.FormatIssues(finalValidation.Issues)
|
||||
: string.Empty),
|
||||
false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
finalResponse = revisionResult.Response;
|
||||
finalValidation = AIAnalysisValidation.ValidateMachineReadableClaims(
|
||||
revisionResult.Response, _factIndex, playerNameToIndex, structuredKnowledge);
|
||||
CollapseCurrentVersion();
|
||||
StartVersion("版本 2:修订后正文", collapsedByDefault: false);
|
||||
AppendDirectText(finalResponse);
|
||||
AppendMachineJsonAndValidation(finalResponse, finalValidation);
|
||||
AppendLog(
|
||||
"修订完成",
|
||||
"已采用修正后的分析。" + (finalValidation.HasIssues
|
||||
? "\n仍存在的问题:\n" + AIAnalysisValidation.FormatIssues(finalValidation.Issues)
|
||||
: string.Empty),
|
||||
false);
|
||||
}
|
||||
}
|
||||
FinishCurrentContent();
|
||||
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());
|
||||
// 追加本窗口已发现事实(同时进入全局 _findings,供后续段使用)
|
||||
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)
|
||||
{
|
||||
var entryText = entry.ToString().TrimEnd();
|
||||
segmentFindings.Add(entryText);
|
||||
_findings.Add(entryText);
|
||||
}
|
||||
|
||||
_segmentResponses.Add(finalResponse);
|
||||
AppendLog(
|
||||
$"第{segmentIndex}段窗口 {windowIndex + 1}/{windows.Length} 完成。",
|
||||
null,
|
||||
true);
|
||||
}
|
||||
|
||||
_segmentResponses.Add(finalResponse);
|
||||
_lastSuccessfulSegment = _currentSegmentIndex;
|
||||
_currentSegmentIndex++;
|
||||
AppendLog($"第{segmentIndex}段完成。", null, true);
|
||||
@@ -1547,32 +1589,6 @@ namespace AnotherReplayReader
|
||||
{
|
||||
AutoScroll();
|
||||
|
||||
// 修订 pass 期间:不显示中间内容,只统计字符数
|
||||
if (_suppressDisplay)
|
||||
{
|
||||
while (_chunkQueue.TryDequeue(out var data))
|
||||
{
|
||||
if (data.Chunk.Type == AIAnalyze.AIChunkType.ReasoningGuard)
|
||||
{
|
||||
var text = data.Chunk.Text;
|
||||
_nextThinkingIsContinuation = true;
|
||||
_pendingUiActions.Enqueue(() =>
|
||||
AppendLog("推理保护", text, false, _currentStage));
|
||||
}
|
||||
else if (data.Chunk.Type == AIAnalyze.AIChunkType.ReasoningGuardRequest)
|
||||
{
|
||||
var text = data.Chunk.Text;
|
||||
_pendingUiActions.Enqueue(() =>
|
||||
AppendReasoningDiagnostics(text));
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentOutputChars += data.Chunk.Text.Length;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var thinkSb = new StringBuilder();
|
||||
var contentSb = new StringBuilder();
|
||||
var errorSb = new StringBuilder();
|
||||
|
||||
Reference in New Issue
Block a user