Semantic Kernel OpenAPI 函数 Payload 处理机制全解析:从静态传递到动态构建(ADR 0062)
2026/9/11 18:52:38 网站建设 项目流程

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)的叶子属性构造,以及被评审后否决的"基于根属性构造"方案。读完本文,你将掌握EnableDynamicPayloadEnablePayloadNamespacing两个核心开关的行为边界、适用场景与限制,并能结合仓库源码理解其底层实现,在实际项目中正确选择 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):

属性默认值作用
EnableDynamicPayloadtrue是否根据 OpenAPI 元数据 + 传入参数动态构造 payload;设为false时,payload 必须通过payload参数由调用方提供
EnablePayloadNamespacingfalse是否用父属性名作为前缀(点分隔)来消除同名属性冲突;仅在EnableDynamicPayload = true时生效

两个默认值在构造函数中明确给出(enableDynamicOperationPayload = trueenablePayloadNamespacing = false),并在 OpenApiKernelPluginFactory.cs 组装RestApiOperationRunner时以executionParameters?.EnableDynamicPayload ?? trueexecutionParameters?.EnablePayloadNamespacing ?? false的方式落地。


现有方案一:payloadcontent-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 中包含oneOfallOfanyOf等组合 schema(见下文各方案的通用限制);
  • 调用方已有现成的序列化对象或模板。

从源码看,这一机制对应RestApiOperationExtensions中的"人工参数"(artificial parameter)创建逻辑:当关闭动态构造时,SK 会为操作生成payloadcontent-typecontent_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 结构。

三个必须注意的限制

  1. 同名属性冲突:当 payload 在不同层级出现同名属性(例如start.dateTimeend.dateTime)时,由于导入过程会为每个 OpenAPI 操作生成一个 Kernel Function,而一个函数不可能拥有两个同名参数,插件导入会直接失败,报错信息为:The function has two or more parameters with the same name "<property-name>".

  2. 循环引用检测:若 schema 中两个或多个属性相互引用形成环路,SK 会检测到循环引用并抛出异常,导致操作导入失败。

  3. 数组属性按叶子处理:该方案不会继续遍历数组元素,而是把数组属性本身视为叶子。也就是说,调用方要为数组属性(如tags)提供整体值,而不是为数组元素的字段逐个提供参数——上例中tags就是作为一个完整的对象数组传入的。

对应到源码,payload 的属性树由RestApiPayloadProperty表示(见 RestApiPayloadProperty.cs),其NameProperties(子属性列表)、IsRequiredDefaultValue等字段完整刻画了 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):没有命名空间时,senderreceiver两个父对象下的email参数都会从同一个email参数解析,这显然是错误的;启用命名空间后,sender.emailreceiver.email就能从同名参数分别正确解析。

从函数元数据上也可以直观看到差异:在示例 OpenApiPlugin_PayloadHandling.cs 中,开启命名空间后createMeeting的参数列表变为subjectstart_dateTimestart_timeZoneend_dateTimeend_timeZonetags,每个嵌套字段都被父名唯一化了。

仍需注意

  • 方案三同样把数组属性视为叶子;
  • 方案三同样对 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.payloadcontent-type参数构造完整 payload原样使用无限制
4. 基于根属性动态构造(已否决)提供根属性参数构造 payload1. 不支持anyOfallOfoneOf
2. 基于叶子属性动态构造提供叶子属性参数构造 payload1. 不支持anyOfallOfoneOf;2. 叶子属性必须唯一;3. 存在循环引用风险
3. 叶子属性 + 命名空间动态构造提供命名空间化属性参数构造 payload1. 不支持anyOfallOfoneOf;2. 存在循环引用风险

表格中的两个共性限制值得展开

其一:组合 schema(anyOf/allOf/oneOf)不支持动态构造。三种动态构造方案(2、3、4)都明确标注"不支持anyOfallOfoneOf",因为这类 schema 的合法结构取决于运行时多选一/多选多的分支,难以从扁平的 KernelArguments 中无歧义还原。因此当 payload schema 含有组合类型时,只能回退到方案一,由调用方(或交给 LLM 通过 Function Calling 生成)提供完整 payload。仓库示例 OpenApiPlugin_PayloadHandling.cs 中的oneOfV3.jsonallOfV3.jsonanyOfV3.json三个 Pets 插件用例正是这一场景:它们全部显式设置EnableDynamicPayload = false,再由 AI 通过FunctionChoiceBehavior.Auto()自主构造符合分支条件的 payload。

其二:动态构造仅覆盖 schema 的树形结构。RestApiPayloadProperty的属性树模型可以看出,动态构造依赖"根 → 中间节点 → 叶子"的完整遍历;一旦出现循环引用,遍历便无法终止,SK 因此选择在导入阶段直接失败以保护运行时。


实践指南:如何选择 Payload 处理策略

结合 ADR 与仓库实现,给出如下决策路径(基于 .NET 版 Semantic Kernel 当前仓库 dotnet/src/Functions/Functions.OpenApi 的实际行为):

  1. payload 结构简单、无跨层同名属性→ 使用默认配置即可:EnableDynamicPayload保持true(默认),按叶子属性传参,SK 自动组装;
  2. 存在跨层同名属性(如start.dateTime/end.dateTime→ 开启EnablePayloadNamespacing = true,使用parent.child形式的参数名;
  3. schema 含anyOf/allOf/oneOf组合类型→ 关闭动态构造(EnableDynamicPayload = false),通过payloadcontent-type参数传入完整请求体;此场景也最适合结合 LLM Function Calling,让模型依据用户意图生成符合分支 schema 的 payload;
  4. 需要完全掌控请求体(如复用已有序列化 DTO、签名、加密等)→ 同样关闭动态构造,走方案一;
  5. 导入时遇到 "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 中EnableDynamicPayloadEnablePayloadNamespacing的默认值传递;
  • 参数元数据生成:RestApiOperationExtensions.cs 中CreatePayloadArtificialParameterpayload/content-type人工参数的处理;
  • 执行期组装:RestApiOperationRunner.cs 的BuildOperationPayloadRestApiOperationPayloadFactory委托。

总结

ADR 0062 完整记录了 Semantic Kernel .NET 在 OpenAPI 函数 payload 处理上的设计权衡:方案一以"完全可控"换取"完全自理",方案二以"零手工拼装"换取"叶子属性唯一性"约束,方案三用命名空间化解同名冲突但保留循环引用与组合 schema 限制,方案四(根属性构造)则在评审后因缺乏相对方案一的增量价值而被否决。

对使用者而言,核心结论只有一条:默认开启的动态构造适合结构规整的 payload;一旦遇到跨层同名、组合 schema 或需要精确控制请求体,就应关闭动态构造并显式提供 payload。理解这两个开关(EnableDynamicPayloadEnablePayloadNamespacing)及其边界,是正确驾驭 SK OpenAPI 插件能力的关键。

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询