wip 2
This commit is contained in:
+59
-1
@@ -110,6 +110,7 @@ namespace AnotherReplayReader
|
|||||||
// Token 累计
|
// Token 累计
|
||||||
private int _totalPromptTokens;
|
private int _totalPromptTokens;
|
||||||
private int _totalCompletionTokens;
|
private int _totalCompletionTokens;
|
||||||
|
private int _totalReasoningTokens;
|
||||||
|
|
||||||
// ---------- properties ----------
|
// ---------- properties ----------
|
||||||
// 外部注入:每次请求前调用获取最新配置
|
// 外部注入:每次请求前调用获取最新配置
|
||||||
@@ -220,6 +221,7 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
_totalPromptTokens = 0;
|
_totalPromptTokens = 0;
|
||||||
_totalCompletionTokens = 0;
|
_totalCompletionTokens = 0;
|
||||||
|
_totalReasoningTokens = 0;
|
||||||
|
|
||||||
StopUiTimer();
|
StopUiTimer();
|
||||||
UpdateButtons();
|
UpdateButtons();
|
||||||
@@ -673,6 +675,59 @@ namespace AnotherReplayReader
|
|||||||
_linkedCts.Token);
|
_linkedCts.Token);
|
||||||
|
|
||||||
UpdateTokenDisplay(result);
|
UpdateTokenDisplay(result);
|
||||||
|
|
||||||
|
// 总结轮回查:允许模型请求一次远处原始区间,作为同一会话的追加输入。
|
||||||
|
if (_replayData is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var backqueries = BackqueryParser.Parse(result.Response);
|
||||||
|
if (backqueries.IsEmpty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxSummaryBackqueries = 3;
|
||||||
|
var pendingTexts = new List<string>();
|
||||||
|
foreach (var (start, end) in backqueries)
|
||||||
|
{
|
||||||
|
if (pendingTexts.Count >= maxSummaryBackqueries)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_phaseText.Text = "正在根据总结回查补充信息...";
|
||||||
|
var backqueryMessages = messages
|
||||||
|
.Add(new AIAnalyze.ChatMessage("assistant", result.Response))
|
||||||
|
.Add(new AIAnalyze.ChatMessage(
|
||||||
|
"user",
|
||||||
|
AIAnalyze.BuildBackqueryUserPrompt(string.Join("\n\n", pendingTexts))));
|
||||||
|
AppendLog(
|
||||||
|
"总结回查",
|
||||||
|
$"已提供 {pendingTexts.Count} 个区间,正在生成最终总结。",
|
||||||
|
true);
|
||||||
|
CheckAndLogContextUsage(backqueryMessages, requestContext);
|
||||||
|
var finalSummary = await _analyzer!.CompleteAsync(
|
||||||
|
backqueryMessages,
|
||||||
|
requestContext,
|
||||||
|
OnChunk,
|
||||||
|
_linkedCts.Token);
|
||||||
|
UpdateTokenDisplay(finalSummary);
|
||||||
|
AppendLog("总结完成", "已根据回查区间补充最终总结。", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- v2 管线辅助 ----
|
// ---- v2 管线辅助 ----
|
||||||
@@ -1050,13 +1105,16 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
_totalPromptTokens += result.PromptTokens ?? 0;
|
_totalPromptTokens += result.PromptTokens ?? 0;
|
||||||
_totalCompletionTokens += result.CompletionTokens ?? 0;
|
_totalCompletionTokens += result.CompletionTokens ?? 0;
|
||||||
|
_totalReasoningTokens += result.ReasoningTokens ?? 0;
|
||||||
var total = _totalPromptTokens + _totalCompletionTokens;
|
var total = _totalPromptTokens + _totalCompletionTokens;
|
||||||
_currentTokensText.Text =
|
_currentTokensText.Text =
|
||||||
$"上次请求 Token:输入 {FormatNumber(result.PromptTokens, false)}"
|
$"上次请求 Token:输入 {FormatNumber(result.PromptTokens, false)}"
|
||||||
+ $" 输出 {FormatNumber(result.CompletionTokens, false)}";
|
+ $" 输出 {FormatNumber(result.CompletionTokens, false)}"
|
||||||
|
+ (result.ReasoningTokens is { } reasoning ? $" 推理 {FormatNumber(reasoning, false)}" : "");
|
||||||
_conversationTokensText.Text =
|
_conversationTokensText.Text =
|
||||||
$"累计 Token:输入 {FormatNumber(_totalPromptTokens, false)}"
|
$"累计 Token:输入 {FormatNumber(_totalPromptTokens, false)}"
|
||||||
+ $" 输出 {FormatNumber(_totalCompletionTokens, false)}"
|
+ $" 输出 {FormatNumber(_totalCompletionTokens, false)}"
|
||||||
|
+ (_totalReasoningTokens > 0 ? $" 推理 {FormatNumber(_totalReasoningTokens, false)}" : "")
|
||||||
+ $" 总计 {FormatNumber(total, false)}";
|
+ $" 总计 {FormatNumber(total, false)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,27 @@ namespace AiV2.Tests
|
|||||||
// 小预算 → 至少 MinSliceTokens 才切
|
// 小预算 → 至少 MinSliceTokens 才切
|
||||||
var (minSlices, _) = MechanicalSegmenter.Slice(fullText, spans.ToImmutable(), 100, 50);
|
var (minSlices, _) = MechanicalSegmenter.Slice(fullText, spans.ToImmutable(), 100, 50);
|
||||||
Program.Assert(!minSlices.IsEmpty, "小预算仍应有切片");
|
Program.Assert(!minSlices.IsEmpty, "小预算仍应有切片");
|
||||||
|
|
||||||
|
// 超过 20 段时优先压缩纯选择块
|
||||||
|
var mixedBuilder = ImmutableArray.CreateBuilder<EventSpan>();
|
||||||
|
var mixedTextBuilder = new StringBuilder();
|
||||||
|
for (var i = 0; i < 30; ++i)
|
||||||
|
{
|
||||||
|
var text = $"[{i}:00]\nPlayerA: 选择单位\n [UnitId]{i}\n\n";
|
||||||
|
mixedBuilder.Add(new EventSpan(TimeSpan.FromMinutes(i), mixedTextBuilder.Length, text.Length, 1000));
|
||||||
|
mixedTextBuilder.Append(text);
|
||||||
|
}
|
||||||
|
for (var i = 30; i < 60; ++i)
|
||||||
|
{
|
||||||
|
var text = $"[{i}:00]\nPlayerA: 开始建造\n [UnitId]{i}\n\n";
|
||||||
|
mixedBuilder.Add(new EventSpan(TimeSpan.FromMinutes(i), mixedTextBuilder.Length, text.Length, 1000));
|
||||||
|
mixedTextBuilder.Append(text);
|
||||||
|
}
|
||||||
|
var mixedText = mixedTextBuilder.ToString();
|
||||||
|
var (mixedSlices, mixedWarnings) = MechanicalSegmenter.Slice(
|
||||||
|
mixedText, mixedBuilder.ToImmutable(), 2000, 400);
|
||||||
|
Program.Assert(mixedSlices.Length <= MechanicalSegmenter.MaxSlices, "压缩后分段数不超过上限");
|
||||||
|
Program.Assert(mixedWarnings.Any(w => w.Contains("过滤纯选择")), "超过上限时先压缩噪声块");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +476,23 @@ namespace AiV2.Tests
|
|||||||
var conflictingPower = Validate("[机器可读声明]\n```json\n{\"unitClaims\":[{\"unitId\":7,\"player\":\"PlayerA\",\"claim\":\"AlliedMCV\",\"evidenceLevel\":\"confirmed\",\"evidence\":[\"power|0:01.00|SpecialPower_UnpackReplaceSelf|7\"]}]}\n```", index);
|
var conflictingPower = Validate("[机器可读声明]\n```json\n{\"unitClaims\":[{\"unitId\":7,\"player\":\"PlayerA\",\"claim\":\"AlliedMCV\",\"evidenceLevel\":\"confirmed\",\"evidence\":[\"power|0:01.00|SpecialPower_UnpackReplaceSelf|7\"]}]}\n```", index);
|
||||||
Program.Assert(conflictingPower.Issues.Any(i => i.Kind == AIValidationIssueKind.UnitCapabilityContradiction
|
Program.Assert(conflictingPower.Issues.Any(i => i.Kind == AIValidationIssueKind.UnitCapabilityContradiction
|
||||||
&& i.Message.Contains("SpecialPower_UnpackReplaceSelf")), "记录过其他能力 → Contradiction");
|
&& i.Message.Contains("SpecialPower_UnpackReplaceSelf")), "记录过其他能力 → Contradiction");
|
||||||
|
|
||||||
|
// 事件声明:引用不存在的 UnitId → Warning
|
||||||
|
var badEventUnit = Validate("[机器可读声明]\n```json\n{\"eventClaims\":[{\"claim\":\"某事件\",\"evidenceLevel\":\"confirmed\",\"evidence\":[\"power|0:01.00|SpecialPower_Nonexistent|999\"]}]}\n```", index);
|
||||||
|
Program.Assert(badEventUnit.Issues.Any(i => i.Kind == AIValidationIssueKind.InvalidMachineReadableClaims
|
||||||
|
&& i.Severity == AIValidationSeverity.Warning
|
||||||
|
&& i.Message.Contains("999")), "事件声明引用不存在 UnitId → Warning");
|
||||||
|
|
||||||
|
// 事件声明:技能与事实索引冲突 → Contradiction
|
||||||
|
var badEventPower = Validate("[机器可读声明]\n```json\n{\"eventClaims\":[{\"claim\":\"基地迁移\",\"evidenceLevel\":\"confirmed\",\"evidence\":[\"power|0:01.00|SpecialPower_UnpackReplaceSelf|7\"]}]}\n```", index);
|
||||||
|
Program.Assert(badEventPower.Issues.Any(i => i.Kind == AIValidationIssueKind.UnitCapabilityContradiction
|
||||||
|
&& i.Severity == AIValidationSeverity.Contradiction), "事件声明技能冲突 → Contradiction");
|
||||||
|
|
||||||
|
// 事件声明:协议未在任何玩家选择中观察到 → Warning
|
||||||
|
var badEventTech = Validate("[机器可读声明]\n```json\n{\"eventClaims\":[{\"claim\":\"协议事件\",\"evidenceLevel\":\"confirmed\",\"evidence\":[\"protocol|0:02.00|PlayerTech_Unknown\"]}]}\n```", index);
|
||||||
|
Program.Assert(badEventTech.Issues.Any(i => i.Kind == AIValidationIssueKind.InvalidMachineReadableClaims
|
||||||
|
&& i.Severity == AIValidationSeverity.Warning
|
||||||
|
&& i.Message.Contains("PlayerTech_Unknown")), "事件声明未观察协议 → Warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AIValidationResult Validate(string response, ReplayFactIndex index) =>
|
private static AIValidationResult Validate(string response, ReplayFactIndex index) =>
|
||||||
@@ -608,6 +646,11 @@ namespace AiV2.Tests
|
|||||||
Program.Assert(prompt.Contains("基地车"), "结构化渲染包含盟军单位");
|
Program.Assert(prompt.Contains("基地车"), "结构化渲染包含盟军单位");
|
||||||
Program.Assert(!prompt.Contains("神州常用建筑"), "未参战阵营(神州)被过滤");
|
Program.Assert(!prompt.Contains("神州常用建筑"), "未参战阵营(神州)被过滤");
|
||||||
Program.Assert(prompt.Contains("地图参数") || prompt.Contains("出生点"), "地图知识保留");
|
Program.Assert(prompt.Contains("地图参数") || prompt.Contains("出生点"), "地图知识保留");
|
||||||
|
|
||||||
|
var corona = KnowledgeSet.ForMod("corona", AppContext.BaseDirectory);
|
||||||
|
var coronaPrompt = corona.RenderAsPrompt(new[] { "盟军" }, "map_mp_2_rao1");
|
||||||
|
Program.Assert(coronaPrompt.Contains("盟军"), "Corona 盟军知识保留");
|
||||||
|
Program.Assert(!coronaPrompt.Contains("神州常用建筑"), "Corona 未参战阵营(神州)被过滤");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,6 +752,11 @@ namespace AiV2.Tests
|
|||||||
{
|
{
|
||||||
public static void Run()
|
public static void Run()
|
||||||
{
|
{
|
||||||
|
if (Environment.GetEnvironmentVariable("ARR_E2E_REPLAY") != "1")
|
||||||
|
{
|
||||||
|
Console.WriteLine(" [跳过] 未设置 ARR_E2E_REPLAY=1,跳过真实回放诊断");
|
||||||
|
return;
|
||||||
|
}
|
||||||
var replayPath = @"C:\Users\lanyi\Documents\Red Alert 3\Replays\安洁莉娜.(C)_VS_机枢舞者(A)[1V1][无限岛][2026_06_16 05_57][ra3battle.net].RA3Replay";
|
var replayPath = @"C:\Users\lanyi\Documents\Red Alert 3\Replays\安洁莉娜.(C)_VS_机枢舞者(A)[1V1][无限岛][2026_06_16 05_57][ra3battle.net].RA3Replay";
|
||||||
if (!File.Exists(replayPath))
|
if (!File.Exists(replayPath))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,15 @@
|
|||||||
- 预算检查改为“估算 prompt + 输出/推理余量”,并在回查/修订前重新检查;`stream` 标志尊重模型配置。
|
- 预算检查改为“估算 prompt + 输出/推理余量”,并在回查/修订前重新检查;`stream` 标志尊重模型配置。
|
||||||
- 事实索引:编队所有权按玩家隔离;接入 `0x1F6/0x22A`;unpack 歧义规则只作用于 MCV/基地车类实体。
|
- 事实索引:编队所有权按玩家隔离;接入 `0x1F6/0x22A`;unpack 歧义规则只作用于 MCV/基地车类实体。
|
||||||
- 知识:`aliases/alsoProducedBy` 已解析;摘要补充协议选择与所有权证据;总览的段落描述和回查提示会传入分段指令。
|
- 知识:`aliases/alsoProducedBy` 已解析;摘要补充协议选择与所有权证据;总览的段落描述和回查提示会传入分段指令。
|
||||||
- 测试:当前 `AiV2.Tests` 为 144 项断言全部通过。
|
- 测试:当前 `AiV2.Tests` 默认 149 项断言全部通过;启用 `ARR_E2E_REPLAY=1` 时为 151 项。
|
||||||
|
|
||||||
|
### 2026-08-22 第二轮修订
|
||||||
|
|
||||||
|
- `eventClaims/timelineClaims` 现在也会校验:不存在 UnitId、技能与事实索引冲突、协议未在任何玩家选择中观察到都会产生验证问题。
|
||||||
|
- 总结轮支持一次回查:模型可请求远处原始区间,程序在同一总结会话中追加提供。
|
||||||
|
- 机械分段超过上限时先过滤纯选择/编队事件块,压缩无效才放宽预算。
|
||||||
|
- Corona flat 文本现在也按参战阵营过滤。
|
||||||
|
- `AiV2.Tests` 的真实回放诊断改为默认跳过(设置 `ARR_E2E_REPLAY=1` 启用);UI 状态栏显示推理 token。
|
||||||
|
|
||||||
## 1. 背景与目标
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ namespace AnotherReplayReader.Utils
|
|||||||
if (factIndex is not null)
|
if (factIndex is not null)
|
||||||
{
|
{
|
||||||
ValidateTimelineConsistency(claims, factIndex, aiNameToPlayerIndex, structuredKnowledge, issues);
|
ValidateTimelineConsistency(claims, factIndex, aiNameToPlayerIndex, structuredKnowledge, issues);
|
||||||
|
ValidateSimpleClaimEvidence(claims, factIndex, issues);
|
||||||
}
|
}
|
||||||
return new AIValidationResult(claims, issues.ToImmutable());
|
return new AIValidationResult(claims, issues.ToImmutable());
|
||||||
}
|
}
|
||||||
@@ -711,6 +712,119 @@ namespace AnotherReplayReader.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>事件/时间线声明只有 claim + evidence,没有 player,因此只校验不依赖归属的事实。</summary>
|
||||||
|
private static void ValidateSimpleClaimEvidence(
|
||||||
|
AIMachineReadableClaims claims,
|
||||||
|
ReplayFactIndex factIndex,
|
||||||
|
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||||
|
{
|
||||||
|
foreach (var claim in claims.EventClaims)
|
||||||
|
{
|
||||||
|
ValidateSimpleClaim(claim.Claim, claim.Evidence, "事件声明", factIndex, issues);
|
||||||
|
}
|
||||||
|
foreach (var claim in claims.TimelineClaims)
|
||||||
|
{
|
||||||
|
ValidateSimpleClaim(claim.Claim, claim.Evidence, "时间线声明", factIndex, issues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateSimpleClaim(
|
||||||
|
string claimText,
|
||||||
|
ImmutableArray<string> evidenceStrings,
|
||||||
|
string kind,
|
||||||
|
ReplayFactIndex factIndex,
|
||||||
|
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||||
|
{
|
||||||
|
foreach (var ev in ParseAllEvidence(evidenceStrings))
|
||||||
|
{
|
||||||
|
if (ev.Type == AIEvidenceType.Protocol)
|
||||||
|
{
|
||||||
|
var techName = ev.GetTechName();
|
||||||
|
if (string.IsNullOrWhiteSpace(techName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var allTechChoices = new HashSet<string>(
|
||||||
|
factIndex.PlayerTechChoices.Values.SelectMany(set => set),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (allTechChoices.Count > 0 && !allTechChoices.Contains(techName))
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||||
|
$"{kind}“{claimText}”引用了未在任何玩家选择中观察到的协议“{techName}”。"));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.Type == AIEvidenceType.Power)
|
||||||
|
{
|
||||||
|
var unitIdText = ev.GetUnitId();
|
||||||
|
if (unitIdText is null || !uint.TryParse(unitIdText, out var unitId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var powerName = ev.GetSpecialPowerName();
|
||||||
|
if (string.IsNullOrWhiteSpace(powerName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!factIndex.UnitIdFirstObservedTime.ContainsKey(unitId))
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||||
|
$"{kind}“{claimText}”的证据引用了回放中不存在的 UnitId {unitIdText}。",
|
||||||
|
unitIdText));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factIndex.UnitIdSpecialPowers.TryGetValue(unitId, out var actualPowers))
|
||||||
|
{
|
||||||
|
if (!actualPowers.Contains(powerName))
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Contradiction,
|
||||||
|
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||||
|
$"{kind}“{claimText}”声称 UnitId {unitIdText} 使用了“{powerName}”,但该 UnitId 在回放中使用过:{string.Join("、", actualPowers.OrderBy(x => x))}。",
|
||||||
|
unitIdText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.WeakEvidence,
|
||||||
|
AIValidationIssueKind.WeakEvidence,
|
||||||
|
$"{kind}“{claimText}”声称 UnitId {unitIdText} 使用了“{powerName}”,但该 UnitId 未观察到任何特殊能力。",
|
||||||
|
unitIdText));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.Type is AIEvidenceType.Build
|
||||||
|
or AIEvidenceType.Place
|
||||||
|
or AIEvidenceType.Produce
|
||||||
|
or AIEvidenceType.Select
|
||||||
|
or AIEvidenceType.Sell)
|
||||||
|
{
|
||||||
|
var unitIdText = ev.GetUnitId();
|
||||||
|
if (unitIdText is null || !uint.TryParse(unitIdText, out var unitId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!factIndex.UnitIdFirstObservedTime.ContainsKey(unitId))
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||||
|
$"{kind}“{claimText}”的证据引用了回放中不存在的 UnitId {unitIdText}。",
|
||||||
|
unitIdText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ValidateOwnership(
|
private static void ValidateOwnership(
|
||||||
AIUnitClaim claim,
|
AIUnitClaim claim,
|
||||||
uint unitId,
|
uint unitId,
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ namespace AnotherReplayReader.Utils
|
|||||||
foreach (var entry in ByScope(KnowledgeScopeKind.Global))
|
foreach (var entry in ByScope(KnowledgeScopeKind.Global))
|
||||||
{
|
{
|
||||||
var text = entry.Id.StartsWith("knowledge-text-", StringComparison.Ordinal)
|
var text = entry.Id.StartsWith("knowledge-text-", StringComparison.Ordinal)
|
||||||
|
|| entry.Id.StartsWith("knowledge-file-", StringComparison.Ordinal)
|
||||||
? FilterFlatTextByFactions(entry.Text, factionNames)
|
? FilterFlatTextByFactions(entry.Text, factionNames)
|
||||||
: entry.Text;
|
: entry.Text;
|
||||||
sb.AppendLine(text.Trim());
|
sb.AppendLine(text.Trim());
|
||||||
|
|||||||
+61
-4
@@ -51,14 +51,71 @@ namespace AnotherReplayReader.Utils
|
|||||||
|
|
||||||
if (slices.Count > MaxSlices)
|
if (slices.Count > MaxSlices)
|
||||||
{
|
{
|
||||||
var totalTokens = spans.Sum(s => s.EstimatedTokens);
|
// 先尝试压缩噪声事件块(纯选择/编队类),避免一超限就放宽预算。
|
||||||
var raisedBudget = Math.Max(budget, (int)Math.Ceiling(totalTokens / (double)MaxSlices));
|
var compressedSpans = CompressNoiseSpans(fullText, spans);
|
||||||
slices = SliceCore(fullText, spans, raisedBudget, Math.Max(overlap, raisedBudget / 12));
|
if (compressedSpans.Length < spans.Length && !compressedSpans.IsEmpty)
|
||||||
warnings.Add($"分段数超过上限 {MaxSlices},已放宽单段预算到 {raisedBudget:N0} token。");
|
{
|
||||||
|
var compressedSlices = SliceCore(fullText, compressedSpans, budget, overlap);
|
||||||
|
if (compressedSlices.Count <= MaxSlices)
|
||||||
|
{
|
||||||
|
warnings.Add($"分段数超过上限 {MaxSlices},已过滤纯选择/编队事件块,压缩到 {compressedSlices.Count} 段。");
|
||||||
|
return (compressedSlices.ToImmutableArray(), warnings.ToImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
var compressedTokens = compressedSpans.Sum(s => s.EstimatedTokens);
|
||||||
|
var raisedBudget = Math.Max(budget, (int)Math.Ceiling(compressedTokens / (double)MaxSlices));
|
||||||
|
slices = SliceCore(fullText, compressedSpans, raisedBudget, Math.Max(overlap, raisedBudget / 12));
|
||||||
|
warnings.Add($"分段数仍超过上限 {MaxSlices},已过滤噪声并放宽单段预算到 {raisedBudget:N0} token。");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var totalTokens = spans.Sum(s => s.EstimatedTokens);
|
||||||
|
var raisedBudget = Math.Max(budget, (int)Math.Ceiling(totalTokens / (double)MaxSlices));
|
||||||
|
slices = SliceCore(fullText, spans, raisedBudget, Math.Max(overlap, raisedBudget / 12));
|
||||||
|
warnings.Add($"分段数超过上限 {MaxSlices},已放宽单段预算到 {raisedBudget:N0} token。");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (slices.ToImmutableArray(), warnings.ToImmutable());
|
return (slices.ToImmutableArray(), warnings.ToImmutable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ImmutableArray<EventSpan> CompressNoiseSpans(
|
||||||
|
string fullText,
|
||||||
|
ImmutableArray<EventSpan> spans)
|
||||||
|
{
|
||||||
|
var result = ImmutableArray.CreateBuilder<EventSpan>();
|
||||||
|
foreach (var span in spans)
|
||||||
|
{
|
||||||
|
if (!IsNoiseEventBlock(fullText.Substring(span.StartIndex, span.Length)))
|
||||||
|
{
|
||||||
|
result.Add(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNoiseEventBlock(string text)
|
||||||
|
{
|
||||||
|
var hasCommand = false;
|
||||||
|
foreach (var rawLine in text.Replace("\r", "").Split('\n'))
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("[", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令行通常形如 "PlayerA: 选择单位";测试/简写文本把非空行也视作命令。
|
||||||
|
hasCommand = true;
|
||||||
|
if (line.IndexOf("选择", StringComparison.Ordinal) < 0
|
||||||
|
&& line.IndexOf("编队", StringComparison.Ordinal) < 0
|
||||||
|
&& line.IndexOf("取消选择", StringComparison.Ordinal) < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasCommand;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<ReplaySlice> SliceCore(
|
private static List<ReplaySlice> SliceCore(
|
||||||
string fullText,
|
string fullText,
|
||||||
ImmutableArray<EventSpan> spans,
|
ImmutableArray<EventSpan> spans,
|
||||||
|
|||||||
Reference in New Issue
Block a user