deepseek wip
This commit is contained in:
+288
-14
@@ -95,7 +95,9 @@ namespace AnotherReplayReader.Utils
|
||||
@"```(?:json)?\s*(\{[\s\S]*?\})\s*```",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public static AIValidationResult ValidateMachineReadableClaims(string response)
|
||||
public static AIValidationResult ValidateMachineReadableClaims(
|
||||
string response,
|
||||
ReplayFactIndex? factIndex = null)
|
||||
{
|
||||
var issues = ImmutableArray.CreateBuilder<AIValidationIssue>();
|
||||
var json = ExtractJsonObject(response);
|
||||
@@ -115,6 +117,11 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
ValidateClaimSelfConsistency(claims, issues);
|
||||
ValidateUnpackAmbiguity(claims, issues);
|
||||
if (factIndex is not null)
|
||||
{
|
||||
ValidateTimelineConsistency(claims, factIndex, issues);
|
||||
}
|
||||
return new AIValidationResult(claims, issues.ToImmutable());
|
||||
}
|
||||
|
||||
@@ -180,11 +187,11 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
return new AIMachineReadableClaims(
|
||||
ReadUnitClaims(root),
|
||||
ReadSimpleClaims(root, "eventClaims")
|
||||
ReadUnitClaims(root, issues),
|
||||
ReadSimpleClaims(root, "eventClaims", issues)
|
||||
.Select(c => new AIEventClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray(),
|
||||
ReadSimpleClaims(root, "timelineClaims")
|
||||
ReadSimpleClaims(root, "timelineClaims", issues)
|
||||
.Select(c => new AITimelineClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray());
|
||||
}
|
||||
@@ -198,7 +205,9 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableArray<AIUnitClaim> ReadUnitClaims(JsonElement root)
|
||||
private const int MaxUnitClaims = 10;
|
||||
|
||||
private static ImmutableArray<AIUnitClaim> ReadUnitClaims(JsonElement root, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
if (!TryGetArray(root, "unitClaims", out var unitClaims))
|
||||
{
|
||||
@@ -206,6 +215,7 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
var result = ImmutableArray.CreateBuilder<AIUnitClaim>();
|
||||
var totalCount = 0;
|
||||
foreach (var item in unitClaims.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
@@ -213,15 +223,30 @@ namespace AnotherReplayReader.Utils
|
||||
continue;
|
||||
}
|
||||
|
||||
totalCount++;
|
||||
if (result.Count >= MaxUnitClaims)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new AIUnitClaim(
|
||||
ReadFlexibleString(item, "unitId"),
|
||||
ReadFlexibleString(item, "player"),
|
||||
ReadFlexibleString(item, "claim"),
|
||||
ReadEvidenceLevel(item),
|
||||
ReadEvidenceLevel(item, issues),
|
||||
ReadStringArray(item, "evidence"),
|
||||
ReadStringArray(item, "alternatives"),
|
||||
ReadStringArray(item, "needsConfirmation")));
|
||||
}
|
||||
|
||||
if (totalCount > MaxUnitClaims)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"unitClaims 包含 {totalCount} 条声明,仅处理前 {MaxUnitClaims} 条,其余已忽略。"));
|
||||
}
|
||||
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
@@ -230,7 +255,7 @@ namespace AnotherReplayReader.Utils
|
||||
AIEvidenceLevel EvidenceLevel,
|
||||
ImmutableArray<string> Evidence);
|
||||
|
||||
private static ImmutableArray<SimpleClaim> ReadSimpleClaims(JsonElement root, string propertyName)
|
||||
private static ImmutableArray<SimpleClaim> ReadSimpleClaims(JsonElement root, string propertyName, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
if (!TryGetArray(root, propertyName, out var claims))
|
||||
{
|
||||
@@ -238,6 +263,13 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
var result = ImmutableArray.CreateBuilder<SimpleClaim>();
|
||||
var maxClaims = propertyName switch
|
||||
{
|
||||
"eventClaims" => 5,
|
||||
"timelineClaims" => 3,
|
||||
_ => 50,
|
||||
};
|
||||
var totalCount = 0;
|
||||
foreach (var item in claims.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
@@ -245,11 +277,32 @@ namespace AnotherReplayReader.Utils
|
||||
continue;
|
||||
}
|
||||
|
||||
totalCount++;
|
||||
if (result.Count >= maxClaims)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var claim = ReadFlexibleString(item, "claim");
|
||||
if (string.IsNullOrWhiteSpace(claim) && propertyName == "eventClaims")
|
||||
{
|
||||
claim = ReadFlexibleString(item, "event");
|
||||
}
|
||||
|
||||
result.Add(new SimpleClaim(
|
||||
ReadFlexibleString(item, "claim"),
|
||||
ReadEvidenceLevel(item),
|
||||
claim,
|
||||
ReadEvidenceLevel(item, issues),
|
||||
ReadStringArray(item, "evidence")));
|
||||
}
|
||||
|
||||
if (totalCount > maxClaims)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"{propertyName} 包含 {totalCount} 条声明,仅处理前 {maxClaims} 条,其余已忽略。"));
|
||||
}
|
||||
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
@@ -296,6 +349,216 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
}
|
||||
|
||||
#region Structured evidence parsing
|
||||
|
||||
internal enum AIEvidenceType
|
||||
{
|
||||
Build,
|
||||
Place,
|
||||
Produce,
|
||||
Sell,
|
||||
Select,
|
||||
Move,
|
||||
Power,
|
||||
Unknown
|
||||
}
|
||||
|
||||
internal sealed record StructuredEvidence(
|
||||
AIEvidenceType Type,
|
||||
string Time,
|
||||
ImmutableArray<string> Parameters,
|
||||
string Raw)
|
||||
{
|
||||
public string? GetSpecialPowerName() =>
|
||||
Type == AIEvidenceType.Power && Parameters.Length >= 1 ? Parameters[0] : null;
|
||||
|
||||
public string? GetUnitId() => Type switch
|
||||
{
|
||||
AIEvidenceType.Build or AIEvidenceType.Place when Parameters.Length >= 2 => Parameters[1],
|
||||
AIEvidenceType.Produce when Parameters.Length >= 2 => Parameters[1],
|
||||
AIEvidenceType.Power when Parameters.Length >= 2 => Parameters[1],
|
||||
AIEvidenceType.Sell when Parameters.Length >= 1 => Parameters[0],
|
||||
AIEvidenceType.Select when Parameters.Length >= 1 => Parameters[0],
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public string? GetAssetName() => Type switch
|
||||
{
|
||||
AIEvidenceType.Build or AIEvidenceType.Place or AIEvidenceType.Produce
|
||||
when Parameters.Length >= 1 => Parameters[0],
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly Regex _structuredEvidenceRegex = new(
|
||||
@"^(build|place|produce|sell|select|move|power)\|([^|]+(?:\|(?!\|).*)?)$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
internal static StructuredEvidence ParseStructuredEvidence(string text)
|
||||
{
|
||||
var match = _structuredEvidenceRegex.Match(text.Trim());
|
||||
if (!match.Success)
|
||||
{
|
||||
return new StructuredEvidence(AIEvidenceType.Unknown, string.Empty, ImmutableArray<string>.Empty, text);
|
||||
}
|
||||
|
||||
var type = match.Groups[1].Value.ToLowerInvariant() switch
|
||||
{
|
||||
"build" => AIEvidenceType.Build,
|
||||
"place" => AIEvidenceType.Place,
|
||||
"produce" => AIEvidenceType.Produce,
|
||||
"sell" => AIEvidenceType.Sell,
|
||||
"select" => AIEvidenceType.Select,
|
||||
"move" => AIEvidenceType.Move,
|
||||
"power" => AIEvidenceType.Power,
|
||||
_ => AIEvidenceType.Unknown,
|
||||
};
|
||||
|
||||
var rest = match.Groups[2].Value;
|
||||
var parts = rest.Split('|');
|
||||
var time = parts.Length >= 1 ? parts[0].Trim() : string.Empty;
|
||||
var parameters = parts.Skip(1).Select(p => p.Trim()).ToImmutableArray();
|
||||
|
||||
return new StructuredEvidence(type, time, parameters, text);
|
||||
}
|
||||
|
||||
internal static ImmutableArray<StructuredEvidence> ParseAllEvidence(ImmutableArray<string> evidenceStrings)
|
||||
{
|
||||
return evidenceStrings
|
||||
.Select(ParseStructuredEvidence)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation rules
|
||||
|
||||
private static void ValidateUnpackAmbiguity(
|
||||
AIMachineReadableClaims claims,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
foreach (var claim in claims.UnitClaims)
|
||||
{
|
||||
if (claim.EvidenceLevel is not (AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var evidence = ParseAllEvidence(claim.Evidence);
|
||||
var hasUnpack = evidence.Any(e =>
|
||||
e.GetSpecialPowerName() is string p &&
|
||||
p.IndexOf("UnpackReplaceSelf", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (!hasUnpack)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasPack = evidence.Any(e =>
|
||||
e.GetSpecialPowerName() is string p &&
|
||||
p.IndexOf("PackReplaceSelf", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (hasPack)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.MissingAlternative,
|
||||
$"UnitId {claim.UnitId} 使用了 UnpackReplaceSelf 但证据中无对应 PackReplaceSelf。UnpackReplaceSelf 可能对应基地车展开或矿车展开成指挥中心,建议降低置信度或添加 alternative。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTimelineConsistency(
|
||||
AIMachineReadableClaims claims,
|
||||
ReplayFactIndex factIndex,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
foreach (var claim in claims.UnitClaims)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(claim.UnitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!uint.TryParse(claim.UnitId, out var unitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 1: UnitId referenced in claim exists in the replay
|
||||
if (!factIndex.UnitIdFirstObservedTime.ContainsKey(unitId))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"UnitId {claim.UnitId} 在回放数据中从未出现过,AI 可能编造了不存在的 UnitId。",
|
||||
claim.UnitId));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 2: Verify special power evidence against fact index
|
||||
var evidence = ParseAllEvidence(claim.Evidence);
|
||||
foreach (var ev in evidence)
|
||||
{
|
||||
if (ev.Type != AIEvidenceType.Power)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var evUnitIdStr = ev.GetUnitId();
|
||||
if (evUnitIdStr is null || !uint.TryParse(evUnitIdStr, out var evUnitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var powerName = ev.GetSpecialPowerName();
|
||||
if (powerName is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Does this UnitId exist in the fact index?
|
||||
if (!factIndex.UnitIdFirstObservedTime.ContainsKey(evUnitId))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"证据引用了回放中不存在的 UnitId {evUnitIdStr}。",
|
||||
claim.UnitId));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Did this UnitId actually use this special power?
|
||||
if (factIndex.UnitIdSpecialPowers.TryGetValue(evUnitId, out var actualPowers)
|
||||
&& !actualPowers.Contains(powerName))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Contradiction,
|
||||
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||
$"UnitId {evUnitIdStr} 在回放中使用过以下特殊能力:{string.Join(", ", actualPowers.OrderBy(x => x))},但 AI 声称其使用了“{powerName}”——此能力未在该 UnitId 上观察到。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: UnitId used as builder vs claim
|
||||
var isBuilderInReplay = factIndex.BuilderUnitIds.Contains(unitId);
|
||||
var claimLooksLikeBuilder = claim.Claim.IndexOf("MCV", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("基地车", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("Nanocore", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("纳米核心", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("builder", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("建造者", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if (claimLooksLikeBuilder && !isBuilderInReplay)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||
$"AI 推测 UnitId {claim.UnitId} 是“{claim.Claim}”(推测是建造单位),但该 UnitId 在回放中从未作为建造者(建造建筑)出现。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryGetArray(JsonElement root, string propertyName, out JsonElement array)
|
||||
{
|
||||
if (root.TryGetProperty(propertyName, out array)
|
||||
@@ -308,23 +571,34 @@ namespace AnotherReplayReader.Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AIEvidenceLevel ReadEvidenceLevel(JsonElement item)
|
||||
private static AIEvidenceLevel ReadEvidenceLevel(JsonElement item, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
var value = ReadFlexibleString(item, "evidenceLevel");
|
||||
return NormalizeEvidenceLevel(value);
|
||||
return NormalizeEvidenceLevel(value, issues);
|
||||
}
|
||||
|
||||
private static AIEvidenceLevel NormalizeEvidenceLevel(string value)
|
||||
private static AIEvidenceLevel NormalizeEvidenceLevel(string value, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
value = value.Trim().Replace("_", "").Replace("-", "").Replace(" ", "");
|
||||
return value.ToLowerInvariant() switch
|
||||
var result = value.ToLowerInvariant() switch
|
||||
{
|
||||
"confirmed" or "确定" => AIEvidenceLevel.Confirmed,
|
||||
"highlylikely" or "high" or "高度可能" => AIEvidenceLevel.HighlyLikely,
|
||||
"possible" or "可能" => AIEvidenceLevel.Possible,
|
||||
"ruledout" or "excluded" or "已排除" => AIEvidenceLevel.RuledOut,
|
||||
_ => AIEvidenceLevel.Uncertain,
|
||||
_ => (AIEvidenceLevel?)null,
|
||||
};
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidEvidenceLevel,
|
||||
$"无法识别的证据等级 \"{value.Trim()}\",已降级为不确定。"));
|
||||
return AIEvidenceLevel.Uncertain;
|
||||
}
|
||||
|
||||
private static string ReadFlexibleString(JsonElement item, string propertyName)
|
||||
|
||||
Reference in New Issue
Block a user