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

发布时间:2026/9/10 18:34:05

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),仅供参考
延伸阅读

更多相关文章

2026/9/10 18:34:05

用Codex+Relay实现移动端全栈开发:从原型到交付的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 18:34:05

大模型API接入实战:GLM-5.3-Flash-Guan性能、成本与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 18:34:05

西门子S7-1200 PLC灌装机自动化系统设计与优化

1. 灌装机自动化系统概述 这套基于西门子S7-1200 PLC和KTP1200触摸屏的灌装机控制系统,是典型的工业自动化解决方案。我在食品饮料行业实施过类似项目,这类系统通常需要处理每分钟60-120瓶的高速灌装,灌装精度要求控制在1%以内。博图V16作为当…

2026/9/10 19:19:10

本科毕业论文怎么写选哪个?三款工具真实对比

毕业论文的焦虑往往从开题就开始了。导师催着交初稿,图书馆的参考书翻了几本,文档里却只有两百字。白天上课晚上实习,留给论文的时间被挤压得所剩无几。这种时候,论文辅助工具就成了不少人的救命稻草。市面上叫得出名字的产品不少…

2026/9/10 19:19:10

CANN/ge KV缓存块推送

PushKvBlocks 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前…

2026/9/10 19:19:09

本科毕业论文怎么写?AI辅助全流程清单

毕业论文这件事,多数人第一次碰。选题、开题、文献、初稿、降重、排版,六个环节各有各的坑。本文按真实写作流程整理一份AI工具清单,把每个阶段能用的手段说清楚,aibiye、aicheck、passbug这三款会重点展开,另有几款工…

2026/9/10 19:19:09

伺服电机调试六大方法:从新手预设到频响分析的系统级实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 16:39:38

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/10 11:16:38

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/10 12:32:02

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/10 15:19:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/10 15:49:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码