wip 2
This commit is contained in:
@@ -135,6 +135,7 @@ namespace AnotherReplayReader.Utils
|
||||
if (factIndex is not null)
|
||||
{
|
||||
ValidateTimelineConsistency(claims, factIndex, aiNameToPlayerIndex, structuredKnowledge, issues);
|
||||
ValidateSimpleClaimEvidence(claims, factIndex, issues);
|
||||
}
|
||||
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(
|
||||
AIUnitClaim claim,
|
||||
uint unitId,
|
||||
|
||||
@@ -432,6 +432,7 @@ namespace AnotherReplayReader.Utils
|
||||
foreach (var entry in ByScope(KnowledgeScopeKind.Global))
|
||||
{
|
||||
var text = entry.Id.StartsWith("knowledge-text-", StringComparison.Ordinal)
|
||||
|| entry.Id.StartsWith("knowledge-file-", StringComparison.Ordinal)
|
||||
? FilterFlatTextByFactions(entry.Text, factionNames)
|
||||
: entry.Text;
|
||||
sb.AppendLine(text.Trim());
|
||||
|
||||
+61
-4
@@ -51,14 +51,71 @@ namespace AnotherReplayReader.Utils
|
||||
|
||||
if (slices.Count > MaxSlices)
|
||||
{
|
||||
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。");
|
||||
// 先尝试压缩噪声事件块(纯选择/编队类),避免一超限就放宽预算。
|
||||
var compressedSpans = CompressNoiseSpans(fullText, spans);
|
||||
if (compressedSpans.Length < spans.Length && !compressedSpans.IsEmpty)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
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(
|
||||
string fullText,
|
||||
ImmutableArray<EventSpan> spans,
|
||||
|
||||
Reference in New Issue
Block a user