182 lines
6.6 KiB
C#
182 lines
6.6 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|