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; } } }