ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Semantic Kernel 中的 JSON 可序列化自定义类型:从 TypeConverter 到 System.Text.Json 的演进决策解析

Semantic Kernel 中的 JSON 可序列化自定义类型:从 TypeConverter 到 System.Text.Json 的演进决策解析 Semantic Kernel 中的 JSON 可序列化自定义类型从 TypeConverter 到 System.Text.Json 的演进决策解析【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel导读本文以仓库中的架构决策记录 0021-json-serializable-custom-types.md 为骨架深入剖析 .NET 版 Semantic Kernel 中自定义类型如何跨越「原生函数Native Function」与「语义函数Semantic Function」之间的边界进行序列化与反序列化。文章覆盖 ADR 提出的核心问题、TypeConverter 现状、基于System.Text.Json的回退方案设计、四种变量传递场景的序列化时机以及当前仓库源码中的实际落地形态读完即可掌握自定义类型接入 Semantic Kernel 的完整技术原理与实操模式。一、背景与问题为什么需要 JSON 可序列化的自定义类型在 Semantic Kernel 中Kernel Function 的输入输出本质上都是「字符串」。当开发者使用自定义的复杂类型如一个同时包含Number与Text两个属性的类作为函数参数或返回值时系统必须知道如何把这个对象转换成字符串交给 LLM以及如何把 LLM 返回的字符串还原成对象。在 ADR 提出之前2023-11-06使用自定义类型的唯一途径是为每个类型手写一个TypeConverter即同时实现ConvertFrom字符串 → 对象与ConvertTo对象 → 字符串两个方向。该 ADR 的核心诉求在于简化自定义类型的使用允许开发者使用任何能用System.Text.Json序列化的类型而无需额外编写转换器。该诉求并非单纯为了省代码而是有一个更重要的驱动因素标准化 JSON 序列化类型后函数的手册function manual就可以用 JSON Schema 描述函数的输入与输出类型从而让 planner 在调用函数前校验「参数类型是否正确、返回值能否被下游正确消费」。JSON Schema 无法理解任意自定义格式因此将「可序列化」收敛到System.Text.Json这一个标准上是让函数自描述成为可能的前提。二、现状基线手写 TypeConverter 的代价ADR 原文明确指出在方案落地前自定义类型必须显式标注[TypeConverter(typeof(...))]并实现完整的转换逻辑。该模式在仓库示例 MethodFunctions_Advanced.cs 中完整保留[TypeConverter(typeof(MyCustomTypeConverter))] private sealed class MyCustomType { public int Number { get; set; } public string? Text { get; set; } } private sealed class MyCustomTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) true; public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return JsonSerializer.DeserializeMyCustomType((string)value); } public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { return JsonSerializer.Serialize(value); } }该示例的注释还揭示了一个关键设计取向TypeConverter的作用是「把复杂对象表示为有意义的字符串以便传给 AI 进一步处理」并且转换格式并不强制为 JSON——开发者完全可以选用 XML、YAML 等任意格式来表达对象。这意味着 TypeConverter 机制本身就是一种可插拔的序列化策略。这种做法的缺点也很明显每个自定义类型都要多写一个转换器类样板代码多且类型一旦增多便难以维护——这正是 ADR 要解决的问题。三、方案一选定方案回退到 System.Text.Json 序列化ADR 选定的方案是对GetTypeConverter()方法的查找逻辑做「三级回退」改造原生类型Primitive继续使用 .NET 自带的TypeConverter如Int32Converter、DoubleConverter、DateTimeConverter等避免转换失真显式注册了 TypeConverter 的复杂类型继续使用其注册的转换器尊重开发者的自定义逻辑没有任何 TypeConverter 的复杂类型回退到框架内置的JsonSerializationTypeConverter直接尝试用System.Text.Json完成序列化/反序列化如果该类型无法被 JSON 序列化则抛出带详细说明的错误信息。ADR 中给出了改造后的GetTypeConverter()示意实现原文档中位于NativeFunction.csprivate static TypeConverter GetTypeConverter(Type targetType) { if (targetType typeof(byte)) { return new ByteConverter(); } if (targetType typeof(sbyte)) { return new SByteConverter(); } if (targetType typeof(bool)) { return new BooleanConverter(); } if (targetType typeof(ushort)) { return new UInt16Converter(); } if (targetType typeof(short)) { return new Int16Converter(); } if (targetType typeof(char)) { return new CharConverter(); } if (targetType typeof(uint)) { return new UInt32Converter(); } if (targetType typeof(int)) { return new Int32Converter(); } if (targetType typeof(ulong)) { return new UInt64Converter(); } if (targetType typeof(long)) { return new Int64Converter(); } if (targetType typeof(float)) { return new SingleConverter(); } if (targetType typeof(double)) { return new DoubleConverter(); } if (targetType typeof(decimal)) { return new DecimalConverter(); } if (targetType typeof(TimeSpan)) { return new TimeSpanConverter(); } if (targetType typeof(DateTime)) { return new DateTimeConverter(); } if (targetType typeof(DateTimeOffset)) { return new DateTimeOffsetConverter(); } if (targetType typeof(Uri)) { return new UriTypeConverter(); } if (targetType typeof(Guid)) { return new GuidConverter(); } if (targetType.GetCustomAttributeTypeConverterAttribute() is TypeConverterAttribute tca Type.GetType(tca.ConverterTypeName, throwOnError: false) is Type converterType Activator.CreateInstance(converterType) is TypeConverter converter) { return converter; } // 与改造前相比这里不再返回 null而是返回一个基于 JSON 序列化的 TypeConverter return new JsonSerializationTypeConverter(); } private sealed class JsonSerializationTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) true; public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return JsonSerializer.Deserializeobject((string)value); } public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { return JsonSerializer.Serialize(value); } }核心变化一目了然从「找不到转换器就返回 null失败」变成「找不到转换器就默认走 JSON 序列化兜底」。注意CanConvertFrom恒返回true表示该兜底转换器接受任意来源的转换请求实际成败由System.Text.Json在运行时决定。需要特别说明的是该方案保留了 primitive 类型的原生 TypeConverter而不是统统交给 JSON——ADR 明确强调这是为了防止「有损转换」lossy conversions例如float与int之间互相转换时可能发生精度截断原生转换器的行为更符合 .NET 语义预期。四、序列化时机的矩阵分析四种传递场景一个容易混淆的问题是自定义类型到底什么时候需要序列化ADR 用一张二维矩阵给出了精确答案维度是「来源函数类型」与「目标函数类型」传递方向是否需要序列化/反序列化原因Native → Semantic需要序列化Native Function 输出的复杂类型必须转为字符串才能作为提示词输入传给 LLMSemantic → Native需要反序列化Semantic Function 输出的字符串必须还原为 Native Function 期望的复杂类型Native → Native不需要复杂类型对象可以原样传递无需任何转换Semantic → Semantic不需要复杂类型始终以字符串表示形式在两者之间传递这四象限清晰地界定了序列化工作的边界转换只发生在「函数/LLM 边界」上。语义函数Semantic侧的一切都以字符串形式存在原生函数Native侧的一切都以强类型对象存在只有跨越这两个世界时才需要 JSON 桥接。这也是为什么「只引入一种 JSON 序列化约定」就足以覆盖绝大多数场景——它恰好只出现在那条唯一的边界上。五、备选方案对比为什么不彻底抛弃 TypeConverterADR 还记录了一个被否决的备选方案方案二仅使用原生序列化方法即直接用一个简单的JsonConverter取代所有TypeConverter。该方案虽然更彻底、代码更简洁但被否决的原因在于primitive 类型的转换准确性如果所有类型都走System.Text.Json那么像float转int这类数值类型间的转换可能因原生序列化行为而产生不精确的截断结果。换句话说方案二「为了统一而牺牲了基础类型的精度保证」而方案一则通过「primitive 保留原生转换器 复杂类型回退 JSON」的分层策略在统一性与准确性之间取得了平衡。从决策过程可以提炼出两层设计原则默认值要足够聪明兜底策略JSON 回退让 80% 的常规类型零成本接入特例要足够保守对精度敏感的 primitive 类型绝不轻易更换其成熟的转换路径。六、决策的仓库落地从 ADR 草案到当前源码的演进ADR 状态为proposed提议而当前仓库的实现已经走过了后续演进从源码结构看方案的骨架被保留、实现位置与细节发生了变化。以下是可以在当前仓库中验证的落地事实6.1 转换器查找逻辑TypeConverterFactoryADR 中展示的GetTypeConverter()逻辑在当前仓库中已迁移至独立的内部工具类 TypeConverterFactory.cs。其查找顺序与 ADR 高度一致硬编码的 primitive 类型转换器StringConverter、ByteConverter、BooleanConverter、数值类型、DateTime、DateTimeOffset、TimeSpan、Uri、Guid枚举类型通过CreateEnumConverter(type)动态创建显式标注[TypeConverter]的类型通过反射Activator.CreateInstance实例化注册的转换器最后返回null表示未找到专用转换器。一个值得注意的差异该文件的注释明确解释了为什么不用TypeDescriptor.GetConverter——因为它对 AOTAhead-Of-Time编译不友好可能在裁剪trimming场景下引入运行时缺失的功能。这正是「用硬编码的已知类型集合 显式属性支持」替代全局类型描述查找的根本原因与 ADR 中「保持转换准确性」的保守取向一脉相承。6.2 值反序列化KernelFunctionFromMethod 中的 TryToDeserializeValueJSON 反序列化兜底的实际执行位置当前位于 KernelFunctionFromMethod.cs 的TryToDeserializeValue方法中。它对输入值按类型分派deserializedValue value switch { JsonDocument document document.Deserialize(targetType, jsonSerializerOptions), JsonNode node node.Deserialize(targetType, jsonSerializerOptions), JsonElement element element.Deserialize(targetType, jsonSerializerOptions), // 其他库如 Newtonsoft.Json 的 JObject/JToken/JValue先 ToString 再反序列化 _ JsonSerializer.Deserialize(value.ToString()!, targetType, jsonSerializerOptions) };这里还包含一个实践层面的细节对Newtonsoft.Json等第三方 JSON 库的类型JObject、JToken、JValue代码注释特意警告不要直接JsonSerializer.Serialize而是先调用ToString()再反序列化——因为直接序列化JObject可能产生出乎意料的输出例如{ id: 28 }会被序列化成{ Id: [] }导致Int32反序列化失败。这提醒我们跨 JSON 库的互操作存在隐式陷阱统一先转字符串是最稳妥的路径。同时方法上标注了[RequiresUnreferencedCode]与[RequiresDynamicCode]特性明确告知当没有通过JsonSerializerOptions提供源生成source-generated元数据时反射反序列化与 AOT 场景不兼容——这与 6.1 中 TypeConverterFactory 对 AOT 的顾虑形成了呼应说明该框架在「运行时反射便利」与「AOT 兼容」之间做了明确的取舍声明。6.3 字符串化入口InternalTypeConverter在 InternalTypeConverter.cs 中ConvertToString方法展示了「对象 → 字符串」的完整链路先尝试TypeConverterFactory.GetTypeConverter(sourceType)获取转换器若转换器存在且CanConvertTo(typeof(string))则调用ConvertToString完成字符串化。这印证了 ADR 中「复杂类型在原生世界以对象存在、在语义世界以字符串存在」的边界设计字符串化统一收敛到 TypeConverter 机制而 TypeConverter 的默认兜底行为则由 6.2 的 JSON 反序列化路径在另一端承接。6.4 测试覆盖仓库中存在针对该机制的单元测试 InternalTypeConverterTests.cs从测试文件命名可以推断对象与字符串之间的双向转换行为含 primitive 类型、自定义类型、枚举等分支均被纳入回归保障范围开发者修改转换逻辑时不必担心破坏既有行为。七、实践指南开发者应该怎么用综合 ADR 的决策与当前仓库的源码形态实际开发中遵循以下分层策略即可首选让自定义类型天然 JSON 可序列化。只要类型的属性可以被System.Text.Json处理公开属性、可空引用、基本集合等无需编写任何转换器跨 Native/Semantic 边界时由 JSON 兜底机制自动完成转换。需要特殊表示时显式注册[TypeConverter]。当类型需要以非 JSON 的特定字符串格式呈现给 LLM例如紧凑的日期格式、加密串、特定 DSL继续实现自定义TypeConverter框架会优先采用注册的转换器。尊重 primitive 语义数值、时间、Guid等基础类型不要自行包装成自定义类型绕道 JSON直接使用原生类型可避免精度损失并享受框架内置转换器的优化路径。注意边界时机只在 Native → Semantic序列化与 Semantic → Native反序列化两条路径上关心字符串格式Native → Native 与 Semantic → Semantic 场景不需要任何转换代码。留意 AOT 限制若目标运行环境是 AOT 裁剪场景务必通过JsonSerializerOptions提供源生成的序列化上下文否则反射路径会被裁剪或抛出RequiresDynamicCode相关警告。跨 JSON 库互操作若上游数据来自Newtonsoft.Json等库的JObject/JToken交给 Kernel 前先规范化为字符串或JsonElement避免直接序列化第三方 JSON 节点导致的异常反序列化结果。结语0021-json-serializable-custom-types这份 ADR 解决的不是一个孤立的小问题而是 Semantic Kernel 函数体系可自描述化的基石当自定义类型统一收敛到System.Text.Json序列化标准后函数手册才能用 JSON Schema 描述输入输出planner 才能据此做类型级校验。从 ADR 草案到 TypeConverterFactory.cs、KernelFunctionFromMethod.cs 与 MethodFunctions_Advanced.cs 的演进脉络中可以看到一个「看似简单」的默认值设计背后是对精度、AOT 兼容性、第三方库互操作与开发者体验的多重权衡——这正是架构决策记录之于开源项目的价值所在。【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表