Semantic Kernel OpenAPI 函数 Payload 处理机制全解析:从静态传递到动态构建(ADR 0062)
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
本文基于 Semantic Kernel 仓库中的架构决策记录(ADR)0062-open-api-payload.md,系统梳理 .NET 版 Semantic Kernel 中 OpenAPI 函数请求体(Payload)的处理方式:调用方手动构造、基于叶子属性动态构造、基于命名空间(Namespace)的叶子属性构造,以及被评审后否决的"基于根属性构造"方案。读完本文,你将掌握EnableDynamicPayload与EnablePayloadNamespacing两个核心开关的行为边界、适用场景与限制,并能结合仓库源码理解其底层实现,在实际项目中正确选择 Payload 处理策略。
背景:为什么需要专门讨论 OpenAPI 函数的 Payload
在 Semantic Kernel(下文简称 SK)中,将第三方 REST API 以 OpenAPI 文档形式导入为 Plugin 后,每个 OpenAPI 操作(operation)会被转换成一个 Kernel Function。这些操作通常需要一个请求体(request body / payload),而 payload 的提供方式直接决定了调用方的编码复杂度。
- 简单场景:payload 结构扁平,调用方手工拼一个 JSON 字符串即可;
- 复杂场景:payload 嵌套多级对象、同一属性名在不同层级重复出现、甚至存在循环引用。
ADR 0062(状态为 proposed,日期 2024-10-25)正是围绕"SK 如何在调用 OpenAPI 函数时获得 payload"这一问题展开的:它先盘点当时已存在的三种处理选项,再提出一个新选项(Option #4),最后给出评审结论。该 ADR 对应的实现逻辑集中在 dotnet/src/Functions/Functions.OpenApi 目录下,核心入口为OpenApiFunctionExecutionParameters类与OpenApiKernelPluginFactory类。
在进入各选项之前,需要先认识两个贯穿全文的配置开关(定义见 OpenApiFunctionExecutionParameters.cs):
| 属性 | 默认值 | 作用 |
|---|---|---|
EnableDynamicPayload | true | 是否根据 OpenAPI 元数据 + 传入参数动态构造 payload;设为false时,payload 必须通过payload参数由调用方提供 |
EnablePayloadNamespacing | false | 是否用父属性名作为前缀(点分隔)来消除同名属性冲突;仅在EnableDynamicPayload = true时生效 |
两个默认值在构造函数中明确给出(enableDynamicOperationPayload = true、enablePayloadNamespacing = false),并在 OpenApiKernelPluginFactory.cs 组装RestApiOperationRunner时以executionParameters?.EnableDynamicPayload ?? true、executionParameters?.EnablePayloadNamespacing ?? false的方式落地。
现有方案一:payload与content-type参数(调用方全权构造)
这是最直接、也最可控的方式:调用方自己按照 OpenAPI schema 手工构造请求体,以普通参数的形式传给 OpenAPI 函数。
// 导入 OpenAPI 插件并关闭动态 payload 构造 KernelPlugin plugin = await kernel.ImportPluginFromOpenApiAsync("<plugin-name>", new Uri("<plugin-uri>"), new OpenApiFunctionExecutionParameters { EnableDynamicPayload = false }); // 为 createEvent 函数构造 payload string payload = """ { "subject": "IT Meeting", "start": { "dateTime": "2023-10-01T10:00:00", "timeZone": "UTC" }, "end": { "dateTime": "2023-10-01T11:00:00", "timeZone": "UTC" }, "tags": [ { "name": "IT" }, { "name": "Meeting" } ] } """; // 构造函数参数 KernelArguments arguments = new () { ["payload"] = payload, ["content-type"] = "application/json" }; // 调用 createEvent 函数 FunctionResult functionResult = await kernel.InvokeAsync(plugin["createEvent"], arguments);需要特别强调的是:该方式下 SK 不会对 payload 做任何校验或修改。SK 只负责把payload参数原样作为 HTTP 请求体发出,payload 是否合法、是否符合 OpenAPI schema,完全由调用方负责。因此这一选项适合以下场景:
- payload 结构复杂或特殊,动态构造难以覆盖;
- payload 中包含
oneOf、allOf、anyOf等组合 schema(见下文各方案的通用限制); - 调用方已有现成的序列化对象或模板。
从源码看,这一机制对应RestApiOperationExtensions中的"人工参数"(artificial parameter)创建逻辑:当关闭动态构造时,SK 会为操作生成payload与content-type(content_type)两个特殊参数。在仓库示例 OpenApiPlugin_PayloadHandling.cs 中可以看到,此时createMeeting函数的元数据参数列表恰好就是payload(描述为 "REST API request body.",schema 为完整对象结构)和content_type("Content type of REST API request body.")两个参数。
现有方案二:基于叶子属性(Leaf Properties)的动态构造
这是 SK 的默认行为(EnableDynamicPayload默认即true)。调用方无需提供完整 payload,只需为 schema 中的"叶子属性"提供同名参数,SK 负责按 schema 结构把它们组装成请求体。
// 导入插件(动态 payload 构造默认开启,也可显式写出) KernelPlugin plugin = await kernel.ImportPluginFromOpenApiAsync("<plugin-name>", new Uri("<plugin-uri>"), new OpenApiFunctionExecutionParameters { EnableDynamicPayload = true // 默认即为 true }); // 期望构造出的 payload 结构 //{ // "subject": "...", // "start": { // "dateTime": "...", // "timeZone": "..." // }, // "duration": "PT1H", // "tags":[{ // "name": "...", // } // ], //} // 为 createEvent 函数提供叶子属性参数 KernelArguments arguments = new() { ["subject"] = "IT Meeting", ["dateTime"] = DateTimeOffset.Parse("2023-10-01T10:00:00"), ["timeZone"] = "UTC", ["duration"] = "PT1H", ["tags"] = new[] { new Tag("work"), new Tag("important") } }; // 调用 createEvent 函数 FunctionResult functionResult = await kernel.InvokeAsync(plugin["createEvent"], arguments);工作原理
SK 从 payload schema 的根属性出发向下遍历,沿途收集所有叶子属性(即不再拥有子属性的属性)。调用方需要为这些叶子属性提供参数,SK 再依据 schema 层级把它们嵌套组装成最终 JSON 结构。
三个必须注意的限制
同名属性冲突:当 payload 在不同层级出现同名属性(例如
start.dateTime与end.dateTime)时,由于导入过程会为每个 OpenAPI 操作生成一个 Kernel Function,而一个函数不可能拥有两个同名参数,插件导入会直接失败,报错信息为:The function has two or more parameters with the same name "<property-name>".循环引用检测:若 schema 中两个或多个属性相互引用形成环路,SK 会检测到循环引用并抛出异常,导致操作导入失败。
数组属性按叶子处理:该方案不会继续遍历数组元素,而是把数组属性本身视为叶子。也就是说,调用方要为数组属性(如
tags)提供整体值,而不是为数组元素的字段逐个提供参数——上例中tags就是作为一个完整的对象数组传入的。
对应到源码,payload 的属性树由RestApiPayloadProperty表示(见 RestApiPayloadProperty.cs),其Name、Properties(子属性列表)、IsRequired、DefaultValue等字段完整刻画了 schema 树形结构;而真正的组装逻辑位于 RestApiOperationRunner.cs 的BuildOperationPayload方法中,运行时通过内部委托RestApiOperationPayloadFactory(见 RestApiOperationPayloadFactory.cs)把操作元数据与参数映射为最终 payload。
现有方案三:叶子属性 + 命名空间(Namespaces)动态构造
方案二最大的痛点是同名属性冲突。方案三通过**给叶子属性名添加父属性名前缀(点分隔)**来生成全局唯一参数名,从而化解冲突。
// 导入插件:同时开启动态构造与命名空间 KernelPlugin plugin = await kernel.ImportPluginFromOpenApiAsync("<plugin-name>", new Uri("<plugin-uri>"), new OpenApiFunctionExecutionParameters { EnableDynamicPayload = true, EnablePayloadNamespacing = true }); // 期望构造出的 payload 结构 //{ // "subject": "...", // "start": { // "dateTime": "...", // "timeZone": "..." // }, // "end": { // "dateTime": "...", // "timeZone": "..." // }, // "tags":[{ // "name": "...", // } // ], //} // 使用带命名空间的参数名 KernelArguments arguments = new() { ["subject"] = "IT Meeting", ["start.dateTime"] = DateTimeOffset.Parse("2023-10-01T10:00:00"), ["start.timeZone"] = "UTC", ["end.dateTime"] = DateTimeOffset.Parse("2023-10-01T11:00:00"), ["end.timeZone"] = "UTC", ["tags"] = new[] { new Tag("work"), new Tag("important") } }; // 调用 createEvent 函数 FunctionResult functionResult = await kernel.InvokeAsync(plugin["createEvent"], arguments);工作原理与差异点
- 与方案二相同,SK 仍然从根属性向下遍历收集叶子属性;
- 当遇到叶子属性时,SK 检查其是否存在父属性:若存在,则以"父属性名.叶子属性名"的形式生成唯一名称。例如
start对象下的dateTime属性,参数名即为start.dateTime; - 数组属性的处理方式与方案二一致:仍被视为叶子,调用方需要提供完整数组值。
这一点在EnablePayloadNamespacing的 XML 文档注释中解释得很清楚(见 OpenApiFunctionExecutionParameters.cs):没有命名空间时,sender与receiver两个父对象下的email参数都会从同一个email参数解析,这显然是错误的;启用命名空间后,sender.email与receiver.email就能从同名参数分别正确解析。
从函数元数据上也可以直观看到差异:在示例 OpenApiPlugin_PayloadHandling.cs 中,开启命名空间后createMeeting的参数列表变为subject、start_dateTime、start_timeZone、end_dateTime、end_timeZone、tags,每个嵌套字段都被父名唯一化了。
仍需注意
- 方案三同样把数组属性视为叶子;
- 方案三同样对 schema 中的循环引用敏感——一旦检测到循环引用,操作导入仍会失败。
候选方案四:基于根属性(Root Properties)的动态构造(已否决)
提出动机
前三种方案仍不能覆盖一类复杂场景:payload 在不同层级存在同名属性,而使用命名空间又不可行(例如命名空间与既有函数命名规范冲突、或调用方就是希望按对象整体传参)。为了既不让调用方手工拼整段 JSON,又能处理这种嵌套结构,ADR 提出了 Option #4:由 SK 基于根属性构造 payload,调用方为每个根属性提供完整对象值。
// 注意:这里关闭动态构造但开启命名空间,仅为示例展示组合方式 KernelPlugin plugin = await kernel.ImportPluginFromOpenApiAsync("<plugin-name>", new Uri("<plugin-uri>"), new OpenApiFunctionExecutionParameters { EnableDynamicPayload = false, EnablePayloadNamespacing = true }); // 期望构造出的 payload 结构 //{ // "subject": "...", // "start": { // "dateTime": "...", // "timeZone": "..." // }, // "end": { // "dateTime": "...", // "timeZone": "..." // }, // "tags":[{ // "name": "...", // } // ], //} // 为根属性提供完整对象值 KernelArguments arguments = new() { ["subject"] = "IT Meeting", ["start"] = new MeetingTime() { DateTime = DateTimeOffset.Parse("2023-10-01T10:00:00"), TimeZone = TimeZoneInfo.Utc }, ["end"] = new MeetingTime() { DateTime = DateTimeOffset.Parse("2023-10-01T10:00:00"), TimeZone = TimeZoneInfo.Utc }, ["tags"] = new[] { new Tag("work"), new Tag("important") } }; // 调用 createEvent 函数 FunctionResult functionResult = await kernel.InvokeAsync(plugin["createEvent"], arguments);该方案的定位是"自然介于方案一与方案二之间":相对于方案一,调用方无需手工拼 JSON 字符串;相对于方案二,嵌套对象的构建由调用方以强类型对象形式完成,规避了叶子属性同名冲突。其代价是:为根属性构造对象参数的复杂度转移到了调用方——ADR 也承认,在不允许使用命名空间、且不同层级同名属性必须从扁平参数列表解析的前提下,SK 能做的确实有限。
决策结果
经过评审,该方案最终被否决,理由是没有充分证据表明它能比现有方案一(payload+content-type)带来额外收益。因此当前仓库中并不存在基于根属性动态构造的落地实现,读者应把该节视为一次完整的架构权衡记录而非可用功能。
四方案总览对比
ADR 用一张表格完整对比了各方案的职责划分与限制,此处原样继承并补充说明:
| 方案 | 调用方职责 | SK 职责 | 限制 |
|---|---|---|---|
1.payload与content-type参数 | 构造完整 payload | 原样使用 | 无限制 |
| 4. 基于根属性动态构造(已否决) | 提供根属性参数 | 构造 payload | 1. 不支持anyOf、allOf、oneOf |
| 2. 基于叶子属性动态构造 | 提供叶子属性参数 | 构造 payload | 1. 不支持anyOf、allOf、oneOf;2. 叶子属性必须唯一;3. 存在循环引用风险 |
| 3. 叶子属性 + 命名空间动态构造 | 提供命名空间化属性参数 | 构造 payload | 1. 不支持anyOf、allOf、oneOf;2. 存在循环引用风险 |
表格中的两个共性限制值得展开
其一:组合 schema(anyOf/allOf/oneOf)不支持动态构造。三种动态构造方案(2、3、4)都明确标注"不支持anyOf、allOf、oneOf",因为这类 schema 的合法结构取决于运行时多选一/多选多的分支,难以从扁平的 KernelArguments 中无歧义还原。因此当 payload schema 含有组合类型时,只能回退到方案一,由调用方(或交给 LLM 通过 Function Calling 生成)提供完整 payload。仓库示例 OpenApiPlugin_PayloadHandling.cs 中的oneOfV3.json、allOfV3.json、anyOfV3.json三个 Pets 插件用例正是这一场景:它们全部显式设置EnableDynamicPayload = false,再由 AI 通过FunctionChoiceBehavior.Auto()自主构造符合分支条件的 payload。
其二:动态构造仅覆盖 schema 的树形结构。从RestApiPayloadProperty的属性树模型可以看出,动态构造依赖"根 → 中间节点 → 叶子"的完整遍历;一旦出现循环引用,遍历便无法终止,SK 因此选择在导入阶段直接失败以保护运行时。
实践指南:如何选择 Payload 处理策略
结合 ADR 与仓库实现,给出如下决策路径(基于 .NET 版 Semantic Kernel 当前仓库 dotnet/src/Functions/Functions.OpenApi 的实际行为):
- payload 结构简单、无跨层同名属性→ 使用默认配置即可:
EnableDynamicPayload保持true(默认),按叶子属性传参,SK 自动组装; - 存在跨层同名属性(如
start.dateTime/end.dateTime)→ 开启EnablePayloadNamespacing = true,使用parent.child形式的参数名; - schema 含
anyOf/allOf/oneOf组合类型→ 关闭动态构造(EnableDynamicPayload = false),通过payload与content-type参数传入完整请求体;此场景也最适合结合 LLM Function Calling,让模型依据用户意图生成符合分支 schema 的 payload; - 需要完全掌控请求体(如复用已有序列化 DTO、签名、加密等)→ 同样关闭动态构造,走方案一;
- 导入时遇到 "The function has two or more parameters with the same name" 或循环引用报错→ 按上述规则 2/3 调整配置;若确认 schema 存在循环引用,应先修正 OpenAPI 文档本身。
验证手段
仓库中可直接运行的验证素材:
- 完整示例:dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs —— 覆盖方案一、二、三以及
oneOf/allOf/anyOf四种场景,并通过StubHttpHandler拦截并打印实际发出的请求 payload,方便对照预期结构; - 参数默认值接线:OpenApiKernelPluginFactory.cs 中
EnableDynamicPayload与EnablePayloadNamespacing的默认值传递; - 参数元数据生成:RestApiOperationExtensions.cs 中
CreatePayloadArtificialParameter对payload/content-type人工参数的处理; - 执行期组装:RestApiOperationRunner.cs 的
BuildOperationPayload与RestApiOperationPayloadFactory委托。
总结
ADR 0062 完整记录了 Semantic Kernel .NET 在 OpenAPI 函数 payload 处理上的设计权衡:方案一以"完全可控"换取"完全自理",方案二以"零手工拼装"换取"叶子属性唯一性"约束,方案三用命名空间化解同名冲突但保留循环引用与组合 schema 限制,方案四(根属性构造)则在评审后因缺乏相对方案一的增量价值而被否决。
对使用者而言,核心结论只有一条:默认开启的动态构造适合结构规整的 payload;一旦遇到跨层同名、组合 schema 或需要精确控制请求体,就应关闭动态构造并显式提供 payload。理解这两个开关(EnableDynamicPayload、EnablePayloadNamespacing)及其边界,是正确驾驭 SK OpenAPI 插件能力的关键。
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考