Files
AnotherReplayReader/Utils/AiReasoningGuard.cs
T
2026-08-23 22:34:30 +02:00

182 lines
6.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}