using System;
using System.Collections.Generic;
namespace AnotherReplayReader.Utils
{
///
/// 推理保护策略:截断 reasoning_content,并通过研究验证过的 tool call 历史
/// 让模型从已有推理自然收尾。
///
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" }
}
}
};
}
///
/// 保留尽可能多的推理前缀,并在末尾追加截断标记与收尾决定。
///
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();
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;
}
}
}