wip
This commit is contained in:
@@ -86,8 +86,13 @@ namespace AnotherReplayReader.Utils
|
||||
public static AIValidationResult Empty { get; } =
|
||||
new(AIMachineReadableClaims.Empty, ImmutableArray<AIValidationIssue>.Empty);
|
||||
|
||||
// 除 Info 外,任何需要模型修正/降级的验证问题都触发一次隐藏修订;
|
||||
// 一次修订后仍遗留的问题只记录,不再循环请求。
|
||||
public bool RequiresRevision =>
|
||||
Issues.Any(i => i.Severity is AIValidationSeverity.Contradiction or AIValidationSeverity.Fatal);
|
||||
Issues.Any(i => i.Severity is AIValidationSeverity.Contradiction
|
||||
or AIValidationSeverity.Fatal
|
||||
or AIValidationSeverity.Warning
|
||||
or AIValidationSeverity.WeakEvidence);
|
||||
|
||||
public bool HasIssues => !Issues.IsEmpty;
|
||||
}
|
||||
@@ -118,6 +123,10 @@ namespace AnotherReplayReader.Utils
|
||||
var claims = ParseClaims(jsonBlocks, issues);
|
||||
if (claims is null)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Fatal,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
"所有机器可读声明块都无法解析,当前输出不可用于自动验证。"));
|
||||
return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable());
|
||||
}
|
||||
|
||||
@@ -514,6 +523,13 @@ namespace AnotherReplayReader.Utils
|
||||
continue;
|
||||
}
|
||||
|
||||
// 只有“基地车/主基地”这类同时具备 pack/unpack 的实体才存在 MCV vs 矿车的歧义;
|
||||
// 矿车本身只有 unpack,不应被这条规则误伤。
|
||||
if (!LooksLikeUnpackAmbiguousUnit(claim.Claim))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.MissingAlternative,
|
||||
@@ -522,6 +538,30 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
}
|
||||
|
||||
private static bool LooksLikeUnpackAmbiguousUnit(string claimText)
|
||||
{
|
||||
if (claimText.IndexOf("MCV", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claimText.IndexOf("基地车", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var structured = StructuredKnowledge.Instance;
|
||||
if (structured is not null)
|
||||
{
|
||||
foreach (var entity in structured.EntitiesWithTag(KnowledgeTag.Pack))
|
||||
{
|
||||
if (claimText.IndexOf(entity.AssetName, StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (!string.IsNullOrWhiteSpace(entity.DisplayName)
|
||||
&& claimText.IndexOf(entity.DisplayName, StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ValidateTimelineConsistency(
|
||||
AIMachineReadableClaims claims,
|
||||
ReplayFactIndex factIndex,
|
||||
|
||||
+24
-10
@@ -204,7 +204,7 @@ namespace AnotherReplayReader.Utils
|
||||
- `power|时间|技能名|单位UnitId` — 释放特殊能力,例如 `power|1:24.00|SpecialPower_PackReplaceSelf|246`
|
||||
- `protocol|时间|科技名` — 选择协议(全局生效,无单位),例如 `protocol|0:02.33|PlayerTech_Allied_AirPower`
|
||||
|
||||
如果没有可验证推测,可以输出空数组。不要在 JSON 里写注释。
|
||||
如果没有可验证推测,请输出空 JSON 对象(三个字段均为空数组)。不要在 JSON 里写注释。
|
||||
|
||||
# 推理指南
|
||||
推理需要分成多个阶段
|
||||
@@ -1040,20 +1040,27 @@ PlayerA: 开始出兵
|
||||
int totalSegments,
|
||||
ReplaySlice slice,
|
||||
int eventCount,
|
||||
string? title)
|
||||
string? title,
|
||||
string? description = null,
|
||||
IEnumerable<string>? backqueryHints = null)
|
||||
{
|
||||
var beginText = segmentIndex <= 0 ? "游戏开始" : MatchDigestBuilder.FormatTime(slice.Start);
|
||||
var endText = segmentIndex >= totalSegments - 1 ? "游戏结束" : MatchDigestBuilder.FormatTime(slice.End);
|
||||
var titleLine = string.IsNullOrWhiteSpace(title) ? "" : $"\n段落标题:{title}";
|
||||
var descriptionLine = string.IsNullOrWhiteSpace(description) ? "" : $"\n段落概述:{description}";
|
||||
var hintLine = backqueryHints is { } hints && hints.Any()
|
||||
? "\n总览建议可回查区间:" + string.Join("、", hints)
|
||||
: "";
|
||||
var instruction = @$"
|
||||
请重点分析第{segmentIndex + 1}/{totalSegments}段({beginText}至{endText})的数据。
|
||||
本段共有{eventCount}条操作信息,数量很大,因此**不要**在思考推理时直接列出所有操作信息,也不要把每一条操作信息都视作一个单独事件。{titleLine}
|
||||
本段共有{eventCount}条操作信息,数量很大,因此**不要**在思考推理时直接列出所有操作信息,也不要把每一条操作信息都视作一个单独事件。{titleLine}{descriptionLine}
|
||||
{hintLine}
|
||||
你可以参考输入中的对局摘要、整局总览与之前各段的已发现事实。
|
||||
如果某个远距离事件与当前分析相关,可以输出`[回查] mm:ss~mm:ss`(每段最多 3 次)请求对应原始区间。
|
||||
请按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。
|
||||
|
||||
假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测。UnitId 推测最多 10 个,事件推测最多 5 个,时间线推测最多 3 个。
|
||||
如果你列出了 UnitId 推测、关键事件推测或时间线推测,请在回答末尾附加`[机器可读声明]` JSON 代码块;如果没有相关推测,则输出空数组。
|
||||
如果你列出了 UnitId 推测、关键事件推测或时间线推测,请在回答末尾附加`[机器可读声明]` JSON 代码块;如果没有相关推测,则输出空 JSON 对象。
|
||||
最后用一行 `[小结]` 输出 2~3 句该段最重要的结论。
|
||||
";
|
||||
return instruction.Trim().Replace("\r", "");
|
||||
@@ -1383,15 +1390,22 @@ PlayerA: 开始出兵
|
||||
ImmutableList<ChatMessage> messages,
|
||||
Dictionary<string, object> inputRequestParams)
|
||||
{
|
||||
return new Dictionary<string, object>(inputRequestParams)
|
||||
var parameters = new Dictionary<string, object>(inputRequestParams)
|
||||
{
|
||||
["messages"] = messages.ToArray(),
|
||||
["stream"] = true,
|
||||
["stream_options"] = new
|
||||
["messages"] = messages.ToArray()
|
||||
};
|
||||
// 尊重模型配置里的 IsStream;只有流式请求才附加 stream_options,
|
||||
// 避免非 SSE 端点收到 stream=true 后失败。
|
||||
if (inputRequestParams.TryGetValue("stream", out var streamValue)
|
||||
&& streamValue is bool isStream
|
||||
&& isStream)
|
||||
{
|
||||
parameters["stream_options"] = new
|
||||
{
|
||||
include_usage = true
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,29 +43,53 @@ namespace AnotherReplayReader.Utils
|
||||
public static int EstimateTokens(string text) =>
|
||||
(int)Math.Ceiling(AIAnalyze.EstimateTokenCount(text).EstimatedTokenCount * EstimatorSafetyFactor);
|
||||
|
||||
/// <summary>请求护栏:超过硬上限返回 Block,超过软预算返回 Warn,否则 null。</summary>
|
||||
/// <summary>请求护栏:只按 prompt 估算检查(保留给旧调用/测试使用)。</summary>
|
||||
public static ContextCheckResult CheckPromptUsage(
|
||||
int estimatedPromptTokens,
|
||||
AiProvider provider,
|
||||
AiModel model)
|
||||
{
|
||||
return CheckUsage(estimatedPromptTokens, provider, model, includeOutputHeadroom: false);
|
||||
}
|
||||
|
||||
/// <summary>请求护栏:把输出/推理余量也算入总用量。</summary>
|
||||
public static ContextCheckResult CheckRequestUsage(
|
||||
int estimatedPromptTokens,
|
||||
AiProvider provider,
|
||||
AiModel model)
|
||||
{
|
||||
return CheckUsage(estimatedPromptTokens, provider, model, includeOutputHeadroom: true);
|
||||
}
|
||||
|
||||
private static ContextCheckResult CheckUsage(
|
||||
int estimatedPromptTokens,
|
||||
AiProvider provider,
|
||||
AiModel model,
|
||||
bool includeOutputHeadroom)
|
||||
{
|
||||
var headroom = includeOutputHeadroom ? GetOutputHeadroom(provider, model) : 0;
|
||||
var estimatedTotal = estimatedPromptTokens + headroom;
|
||||
if (model.ContextLength > 0)
|
||||
{
|
||||
var hardLimit = (int)(model.ContextLength * HardUsageRatio);
|
||||
if (estimatedPromptTokens > hardLimit)
|
||||
if (estimatedTotal > hardLimit)
|
||||
{
|
||||
return new ContextCheckResult(
|
||||
true,
|
||||
$"估算输入 {estimatedPromptTokens:N0} token 超过模型上下文 {model.ContextLength:N0} 的 90%,已拒绝发起请求。请改用更长上下文的模型,或缩短操作记录。");
|
||||
includeOutputHeadroom
|
||||
? $"估算用量(输入 {estimatedPromptTokens:N0} + 输出余量 {headroom:N0} = {estimatedTotal:N0})超过模型上下文 {model.ContextLength:N0} 的 90%,已拒绝发起请求。请改用更长上下文的模型,或缩短操作记录。"
|
||||
: $"估算输入 {estimatedPromptTokens:N0} token 超过模型上下文 {model.ContextLength:N0} 的 90%,已拒绝发起请求。请改用更长上下文的模型,或缩短操作记录。");
|
||||
}
|
||||
}
|
||||
|
||||
var budget = GetContextBudget(model);
|
||||
if (budget > 0 && estimatedPromptTokens > budget)
|
||||
if (budget > 0 && estimatedTotal > budget)
|
||||
{
|
||||
return new ContextCheckResult(
|
||||
false,
|
||||
$"估算输入 {estimatedPromptTokens:N0} token 超过上下文预算 {budget:N0}(可在模型设置中调整),长录像将自动分段,超出部分会被压缩。");
|
||||
includeOutputHeadroom
|
||||
? $"估算用量(输入 {estimatedPromptTokens:N0} + 输出余量 {headroom:N0} = {estimatedTotal:N0})超过上下文预算 {budget:N0}(可在模型设置中调整)。"
|
||||
: $"估算输入 {estimatedPromptTokens:N0} token 超过上下文预算 {budget:N0}(可在模型设置中调整),长录像将自动分段,超出部分会被压缩。");
|
||||
}
|
||||
return ContextCheckResult.Ok;
|
||||
}
|
||||
|
||||
+19
-2
@@ -123,6 +123,7 @@ namespace AnotherReplayReader.Utils
|
||||
ImmutableArray<string> Tags,
|
||||
ImmutableArray<SpecialPowerInfo> SpecialPowers,
|
||||
ImmutableArray<string> ProducedBy,
|
||||
ImmutableArray<string> Aliases,
|
||||
string Text)
|
||||
{
|
||||
public bool IsBuilding => HasTag(KnowledgeTag.Structure);
|
||||
@@ -243,6 +244,15 @@ namespace AnotherReplayReader.Utils
|
||||
.OrderBy(e => e.Faction, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(e => e.AssetName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableArray();
|
||||
var byAsset = new Dictionary<string, EntityKnowledge>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var entity in allEntities)
|
||||
{
|
||||
byAsset[entity.AssetName] = entity;
|
||||
foreach (var alias in entity.Aliases)
|
||||
{
|
||||
byAsset[alias] = entity;
|
||||
}
|
||||
}
|
||||
var byFaction = builtin.ToImmutableDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value.Values.OrderBy(e => e.AssetName, StringComparer.OrdinalIgnoreCase).ToImmutableArray(),
|
||||
@@ -255,7 +265,7 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
return new StructuredKnowledge(
|
||||
allEntities.ToImmutableDictionary(e => e.AssetName, e => e, StringComparer.OrdinalIgnoreCase),
|
||||
byAsset.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase),
|
||||
allEntities,
|
||||
byFaction,
|
||||
unknownTagsArray);
|
||||
@@ -306,6 +316,12 @@ namespace AnotherReplayReader.Utils
|
||||
unknownTagsLocal.Add(tag);
|
||||
}
|
||||
}
|
||||
var producedBy = GetStringArray(el, "producedBy");
|
||||
var alsoProducedBy = GetStringArray(el, "alsoProducedBy");
|
||||
var combinedProducedBy = producedBy
|
||||
.Concat(alsoProducedBy)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableArray();
|
||||
var ek = new EntityKnowledge(
|
||||
assetName,
|
||||
GetString(el, "displayName") ?? "",
|
||||
@@ -313,7 +329,8 @@ namespace AnotherReplayReader.Utils
|
||||
isBuilding ? null : GetString(el, "tier"),
|
||||
tags,
|
||||
ParseSpecialPowers(el),
|
||||
isBuilding ? ImmutableArray<string>.Empty : GetStringArray(el, "producedBy"),
|
||||
isBuilding ? ImmutableArray<string>.Empty : combinedProducedBy,
|
||||
GetStringArray(el, "aliases"),
|
||||
GetString(el, "text") ?? "");
|
||||
entries[assetName] = ek;
|
||||
}
|
||||
|
||||
+26
-1
@@ -2,6 +2,7 @@ using AnotherReplayReader.ReplayFile;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -166,6 +167,28 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("# 协议选择");
|
||||
foreach (var kv in factIndex.PlayerTechChoices.OrderBy(k => k.Key))
|
||||
{
|
||||
sb.AppendLine($"- 玩家#{kv.Key}({names[kv.Key]}):{string.Join("、", kv.Value.OrderBy(x => x))}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("# 所有权证据(节选)");
|
||||
foreach (var kv in factIndex.PlayerStrongOwnershipUnitIds.OrderBy(k => k.Key))
|
||||
{
|
||||
var ids = kv.Value.OrderBy(x => x).Take(20);
|
||||
var suffix = kv.Value.Count > 20 ? "…" : string.Empty;
|
||||
sb.AppendLine($"- 玩家#{kv.Key}({names[kv.Key]}):强证据 {kv.Value.Count} 个 UnitId({string.Join("、", ids)}{suffix})");
|
||||
}
|
||||
foreach (var kv in factIndex.PlayerWeakOwnershipUnitIds.OrderBy(k => k.Key))
|
||||
{
|
||||
var ids = kv.Value.OrderBy(x => x).Take(20);
|
||||
var suffix = kv.Value.Count > 20 ? "…" : string.Empty;
|
||||
sb.AppendLine($"- 玩家#{kv.Key}({names[kv.Key]}):弱证据 {kv.Value.Count} 个 UnitId({string.Join("、", ids)}{suffix})");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("# 打包/展开");
|
||||
var packUnits = factIndex.UnitIdSpecialPowers
|
||||
.Where(kv2 => kv2.Value.Any(p => p.Contains("PackReplaceSelf") || p.Contains("UnpackReplaceSelf")))
|
||||
@@ -414,7 +437,9 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
input = input.Trim();
|
||||
var parts = input.Split(':');
|
||||
if (parts.Length == 0 || !float.TryParse(parts[parts.Length - 1], out var seconds) || seconds < 0)
|
||||
if (parts.Length == 0
|
||||
|| !float.TryParse(parts[parts.Length - 1], NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds)
|
||||
|| seconds < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ namespace AnotherReplayReader.Utils
|
||||
var playerStrongOwnership = new Dictionary<int, HashSet<uint>>();
|
||||
var playerWeakOwnership = new Dictionary<int, HashSet<uint>>();
|
||||
var playerTechChoices = new Dictionary<int, HashSet<string>>();
|
||||
var controlGroups = new Dictionary<int, HashSet<uint>>();
|
||||
// 编队号在同一局内可能被不同玩家复用,因此键必须包含玩家。
|
||||
var controlGroups = new Dictionary<long, HashSet<uint>>();
|
||||
|
||||
foreach (var (time, commands) in timeline)
|
||||
{
|
||||
@@ -121,7 +122,7 @@ namespace AnotherReplayReader.Utils
|
||||
Dictionary<int, HashSet<uint>> playerStrongOwnership,
|
||||
Dictionary<int, HashSet<uint>> playerWeakOwnership,
|
||||
Dictionary<int, HashSet<string>> playerTechChoices,
|
||||
Dictionary<int, HashSet<uint>> controlGroups)
|
||||
Dictionary<long, HashSet<uint>> controlGroups)
|
||||
{
|
||||
var player = command.PlayerIndex;
|
||||
var cmdId = command.CommandId;
|
||||
@@ -130,6 +131,10 @@ namespace AnotherReplayReader.Utils
|
||||
{
|
||||
// select unit(s): 0x1F5
|
||||
case 0x1F5:
|
||||
// 选择相同单位(W):ObjectId 语义与选择相同,作为弱所有权
|
||||
case 0x1F6:
|
||||
// 选择所有单位(Q):若能解析出 UnitId,同样作为弱所有权
|
||||
case 0x22A:
|
||||
RecordSelectUnit(time, command, player, unitFirstObserved, playerSelected, playerWeakOwnership);
|
||||
break;
|
||||
|
||||
@@ -460,7 +465,7 @@ namespace AnotherReplayReader.Utils
|
||||
int player,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<int, HashSet<uint>> playerStrongOwnership,
|
||||
Dictionary<int, HashSet<uint>> controlGroups)
|
||||
Dictionary<long, HashSet<uint>> controlGroups)
|
||||
{
|
||||
// Data[0]: Int32 编队号;Data[1..]: 成员 ObjectId
|
||||
int? groupNumber = null;
|
||||
@@ -498,7 +503,7 @@ namespace AnotherReplayReader.Utils
|
||||
return;
|
||||
}
|
||||
var group = new HashSet<uint>(members);
|
||||
controlGroups[groupNumber.Value] = group;
|
||||
controlGroups[ControlGroupKey(player, groupNumber.Value)] = group;
|
||||
foreach (var id in members)
|
||||
{
|
||||
TryRecordFirstObserved(id, time, unitFirstObserved);
|
||||
@@ -512,7 +517,7 @@ namespace AnotherReplayReader.Utils
|
||||
int player,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<int, HashSet<uint>> playerStrongOwnership,
|
||||
Dictionary<int, HashSet<uint>> controlGroups)
|
||||
Dictionary<long, HashSet<uint>> controlGroups)
|
||||
{
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
@@ -529,7 +534,7 @@ namespace AnotherReplayReader.Utils
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (controlGroups.TryGetValue(groupNumber.Value, out var members))
|
||||
if (controlGroups.TryGetValue(ControlGroupKey(player, groupNumber.Value), out var members))
|
||||
{
|
||||
foreach (var id in members)
|
||||
{
|
||||
@@ -540,6 +545,9 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
}
|
||||
|
||||
private static long ControlGroupKey(int player, int group) =>
|
||||
((long)player << 32) | (uint)group;
|
||||
|
||||
private static void RecordTechChoice(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
|
||||
Reference in New Issue
Block a user