强 schema 清洗:所有工具定义在生成时统一清洗,补齐 properties、规整 type、强制 additionalProperties=false,避免 schema 漏洞导致模型随意输出。AITool.cs、ToolSchemaSanitizer.cs
预执行拦截(Claude Code 风格):工具执行前校验 JSON、必填字段、类型、未知字段;失败则返回错误 ToolResult 反馈给模型,允许自修正或降级回复。ToolCallValidator.cs、AIIntelligenceCore.cs 重试清洗:retry_tools 解析前剥离 json 围栏,避免误判。AIIntelligenceCore.cs 工具选择:Query 阶段强制 tool_choice: "required",Action 维持 auto。AIIntelligenceCore.cs
This commit is contained in:
Binary file not shown.
@@ -1909,21 +1909,33 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
try
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
try
|
||||
|
||||
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
var historyManager = Find.World?.GetComponent<AIHistoryManager>();
|
||||
|
||||
|
||||
|
||||
historyManager?.SaveHistory(_activeEventDefName, _history);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
catch (Exception ex)
|
||||
|
||||
|
||||
@@ -4431,7 +4443,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
if (string.IsNullOrWhiteSpace(cleaned))
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
|
||||
|
||||
@@ -4521,7 +4533,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5081,6 +5093,20 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
|
||||
|
||||
continue;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
sb.AppendLine($"{role}: {message}");
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -5863,3 +5889,11 @@ private async Task<PhaseExecutionResult> ExecuteJsonToolsForPhase(string json, R
|
||||
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
|
||||
|
||||
sb.Append("{\"facts\":[");
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public virtual Dictionary<string, object> GetFunctionDefinition()
|
||||
{
|
||||
var parameters = GetParametersSchema() ?? SchemaObject(new Dictionary<string, object>(), new string[] { });
|
||||
parameters = ToolSchemaSanitizer.Sanitize(parameters);
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "function",
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using WulaFallenEmpire.EventSystem.AI.Tools;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Utils
|
||||
{
|
||||
public static class ToolCallValidator
|
||||
{
|
||||
public static bool TryValidate(AITool tool, string argsJson, out Dictionary<string, object> args, out string error)
|
||||
{
|
||||
args = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
error = null;
|
||||
if (tool == null)
|
||||
{
|
||||
error = "Error: Tool not found.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(argsJson)) argsJson = "{}";
|
||||
if (!JsonToolCallParser.TryParseObject(argsJson, out var parsed))
|
||||
{
|
||||
error = $"Error: Invalid JSON arguments for tool '{tool.Name}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
args = parsed ?? args;
|
||||
var schema = ToolSchemaSanitizer.Sanitize(tool.GetParametersSchema());
|
||||
if (!TryValidateObject(args, schema, out error))
|
||||
{
|
||||
error = $"Error: Tool '{tool.Name}' arguments failed validation. {error}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryValidateObject(Dictionary<string, object> args, Dictionary<string, object> schema, out string error)
|
||||
{
|
||||
error = null;
|
||||
if (schema == null) return true;
|
||||
|
||||
if (schema.TryGetValue("properties", out object propsObj) && propsObj is Dictionary<string, object> props)
|
||||
{
|
||||
if (schema.TryGetValue("required", out object requiredObj) && requiredObj is List<object> requiredList)
|
||||
{
|
||||
foreach (var req in requiredList)
|
||||
{
|
||||
string name = req as string;
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
if (!args.ContainsKey(name))
|
||||
{
|
||||
error = $"Missing required field '{name}'.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.TryGetValue("additionalProperties", out object additionalObj)
|
||||
&& additionalObj is bool allowAdditional && !allowAdditional)
|
||||
{
|
||||
foreach (var key in args.Keys)
|
||||
{
|
||||
if (!props.ContainsKey(key))
|
||||
{
|
||||
error = $"Unexpected field '{key}'.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in args)
|
||||
{
|
||||
if (!props.TryGetValue(entry.Key, out object propSchemaObj)) continue;
|
||||
if (propSchemaObj is not Dictionary<string, object> propSchema) continue;
|
||||
if (!TryValidateValue(entry.Value, propSchema, out error))
|
||||
{
|
||||
error = $"Field '{entry.Key}' is invalid. {error}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryValidateValue(object value, Dictionary<string, object> schema, out string error)
|
||||
{
|
||||
error = null;
|
||||
if (schema == null) return true;
|
||||
|
||||
string type = NormalizeType(schema);
|
||||
if (string.IsNullOrWhiteSpace(type)) return true;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
error = "Value must not be null.";
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "string":
|
||||
if (value is string) return true;
|
||||
error = "Expected string.";
|
||||
return false;
|
||||
case "boolean":
|
||||
if (value is bool) return true;
|
||||
if (value is string s && bool.TryParse(s, out _)) return true;
|
||||
if (IsNumber(value)) return true;
|
||||
error = "Expected boolean.";
|
||||
return false;
|
||||
case "number":
|
||||
if (IsNumber(value)) return true;
|
||||
if (value is string n && double.TryParse(n, NumberStyles.Float, CultureInfo.InvariantCulture, out _)) return true;
|
||||
error = "Expected number.";
|
||||
return false;
|
||||
case "integer":
|
||||
if (value is int || value is long) return true;
|
||||
if (value is string i && long.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) return true;
|
||||
error = "Expected integer.";
|
||||
return false;
|
||||
case "object":
|
||||
if (value is Dictionary<string, object> obj)
|
||||
{
|
||||
return TryValidateObject(obj, schema, out error);
|
||||
}
|
||||
error = "Expected object.";
|
||||
return false;
|
||||
case "array":
|
||||
if (value is List<object> list)
|
||||
{
|
||||
if (schema.TryGetValue("items", out object itemsObj) && itemsObj is Dictionary<string, object> itemSchema)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (!TryValidateValue(item, itemSchema, out error)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
error = "Expected array.";
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeType(Dictionary<string, object> schema)
|
||||
{
|
||||
if (!schema.TryGetValue("type", out object typeObj) || typeObj == null) return null;
|
||||
if (typeObj is string s) return s;
|
||||
if (typeObj is List<object> list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is string candidate && !string.Equals(candidate, "null", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsNumber(object value)
|
||||
{
|
||||
return value is int || value is long || value is float || value is double || value is decimal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Utils
|
||||
{
|
||||
public static class ToolSchemaSanitizer
|
||||
{
|
||||
public static Dictionary<string, object> Sanitize(Dictionary<string, object> schema)
|
||||
{
|
||||
if (schema == null) return new Dictionary<string, object>();
|
||||
|
||||
string schemaType = NormalizeType(schema);
|
||||
if (string.IsNullOrWhiteSpace(schemaType))
|
||||
{
|
||||
schemaType = "object";
|
||||
schema["type"] = schemaType;
|
||||
}
|
||||
|
||||
if (string.Equals(schemaType, "object", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!TryGetDict(schema, "properties", out var props))
|
||||
{
|
||||
props = new Dictionary<string, object>();
|
||||
schema["properties"] = props;
|
||||
}
|
||||
|
||||
var sanitizedProps = new Dictionary<string, object>();
|
||||
foreach (var entry in props)
|
||||
{
|
||||
if (entry.Value is Dictionary<string, object> child)
|
||||
{
|
||||
sanitizedProps[entry.Key] = Sanitize(child);
|
||||
}
|
||||
else
|
||||
{
|
||||
sanitizedProps[entry.Key] = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
}
|
||||
}
|
||||
schema["properties"] = sanitizedProps;
|
||||
|
||||
if (!schema.ContainsKey("additionalProperties"))
|
||||
{
|
||||
schema["additionalProperties"] = false;
|
||||
}
|
||||
|
||||
if (schema.TryGetValue("required", out object requiredRaw) && requiredRaw is List<object> requiredList)
|
||||
{
|
||||
var filtered = new List<object>();
|
||||
foreach (var item in requiredList)
|
||||
{
|
||||
string name = item as string;
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
if (sanitizedProps.ContainsKey(name))
|
||||
{
|
||||
filtered.Add(name);
|
||||
}
|
||||
}
|
||||
schema["required"] = filtered;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(schemaType, "array", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (schema.TryGetValue("items", out object itemsObj) && itemsObj is Dictionary<string, object> itemSchema)
|
||||
{
|
||||
schema["items"] = Sanitize(itemSchema);
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
private static string NormalizeType(Dictionary<string, object> schema)
|
||||
{
|
||||
if (!schema.TryGetValue("type", out object typeObj) || typeObj == null) return null;
|
||||
if (typeObj is string s) return s;
|
||||
if (typeObj is List<object> list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is string candidate && !string.Equals(candidate, "null", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
schema["type"] = candidate;
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryGetDict(Dictionary<string, object> root, string key, out Dictionary<string, object> value)
|
||||
{
|
||||
value = null;
|
||||
if (root == null || string.IsNullOrWhiteSpace(key)) return false;
|
||||
if (root.TryGetValue(key, out object raw) && raw is Dictionary<string, object> dict)
|
||||
{
|
||||
value = dict;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user