codex wip
This commit is contained in:
+10
-1
@@ -97,6 +97,7 @@ namespace AnotherReplayReader
|
|||||||
// 外部注入:每次请求前调用获取最新配置
|
// 外部注入:每次请求前调用获取最新配置
|
||||||
// 委托类型变更
|
// 委托类型变更
|
||||||
public Func<AiRequestContext>? GetRequestContext { get; set; }
|
public Func<AiRequestContext>? GetRequestContext { get; set; }
|
||||||
|
public Func<AiPromptSettings>? GetPromptSettings { get; set; }
|
||||||
|
|
||||||
// ---------- methods ----------
|
// ---------- methods ----------
|
||||||
|
|
||||||
@@ -248,7 +249,7 @@ namespace AnotherReplayReader
|
|||||||
FinishCurrentContent();
|
FinishCurrentContent();
|
||||||
var requestContext = GetRequestContext();
|
var requestContext = GetRequestContext();
|
||||||
|
|
||||||
var systemPrompt = AIAnalyze.GetSystemPrompt(replay, players);
|
var systemPrompt = AIAnalyze.GetSystemPrompt(replay, players, GetPromptSettings?.Invoke());
|
||||||
var userPrompt = AIAnalyze.BuildUserPrompt(replay.Mod, players, replayData, out var userPromptPrefix);
|
var userPrompt = AIAnalyze.BuildUserPrompt(replay.Mod, players, replayData, out var userPromptPrefix);
|
||||||
AppendLog($"让 AI 了解录像...",
|
AppendLog($"让 AI 了解录像...",
|
||||||
userPromptPrefix
|
userPromptPrefix
|
||||||
@@ -358,6 +359,14 @@ namespace AnotherReplayReader
|
|||||||
requestContext,
|
requestContext,
|
||||||
OnChunk,
|
OnChunk,
|
||||||
_linkedCts.Token);
|
_linkedCts.Token);
|
||||||
|
var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims(segmentResult.Response);
|
||||||
|
if (validationResult.HasIssues)
|
||||||
|
{
|
||||||
|
AppendLog(
|
||||||
|
$"第{segmentIndex}段机器可读声明检查",
|
||||||
|
AIAnalysisValidation.FormatIssues(validationResult.Issues),
|
||||||
|
false);
|
||||||
|
}
|
||||||
_lastSuccessfulSegment = lastState.CurrentSegment;
|
_lastSuccessfulSegment = lastState.CurrentSegment;
|
||||||
lastState = segmentResult.State;
|
lastState = segmentResult.State;
|
||||||
UpdateTokenDisplay(segmentResult);
|
UpdateTokenDisplay(segmentResult);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<!-- 右侧:Provider 编辑 + 模型管理 -->
|
<!-- 右侧:Provider 编辑 + 模型管理 -->
|
||||||
<Grid Grid.Column="1" Margin="10,0,0,0">
|
<Grid Grid.Column="1" Margin="10,0,0,0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@@ -69,8 +70,58 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Prompt 管理区域 -->
|
||||||
|
<GroupBox Header="提示词" Grid.Row="1" Margin="0,5,0,0">
|
||||||
|
<Grid Margin="5">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="100"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<CheckBox x:Name="_useCustomPromptCheck"
|
||||||
|
Grid.Row="0" Grid.Column="1"
|
||||||
|
Content="完全使用自定义 System Prompt"
|
||||||
|
Margin="0,0,0,5"/>
|
||||||
|
|
||||||
|
<Label Grid.Row="1" Grid.Column="0" Content="System Prompt"/>
|
||||||
|
<TextBox x:Name="_customPromptBox"
|
||||||
|
Grid.Row="1" Grid.Column="1"
|
||||||
|
MinHeight="80"
|
||||||
|
MaxHeight="160"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" Grid.ColumnSpan="2" Margin="0,5,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="100"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Label Grid.Column="0" Content="补充规则"/>
|
||||||
|
<TextBox x:Name="_additionalRulesBox"
|
||||||
|
Grid.Column="1"
|
||||||
|
MinHeight="50"
|
||||||
|
MaxHeight="100"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"/>
|
||||||
|
<StackPanel Grid.Column="2" Margin="5,0,0,0">
|
||||||
|
<Button Content="应用提示词" Click="OnApplyPromptClick" Margin="2"/>
|
||||||
|
<Button Content="恢复默认" Click="OnResetPromptClick" Margin="2"/>
|
||||||
|
<Button Content="清空补充" Click="OnClearAdditionalRulesClick" Margin="2"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
<!-- 模型管理区域 -->
|
<!-- 模型管理区域 -->
|
||||||
<GroupBox Header="模型" Grid.Row="1" Margin="0,5,0,0">
|
<GroupBox Header="模型" Grid.Row="2" Margin="0,5,0,0">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,5">
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
<ComboBox x:Name="_modelComboBox"
|
<ComboBox x:Name="_modelComboBox"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_settings = AiSettings.Load();
|
_settings = AiSettings.Load();
|
||||||
|
RefreshPromptFields();
|
||||||
RefreshProviderList();
|
RefreshProviderList();
|
||||||
if (_settings.Providers.Count > 0)
|
if (_settings.Providers.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -46,6 +47,52 @@ namespace AnotherReplayReader
|
|||||||
return new AiRequestContext(_currentProvider, _currentModel);
|
return new AiRequestContext(_currentProvider, _currentModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AiPromptSettings GetPromptSettings()
|
||||||
|
{
|
||||||
|
return _settings.Prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshPromptFields()
|
||||||
|
{
|
||||||
|
_useCustomPromptCheck.IsChecked = _settings.Prompt.UseCustomSystemPrompt;
|
||||||
|
_customPromptBox.Text = _settings.Prompt.CustomSystemPrompt;
|
||||||
|
_additionalRulesBox.Text = _settings.Prompt.AdditionalRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplyPromptClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_settings.Prompt.UseCustomSystemPrompt = _useCustomPromptCheck.IsChecked == true;
|
||||||
|
_settings.Prompt.CustomSystemPrompt = _customPromptBox.Text;
|
||||||
|
_settings.Prompt.AdditionalRules = _additionalRulesBox.Text;
|
||||||
|
_settings.Save();
|
||||||
|
MessageBox.Show("提示词配置已保存", "信息", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResetPromptClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var result = MessageBox.Show(
|
||||||
|
"确定要恢复内置 System Prompt 吗?自定义 System Prompt 会被清空,补充规则会保留。",
|
||||||
|
"恢复默认提示词",
|
||||||
|
MessageBoxButton.YesNo,
|
||||||
|
MessageBoxImage.Question);
|
||||||
|
if (result != MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_settings.Prompt.UseCustomSystemPrompt = false;
|
||||||
|
_settings.Prompt.CustomSystemPrompt = string.Empty;
|
||||||
|
_settings.Save();
|
||||||
|
RefreshPromptFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClearAdditionalRulesClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_settings.Prompt.AdditionalRules = string.Empty;
|
||||||
|
_settings.Save();
|
||||||
|
RefreshPromptFields();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Provider 列表管理 ----------
|
// ---------- Provider 列表管理 ----------
|
||||||
private void RefreshProviderList()
|
private void RefreshProviderList()
|
||||||
{
|
{
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Context
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
### Operation Fact
|
||||||
|
A fact directly extracted from replay command data, such as command time, player, command name, UnitId, asset id, special power id, production queue, or target position.
|
||||||
|
|
||||||
|
### UnitId Guess
|
||||||
|
A hypothesis about what game object a UnitId represents. A UnitId Guess is not a fact unless it is directly supported by replay data or game rules.
|
||||||
|
|
||||||
|
### Evidence Level
|
||||||
|
The confidence assigned to a UnitId Guess or tactical conclusion. Valid levels are: confirmed, highly likely, possible, uncertain, and ruled out.
|
||||||
|
|
||||||
|
### Validation Rule
|
||||||
|
A deterministic rule that checks LLM claims against Operation Facts and known game rules.
|
||||||
|
|
||||||
|
### Validation Issue
|
||||||
|
A machine-detected problem in an LLM claim, such as a direct contradiction, weak evidence, missing alternative, or impossible timeline.
|
||||||
|
|
||||||
|
### Revision Pass
|
||||||
|
A hidden LLM request that receives the prior draft, validation issues, and relevant Operation Facts, then produces a corrected analysis without exposing apology or correction chatter to the user.
|
||||||
@@ -449,6 +449,7 @@ namespace AnotherReplayReader
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_aiPanel.GetRequestContext = _aiSettings.GetCurrentContext!;
|
_aiPanel.GetRequestContext = _aiSettings.GetCurrentContext!;
|
||||||
|
_aiPanel.GetPromptSettings = _aiSettings.GetPromptSettings;
|
||||||
|
|
||||||
await _aiPanel.StartAnalysisAsync(replay, _model.Players, cached, cachedPrefixSums, _cancellation.Token);
|
await _aiPanel.StartAnalysisAsync(replay, _model.Players, cached, cachedPrefixSums, _cancellation.Token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal enum AIValidationSeverity
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
WeakEvidence,
|
||||||
|
Warning,
|
||||||
|
Contradiction,
|
||||||
|
Fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum AIValidationIssueKind
|
||||||
|
{
|
||||||
|
InvalidMachineReadableClaims,
|
||||||
|
MissingMachineReadableClaims,
|
||||||
|
MissingAlternative,
|
||||||
|
WeakEvidence,
|
||||||
|
MissingEvidence,
|
||||||
|
InvalidEvidenceLevel,
|
||||||
|
UnitCapabilityContradiction,
|
||||||
|
UnitTimelineContradiction,
|
||||||
|
UnsupportedGameKnowledge
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum AIEvidenceLevel
|
||||||
|
{
|
||||||
|
Confirmed,
|
||||||
|
HighlyLikely,
|
||||||
|
Possible,
|
||||||
|
Uncertain,
|
||||||
|
RuledOut
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record AIUnitClaim(
|
||||||
|
string UnitId,
|
||||||
|
string Player,
|
||||||
|
string Claim,
|
||||||
|
AIEvidenceLevel EvidenceLevel,
|
||||||
|
ImmutableArray<string> Evidence,
|
||||||
|
ImmutableArray<string> Alternatives,
|
||||||
|
ImmutableArray<string> NeedsConfirmation);
|
||||||
|
|
||||||
|
internal sealed record AIEventClaim(
|
||||||
|
string Claim,
|
||||||
|
AIEvidenceLevel EvidenceLevel,
|
||||||
|
ImmutableArray<string> Evidence);
|
||||||
|
|
||||||
|
internal sealed record AITimelineClaim(
|
||||||
|
string Claim,
|
||||||
|
AIEvidenceLevel EvidenceLevel,
|
||||||
|
ImmutableArray<string> Evidence);
|
||||||
|
|
||||||
|
internal sealed record AIMachineReadableClaims(
|
||||||
|
ImmutableArray<AIUnitClaim> UnitClaims,
|
||||||
|
ImmutableArray<AIEventClaim> EventClaims,
|
||||||
|
ImmutableArray<AITimelineClaim> TimelineClaims)
|
||||||
|
{
|
||||||
|
public static AIMachineReadableClaims Empty { get; } = new(
|
||||||
|
ImmutableArray<AIUnitClaim>.Empty,
|
||||||
|
ImmutableArray<AIEventClaim>.Empty,
|
||||||
|
ImmutableArray<AITimelineClaim>.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record AIValidationIssue(
|
||||||
|
AIValidationSeverity Severity,
|
||||||
|
AIValidationIssueKind Kind,
|
||||||
|
string Message,
|
||||||
|
string? UnitId = null,
|
||||||
|
TimeSpan? Time = null);
|
||||||
|
|
||||||
|
internal sealed record AIValidationResult(
|
||||||
|
AIMachineReadableClaims Claims,
|
||||||
|
ImmutableArray<AIValidationIssue> Issues)
|
||||||
|
{
|
||||||
|
public static AIValidationResult Empty { get; } =
|
||||||
|
new(AIMachineReadableClaims.Empty, ImmutableArray<AIValidationIssue>.Empty);
|
||||||
|
|
||||||
|
public bool RequiresRevision =>
|
||||||
|
Issues.Any(i => i.Severity is AIValidationSeverity.Contradiction or AIValidationSeverity.Fatal);
|
||||||
|
|
||||||
|
public bool HasIssues => !Issues.IsEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class AIAnalysisValidation
|
||||||
|
{
|
||||||
|
private static readonly Regex _jsonFenceRegex = new(
|
||||||
|
@"```(?:json)?\s*(\{[\s\S]*?\})\s*```",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static AIValidationResult ValidateMachineReadableClaims(string response)
|
||||||
|
{
|
||||||
|
var issues = ImmutableArray.CreateBuilder<AIValidationIssue>();
|
||||||
|
var json = ExtractJsonObject(response);
|
||||||
|
if (json is null || string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.MissingMachineReadableClaims,
|
||||||
|
"AI 未输出机器可读声明,无法进行自动验证。"));
|
||||||
|
return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = ParseClaims(json, issues);
|
||||||
|
if (claims is null)
|
||||||
|
{
|
||||||
|
return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateClaimSelfConsistency(claims, issues);
|
||||||
|
return new AIValidationResult(claims, issues.ToImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatIssues(ImmutableArray<AIValidationIssue> issues)
|
||||||
|
{
|
||||||
|
if (issues.IsEmpty)
|
||||||
|
{
|
||||||
|
return "未发现机器可读声明问题。";
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var issue in issues)
|
||||||
|
{
|
||||||
|
var unitText = string.IsNullOrWhiteSpace(issue.UnitId)
|
||||||
|
? string.Empty
|
||||||
|
: $" UnitId={issue.UnitId}";
|
||||||
|
sb.AppendLine($"[{issue.Severity}/{issue.Kind}]{unitText} {issue.Message}");
|
||||||
|
}
|
||||||
|
return sb.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractJsonObject(string response)
|
||||||
|
{
|
||||||
|
var markerIndex = response.LastIndexOf("[机器可读声明]", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var searchText = markerIndex >= 0 ? response.Substring(markerIndex) : response;
|
||||||
|
|
||||||
|
var matches = _jsonFenceRegex.Matches(searchText);
|
||||||
|
if (matches.Count > 0)
|
||||||
|
{
|
||||||
|
return matches[matches.Count - 1].Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = searchText.LastIndexOf('{');
|
||||||
|
var end = searchText.LastIndexOf('}');
|
||||||
|
if (start >= 0 && end > start)
|
||||||
|
{
|
||||||
|
return searchText.Substring(start, end - start + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AIMachineReadableClaims? ParseClaims(
|
||||||
|
string json,
|
||||||
|
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json, new JsonDocumentOptions
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
CommentHandling = JsonCommentHandling.Skip
|
||||||
|
});
|
||||||
|
|
||||||
|
var root = doc.RootElement;
|
||||||
|
if (root.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||||
|
"机器可读声明不是 JSON object。"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AIMachineReadableClaims(
|
||||||
|
ReadUnitClaims(root),
|
||||||
|
ReadSimpleClaims(root, "eventClaims")
|
||||||
|
.Select(c => new AIEventClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||||
|
.ToImmutableArray(),
|
||||||
|
ReadSimpleClaims(root, "timelineClaims")
|
||||||
|
.Select(c => new AITimelineClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||||
|
.ToImmutableArray());
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||||
|
$"机器可读声明 JSON 解析失败:{ex.Message}"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableArray<AIUnitClaim> ReadUnitClaims(JsonElement root)
|
||||||
|
{
|
||||||
|
if (!TryGetArray(root, "unitClaims", out var unitClaims))
|
||||||
|
{
|
||||||
|
return ImmutableArray<AIUnitClaim>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ImmutableArray.CreateBuilder<AIUnitClaim>();
|
||||||
|
foreach (var item in unitClaims.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new AIUnitClaim(
|
||||||
|
ReadFlexibleString(item, "unitId"),
|
||||||
|
ReadFlexibleString(item, "player"),
|
||||||
|
ReadFlexibleString(item, "claim"),
|
||||||
|
ReadEvidenceLevel(item),
|
||||||
|
ReadStringArray(item, "evidence"),
|
||||||
|
ReadStringArray(item, "alternatives"),
|
||||||
|
ReadStringArray(item, "needsConfirmation")));
|
||||||
|
}
|
||||||
|
return result.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record SimpleClaim(
|
||||||
|
string Claim,
|
||||||
|
AIEvidenceLevel EvidenceLevel,
|
||||||
|
ImmutableArray<string> Evidence);
|
||||||
|
|
||||||
|
private static ImmutableArray<SimpleClaim> ReadSimpleClaims(JsonElement root, string propertyName)
|
||||||
|
{
|
||||||
|
if (!TryGetArray(root, propertyName, out var claims))
|
||||||
|
{
|
||||||
|
return ImmutableArray<SimpleClaim>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ImmutableArray.CreateBuilder<SimpleClaim>();
|
||||||
|
foreach (var item in claims.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new SimpleClaim(
|
||||||
|
ReadFlexibleString(item, "claim"),
|
||||||
|
ReadEvidenceLevel(item),
|
||||||
|
ReadStringArray(item, "evidence")));
|
||||||
|
}
|
||||||
|
return result.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateClaimSelfConsistency(
|
||||||
|
AIMachineReadableClaims claims,
|
||||||
|
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||||
|
{
|
||||||
|
foreach (var claim in claims.UnitClaims)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(claim.UnitId))
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||||
|
"Unit claim 缺少 unitId。"));
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(claim.Claim))
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||||
|
"Unit claim 缺少 claim。",
|
||||||
|
claim.UnitId));
|
||||||
|
}
|
||||||
|
if (claim.EvidenceLevel is AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely
|
||||||
|
&& claim.Evidence.IsEmpty)
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.Warning,
|
||||||
|
AIValidationIssueKind.MissingEvidence,
|
||||||
|
"高置信 UnitId 推测缺少 evidence。",
|
||||||
|
claim.UnitId));
|
||||||
|
}
|
||||||
|
if (claim.EvidenceLevel is AIEvidenceLevel.Possible or AIEvidenceLevel.Uncertain
|
||||||
|
&& claim.Alternatives.IsEmpty
|
||||||
|
&& claim.NeedsConfirmation.IsEmpty)
|
||||||
|
{
|
||||||
|
issues.Add(new AIValidationIssue(
|
||||||
|
AIValidationSeverity.WeakEvidence,
|
||||||
|
AIValidationIssueKind.MissingAlternative,
|
||||||
|
"低置信 UnitId 推测应提供 alternatives 或 needsConfirmation。",
|
||||||
|
claim.UnitId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetArray(JsonElement root, string propertyName, out JsonElement array)
|
||||||
|
{
|
||||||
|
if (root.TryGetProperty(propertyName, out array)
|
||||||
|
&& array.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
array = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AIEvidenceLevel ReadEvidenceLevel(JsonElement item)
|
||||||
|
{
|
||||||
|
var value = ReadFlexibleString(item, "evidenceLevel");
|
||||||
|
return NormalizeEvidenceLevel(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AIEvidenceLevel NormalizeEvidenceLevel(string value)
|
||||||
|
{
|
||||||
|
value = value.Trim().Replace("_", "").Replace("-", "").Replace(" ", "");
|
||||||
|
return 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadFlexibleString(JsonElement item, string propertyName)
|
||||||
|
{
|
||||||
|
if (!item.TryGetProperty(propertyName, out var value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.String => value.GetString() ?? string.Empty,
|
||||||
|
JsonValueKind.Number => value.GetRawText(),
|
||||||
|
JsonValueKind.True => "true",
|
||||||
|
JsonValueKind.False => "false",
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableArray<string> ReadStringArray(JsonElement item, string propertyName)
|
||||||
|
{
|
||||||
|
if (!item.TryGetProperty(propertyName, out var value))
|
||||||
|
{
|
||||||
|
return ImmutableArray<string>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var text = value.GetString();
|
||||||
|
return string.IsNullOrWhiteSpace(text)
|
||||||
|
? ImmutableArray<string>.Empty
|
||||||
|
: ImmutableArray.Create(text!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return ImmutableArray<string>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<string>();
|
||||||
|
foreach (var element in value.EnumerateArray())
|
||||||
|
{
|
||||||
|
var text = element.ValueKind == JsonValueKind.String
|
||||||
|
? element.GetString()
|
||||||
|
: element.GetRawText();
|
||||||
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
result.Add(text!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.ToImmutableArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
-3
@@ -17,13 +17,51 @@ namespace AnotherReplayReader.Utils
|
|||||||
{
|
{
|
||||||
internal sealed class AIAnalyze
|
internal sealed class AIAnalyze
|
||||||
{
|
{
|
||||||
public static string GetSystemPrompt(Replay replay, ImmutableSortedDictionary<int, Player> players)
|
public static string GetSystemPrompt(
|
||||||
|
Replay replay,
|
||||||
|
ImmutableSortedDictionary<int, Player> players,
|
||||||
|
AiPromptSettings? promptSettings = null)
|
||||||
|
{
|
||||||
|
var defaultPrompt = BuildDefaultSystemPrompt(replay, players);
|
||||||
|
return ComposeSystemPrompt(defaultPrompt, promptSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ComposeSystemPrompt(string defaultPrompt, AiPromptSettings? promptSettings)
|
||||||
|
{
|
||||||
|
var customSystemPrompt = promptSettings?.CustomSystemPrompt ?? string.Empty;
|
||||||
|
if (promptSettings?.UseCustomSystemPrompt == true
|
||||||
|
&& !string.IsNullOrWhiteSpace(customSystemPrompt))
|
||||||
|
{
|
||||||
|
defaultPrompt = customSystemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
var additionalRules = promptSettings?.AdditionalRules ?? string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(additionalRules))
|
||||||
|
{
|
||||||
|
defaultPrompt = defaultPrompt.TrimEnd()
|
||||||
|
+ "\n\n# 用户自定义补充规则\n"
|
||||||
|
+ additionalRules.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultPrompt.Replace("\r", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildDefaultSystemPrompt(Replay replay, ImmutableSortedDictionary<int, Player> players)
|
||||||
{
|
{
|
||||||
|
|
||||||
var generalDescriptions = @"
|
var generalDescriptions = @"
|
||||||
你是一位 RTS 游戏数据分析师,你擅长从大量数据中发现有趣的规律和细节。
|
你是一位 RTS 游戏数据分析师,你擅长从大量数据中发现有趣的规律和细节。
|
||||||
用户则是一位玩家,用户会向你提供玩家操作记录,你要对其进行分析。
|
用户则是一位玩家,用户会向你提供玩家操作记录,你要对其进行分析。
|
||||||
|
|
||||||
|
# 核心原则
|
||||||
|
- 你只能根据用户提供的操作记录、玩家信息、下方游戏规则和明确给出的背景知识进行分析。
|
||||||
|
- 不要使用现实世界常识或其他 RTS 游戏常识覆盖这里的游戏设定。例如:步兵、直升机、建筑水陆摆放、运输能力、两栖能力都必须以这里的规则和单位描述为准。
|
||||||
|
- 不确定时必须保留多个候选,不要为了让解说流畅而过早下定论。
|
||||||
|
- 对 UnitId、单位类型、战术意图的判断必须区分证据等级:确定、高度可能、可能、待确认、已排除。
|
||||||
|
- 每个关键推理都应当包含支持证据;如果存在会推翻该推理的反证,也要主动指出。
|
||||||
|
- 如果某个技能或行为可以对应多个单位,先列出候选,并说明还需要哪些后续迹象才能确认。
|
||||||
|
- 对已经被操作记录直接否定的判断必须修正或放弃,不要坚持原结论。
|
||||||
|
|
||||||
# 输入格式
|
# 输入格式
|
||||||
## 用户初始输入
|
## 用户初始输入
|
||||||
- 玩家信息
|
- 玩家信息
|
||||||
@@ -83,11 +121,36 @@ namespace AnotherReplayReader.Utils
|
|||||||
- 也可以重点关注PlayerTech、英雄、工程师
|
- 也可以重点关注PlayerTech、英雄、工程师
|
||||||
- 按照**推理指南**进行详细的思考与推理,列举你的推理与发现
|
- 按照**推理指南**进行详细的思考与推理,列举你的推理与发现
|
||||||
- 输出:该阶段的各个主要事件,以及你的推理和发现
|
- 输出:该阶段的各个主要事件,以及你的推理和发现
|
||||||
|
- 假如推测 UnitId 对应的单位,请在正文中自然描述,并在末尾输出机器可读声明,方便程序验证
|
||||||
|
|
||||||
## 3. 最终总结阶段
|
## 3. 最终总结阶段
|
||||||
触发条件:用户输入包含:""请对以上内容进行总结""
|
触发条件:用户输入包含:""请对以上内容进行总结""
|
||||||
- 输出:所有分析的总结,以及这次对局的完整介绍
|
- 输出:所有分析的总结,以及这次对局的完整介绍
|
||||||
|
|
||||||
|
# 机器可读声明
|
||||||
|
仅限于:分段分析阶段(第2阶段)
|
||||||
|
如果你对 UnitId、关键事件或时间线做出了可验证推测,请在回答末尾附加下面格式。
|
||||||
|
必须先输出一行`[机器可读声明]`,然后输出一个 JSON 代码块:
|
||||||
|
[机器可读声明]
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
""unitClaims"": [
|
||||||
|
{
|
||||||
|
""unitId"": 123,
|
||||||
|
""player"": ""PlayerA"",
|
||||||
|
""claim"": ""AlliedMCV"",
|
||||||
|
""evidenceLevel"": ""possible"",
|
||||||
|
""evidence"": [""8:30 使用 SpecialPower_UnpackReplaceSelf""],
|
||||||
|
""alternatives"": [""AlliedMiner 展开后的指挥中心""],
|
||||||
|
""needsConfirmation"": [""是否曾使用 SpecialPower_PackReplaceSelf"", ""后续是否作为建造者出现""]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
""eventClaims"": [],
|
||||||
|
""timelineClaims"": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
如果没有可验证推测,可以输出空数组。不要在 JSON 里写注释。
|
||||||
|
|
||||||
# 推理指南
|
# 推理指南
|
||||||
推理需要分成多个阶段
|
推理需要分成多个阶段
|
||||||
1. 观察
|
1. 观察
|
||||||
@@ -953,6 +1016,7 @@ PlayerA: 开始出兵
|
|||||||
请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。
|
请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。
|
||||||
|
|
||||||
假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测
|
假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测
|
||||||
|
如果你列出了 UnitId 推测、关键事件推测或时间线推测,请在回答末尾附加`[机器可读声明]` JSON 代码块;如果没有相关推测,则输出空数组。
|
||||||
";
|
";
|
||||||
return instruction.Trim().Replace("\r", "");
|
return instruction.Trim().Replace("\r", "");
|
||||||
}
|
}
|
||||||
@@ -1273,8 +1337,8 @@ PlayerA: 开始出兵
|
|||||||
result.ReasoningTokens = GetIntegerProperty(usage, "reasoning_tokens") ?? result.ReasoningTokens;
|
result.ReasoningTokens = GetIntegerProperty(usage, "reasoning_tokens") ?? result.ReasoningTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取 usage(如果存在)
|
// 提取 error(如果存在)
|
||||||
if (doc.RootElement.TryGetProperty("error", out var error) && usage.ValueKind == JsonValueKind.Object)
|
if (doc.RootElement.TryGetProperty("error", out var error) && error.ValueKind == JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
if (error.TryGetProperty("message", out var message))
|
if (error.TryGetProperty("message", out var message))
|
||||||
{
|
{
|
||||||
|
|||||||
+13
-1
@@ -18,11 +18,21 @@ namespace AnotherReplayReader
|
|||||||
public string ApiKey { get; set; } = string.Empty;
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
public List<AiModel> Models { get; set; } = [];
|
public List<AiModel> Models { get; set; } = [];
|
||||||
|
|
||||||
public double DefaultTemperature { get; set; } = 0.75;
|
public double DefaultTemperature { get; set; } = 0.35;
|
||||||
public double DefaultTopP { get; set; } = 0.95;
|
public double DefaultTopP { get; set; } = 0.95;
|
||||||
public int DefaultMaxTokens { get; set; } = 16384;
|
public int DefaultMaxTokens { get; set; } = 16384;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AI 分析提示词配置。
|
||||||
|
/// </summary>
|
||||||
|
public class AiPromptSettings
|
||||||
|
{
|
||||||
|
public bool UseCustomSystemPrompt { get; set; }
|
||||||
|
public string CustomSystemPrompt { get; set; } = string.Empty;
|
||||||
|
public string AdditionalRules { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模型条目
|
/// 模型条目
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -270,6 +280,7 @@ namespace AnotherReplayReader
|
|||||||
public class AiSettings
|
public class AiSettings
|
||||||
{
|
{
|
||||||
public List<AiProvider> Providers { get; set; } = [];
|
public List<AiProvider> Providers { get; set; } = [];
|
||||||
|
public AiPromptSettings Prompt { get; set; } = new();
|
||||||
|
|
||||||
// 以下两个不持久化,由 UI 层维护当前选中项
|
// 以下两个不持久化,由 UI 层维护当前选中项
|
||||||
[System.Text.Json.Serialization.JsonIgnore]
|
[System.Text.Json.Serialization.JsonIgnore]
|
||||||
@@ -289,6 +300,7 @@ namespace AnotherReplayReader
|
|||||||
var settings = JsonSerializer.Deserialize<AiSettings>(json);
|
var settings = JsonSerializer.Deserialize<AiSettings>(json);
|
||||||
if (settings is { } value && value.Providers.Count > 0)
|
if (settings is { } value && value.Providers.Count > 0)
|
||||||
{
|
{
|
||||||
|
value.Prompt ??= new AiPromptSettings();
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# AI Analysis WIP
|
||||||
|
|
||||||
|
## User Need
|
||||||
|
|
||||||
|
The application is adding AI analysis for Red Alert 3 replay operation logs. The current flow sends player information and a compacted operation log to a chat-completion-compatible LLM, then displays the analysis in `AIChatPanel`.
|
||||||
|
|
||||||
|
The main goals are:
|
||||||
|
|
||||||
|
- Improve the current system prompt.
|
||||||
|
- Make the system prompt configurable by the user.
|
||||||
|
- Reduce AI analysis errors, especially errors caused by general-world assumptions or overconfident UnitId guesses.
|
||||||
|
- Add a validation path for LLM output so wrong claims can be detected and corrected without wasting all prior reasoning.
|
||||||
|
|
||||||
|
## Current Code Areas
|
||||||
|
|
||||||
|
- `Utils/AIAnalyze.cs`: builds the system prompt, user prompts, segment prompts, final summary prompts, and performs OpenAI-compatible chat completion calls.
|
||||||
|
- `AIChatPanel.xaml.cs`: runs the analysis workflow, displays streaming chunks, retries failed segments, and now logs machine-readable claim validation results.
|
||||||
|
- `Utils/AiSettings.cs`: stores AI provider/model settings and now prompt settings.
|
||||||
|
- `AIProviderSettingsControl.xaml(.cs)`: edits provider/model settings and now prompt settings.
|
||||||
|
- `EventDump.xaml.cs`: generates the replay operation text and starts AI analysis.
|
||||||
|
- `Utils/AIAnalysisValidation.cs`: new validation model and parser for machine-readable AI claims.
|
||||||
|
- `CONTEXT.md`: glossary for the AI analysis domain.
|
||||||
|
|
||||||
|
## Discussion Notes
|
||||||
|
|
||||||
|
### Prompt Problems
|
||||||
|
|
||||||
|
The current prompt has a lot of useful game knowledge, but the LLM can still:
|
||||||
|
|
||||||
|
- Use common sense that is wrong for the game or mod.
|
||||||
|
- Assume infantry, helicopters, transports, amphibious movement, and water placement work like they do in other RTS games.
|
||||||
|
- Treat one observed skill as conclusive evidence when multiple units share that skill.
|
||||||
|
- Overstate UnitId guesses.
|
||||||
|
|
||||||
|
Examples discussed:
|
||||||
|
|
||||||
|
- Only units explicitly marked amphibious can move on both land and water.
|
||||||
|
- Only units explicitly marked as passenger transports can transport infantry.
|
||||||
|
- Building water placement depends on game rules, not common assumptions.
|
||||||
|
- `SpecialPower_UnpackReplaceSelf` does not uniquely identify an Allied MCV because Allied miners can also unpack into a command hub.
|
||||||
|
- A UnitId claimed as an aircraft should be challenged if the same UnitId is observed using an unpack/deploy skill.
|
||||||
|
- A UnitId claimed as a bomber should be challenged if it is operated before the player starts producing their first bomber.
|
||||||
|
|
||||||
|
### Prompt Decisions
|
||||||
|
|
||||||
|
The default prompt should explicitly require:
|
||||||
|
|
||||||
|
- Evidence-first analysis.
|
||||||
|
- No use of external common sense over replay facts and supplied game rules.
|
||||||
|
- UnitId guesses with evidence levels.
|
||||||
|
- Multiple candidates when a behavior has several possible sources.
|
||||||
|
- Support evidence and possible counter-evidence for important claims.
|
||||||
|
- Correction or abandonment of claims contradicted by replay facts.
|
||||||
|
|
||||||
|
Evidence levels currently used:
|
||||||
|
|
||||||
|
- confirmed
|
||||||
|
- highly likely
|
||||||
|
- possible
|
||||||
|
- uncertain
|
||||||
|
- ruled out
|
||||||
|
|
||||||
|
The default provider temperature was lowered from `0.75` to `0.35` because this task is closer to audit/reconstruction than creative writing.
|
||||||
|
|
||||||
|
### Prompt Configuration Decisions
|
||||||
|
|
||||||
|
The prompt is now configurable through AI settings.
|
||||||
|
|
||||||
|
The design has two prompt layers:
|
||||||
|
|
||||||
|
- A built-in dynamic system prompt, still assembled from replay/mod/faction/map context.
|
||||||
|
- User prompt settings:
|
||||||
|
- optional full custom system prompt
|
||||||
|
- additional rules appended to the final system prompt
|
||||||
|
|
||||||
|
This keeps the normal path safe while allowing advanced users to override the whole prompt.
|
||||||
|
|
||||||
|
### Validation Philosophy
|
||||||
|
|
||||||
|
LLM natural-language analysis should not be treated as directly valid. The plan is to validate structured claims emitted by the LLM.
|
||||||
|
|
||||||
|
Important decision:
|
||||||
|
|
||||||
|
- Do not immediately throw away a whole analysis when a problem is found.
|
||||||
|
- Do not show the user two competing analyses or apology text such as "sorry, my previous answer was wrong."
|
||||||
|
- Prefer a hidden revision pass: send the draft, validation issues, and relevant replay facts back to the AI, asking it to output a clean corrected version without mentioning the revision.
|
||||||
|
- Limit retries/revisions. If the model still cannot resolve a claim, downgrade confidence or mark it uncertain instead of looping forever.
|
||||||
|
|
||||||
|
Severity model:
|
||||||
|
|
||||||
|
- `Info`: useful diagnostic only.
|
||||||
|
- `WeakEvidence`: claim may be plausible but lacks enough support.
|
||||||
|
- `Warning`: malformed or questionable claim that should be logged or possibly revised.
|
||||||
|
- `Contradiction`: claim conflicts with replay facts or game rules and should trigger revision.
|
||||||
|
- `Fatal`: output cannot be used for the current phase, such as empty or unparseable required output.
|
||||||
|
|
||||||
|
### JSON Format Decision
|
||||||
|
|
||||||
|
We discussed whether to require JSON or use a simpler line-based format.
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
|
||||||
|
- Use JSON for machine-readable claims.
|
||||||
|
- Keep the schema small.
|
||||||
|
- Make the parser tolerant.
|
||||||
|
|
||||||
|
Reasoning:
|
||||||
|
|
||||||
|
- JSON can naturally represent evidence arrays, alternatives, and needed confirmations.
|
||||||
|
- A custom line format would be easier for a trivial parser but would become fragile once nested data is needed.
|
||||||
|
- The app can tolerate partial or missing JSON by logging validation issues instead of failing the whole analysis.
|
||||||
|
|
||||||
|
Current expected shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"unitClaims": [
|
||||||
|
{
|
||||||
|
"unitId": 123,
|
||||||
|
"player": "PlayerA",
|
||||||
|
"claim": "AlliedMCV",
|
||||||
|
"evidenceLevel": "possible",
|
||||||
|
"evidence": ["8:30 使用 SpecialPower_UnpackReplaceSelf"],
|
||||||
|
"alternatives": ["AlliedMiner 展开后的指挥中心"],
|
||||||
|
"needsConfirmation": ["是否曾使用 SpecialPower_PackReplaceSelf", "后续是否作为建造者出现"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"eventClaims": [],
|
||||||
|
"timelineClaims": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The prompt asks the AI to output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[机器可读声明]
|
||||||
|
```json
|
||||||
|
{ ... }
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
The parser first looks for the last fenced JSON block near `[机器可读声明]`, then falls back to the last `{...}` block.
|
||||||
|
|
||||||
|
## Validation We Can Do
|
||||||
|
|
||||||
|
### Implemented Now
|
||||||
|
|
||||||
|
Format validation:
|
||||||
|
|
||||||
|
- Missing machine-readable claims.
|
||||||
|
- JSON parse failure.
|
||||||
|
- Root value is not an object.
|
||||||
|
|
||||||
|
Self-consistency validation:
|
||||||
|
|
||||||
|
- Unit claim missing `unitId`.
|
||||||
|
- Unit claim missing `claim`.
|
||||||
|
- High-confidence UnitId guess without evidence.
|
||||||
|
- Low-confidence UnitId guess without alternatives or needed confirmation.
|
||||||
|
|
||||||
|
### Near-Term Validations
|
||||||
|
|
||||||
|
These need replay facts extracted from `CommandChunk` or an intermediate fact index:
|
||||||
|
|
||||||
|
- UnitId special power contradictions.
|
||||||
|
- UnitId production timeline contradictions.
|
||||||
|
- UnitId used as builder vs claimed as non-builder unit.
|
||||||
|
- Claims that use game knowledge not present in rules, such as transport/amphibious/building-placement abilities.
|
||||||
|
- Missing alternatives for ambiguous skills such as Allied unpack.
|
||||||
|
|
||||||
|
### Suggested Fact Index
|
||||||
|
|
||||||
|
Useful derived facts:
|
||||||
|
|
||||||
|
- `UnitId -> first observed time`
|
||||||
|
- `UnitId -> observed special powers`
|
||||||
|
- `UnitId -> observed as builder`
|
||||||
|
- `UnitId -> observed as production structure`
|
||||||
|
- `Player -> first production time by asset id`
|
||||||
|
- `Player -> selected UnitIds over time`
|
||||||
|
- `Player -> tech/protocol choices`
|
||||||
|
- `Player -> building placements by asset and position`
|
||||||
|
|
||||||
|
## Current Progress
|
||||||
|
|
||||||
|
Implemented:
|
||||||
|
|
||||||
|
- Added `CONTEXT.md` glossary.
|
||||||
|
- Added prompt settings:
|
||||||
|
- `AiPromptSettings`
|
||||||
|
- `UseCustomSystemPrompt`
|
||||||
|
- `CustomSystemPrompt`
|
||||||
|
- `AdditionalRules`
|
||||||
|
- Added prompt editing UI to `AIProviderSettingsControl`.
|
||||||
|
- Connected prompt settings from `EventDump` to `AIChatPanel` to `AIAnalyze`.
|
||||||
|
- Split default prompt construction from prompt composition.
|
||||||
|
- Strengthened default prompt with evidence-first and uncertainty rules.
|
||||||
|
- Added machine-readable JSON claim instructions to system and segment prompts.
|
||||||
|
- Added `Utils/AIAnalysisValidation.cs` with:
|
||||||
|
- evidence level enum
|
||||||
|
- machine-readable claim records
|
||||||
|
- validation issue records
|
||||||
|
- JSON extraction and parsing
|
||||||
|
- initial self-consistency checks
|
||||||
|
- Added per-segment validation logging in `AIChatPanel`.
|
||||||
|
|
||||||
|
Build status:
|
||||||
|
|
||||||
|
- `dotnet build AnotherReplayReader.csproj --no-restore` succeeds.
|
||||||
|
- Remaining warnings are existing nullable warnings in `AIAnalyze.cs` stream response handling and a `System.Text.Encoding.CodePages` support warning for `net461`.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Should AI natural-language output continue streaming live, or should content be buffered until validation and possible revision are complete?
|
||||||
|
- Should reasoning chunks remain visible during hidden revision, or should only final content be shown?
|
||||||
|
- How strict should missing machine-readable claims be?
|
||||||
|
- Current behavior: warning log only.
|
||||||
|
- Possible future behavior: one hidden repair request asking the model to append valid claims.
|
||||||
|
- Should validation issues be visible by default, or only in an advanced/debug foldout?
|
||||||
|
- How much game-unit knowledge should live in code versus prompt text?
|
||||||
|
- Should the first verifier use hardcoded RA3/Corona knowledge, or should it load a small unit capability table from data files?
|
||||||
|
|
||||||
|
## Suggested Next Steps
|
||||||
|
|
||||||
|
1. Build a replay fact index from `CommandChunk`.
|
||||||
|
2. Add first deterministic validation rules:
|
||||||
|
- ambiguous Allied unpack
|
||||||
|
- pack/unpack consistency
|
||||||
|
- UnitId used as builder
|
||||||
|
- first production time vs first operation time
|
||||||
|
3. Decide whether to buffer per-segment content before display.
|
||||||
|
4. Add one hidden revision pass for `Contradiction` issues.
|
||||||
|
5. Add validation summary UI, such as "验证器发现并修正 N 个问题".
|
||||||
Reference in New Issue
Block a user