remove reasoning guard

This commit is contained in:
2026-09-08 18:39:04 +02:00
parent acad70aaac
commit 6dae58bf42
9 changed files with 29 additions and 1536 deletions
+1 -208
View File
@@ -10,7 +10,6 @@ using System.Diagnostics;
using System.Diagnostics.Tracing; using System.Diagnostics.Tracing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -97,7 +96,6 @@ namespace AnotherReplayReader
// 输出段落 // 输出段落
private (Paragraph? Think, Paragraph? Content)? _currentContent; private (Paragraph? Think, Paragraph? Content)? _currentContent;
private Paragraph? _lastContentParagraph; private Paragraph? _lastContentParagraph;
private bool _nextThinkingIsContinuation;
// 进度计时 // 进度计时
private TimeSpan _previousTotalTime; private TimeSpan _previousTotalTime;
@@ -227,7 +225,6 @@ namespace AnotherReplayReader
_thinkBlockWasExpanded = false; _thinkBlockWasExpanded = false;
_currentContent = null; _currentContent = null;
_lastContentParagraph = null; _lastContentParagraph = null;
_nextThinkingIsContinuation = false;
_previousTotalTime = TimeSpan.Zero; _previousTotalTime = TimeSpan.Zero;
_previousTotalChars = 0; _previousTotalChars = 0;
@@ -395,7 +392,6 @@ namespace AnotherReplayReader
OnChunk, OnChunk,
_linkedCts.Token); _linkedCts.Token);
UpdateTokenDisplay(overviewResult); UpdateTokenDisplay(overviewResult);
ReportReasoningGuardResult(overviewResult);
_overview = OverviewParser.Parse(overviewResult.Response); _overview = OverviewParser.Parse(overviewResult.Response);
_overviewNarrative = _overview.Narrative; _overviewNarrative = _overview.Narrative;
overviewAttempt++; overviewAttempt++;
@@ -555,7 +551,6 @@ namespace AnotherReplayReader
OnChunk, OnChunk,
_linkedCts.Token); _linkedCts.Token);
UpdateTokenDisplay(windowResult); UpdateTokenDisplay(windowResult);
ReportReasoningGuardResult(windowResult);
windowResponse = windowResult.Response; windowResponse = windowResult.Response;
if (backqueryCount >= maxBackqueriesPerSegment) if (backqueryCount >= maxBackqueriesPerSegment)
@@ -649,7 +644,6 @@ namespace AnotherReplayReader
OnChunk, OnChunk,
_linkedCts.Token); _linkedCts.Token);
UpdateTokenDisplay(revisionResult); UpdateTokenDisplay(revisionResult);
ReportReasoningGuardResult(revisionResult);
if (string.IsNullOrWhiteSpace(revisionResult.Response)) if (string.IsNullOrWhiteSpace(revisionResult.Response))
{ {
@@ -760,7 +754,6 @@ namespace AnotherReplayReader
_linkedCts.Token); _linkedCts.Token);
UpdateTokenDisplay(result); UpdateTokenDisplay(result);
ReportReasoningGuardResult(result);
// 总结轮回查:允许模型请求一次远处原始区间,作为同一会话的追加输入。 // 总结轮回查:允许模型请求一次远处原始区间,作为同一会话的追加输入。
if (_replayData is null) if (_replayData is null)
@@ -817,7 +810,6 @@ namespace AnotherReplayReader
OnChunk, OnChunk,
_linkedCts.Token); _linkedCts.Token);
UpdateTokenDisplay(finalSummary); UpdateTokenDisplay(finalSummary);
ReportReasoningGuardResult(finalSummary);
AppendLog("总结完成", "已根据回查区间补充最终总结。", false); AppendLog("总结完成", "已根据回查区间补充最终总结。", false);
} }
@@ -899,22 +891,11 @@ namespace AnotherReplayReader
_chunkQueue.Enqueue((chunk, DateTimeOffset.UtcNow)); _chunkQueue.Enqueue((chunk, DateTimeOffset.UtcNow));
} }
private void ReportReasoningGuardResult(AIAnalyze.Result result)
{
if (string.IsNullOrWhiteSpace(result.ReasoningContinuationError))
{
return;
}
AppendLog("推理保护警告", result.ReasoningContinuationError, false, _currentStage);
}
private void StartThinkingBlock() private void StartThinkingBlock()
{ {
_thinkingStartTime = DateTime.Now; _thinkingStartTime = DateTime.Now;
var stage = _currentStage ?? "分析"; var stage = _currentStage ?? "分析";
var label = _nextThinkingIsContinuation ? "AI 续写中..." : "AI 思考中..."; var label = "AI 思考中...";
_nextThinkingIsContinuation = false;
var parent = _currentVersionAddBlock is null var parent = _currentVersionAddBlock is null
? GetStageBlocks(_currentStage) ? GetStageBlocks(_currentStage)
: null; : null;
@@ -1200,176 +1181,6 @@ namespace AnotherReplayReader
return block.Substring(jsonStart, jsonEnd - jsonStart + 1).Trim(); return block.Substring(jsonStart, jsonEnd - jsonStart + 1).Trim();
} }
private void AppendReasoningDiagnostics(string payload)
{
try
{
using var doc = JsonDocument.Parse(payload);
var root = doc.RootElement;
var originalText = root.TryGetProperty("originalRequestJson", out var original)
&& original.ValueKind == JsonValueKind.String
? original.GetString()
: null;
var continuationText =
root.TryGetProperty("continuationRequestJson", out var continuation)
&& continuation.ValueKind == JsonValueKind.String
? continuation.GetString()
: null;
var summary = BuildReasoningDiagnosticSummary(
originalText,
continuationText);
if (!string.IsNullOrWhiteSpace(summary))
{
AppendLog("推理保护诊断", summary, true, _currentStage);
}
if (!string.IsNullOrWhiteSpace(continuationText))
{
AppendLog(
"完整续写请求 JSON",
PrettyPrintJson(continuationText!),
true,
_currentStage);
}
}
catch
{
AppendLog("推理保护诊断", payload, true, _currentStage);
}
}
private static string BuildReasoningDiagnosticSummary(
string? originalJson,
string? continuationJson)
{
var sb = new StringBuilder();
sb.AppendLine($"消息数:{GetMessageCount(originalJson)} → {GetMessageCount(continuationJson)}");
if (string.IsNullOrWhiteSpace(continuationJson))
{
return sb.ToString().TrimEnd();
}
try
{
using var doc = JsonDocument.Parse(continuationJson!);
if (!doc.RootElement.TryGetProperty("messages", out var messages)
|| messages.ValueKind != JsonValueKind.Array)
{
return sb.ToString().TrimEnd();
}
for (var i = messages.GetArrayLength() - 1; i >= 0; --i)
{
var message = messages[i];
if (!message.TryGetProperty("tool_calls", out var toolCalls)
|| toolCalls.ValueKind != JsonValueKind.Array
|| toolCalls.GetArrayLength() == 0)
{
continue;
}
var toolName = "unknown";
if (toolCalls[0].TryGetProperty("function", out var function)
&& function.TryGetProperty("name", out var name))
{
toolName = name.GetString() ?? toolName;
}
var reasoning = message.TryGetProperty("reasoning_content", out var reasoningProp)
? reasoningProp.GetString()
: null;
sb.AppendLine("新增 assistant 消息:");
sb.AppendLine($" - reasoning_content 长度:{reasoning?.Length ?? 0}");
if (!string.IsNullOrEmpty(reasoning))
{
var markerIndex = reasoning.IndexOf(
AiReasoningGuard.TruncationMarker,
StringComparison.Ordinal);
var tail = markerIndex >= 0
? reasoning.Substring(markerIndex)
: "(未找到截断标记)";
sb.AppendLine($" - 末尾追加:{Excerpt(tail, 160)}");
}
sb.AppendLine($" - tool_calls{toolName}");
break;
}
for (var i = messages.GetArrayLength() - 1; i >= 0; --i)
{
var message = messages[i];
if (!message.TryGetProperty("role", out var role)
|| role.GetString() != "tool")
{
continue;
}
var toolCallId = message.TryGetProperty("tool_call_id", out var id)
? id.GetString()
: "?";
var content = message.TryGetProperty("content", out var contentProp)
? contentProp.GetString()
: null;
sb.AppendLine("新增 tool 消息:");
sb.AppendLine($" - tool_call_id{toolCallId}");
sb.AppendLine($" - 指令开头:{Excerpt(content ?? "()", 160)}");
break;
}
}
catch
{
// 摘要解析失败时保留原始 JSON 回退。
}
return sb.ToString().TrimEnd();
}
private static int GetMessageCount(string? requestJson)
{
if (string.IsNullOrWhiteSpace(requestJson))
{
return 0;
}
try
{
using var doc = JsonDocument.Parse(requestJson!);
return doc.RootElement.TryGetProperty("messages", out var messages)
&& messages.ValueKind == JsonValueKind.Array
? messages.GetArrayLength()
: 0;
}
catch
{
return 0;
}
}
private static string PrettyPrintJson(string json)
{
try
{
using var doc = JsonDocument.Parse(json);
return JsonSerializer.Serialize(
doc.RootElement,
new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
catch
{
return json;
}
}
private static string Excerpt(string text, int maxLength)
{
var normalized = text.Replace("\r", "").Replace("\n", " ");
return normalized.Length <= maxLength
? normalized
: normalized.Substring(0, maxLength) + "…";
}
private void FlushPendingUiActions() private void FlushPendingUiActions()
{ {
_isFlushingPendingUiActions = true; _isFlushingPendingUiActions = true;
@@ -1592,7 +1403,6 @@ namespace AnotherReplayReader
var thinkSb = new StringBuilder(); var thinkSb = new StringBuilder();
var contentSb = new StringBuilder(); var contentSb = new StringBuilder();
var errorSb = new StringBuilder(); var errorSb = new StringBuilder();
var guardTriggered = false;
while (_chunkQueue.TryDequeue(out var data)) while (_chunkQueue.TryDequeue(out var data))
{ {
var (chunk, time) = data; var (chunk, time) = data;
@@ -1620,19 +1430,6 @@ namespace AnotherReplayReader
{ {
errorSb.AppendLine(chunk.Text); errorSb.AppendLine(chunk.Text);
} }
else if (chunk.Type == AIAnalyze.AIChunkType.ReasoningGuard)
{
EndThinkingBlock();
_nextThinkingIsContinuation = true;
_pendingUiActions.Enqueue(() =>
AppendLog("推理保护", chunk.Text, false, _currentStage));
guardTriggered = true;
}
else if (chunk.Type == AIAnalyze.AIChunkType.ReasoningGuardRequest)
{
_pendingUiActions.Enqueue(() =>
AppendReasoningDiagnostics(chunk.Text));
}
} }
if (thinkSb.Length > 0) if (thinkSb.Length > 0)
@@ -1651,10 +1448,6 @@ namespace AnotherReplayReader
{ {
AddErrorBlock(errorSb.ToString()); AddErrorBlock(errorSb.ToString());
} }
if (guardTriggered)
{
_currentContent = null;
}
} }
private void AutoScroll() private void AutoScroll()
+5 -20
View File
@@ -1,4 +1,4 @@
<UserControl x:Class="AnotherReplayReader.AIProviderSettingsControl" <UserControl x:Class="AnotherReplayReader.AIProviderSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -143,7 +143,6 @@
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/> <ColumnDefinition Width="100"/>
@@ -165,34 +164,20 @@
Content="支持 SSE 流式输出" Content="支持 SSE 流式输出"
Margin="0,5"/> Margin="0,5"/>
<Label Grid.Row="4" Grid.Column="0" Content="推理保护"/> <Label Grid.Row="4" Grid.Column="0" Content="高级参数"/>
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" Margin="0,5">
<CheckBox x:Name="_reasoningGuardCheck"
Content="启用"
VerticalAlignment="Center"/>
<Label Content="推理 token 阈值"
VerticalAlignment="Center"
Margin="10,0,0,0"/>
<TextBox x:Name="_reasoningTokenLimitBox"
Width="80"
VerticalAlignment="Center"
ToolTip="留空使用 max(4096, max_tokens / 2);仅流式模型生效"/>
</StackPanel>
<Label Grid.Row="5" Grid.Column="0" Content="高级参数"/>
<TextBox x:Name="_extraParamsBox" <TextBox x:Name="_extraParamsBox"
Grid.Row="5" Grid.Column="1" Grid.Row="4" Grid.Column="1"
MinHeight="80" MinHeight="80"
AcceptsReturn="True" AcceptsReturn="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"/> ScrollViewer.VerticalScrollBarVisibility="Auto"/>
<StackPanel Grid.Row="6" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right"> <StackPanel Grid.Row="5" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="应用模型修改" Click="OnApplyModelClick" Margin="2"/> <Button Content="应用模型修改" Click="OnApplyModelClick" Margin="2"/>
<Button Content="从已知模板填充" Click="OnFillFromKnownModelsClick" Margin="2"/> <Button Content="从已知模板填充" Click="OnFillFromKnownModelsClick" Margin="2"/>
</StackPanel> </StackPanel>
<TextBlock x:Name="_modelStatusText" <TextBlock x:Name="_modelStatusText"
Grid.Row="7" Grid.ColumnSpan="2" Grid.Row="6" Grid.ColumnSpan="2"
Foreground="Gray" Margin="0,5"/> Foreground="Gray" Margin="0,5"/>
</Grid> </Grid>
</DockPanel> </DockPanel>
-21
View File
@@ -311,11 +311,6 @@ namespace AnotherReplayReader
_contextLengthBox.Text = _currentModel.ContextLength.ToString(); _contextLengthBox.Text = _currentModel.ContextLength.ToString();
_contextBudgetBox.Text = _currentModel.ContextBudget is { } budget ? budget.ToString() : "0"; _contextBudgetBox.Text = _currentModel.ContextBudget is { } budget ? budget.ToString() : "0";
_supportsSseCheck.IsChecked = _currentModel.IsStream; _supportsSseCheck.IsChecked = _currentModel.IsStream;
_reasoningGuardCheck.IsChecked = _currentModel.ReasoningGuardEnabled;
_reasoningTokenLimitBox.Text =
_currentModel.ReasoningGuardTokenLimit is { } guardLimit
? guardLimit.ToString()
: "";
// 显示 ExtraParameters 为缩进 JSON // 显示 ExtraParameters 为缩进 JSON
var json = JsonSerializer.Serialize( var json = JsonSerializer.Serialize(
@@ -370,8 +365,6 @@ namespace AnotherReplayReader
ContextLength = known.ContextLength, ContextLength = known.ContextLength,
ContextBudget = known.ContextBudget, ContextBudget = known.ContextBudget,
IsStream = known.IsStream, IsStream = known.IsStream,
ReasoningGuardEnabled = known.ReasoningGuardEnabled,
ReasoningGuardTokenLimit = known.ReasoningGuardTokenLimit,
ExtraParameters = new Dictionary<string, object>(known.ExtraParameters) ExtraParameters = new Dictionary<string, object>(known.ExtraParameters)
}); });
} }
@@ -474,16 +467,6 @@ namespace AnotherReplayReader
_currentModel.ContextBudget = null; _currentModel.ContextBudget = null;
} }
_currentModel.IsStream = _supportsSseCheck.IsChecked == true; _currentModel.IsStream = _supportsSseCheck.IsChecked == true;
_currentModel.ReasoningGuardEnabled = _reasoningGuardCheck.IsChecked == true;
if (int.TryParse(_reasoningTokenLimitBox.Text, out int guardLimit)
&& guardLimit > 0)
{
_currentModel.ReasoningGuardTokenLimit = guardLimit;
}
else
{
_currentModel.ReasoningGuardTokenLimit = null;
}
try try
{ {
@@ -529,8 +512,6 @@ namespace AnotherReplayReader
_currentModel.IsStream = known.IsStream; _currentModel.IsStream = known.IsStream;
_currentModel.ContextLength = known.ContextLength; _currentModel.ContextLength = known.ContextLength;
_currentModel.ContextBudget = known.ContextBudget; _currentModel.ContextBudget = known.ContextBudget;
_currentModel.ReasoningGuardEnabled = known.ReasoningGuardEnabled;
_currentModel.ReasoningGuardTokenLimit = known.ReasoningGuardTokenLimit;
RefreshModelList(); RefreshModelList();
_modelComboBox.SelectedItem = _modelComboBox.Items _modelComboBox.SelectedItem = _modelComboBox.Items
.OfType<ModelDisplayItem>() .OfType<ModelDisplayItem>()
@@ -550,8 +531,6 @@ namespace AnotherReplayReader
_contextLengthBox.Text = ""; _contextLengthBox.Text = "";
_contextBudgetBox.Text = ""; _contextBudgetBox.Text = "";
_supportsSseCheck.IsChecked = false; _supportsSseCheck.IsChecked = false;
_reasoningGuardCheck.IsChecked = false;
_reasoningTokenLimitBox.Text = "";
_extraParamsBox.Text = ""; _extraParamsBox.Text = "";
} }
} }
+8 -8
View File
@@ -2,7 +2,7 @@
日期:2026-08-23 日期:2026-08-23
范围:OpenCodeGo / `deepseek-v4-flash`OpenAI 兼容 `/chat/completions` 范围:OpenCodeGo / `deepseek-v4-flash`OpenAI 兼容 `/chat/completions`
状态:实验性研究,未集成到主项目 状态:实验性研究,未集成到主项目;主项目曾短暂集成推理保护,后因实测影响输出质量而移除(详见 `PLAN_ai_analysis_v2.md`
## 摘要 ## 摘要
@@ -10,7 +10,7 @@
结论是:**仅发送 `reasoning_content` 或“上一轮思维链”并不稳定;成功率最高的方案是伪造一段 tool call 历史,并把续写要求、输出格式和需要引用的内容放入 tool 结果中。** 结论是:**仅发送 `reasoning_content` 或“上一轮思维链”并不稳定;成功率最高的方案是伪造一段 tool call 历史,并把续写要求、输出格式和需要引用的内容放入 tool 结果中。**
与本研究相关的代码在 `AiV2.Tests/Program.cs`,默认不会执行 与本研究相关的实验代码已从 `AiV2.Tests/Program.cs`移除,不再保留可执行实验入口
## 目标 ## 目标
@@ -232,9 +232,11 @@ system
- 不要假设 `reasoning_content` 的末尾内容一定能被模型召回。 - 不要假设 `reasoning_content` 的末尾内容一定能被模型召回。
- 增加原始响应/请求日志,以便区分“模型未看到”和“模型看到了但没引用”。 - 增加原始响应/请求日志,以便区分“模型未看到”和“模型看到了但没引用”。
## 主项目集成状态(2026-08-23 ## 主项目集成状态(2026-08-23,历史记录;该功能已全部移除
- 主项目已落地模型级“推理保护”实验开关,默认关闭,仅在 `IsStream=true` 的模型上生效 > 以下内容是当时的集成状态,不代表当前代码。当前主项目已移除推理保护及所有相关代码/测试
- 主项目曾落地模型级“推理保护”实验开关,默认关闭,仅在 `IsStream=true` 的模型上生效。
- 累计推理 token 达到阈值后停止读取当前 SSE 响应,保留部分推理和正文;最多发起一次续写。 - 累计推理 token 达到阈值后停止读取当前 SSE 响应,保留部分推理和正文;最多发起一次续写。
- 续写采用本报告推荐的伪造 tool call 历史:`assistant(reasoning_content + tool_calls)``tool(tool_call_id + 收尾指令)` - 续写采用本报告推荐的伪造 tool call 历史:`assistant(reasoning_content + tool_calls)``tool(tool_call_id + 收尾指令)`
- 首请求不携带 `tools`,只有续写请求注入 `tools``tool_choice=none`;缓存命中为 best-effort。 - 首请求不携带 `tools`,只有续写请求注入 `tools``tool_choice=none`;缓存命中为 best-effort。
@@ -248,9 +250,7 @@ system
## 专用测试状态 ## 专用测试状态
- 测试代码:`AiV2.Tests/Program.cs` 中的 `OpenCodeGoFakeToolCallTests` - 测试代码:`AiV2.Tests/Program.cs` 中的 `OpenCodeGoFakeToolCallTests`
- 默认行为:不执行 - 现状:已随推理保护一起移除,不再保留可执行实验入口
- 启用条件:`ARR_AI_E2E=1``ARR_AI_E2E_TOOL=1`
- 输出:Markdown 报告,路径通过 `ARR_AI_TOOL_REPORT_PATH` 指定。
主项目中的实验性 `ChatMessage` 扩展、`AiReasoningContinuationSettings`、UI 开关、回传/降级逻辑和 tool call 历史构造均已移除。 主项目中的实验性 `ChatMessage` 扩展、`AiReasoningContinuationSettings`、UI 开关、回传/降级逻辑和 tool call 历史构造均已移除。
+1 -803
View File
@@ -46,9 +46,6 @@ namespace AiV2.Tests
Run("UserKnowledgeOverlay", UserKnowledgeOverlayTests.Run); Run("UserKnowledgeOverlay", UserKnowledgeOverlayTests.Run);
Run("PromptBuilders", PromptBuildersTests.Run); Run("PromptBuilders", PromptBuildersTests.Run);
Run("RevisionFactsAndSerialization", RevisionFactsAndSerializationTests.Run); Run("RevisionFactsAndSerialization", RevisionFactsAndSerializationTests.Run);
Run("ReasoningGuard", ReasoningGuardTests.Run);
Run("OpenCodeGoFakeToolCallE2e", OpenCodeGoFakeToolCallTests.Run);
Run("OpenCodeGoGuardE2e", OpenCodeGoGuardE2e.Run);
Run("UserReplayFactIndexRepro", UserReplayFactIndexReproTests.Run); Run("UserReplayFactIndexRepro", UserReplayFactIndexReproTests.Run);
Console.WriteLine(); Console.WriteLine();
@@ -992,46 +989,12 @@ namespace AiV2.Tests
[property: JsonPropertyName("content"), [property: JsonPropertyName("content"),
JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Content = null, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Content = null,
[property: JsonPropertyName("reasoning_content"), [property: JsonPropertyName("reasoning_content"),
JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? ReasoningContent = null, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? ReasoningContent = null)
[property: JsonPropertyName("tool_calls"),
JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] IReadOnlyList<ChatMessage.ToolCall>? ToolCalls = null,
[property: JsonPropertyName("tool_call_id"),
JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? ToolCallId = null)
{ {
internal sealed record ToolCall(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("function")] ToolCallFunction Function);
internal sealed record ToolCallFunction(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("arguments")] string Arguments);
internal static ChatMessage Assistant(string? content, string? reasoningContent = null) => internal static ChatMessage Assistant(string? content, string? reasoningContent = null) =>
new("assistant", new("assistant",
string.IsNullOrEmpty(content) ? null : content, string.IsNullOrEmpty(content) ? null : content,
string.IsNullOrEmpty(reasoningContent) ? null : reasoningContent); string.IsNullOrEmpty(reasoningContent) ? null : reasoningContent);
internal static ChatMessage AssistantToolCall(
string? reasoningContent,
string toolCallId,
string toolName,
string arguments) =>
new(
"assistant",
null,
reasoningContent,
new[]
{
new ChatMessage.ToolCall(
toolCallId,
"function",
new ChatMessage.ToolCallFunction(toolName, arguments))
},
null);
internal static ChatMessage ToolResult(string toolCallId, string content) =>
new("tool", content, null, null, toolCallId);
} }
public async Task<Result> CompleteAsync( public async Task<Result> CompleteAsync(
@@ -1189,771 +1152,6 @@ namespace AiV2.Tests
} }
} }
internal sealed class GuardHttpHandler : HttpMessageHandler
{
public int RequestCount;
public string? FirstRequestBody;
public string? SecondRequestBody;
public bool FirstResponseContentOnly;
public bool SecondResponseInterrupted;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var body = await request.Content!.ReadAsStringAsync();
RequestCount++;
if (RequestCount == 1)
{
FirstRequestBody = body;
if (FirstResponseContentOnly)
{
return CreateSse(
"data: {\"choices\":[{\"delta\":{\"content\":\"final\"}}]}\n",
"data: [DONE]\n");
}
return CreateSse(
"data: {\"choices\":[{\"delta\":{\"content\":\"partial first\"}}]}\n",
"data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"reasoning\"}}]}\n");
}
SecondRequestBody = body;
if (SecondResponseInterrupted)
{
return CreateSse(
"data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"second reasoning\"}}]}\n");
}
return CreateSse(
"data: {\"choices\":[{\"delta\":{\"content\":\"final\"}}]}\n",
"data: [DONE]\n");
}
private static HttpResponseMessage CreateSse(params string[] lines)
{
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(
string.Concat(lines),
Encoding.UTF8,
"text/event-stream")
};
}
}
internal static class ReasoningGuardTests
{
public static void Run()
{
TestPrepareReasoning();
TestToolPrompt();
TestMessageSerialization();
TestDisabledAsync().GetAwaiter().GetResult();
TestGuardedContinuationAsync().GetAwaiter().GetResult();
TestFallbackAsync().GetAwaiter().GetResult();
}
private static AiProvider CreateProvider() =>
new()
{
Name = "测试",
BaseUrl = "http://localhost/v1",
ApiKey = "test-key",
DefaultMaxTokens = 16384
};
private static AiModel CreateModel(bool enabled = true) =>
new()
{
ModelId = "deepseek-v4-flash",
IsStream = true,
ContextLength = 1_000_000,
ContextBudget = 160_000,
ReasoningGuardEnabled = enabled,
ReasoningGuardTokenLimit = enabled ? 1 : null,
ExtraParameters = new Dictionary<string, object>()
};
private static ImmutableList<MainAIAnalyze.ChatMessage> CreateMessages() =>
ImmutableList<MainAIAnalyze.ChatMessage>.Empty
.Add(new MainAIAnalyze.ChatMessage("system", "测试系统"))
.Add(new MainAIAnalyze.ChatMessage("user", "请分析。"));
private static async Task<MainAIAnalyze.Result> CompleteAsync(
GuardHttpHandler handler,
AiModel model)
{
using var http = new HttpClient(handler);
var analyzer = new MainAIAnalyze(http);
return await analyzer.CompleteAsync(
CreateMessages(),
new AiRequestContext(CreateProvider(), model),
_ => { },
CancellationToken.None);
}
private static void TestPrepareReasoning()
{
var shortReasoning = "第一行。\n第二行。\n第三行。";
var prepared = AiReasoningGuard.PrepareReasoning(shortReasoning, 1000);
Program.Assert(
prepared.Text.TrimEnd().EndsWith(AiReasoningGuard.WrapUpStatement),
"末尾收尾语句");
Program.Assert(
prepared.Text.IndexOf(AiReasoningGuard.TruncationMarker) >= 0
&& prepared.Text.IndexOf(AiReasoningGuard.TruncationMarker)
== prepared.Text.LastIndexOf(AiReasoningGuard.TruncationMarker),
"只追加一次截断标记");
Program.Assert(
prepared.Text.IndexOf(AiReasoningGuard.TruncationMarker)
< prepared.Text.IndexOf(AiReasoningGuard.WrapUpStatement),
"截断标记在收尾语句之前");
var longReasoning = "前文。\n" + new string('x', 500) + "后文。";
var truncated = AiReasoningGuard.PrepareReasoning(longReasoning, 10);
Program.Assert(!truncated.Text.Contains("后文"), "超限部分被截掉");
var formatState = AiReasoningGuard.DescribeFormatState(
"```json\n{\"unitClaims\":[");
Program.Assert(
formatState.Contains("代码围栏未闭合")
&& formatState.Contains("JSON"),
"格式状态检测");
}
private static void TestToolPrompt()
{
var result = AiReasoningGuard.BuildToolResult();
Program.Assert(
result.Contains("立即停止继续展开推理")
&& result.Contains("直接输出最终结果")
&& result.Contains("最后一句已经宣告收尾"),
"强收尾措辞");
Program.Assert(
!result.Contains("上一轮思维链")
&& !result.Contains("截断点")
&& !result.Contains("My reasoning has been truncated")
&& !result.Contains("token limit"),
"避免歧义措辞");
}
private static void TestMessageSerialization()
{
var assistant = MainAIAnalyze.ChatMessage.AssistantToolCall(
"thinking",
"call_1",
"analysis_hint",
"{\"instruction\":\"wrap up\"}");
var assistantJson = JsonSerializer.Serialize(assistant);
Program.Assert(
assistantJson.Contains("\"reasoning_content\":\"thinking\"")
&& assistantJson.Contains("\"tool_calls\"")
&& assistantJson.Contains("\"analysis_hint\""),
"assistant 工具历史序列化");
var tool = MainAIAnalyze.ChatMessage.ToolResult(
"call_1",
AiReasoningGuard.BuildToolResult());
var toolJson = JsonSerializer.Serialize(tool);
Program.Assert(
toolJson.Contains("\"tool_call_id\":\"call_1\""),
"tool 结果序列化");
}
private static async Task TestDisabledAsync()
{
var handler = new GuardHttpHandler { FirstResponseContentOnly = true };
var result = await CompleteAsync(handler, CreateModel(enabled: false));
Program.AssertEqual(1, handler.RequestCount, "未启用时只发一次请求");
Program.Assert(!result.ContinuationApplied, "未启用不续写");
Program.AssertEqual("final", result.Response, "未启用保持普通结果");
}
private static async Task TestGuardedContinuationAsync()
{
var handler = new GuardHttpHandler();
var result = await CompleteAsync(handler, CreateModel());
Program.AssertEqual(2, handler.RequestCount, "触发后发起一次续写");
Program.Assert(result.ContinuationApplied, "续写成功");
Program.AssertEqual("final", result.Response, "续写返回最终正文");
Program.Assert(
handler.FirstRequestBody is { } firstBody
&& !firstBody.Contains("\"tools\""),
"首次请求不携带 tool 定义");
Program.Assert(
handler.SecondRequestBody is { } secondBody
&& secondBody.Contains("\"tools\"")
&& secondBody.Contains("\"tool_choice\":\"none\"")
&& secondBody.Contains("\"reasoning_content\"")
&& secondBody.Contains("\"tool_calls\"")
&& secondBody.Contains("\"tool_call_id\""),
"续写请求携带工具历史");
Program.Assert(
result.ReasoningOriginalRequestJson is { } originalJson
&& originalJson.Contains("\"messages\""),
"首次请求 JSON 回传 UI");
Program.Assert(
result.ReasoningContinuationRequestJson is { } continuationJson
&& continuationJson.Contains("\"tools\"")
&& continuationJson.Contains("\"reasoning_content\"")
&& continuationJson.Contains("我已经整理出足够的信息"),
"修改后请求 JSON 回传 UI");
}
private static async Task TestFallbackAsync()
{
var handler = new GuardHttpHandler { SecondResponseInterrupted = true };
var result = await CompleteAsync(handler, CreateModel());
Program.Assert(!result.ContinuationApplied, "二次超限不再续写");
Program.Assert(result.ReasoningInterrupted, "保留中断状态");
Program.Assert(
result.ReasoningContinuationError is { } error
&& error.Contains("回退"),
"回退提示");
Program.Assert(
result.ReasoningContinuationRequestJson is { } continuationJson
&& continuationJson.Contains("\"tools\""),
"回退时仍提供修改后请求 JSON");
Program.AssertEqual("partial first", result.Response, "回退首次部分正文");
}
}
internal static class OpenCodeGoFakeToolCallTests
{
private const string SystemPrompt =
"你是用于验证思维链回传的实验辅助。请先用 reasoning_content 进行内部推理,再给出简短、准确的回答。";
private const string FirstUserPrompt =
"请分析:A 比 B 高 20%B 比 C 高 25%,那么 A 比 C 高多少?请先思考,再回答最终百分比。";
private const string ToolName = "analysis_hint";
private const string MemoryMarker = "TOKEN_MARK=731942";
public static void Run()
{
if (Environment.GetEnvironmentVariable("ARR_AI_E2E") != "1"
|| Environment.GetEnvironmentVariable("ARR_AI_E2E_TOOL") != "1")
{
Console.WriteLine(
" [跳过] 未同时设置 ARR_AI_E2E=1 与 ARR_AI_E2E_TOOL=1"
+ "跳过伪造 tool call 历史实验");
return;
}
RunAsync().GetAwaiter().GetResult();
}
private static async Task RunAsync()
{
var settingsPath = FindSettingsPath();
Console.WriteLine($" [E2E-TOOL] 配置:{settingsPath}");
var settings = JsonSerializer.Deserialize<AiSettings>(
File.ReadAllText(settingsPath, Encoding.UTF8))
?? throw new InvalidOperationException("AI 设置文件无法解析");
var provider = settings.Providers.FirstOrDefault(p =>
p.Name.Equals("OpenCodeGo", StringComparison.OrdinalIgnoreCase)
&& p.Models.Any(m =>
m.ModelId.Equals("deepseek-v4-flash", StringComparison.OrdinalIgnoreCase)))
?? throw new InvalidOperationException(
"当前配置中未找到 OpenCodeGo / deepseek-v4-flash 服务与模型");
var model = provider.Models.First(m =>
m.ModelId.Equals("deepseek-v4-flash", StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrWhiteSpace(provider.ApiKey))
{
throw new InvalidOperationException("OpenCodeGo 的 API Key 为空");
}
Console.WriteLine(
$" [E2E-TOOL] Provider={provider.Name}; Model={model.ModelId}; "
+ $"BaseUrl={provider.BaseUrl}; Stream={model.IsStream}; ApiKey=***");
var report = new StringBuilder();
AppendHeader(report, provider, model, settingsPath);
var reportPath = Environment.GetEnvironmentVariable("ARR_AI_TOOL_REPORT_PATH");
if (string.IsNullOrWhiteSpace(reportPath))
{
reportPath = Path.Combine(
Environment.CurrentDirectory, "AI_reasoning_continuation_position_sweep_report.md");
}
reportPath = Path.GetFullPath(reportPath);
try
{
var analyzer = new AIAnalyze();
var normalContext = new AiRequestContext(provider, model);
var firstMessages = ImmutableList<AIAnalyze.ChatMessage>.Empty
.Add(new AIAnalyze.ChatMessage("system", SystemPrompt))
.Add(new AIAnalyze.ChatMessage("user", FirstUserPrompt));
var first = await CompleteAndRecordAsync(
analyzer, normalContext, firstMessages, "初始请求(system + user", report);
var firstReasoning = first?.Reasoning ?? string.Empty;
if (first is null || firstReasoning.Length == 0)
{
report.AppendLine("初始请求未获得 reasoning_content,无法继续伪造 tool call 实验。");
return;
}
var initialFirst = first;
var toolContext = BuildToolContext(provider, model);
var wordings = new[]
{
new
{
Name = "措辞B:当前推理",
Target = "你当前正在进行的内部推理",
ArgumentInstruction = "继续你当前正在进行的内部推理,并参考工具结果。"
},
new
{
Name = "措辞Dtool_calls 携带的 reasoning_content",
Target = "你本次 tool_calls 消息中携带的 reasoning_content",
ArgumentInstruction = "继续你本次 tool_calls 消息中携带的 reasoning_content,并参考工具结果。"
}
};
var positions = new[]
{
new { Name = "25%", Ratio = 0.25 },
new { Name = "50%", Ratio = 0.50 },
new { Name = "75%", Ratio = 0.75 },
new { Name = "末尾", Ratio = 1.0 }
};
const int repetitions = 5;
var combinedCount = wordings.Length * positions.Length;
var foundCounts = new int[combinedCount];
var missingCounts = new int[combinedCount];
var failedCounts = new int[combinedCount];
var names = new string[combinedCount];
var markerInSent = new bool[combinedCount];
var comboIndex = 0;
foreach (var wording in wordings)
{
foreach (var position in positions)
{
var comboName = $"{wording.Name} / 标记位置 {position.Name}";
names[comboIndex] = comboName;
var reasoningWithMarker = InsertMemoryMarker(
firstReasoning, position.Ratio);
markerInSent[comboIndex] = reasoningWithMarker.Contains("731942");
Console.WriteLine(
$" [E2E-TOOL] 正在测试:{comboName}(重复 5 次)");
for (var rep = 0; rep < repetitions; ++rep)
{
var toolCallId = $"call_position_{comboIndex + 1}_{rep + 1}";
var arguments = JsonSerializer.Serialize(new
{
instruction = wording.ArgumentInstruction
});
var messages = ImmutableList<AIAnalyze.ChatMessage>.Empty
.Add(new AIAnalyze.ChatMessage("system", SystemPrompt))
.Add(new AIAnalyze.ChatMessage("user", FirstUserPrompt))
.Add(AIAnalyze.ChatMessage.AssistantToolCall(
reasoningWithMarker, toolCallId, ToolName, arguments))
.Add(AIAnalyze.ChatMessage.ToolResult(
toolCallId, BuildToolResult(wording.Target)));
var result = await CompleteAndRecordAsync(
analyzer, toolContext, messages,
$"记忆标记 {comboName}(重复 {rep + 1}/{repetitions}", report);
if (result is { } completed)
{
var combined = (completed.Reasoning ?? string.Empty)
+ "\n" + completed.Response;
var found = ContainsMemoryMarker(combined);
var missing = SaysMarkerMissing(combined);
if (found)
{
foundCounts[comboIndex]++;
}
if (missing)
{
missingCounts[comboIndex]++;
}
Console.WriteLine(
$" [E2E-TOOL] {comboName} [{rep + 1}/{repetitions}]"
+ $"content={completed.Response.Length}"
+ $"reasoning={(completed.Reasoning ?? string.Empty).Length}"
+ $"markerFound={found}missing={missing}");
}
else
{
failedCounts[comboIndex]++;
Console.WriteLine(
$" [E2E-TOOL] {comboName} [{rep + 1}/{repetitions}]:请求失败(详见报告)");
}
}
comboIndex++;
}
}
AppendSummary(
report,
names,
markerInSent,
foundCounts,
missingCounts,
failedCounts);
}
finally
{
var reportDir = Path.GetDirectoryName(reportPath);
if (!string.IsNullOrEmpty(reportDir))
{
Directory.CreateDirectory(reportDir);
}
File.WriteAllText(reportPath, report.ToString(), new UTF8Encoding(false));
Console.WriteLine($" [E2E-TOOL] 人工审阅报告已写入:{reportPath}");
}
}
private static AiRequestContext BuildToolContext(
AiProvider provider,
AiModel sourceModel)
{
var model = new AiModel
{
ModelId = sourceModel.ModelId,
DisplayName = sourceModel.DisplayName,
IsStream = sourceModel.IsStream,
ContextLength = sourceModel.ContextLength,
ContextBudget = sourceModel.ContextBudget,
ExtraParameters = new Dictionary<string, object>(sourceModel.ExtraParameters)
};
model.ExtraParameters["tools"] = new[]
{
new
{
type = "function",
function = new
{
name = ToolName,
description = "向模型提供上一轮思维链的继续提示。",
parameters = new
{
type = "object",
properties = new
{
instruction = new
{
type = "string",
description = "模型应遵循的续写提示。"
}
},
required = new[] { "instruction" }
}
}
}
};
model.ExtraParameters["tool_choice"] = "none";
return new AiRequestContext(provider, model);
}
private static string BuildToolResult(string target) =>
"工具结果:请继续" + target + "。这是一次确定性记忆测试。"
+ "该思考内容中有一个内部记忆标记,请不要从本工具结果中猜测或寻找。"
+ "请原样写出该标记;如果该思考内容中没有该标记,请明确回答“标记不存在”。"
+ "不要从零重新推导,也不要杜撰标记。";
private static string InsertMemoryMarker(string text, double ratio)
{
if (ratio >= 1)
{
return text + "\n\n[内部记忆标记 " + MemoryMarker + "]";
}
var index = Math.Max(
0,
Math.Min(text.Length, (int)(text.Length * ratio)));
return text.Insert(
index,
"\n\n[内部记忆标记 " + MemoryMarker + "]\n\n");
}
private static bool ContainsMemoryMarker(string text) =>
text.Contains("731942") || text.Contains("TOKEN_MARK");
private static bool SaysMarkerMissing(string text) =>
text.Contains("标记不存在")
|| text.Contains("没有该标记")
|| text.Contains("没有标记");
private static void AppendSummary(
StringBuilder report,
string[] names,
bool[] markerInSent,
int[] foundCounts,
int[] missingCounts,
int[] failedCounts)
{
report.AppendLine("## 记忆标记统计");
report.AppendLine();
report.AppendLine($"- 记忆标记:`{MemoryMarker}`");
report.AppendLine("- 每个组合重复 5 次。");
report.AppendLine();
for (var i = 0; i < names.Length; ++i)
{
report.AppendLine(
$"- **{names[i]}**:发送链包含标记 = {markerInSent[i]}"
+ $"正确写出标记 = {foundCounts[i]}/5"
+ $"明确说没有 = {missingCounts[i]}/5"
+ $"请求失败 = {failedCounts[i]}/5");
}
report.AppendLine();
}
private static async Task<AIAnalyze.Result?> CompleteAndRecordAsync(
AIAnalyze analyzer,
AiRequestContext context,
ImmutableList<AIAnalyze.ChatMessage> messages,
string label,
StringBuilder report)
{
var sw = Stopwatch.StartNew();
try
{
var result = await analyzer.CompleteAsync(
messages, context, null, CancellationToken.None);
sw.Stop();
AppendRecord(report, label, messages, result, sw.Elapsed);
return result;
}
catch (Exception ex)
{
sw.Stop();
report.AppendLine($"## {label}(请求失败)");
report.AppendLine();
report.AppendLine($"- 耗时:{sw.Elapsed.TotalSeconds:0.00} 秒");
report.AppendLine($"- 错误:{ex.Message}");
report.AppendLine("- 关键点:assistant 消息包含 tool_calls,随后有 tool 回复;请求携带 tools 定义。");
report.AppendLine();
report.AppendLine("---");
report.AppendLine();
Console.WriteLine($" [E2E-TOOL] {label} 失败:{ex.Message}");
return null;
}
}
private static void AppendHeader(
StringBuilder report,
AiProvider provider,
AiModel model,
string settingsPath)
{
report.AppendLine("# 伪造 tool call 历史实验");
report.AppendLine();
report.AppendLine($"- 配置:{provider.Name} / {model.ModelId}");
report.AppendLine($"- BaseUrl{provider.BaseUrl}");
report.AppendLine($"- 配置文件:{settingsPath}");
report.AppendLine("- API Key***(不写入报告)");
report.AppendLine();
report.AppendLine("本实验在请求中声明了 `tools` 并将 `tool_choice` 设为 `none`");
report.AppendLine("构造的历史为:system → 初始 user → assistant(reasoning_content + tool_calls) → tool(tool_call_id + 提示结果)。");
report.AppendLine(
$"完整思维链按 25%/50%/75%/末尾四种位置注入确定性记忆标记 `{MemoryMarker}`"
+ "工具结果使用“当前推理”和“本次 tool_calls 消息携带的 reasoning_content”两种措辞,"
+ "且不提及“上一轮”“截断点”等概念,要求模型原样写出标记。");
report.AppendLine("没有第 5 条新的 user 消息。");
report.AppendLine();
}
private static void AppendRecord(
StringBuilder report,
string label,
ImmutableList<AIAnalyze.ChatMessage> messages,
AIAnalyze.Result result,
TimeSpan elapsed)
{
report.AppendLine($"## {label}");
report.AppendLine();
report.AppendLine($"- 耗时:{elapsed.TotalSeconds:0.00} 秒");
report.AppendLine($"- prompt_tokens{result.PromptTokens?.ToString() ?? "?"}");
report.AppendLine($"- completion_tokens{result.CompletionTokens?.ToString() ?? "?"}");
report.AppendLine($"- reasoning_tokens{result.ReasoningTokens?.ToString() ?? "?"}");
report.AppendLine();
report.AppendLine("### 发送的 messages");
report.AppendLine();
for (var i = 0; i < messages.Count; ++i)
{
var message = messages[i];
report.AppendLine($"**{i + 1}. role = `{message.Role}`**");
report.AppendLine();
if (message.Content is null)
{
report.AppendLine("content`未发送`");
}
else
{
report.AppendLine("content");
AppendTextBlock(report, message.Content);
}
report.AppendLine();
if (message.ToolCallId is not null)
{
report.AppendLine($"tool_call_id`{message.ToolCallId}`");
report.AppendLine();
}
if (message.ToolCalls is { } toolCalls)
{
report.AppendLine("tool_calls");
foreach (var toolCall in toolCalls)
{
report.AppendLine(
$"- id=`{toolCall.Id}`; type=`{toolCall.Type}`; "
+ $"name=`{toolCall.Function.Name}`");
report.AppendLine();
report.AppendLine("arguments");
AppendTextBlock(report, toolCall.Function.Arguments);
}
report.AppendLine();
}
if (message.Role == "assistant")
{
report.AppendLine(
message.ReasoningContent is null
? "reasoning_content`未发送`"
: "reasoning_content");
if (message.ReasoningContent is not null)
{
AppendTextBlock(report, message.ReasoningContent);
}
report.AppendLine();
}
report.AppendLine();
}
report.AppendLine("### 模型响应");
report.AppendLine();
report.AppendLine("新 reasoning_content");
AppendTextBlock(report, result.Reasoning ?? string.Empty);
report.AppendLine();
report.AppendLine("回答正文(content):");
AppendTextBlock(report, result.Response);
report.AppendLine();
report.AppendLine("---");
report.AppendLine();
}
private static void AppendTextBlock(StringBuilder report, string? text)
{
report.AppendLine("```text");
report.AppendLine(string.IsNullOrEmpty(text) ? "(空)" : text!);
report.AppendLine("```");
}
internal static string FindSettingsPath()
{
var explicitPath = Environment.GetEnvironmentVariable("ARR_AI_SETTINGS_PATH");
if (!string.IsNullOrWhiteSpace(explicitPath) && File.Exists(explicitPath))
{
return explicitPath;
}
var dir = new DirectoryInfo(AppContext.BaseDirectory);
for (var i = 0; i < 6 && dir is not null; ++i)
{
var candidate = Path.Combine(
dir.FullName, "bin", "Debug", "net461", "AnotherReplayReader.ai_settings.json");
if (File.Exists(candidate))
{
return candidate;
}
var directCandidate = Path.Combine(
dir.FullName, "AnotherReplayReader.ai_settings.json");
if (File.Exists(directCandidate))
{
return directCandidate;
}
dir = dir.Parent;
}
throw new InvalidOperationException(
"未找到 AnotherReplayReader.ai_settings.json;可用 ARR_AI_SETTINGS_PATH 指定");
}
}
internal static class OpenCodeGoGuardE2e
{
public static void Run()
{
if (Environment.GetEnvironmentVariable("ARR_AI_GUARD_E2E") != "1")
{
Console.WriteLine(
" [跳过] 未设置 ARR_AI_GUARD_E2E=1,跳过真实推理保护实验");
return;
}
RunAsync().GetAwaiter().GetResult();
}
private static async Task RunAsync()
{
var settingsPath = OpenCodeGoFakeToolCallTests.FindSettingsPath();
var settings = JsonSerializer.Deserialize<AiSettings>(
File.ReadAllText(settingsPath, Encoding.UTF8))
?? throw new InvalidOperationException("AI 设置文件无法解析");
var provider = settings.Providers.FirstOrDefault(p =>
p.Name.Equals("OpenCodeGo", StringComparison.OrdinalIgnoreCase)
&& p.Models.Any(m =>
m.ModelId.Equals("deepseek-v4-flash", StringComparison.OrdinalIgnoreCase)))
?? throw new InvalidOperationException(
"当前配置中未找到 OpenCodeGo / deepseek-v4-flash 服务与模型");
var model = provider.Models.First(m =>
m.ModelId.Equals("deepseek-v4-flash", StringComparison.OrdinalIgnoreCase));
if (!model.IsStream)
{
Console.WriteLine(
" [跳过] OpenCodeGo/deepseek-v4-flash 当前配置为非流式模型,无法验证中断逻辑");
return;
}
if (string.IsNullOrWhiteSpace(provider.ApiKey))
{
throw new InvalidOperationException("OpenCodeGo 的 API Key 为空");
}
var guardModel = new AiModel
{
ModelId = model.ModelId,
DisplayName = model.DisplayName,
IsStream = model.IsStream,
ContextLength = model.ContextLength,
ContextBudget = model.ContextBudget,
ReasoningGuardEnabled = true,
ReasoningGuardTokenLimit = 1,
ExtraParameters = new Dictionary<string, object>(model.ExtraParameters)
};
var messages = ImmutableList<MainAIAnalyze.ChatMessage>.Empty
.Add(new MainAIAnalyze.ChatMessage(
"system",
"请先进行详细推理,再给出简短结论。"))
.Add(new MainAIAnalyze.ChatMessage(
"user",
"请逐条说明分析步骤,最后用一到两句话给出结论。"));
var analyzer = new MainAIAnalyze();
var result = await analyzer.CompleteAsync(
messages,
new AiRequestContext(provider, guardModel),
_ => { },
CancellationToken.None);
var report = new StringBuilder();
report.AppendLine("# AI 推理保护 E2E 报告");
report.AppendLine();
report.AppendLine($"- Provider{provider.Name}");
report.AppendLine($"- Model{guardModel.ModelId}");
report.AppendLine($"- ReasoningInterrupted{result.ReasoningInterrupted}");
report.AppendLine($"- ContinuationApplied{result.ContinuationApplied}");
report.AppendLine($"- Reasoning 长度:{result.Reasoning?.Length ?? 0}");
report.AppendLine($"- Response 长度:{result.Response.Length}");
report.AppendLine($"- FormatState{result.ReasoningFormatState ?? "()"}");
if (!string.IsNullOrWhiteSpace(result.ReasoningContinuationError))
{
report.AppendLine($"- Error{result.ReasoningContinuationError}");
}
var reportPath = Environment.GetEnvironmentVariable("ARR_AI_GUARD_REPORT_PATH");
if (!string.IsNullOrWhiteSpace(reportPath))
{
File.WriteAllText(reportPath, report.ToString(), new UTF8Encoding(false));
Console.WriteLine($" [E2E-GUARD] 报告已写入:{reportPath}");
}
Console.WriteLine(
$" [E2E-GUARD] interrupted={result.ReasoningInterrupted}; "
+ $"continued={result.ContinuationApplied}; "
+ $"reasoning={result.Reasoning?.Length ?? 0}; response={result.Response.Length}");
}
}
internal static class UserReplayFactIndexReproTests internal static class UserReplayFactIndexReproTests
{ {
public static void Run() public static void Run()
+8 -1
View File
@@ -6,6 +6,13 @@
- 版本:v2.2。WIP/CONTEXT/ADR 旧文档已删除;思维链回传实验单独记录在 `AI_REASONING_CONTINUATION_RESEARCH.md` - 版本:v2.2。WIP/CONTEXT/ADR 旧文档已删除;思维链回传实验单独记录在 `AI_REASONING_CONTINUATION_RESEARCH.md`
- 关联文档:[AI_REASONING_CONTINUATION_RESEARCH.md](AI_REASONING_CONTINUATION_RESEARCH.md) - 关联文档:[AI_REASONING_CONTINUATION_RESEARCH.md](AI_REASONING_CONTINUATION_RESEARCH.md)
## 实施状态修订(推理保护已移除)
- 实测表明:截断/修改 `reasoning_content` 并伪造 tool call 续写会影响输出质量,已取消该方案。
- 当前代码不再包含 `AiReasoningGuard``ReasoningGuardEnabled`/`ReasoningGuardTokenLimit` 设置、推理保护 UI、`ReasoningGuard` 测试或相关 E2E。
- 仍保留对模型返回的 `reasoning_content` 的流式展示与 usage 统计;但不会截断、改写、回传或让它触发额外请求。
- 解决长思考的策略改为:提高输出 token 上限 + 让模型专注更短的时间范围(段内焦点窗口),同时仍提供尽量长的切片上下文并鼓励跨时间关联。
## 实施状态修订(2026-08-24 段内焦点窗口) ## 实施状态修订(2026-08-24 段内焦点窗口)
- 新增「段内焦点窗口」设计:**数据层尽量宽**(整段机械切片按上下文上限提供),**注意力层聚焦窄时间窗**(每个切片再切分为若干分析窗口,每轮一个窗口作为重点)。 - 新增「段内焦点窗口」设计:**数据层尽量宽**(整段机械切片按上下文上限提供),**注意力层聚焦窄时间窗**(每个切片再切分为若干分析窗口,每轮一个窗口作为重点)。
@@ -62,7 +69,7 @@
- `AiV2.Tests` 仅保留 `OpenCodeGoFakeToolCallE2e` 专用测试:默认跳过,需同时设置 `ARR_AI_E2E=1``ARR_AI_E2E_TOOL=1` - `AiV2.Tests` 仅保留 `OpenCodeGoFakeToolCallE2e` 专用测试:默认跳过,需同时设置 `ARR_AI_E2E=1``ARR_AI_E2E_TOOL=1`
- 实验结果、推荐方案与数据表格见 `AI_REASONING_CONTINUATION_RESEARCH.md` - 实验结果、推荐方案与数据表格见 `AI_REASONING_CONTINUATION_RESEARCH.md`
### 2026-08-23 推理保护实现 ### 2026-08-23 推理保护实现(历史记录,后续已移除)
- 主项目已实现默认关闭、模型级配置的推理保护:累计 `reasoning_content` 达到阈值后中断流式响应,并通过研究验证的 tool call 载体请求一次续写。 - 主项目已实现默认关闭、模型级配置的推理保护:累计 `reasoning_content` 达到阈值后中断流式响应,并通过研究验证的 tool call 载体请求一次续写。
- 续写只在保留推理末尾依次追加 `[INTERNAL_REASONING_TRUNCATED]` 与中文收尾句,不插入中间 checkpoint;首请求不携带 `tools`,续写请求才注入工具定义和 `tool_choice=none` - 续写只在保留推理末尾依次追加 `[INTERNAL_REASONING_TRUNCATED]` 与中文收尾句,不插入中间 checkpoint;首请求不携带 `tools`,续写请求才注入工具定义和 `tool_choice=none`
+4 -281
View File
@@ -7,7 +7,6 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
@@ -26,48 +25,13 @@ namespace AnotherReplayReader.Utils
Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] string? Content = null, Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] string? Content = null,
[property: System.Text.Json.Serialization.JsonPropertyName("reasoning_content"), [property: System.Text.Json.Serialization.JsonPropertyName("reasoning_content"),
System.Text.Json.Serialization.JsonIgnore( System.Text.Json.Serialization.JsonIgnore(
Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] string? ReasoningContent = null, Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] string? ReasoningContent = null)
[property: System.Text.Json.Serialization.JsonPropertyName("tool_calls"),
System.Text.Json.Serialization.JsonIgnore(
Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] IReadOnlyList<ChatMessage.ToolCall>? ToolCalls = null,
[property: System.Text.Json.Serialization.JsonPropertyName("tool_call_id"),
System.Text.Json.Serialization.JsonIgnore(
Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] string? ToolCallId = null)
{ {
public sealed record ToolCall(
[property: System.Text.Json.Serialization.JsonPropertyName("id")] string Id,
[property: System.Text.Json.Serialization.JsonPropertyName("type")] string Type,
[property: System.Text.Json.Serialization.JsonPropertyName("function")] ToolCallFunction Function);
public sealed record ToolCallFunction(
[property: System.Text.Json.Serialization.JsonPropertyName("name")] string Name,
[property: System.Text.Json.Serialization.JsonPropertyName("arguments")] string Arguments);
public static ChatMessage Assistant(string? content, string? reasoningContent = null) => public static ChatMessage Assistant(string? content, string? reasoningContent = null) =>
new( new(
"assistant", "assistant",
string.IsNullOrEmpty(content) ? null : content, string.IsNullOrEmpty(content) ? null : content,
string.IsNullOrEmpty(reasoningContent) ? null : reasoningContent); string.IsNullOrEmpty(reasoningContent) ? null : reasoningContent);
public static ChatMessage AssistantToolCall(
string? reasoningContent,
string toolCallId,
string toolName,
string arguments) =>
new(
"assistant",
null,
string.IsNullOrEmpty(reasoningContent) ? null : reasoningContent,
new[]
{
new ToolCall(
toolCallId,
"function",
new ToolCallFunction(toolName, arguments))
});
public static ChatMessage ToolResult(string toolCallId, string content) =>
new("tool", content, null, null, toolCallId);
} }
public static string GetSystemPrompt( public static string GetSystemPrompt(
@@ -1277,8 +1241,6 @@ PlayerA: 开始出兵
public enum AIChunkType public enum AIChunkType
{ {
Reasoning, Reasoning,
ReasoningGuard,
ReasoningGuardRequest,
Content, Content,
Error, Error,
Json Json
@@ -1294,13 +1256,6 @@ PlayerA: 开始出兵
{ {
public string Response; public string Response;
public string Reasoning; public string Reasoning;
public string? ReasoningFormatState;
public bool ReasoningInterrupted;
public bool ContinuationApplied;
public string? ReasoningContinuationError;
public string? RequestJson;
public string? ReasoningOriginalRequestJson;
public string? ReasoningContinuationRequestJson;
public int? PromptTokens; public int? PromptTokens;
public int? CompletionTokens; public int? CompletionTokens;
public int? TotalTokens; public int? TotalTokens;
@@ -1328,227 +1283,20 @@ PlayerA: 开始出兵
AiRequestContext requestContext, AiRequestContext requestContext,
Action<AIChunk>? onChunk, Action<AIChunk>? onChunk,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{
var model = requestContext.Model;
var guardEnabled = model.ReasoningGuardEnabled && model.IsStream;
if (!guardEnabled)
{ {
return await Task.Run( return await Task.Run(
() => DoRequest(_http, messages, requestContext, onChunk, cancellationToken), () => DoRequest(_http, messages, requestContext, onChunk, cancellationToken),
cancellationToken); cancellationToken);
} }
var tokenLimit = AiReasoningGuard.GetEffectiveTokenLimit(
requestContext.Provider, model);
var firstResult = await Task.Run(
() => DoRequest(
_http,
messages,
requestContext,
onChunk,
cancellationToken,
tokenLimit),
cancellationToken);
if (!firstResult.ReasoningInterrupted)
{
return firstResult;
}
var continuationMessages = BuildReasoningContinuationMessages(
messages,
firstResult.Reasoning,
tokenLimit,
out var formatState);
firstResult.ReasoningFormatState = formatState;
var continuationContext = BuildReasoningToolContext(requestContext);
var originalRequestJson = firstResult.RequestJson;
var continuationRequestJson = SerializeRequestParams(
continuationMessages,
continuationContext,
writeIndented: true);
var continuationPromptTokens = 0;
foreach (var message in continuationMessages)
{
continuationPromptTokens += AiContextBudget.EstimateTokens(
(message.Content ?? string.Empty)
+ "\n"
+ (message.ReasoningContent ?? string.Empty));
}
var continuationCheck = AiContextBudget.CheckRequestUsage(
continuationPromptTokens,
requestContext.Provider,
continuationContext.Model);
var continuationBudgetWarning = string.Empty;
if (continuationCheck.Block)
{
var message = "续写请求未通过预算检查:" + continuationCheck.Message;
if (!string.IsNullOrEmpty(firstResult.Response))
{
firstResult.ReasoningContinuationError = message;
firstResult.ContinuationApplied = false;
firstResult.ReasoningOriginalRequestJson = originalRequestJson;
firstResult.ReasoningContinuationRequestJson =
continuationRequestJson;
return firstResult;
}
throw new InvalidOperationException(message);
}
if (!continuationCheck.IsOk)
{
continuationBudgetWarning = continuationCheck.Message;
}
onChunk?.Invoke(new AIChunk
{
Type = AIChunkType.ReasoningGuardRequest,
Text = JsonSerializer.Serialize(
new
{
originalRequestJson,
continuationRequestJson
},
new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
});
Result secondResult = default;
var continuationError = string.Empty;
try
{
secondResult = await Task.Run(
() => DoRequest(
_http,
continuationMessages,
continuationContext,
onChunk,
cancellationToken,
tokenLimit),
cancellationToken);
}
catch (Exception ex) when (
!cancellationToken.IsCancellationRequested
&& ex is not OperationCanceledException)
{
continuationError = ex.Message;
}
if (!string.IsNullOrEmpty(continuationError))
{
if (!string.IsNullOrEmpty(firstResult.Response))
{
firstResult.ReasoningContinuationError =
"续写请求失败,已保留首次部分正文:" + continuationError;
firstResult.ContinuationApplied = false;
firstResult.ReasoningOriginalRequestJson = originalRequestJson;
firstResult.ReasoningContinuationRequestJson =
continuationRequestJson;
return firstResult;
}
throw new InvalidOperationException(
"推理续写失败,且没有可用的部分回答:" + continuationError);
}
if (secondResult.ReasoningInterrupted)
{
if (!string.IsNullOrEmpty(secondResult.Response))
{
secondResult.ReasoningContinuationError =
"续写仍超过推理上限,已使用续写部分正文。";
secondResult.ContinuationApplied = false;
secondResult.ReasoningOriginalRequestJson = originalRequestJson;
secondResult.ReasoningContinuationRequestJson =
continuationRequestJson;
return secondResult;
}
if (!string.IsNullOrEmpty(firstResult.Response))
{
firstResult.ReasoningContinuationError =
"续写仍超过推理上限,已回退到首次部分正文。";
firstResult.ContinuationApplied = false;
firstResult.ReasoningOriginalRequestJson = originalRequestJson;
firstResult.ReasoningContinuationRequestJson =
continuationRequestJson;
return firstResult;
}
throw new InvalidOperationException(
"推理续写仍超过上限,且没有可用的部分回答。");
}
secondResult.ContinuationApplied = true;
secondResult.ReasoningOriginalRequestJson = originalRequestJson;
secondResult.ReasoningContinuationRequestJson =
continuationRequestJson;
if (!string.IsNullOrEmpty(continuationBudgetWarning))
{
secondResult.ReasoningContinuationError = continuationBudgetWarning;
}
return secondResult;
}
private static ImmutableList<ChatMessage> BuildReasoningContinuationMessages(
ImmutableList<ChatMessage> messages,
string reasoning,
int tokenLimit,
out string formatState)
{
var prepared = AiReasoningGuard.PrepareReasoning(reasoning, tokenLimit);
formatState = prepared.FormatState;
var instruction = AiReasoningGuard.BuildToolInstruction();
var arguments = JsonSerializer.Serialize(new { instruction });
return messages
.Add(ChatMessage.AssistantToolCall(
prepared.Text,
AiReasoningGuard.ToolCallId,
AiReasoningGuard.ToolName,
arguments))
.Add(ChatMessage.ToolResult(
AiReasoningGuard.ToolCallId,
AiReasoningGuard.BuildToolResult()));
}
private static AiRequestContext BuildReasoningToolContext(
AiRequestContext context)
{
var model = new AiModel
{
ModelId = context.Model.ModelId,
DisplayName = context.Model.DisplayName,
IsStream = context.Model.IsStream,
ContextLength = context.Model.ContextLength,
ContextBudget = context.Model.ContextBudget,
ReasoningGuardEnabled = context.Model.ReasoningGuardEnabled,
ReasoningGuardTokenLimit = context.Model.ReasoningGuardTokenLimit,
ExtraParameters = new Dictionary<string, object>(
context.Model.ExtraParameters)
};
model.ExtraParameters["tools"] =
new[] { AiReasoningGuard.BuildToolDefinition() };
model.ExtraParameters["tool_choice"] = "none";
return new AiRequestContext(context.Provider, model);
}
private static string SerializeRequestParams( private static string SerializeRequestParams(
ImmutableList<ChatMessage> messages, ImmutableList<ChatMessage> messages,
AiRequestContext context, AiRequestContext context)
bool writeIndented = false)
{ {
var requestParams = ProcessRequestParams( var requestParams = ProcessRequestParams(
messages, messages,
context.BuildRequestParams()); context.BuildRequestParams());
return JsonSerializer.Serialize( return JsonSerializer.Serialize(requestParams);
requestParams,
writeIndented
? new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}
: null);
} }
private static async Task<Result> DoRequest( private static async Task<Result> DoRequest(
@@ -1556,8 +1304,7 @@ PlayerA: 开始出兵
ImmutableList<ChatMessage> messages, ImmutableList<ChatMessage> messages,
AiRequestContext requestContext, AiRequestContext requestContext,
Action<AIChunk>? onChunk, Action<AIChunk>? onChunk,
CancellationToken cancellationToken, CancellationToken cancellationToken)
int? reasoningTokenLimit = null)
{ {
var provider = requestContext.Provider; var provider = requestContext.Provider;
var isStream = requestContext.Model.IsStream; var isStream = requestContext.Model.IsStream;
@@ -1579,8 +1326,6 @@ PlayerA: 开始出兵
var fullBuilder = new StringBuilder(); var fullBuilder = new StringBuilder();
var reasoningBuilder = new StringBuilder(); var reasoningBuilder = new StringBuilder();
var result = new Result(); var result = new Result();
result.RequestJson = inputJson;
var interrupted = false;
// 根据模式分别读取响应 // 根据模式分别读取响应
// if response.Content.Headers.ContentType is "text/event-stream", then it's stream mode, otherwise it's non-stream mode // if response.Content.Headers.ContentType is "text/event-stream", then it's stream mode, otherwise it's non-stream mode
@@ -1625,20 +1370,6 @@ PlayerA: 开始出兵
reasoningBuilder, reasoningBuilder,
result, result,
onChunk); onChunk);
if (reasoningTokenLimit is { } guardLimit
&& guardLimit > 0
&& AiContextBudget.EstimateTokens(reasoningBuilder.ToString())
>= guardLimit)
{
interrupted = true;
onChunk?.Invoke(new AIChunk
{
Type = AIChunkType.ReasoningGuard,
Text = "检测到推理内容超过保护阈值,正在请求 AI 尽快收尾..."
});
break;
}
} }
} }
else else
@@ -1656,14 +1387,6 @@ PlayerA: 开始出兵
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
result.Reasoning = reasoningBuilder.ToString(); result.Reasoning = reasoningBuilder.ToString();
result.ReasoningFormatState = AiReasoningGuard.DescribeFormatState(
result.Reasoning);
if (interrupted)
{
result.Response = fullBuilder.ToString();
result.ReasoningInterrupted = true;
return result;
}
if (fullBuilder.Length == 0) if (fullBuilder.Length == 0)
{ {
-181
View File
@@ -1,181 +0,0 @@
using System;
using System.Collections.Generic;
namespace AnotherReplayReader.Utils
{
/// <summary>
/// 推理保护策略:截断 reasoning_content,并通过研究验证过的 tool call 历史
/// 让模型从已有推理自然收尾。
/// </summary>
internal static class AiReasoningGuard
{
public const string ToolName = "analysis_hint";
public const string TruncationMarker = "[INTERNAL_REASONING_TRUNCATED]";
public const string WrapUpStatement =
"我已经整理出足够的信息,现在立即结束内部推理,直接输出最终回答。";
public const string ToolCallId = "call_reasoning_guard";
public const int MinTokenLimit = 4096;
public static int GetEffectiveTokenLimit(AiProvider provider, AiModel model)
{
if (model.ReasoningGuardTokenLimit is { } explicitLimit && explicitLimit > 0)
{
return explicitLimit;
}
return Math.Max(MinTokenLimit, provider.DefaultMaxTokens / 2);
}
public static string BuildToolInstruction()
{
return "请继续你本次 tool_calls 消息中携带的 reasoning_content。"
+ "你已经完成了足够的分析,现在进入收尾阶段:"
+ "立即停止继续展开推理,不要再逐条枚举输入事件,不要再做新的检查,也不要继续“再确认一下”。"
+ "请直接输出最终结果,包括正文、[机器可读声明] 和 [小结]。"
+ "如果某些细节不确定,就用证据等级说明。";
}
public static string BuildToolResult()
{
return "工具结果:" + BuildToolInstruction()
+ "\n你本次 tool_calls 消息中携带的 reasoning_content 的最后一句已经宣告收尾,"
+ "请立即执行,不要继续讨论是否还需要分析。"
+ "\n这份推理内容末尾有内部标记 " + TruncationMarker
+ ",表示前面的推理已被安全截断。"
+ "\n如果旧推理中存在未完成的 JSON、JSON 代码块或其他结构性内容,"
+ "请丢弃其未完成部分,并重新输出完整、格式正确的结果。"
+ "\n不要复述该标记。";
}
public static object BuildToolDefinition()
{
return new
{
type = "function",
function = new
{
name = ToolName,
description = "为当前内部推理提供收尾提示。",
parameters = new
{
type = "object",
properties = new
{
instruction = new
{
type = "string",
description = "模型应遵循的收尾提示。"
}
},
required = new[] { "instruction" }
}
}
};
}
/// <summary>
/// 保留尽可能多的推理前缀,并在末尾追加截断标记与收尾决定。
/// </summary>
public static (string Text, string FormatState) PrepareReasoning(
string reasoning,
int tokenLimit)
{
if (string.IsNullOrEmpty(reasoning))
{
return ("\n\n" + TruncationMarker + "\n\n" + WrapUpStatement, "无格式问题");
}
var maxIndex = FindMaxPrefixIndexByTokens(reasoning, tokenLimit);
var boundary = FindSafeBoundary(reasoning, maxIndex);
var prefix = reasoning.Substring(0, boundary).TrimEnd();
var formatState = DescribeFormatState(prefix);
return (
prefix + "\n\n" + TruncationMarker + "\n\n" + WrapUpStatement,
formatState);
}
public static string DescribeFormatState(string text)
{
var lines = new List<string>();
var codeFenceCount = CountOccurrences(text, "```");
if (codeFenceCount % 2 == 1)
{
lines.Add("代码围栏未闭合");
}
var braceDelta = CountDifference(text, '{', '}');
var bracketDelta = CountDifference(text, '[', ']');
if (braceDelta > 0 || bracketDelta > 0)
{
lines.Add("JSON/方括号结构可能未闭合");
}
return lines.Count == 0 ? "无格式问题" : string.Join("", lines);
}
private static int FindMaxPrefixIndexByTokens(string text, int tokenLimit)
{
var low = 0;
var high = text.Length;
while (low < high)
{
var mid = (low + high + 1) / 2;
if (AiContextBudget.EstimateTokens(text.Substring(0, mid)) <= tokenLimit)
{
low = mid;
}
else
{
high = mid - 1;
}
}
return low;
}
private static int FindSafeBoundary(string text, int maxIndex)
{
const int searchBack = 160;
var start = Math.Max(0, maxIndex - searchBack);
for (var i = maxIndex - 1; i >= start; --i)
{
var c = text[i];
if (c == '\n' || c == '。' || c == '' || c == ''
|| c == ':' || c == ';' || c == '.'
|| c == ',' || c == ')' || c == '}' || c == ']')
{
return i + 1;
}
}
return maxIndex;
}
private static int CountOccurrences(string text, string value)
{
var count = 0;
var index = 0;
while ((index = text.IndexOf(value, index, StringComparison.Ordinal)) >= 0)
{
count++;
index += value.Length;
}
return count;
}
private static int CountDifference(string text, char open, char close)
{
var delta = 0;
foreach (var c in text)
{
if (c == open)
{
delta++;
}
else if (c == close)
{
delta--;
}
}
return delta;
}
}
}
-11
View File
@@ -48,17 +48,6 @@ namespace AnotherReplayReader
/// </summary> /// </summary>
public int? ContextBudget { get; set; } public int? ContextBudget { get; set; }
/// <summary>
/// 推理保护:累计 reasoning_content 达到阈值时中断流式响应并请求一次续写。
/// 默认关闭。
/// </summary>
public bool ReasoningGuardEnabled { get; set; }
/// <summary>
/// 推理保护的 token 阈值。null 或 0 使用派生值 max(4096, provider.DefaultMaxTokens / 2)。
/// </summary>
public int? ReasoningGuardTokenLimit { get; set; }
public Dictionary<string, object> ExtraParameters { get; set; } = []; public Dictionary<string, object> ExtraParameters { get; set; } = [];
/// <summary> /// <summary>