基于 ADR-0035 解析 Semantic Kernel 如何为函数提供完整类型描述:从 Functions Manual 到 JSON Schema 输出建模
2026/9/10 21:27:18 网站建设 项目流程

基于 ADR-0035 解析 Semantic Kernel 如何为函数提供完整类型描述:从 Functions Manual 到 JSON Schema 输出建模

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

导读:本文围绕 Semantic Kernel 的设计决策文档 docs/decisions/0035-skfunction-type-descriptions.md 展开,深入剖析"仅为 LLM 提供函数参数文本描述、却缺乏输入输出类型信息"这一核心痛点,以及社区如何通过为函数元数据补充原生类型与 JSON Schema 来解决 Planner(规划器)无法正确串联多个函数的问题。读完本文,你将理解 Functions Manual 的演变、FunctionView/ParameterView/ReturnParameterView的类型设计权衡,以及这些设计在当前仓库源码中的落地形态,并能在自己的插件函数中正确书写类型元数据。

一、问题背景:Planner 为什么需要"类型信息"

在 Semantic Kernel 中,Planner(规划器)的工作方式是:给定一个用户目标(例如"明天的天气怎么样?"),LLM 需要从一组可用的 Plugin 函数中挑选若干函数、编排调用顺序并传递参数。要让 LLM 能做到这一点,就必须把"有哪些函数、各自有什么输入输出"描述清楚——这份描述在文档中被称为Functions Manual(函数手册),可以理解为交给 LLM 阅读的"插件使用说明书"。

当时的缺陷在决策文档中被直白地指出:

Today, Semantic Kernel only retains a small amount of information about the parameters of SKFunctions, and no information at all about the output of an SKFunction.

也就是说,旧版 SK 只保留了函数参数的一小部分信息,对于函数输出完全没有记录。这直接导致 Planner 无法判断"函数 A 的返回值类型是否与函数 B 的参数类型匹配",进而影响其规划质量。

1.1 旧版 Functions Manual 的样子

以 Sequential Planner 为例,文档给出了一份当时的 Functions Manual:

DatePluginSimpleComplex.GetDate1: description: Gets the date with the current date offset by the specified number of days. inputs: - numDays: The number of days to offset the date by from today. Positive for future, negative for past. WeatherPluginSimpleComplex.GetWeatherForecast1: description: Gets the weather forecast for the specified date and the current location, and time. inputs: - date: The date for the forecast

这份手册只包含"函数名、一句话描述、参数名 + 参数描述",没有任何类型信息。LLM 面对"明天的天气怎么样?"这个问题时,可能给出如下伪代码计划:

var dateResponse = DatePluginSimpleComplex.GetDate1(1); var forecastResponse = WeatherPluginSimpleComplex.GetWeatherForecast1(dateResponse); return forecastResponse;

表面上看,这个计划合理——先用GetDate1(1)计算明天的日期,再把它传给GetWeatherForecast1。但文档敏锐地指出:"只要第一个函数的未知返回类型碰巧与第二个函数的未知参数类型匹配,这个计划才可能成功"。由于 Functions Manual 缺少类型信息,LLM 无法知道GetDate1返回的是一个字符串、一个对象还是一个整数,也就无法可靠判断输出能否直接作为下一个函数的输入。

二、解决方案:用 JSON Schema 补齐输入输出类型

文档提出了一个关键设计取向:复用 JSON Schema。理由很充分:

  1. JSON Schema 是描述输入输出类型的通用标准;
  2. OpenAPI 规范正是用 JSON Schema 来描述接口的输入输出,因此本地插件与远程插件(如通过 OpenAPI 加载的插件)可以共用一套类型描述机制,形成一致的解决方案。

引入 JSON Schema 之后,Functions Manual 变成这样(完整示例继承自原文档):

[ { "name": "DatePluginSimpleComplex.GetDate1", "description": "Gets the date with the current date offset by the specified number of days.", "parameters": { "type": "object", "required": ["numDays"], "properties": { "numDays": { "type": "integer", "description": "The number of days to offset the date by from today. Positive for future, negative for past." } } }, "responses": { "200": { "description": "Successful response.", "content": { "application/json": { "schema": { "type": "object", "properties": { "date": { "type": "string" } }, "description": "The date." } } } } } }, { "name": "WeatherPluginSimpleComplex.GetWeatherForecast1", "description": "Gets the weather forecast for the specified date and the current location, and time.", "parameters": { "type": "object", "required": ["date"], "properties": { "date": { "type": "string", "description": "The date for the forecast" } } }, "responses": { "200": { "description": "Successful response.", "content": { "application/json": { "schema": { "type": "object", "properties": { "degreesFahrenheit": { "type": "integer" } }, "description": "The forecasted temperature in Fahrenheit." } } } } } } ]

对比旧版手册,这份 JSON Schema 版手册带来了质的提升:

  • 输入被结构化为parameters:声明type: object、必填项required、每个属性的类型与描述;
  • 输出被结构化为responses:遵循 OpenAPI 的响应格式(200状态码 +application/json内容 + schema),例如GetDate1返回{ date: string }对象,GetWeatherForecast1返回{ degreesFahrenheit: integer }

LLM 现在能够"看出"第一个函数的输出是一个包含date字段的对象,而第二个函数恰好需要date字符串作为输入。文档还分享了一个在测试中验证有效的技巧:让 LLM 用 JSON Path 指定输入应当从哪个输出字段提取。此时计划演变为:

var dateResponse = DatePluginSimpleComplex.GetDate1(1); var forecastResponse = WeatherPluginSimpleComplex.GetWeatherForecast1(dateResponse.date); return forecastResponse.degreesFahrenheit;

dateResponse.dateforecastResponse.degreesFahrenheit的显式字段提取,正是 LLM 具备"类型感知"后的直接体现。

2.1 关于 Token 成本的权衡

文档也坦诚地指出:更完整的手册必然带来 Token 用量的增加。但结论是明确的——类型信息带来的功能增益(更可靠的多函数串联、更准确的字段提取)超过额外的 Token 开销。这是任何想在应用里启用复杂 Planner 场景的开发者都需要接受的工程权衡。

三、设计提案:扩展 FunctionView 与 ParameterView

要让上述 JSON Schema 手册可生成,底层的数据模型必须先具备承载类型信息的能力。文档提出的改造点是FunctionView(函数视图)这一元数据结构。

3.1 改造前的 FunctionView

public sealed record FunctionView( string Name, string PluginName, string Description = "", IReadOnlyList<ParameterView>? Parameters = null) { /// <summary> /// List of function parameters /// </summary> public IReadOnlyList<ParameterView> Parameters { get; init; } = Parameters ?? Array.Empty<ParameterView>(); }

可以看到,旧结构只有NamePluginNameDescription和参数列表Parameters。参数由ParameterView描述(含语义描述,可承载部分类型信息),但函数的输出没有任何位置存放类型信息与语义描述

3.2 改造后的 FunctionView

public sealed record FunctionView( string Name, string PluginName, string Description = "", IReadOnlyList<ParameterView>? Parameters = null, ReturnParameterView? ReturnParameter = null) { /// <summary> /// List of function parameters /// </summary> public IReadOnlyList<ParameterView> Parameters { get; init; } = Parameters ?? Array.Empty<ParameterView>(); /// <summary> /// Function output /// </summary> public ReturnParameterView ReturnParameter { get; init; } = ReturnParameter ?? new ReturnParameterView(); }

核心变更是新增ReturnParameterView? ReturnParameter属性,默认值是一个空的ReturnParameterView实例,用于承载函数输出的类型信息与语义描述。

3.3 扩充后的 ParameterView

ParameterView原本带有ParameterViewType属性,但它只覆盖 JSON 的基础类型(string、number、boolean、null、object、array),无法描述对象内部的结构。文档为此设计了两条互补的路径:

  • NativeTypeSystem.Type:面向本地函数。导入 SKFunction 时参数类型总是可获取的,且后续"从 LLM 响应水合(hydrate)为原生类型"也需要它;
  • SchemaJsonDocument:面向远程插件。远程插件的对象类型没有对应的本地 .NET 类型(甚至可能不存在),因此需要从 OpenAPI 规范中提取 schema 并原样保存,支持"先前未知的 schema"。
public sealed record ParameterView( string Name, string? Description = null, string? DefaultValue = null, ParameterViewType? Type = null, bool? IsRequired = null, Type? NativeType = null, JsonDocument? Schema = null);

3.4 承载 Schema 的四种候选类型对比

文档对Schema属性的可选实现类型做了系统的对比分析(这是理解设计取舍的关键表格,完整继承如下):

TypeProsCons
JsonSchema.Net.JsonSchemaPopular and has frequent updates, built on top of System.NetTakes a dependency on OSS in SK core
NJsonSchema.JsonSchemaVery popular, frequent updates, long term projectBuilt on top of Json.Net (Newtonsoft)
JsonDocumentNative C# type, fast and flexibleNot a Json Schema, but a Json DOM container for the schema
StringNative C# typeNot a Json Schema or Json DOM, very poor type hinting

最终决策是:为了不在核心抽象(Abstractions)项目中引入第三方依赖,选择JsonDocument来保存远程插件加载时生成的 JSON Schema。而真正负责"创建或提取 schema"的库(如Functions.OpenAPIPlanners.CoreConnectors.AI.OpenAI)则按需引入各自所需的包。

四、落地验证:从决策到当前仓库源码

决策文档记录的是 2023 年 11 月的设计方向,而当前仓库中的实现已经清晰印证了这套方案的演进与落地。从源码结构看,FunctionView/ParameterView的职责在后来的版本中被迁移、演进为一组KernelFunctionMetadata系列类型。

4.1 KernelFunctionMetadata:参数与返回值元数据的现代形态

在 dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionMetadata.cs 中,可以看到与 ADR-0035 中FunctionView一一对应的设计:

  • ParametersIReadOnlyList<KernelParameterMetadata>)——对应原Parameters列表;
  • ReturnParameterKernelReturnParameterMetadata)——正是文档中提议新增的"函数输出"元数据,且默认返回KernelReturnParameterMetadata.Empty(对应ReturnParameter ?? new ReturnParameterView()的缺省策略);
  • 此外还包含NamePluginNameDescription与可选的AdditionalProperties

类注释明确写道:"Provides read-only metadata for aKernelFunction",且ReturnParameter在未设置时返回默认实例,与决策文档中的空默认值设计完全一致。

4.2 KernelParameterMetadata 与 KernelReturnParameterMetadata:NativeType + Schema 的落地

参数侧的实现位于 dotnet/src/SemanticKernel.Abstractions/Functions/KernelParameterMetadata.cs,它同时提供:

  • ParameterTypeType?)——对应文档中的NativeType
  • SchemaKernelJsonSchema?)——对应文档中的Schema
  • 以及NameDescriptionDefaultValueIsRequired等字段。

返回值侧的实现位于 dotnet/src/SemanticKernel.Abstractions/Functions/KernelReturnParameterMetadata.cs,同样暴露ParameterTypeSchemaDescription,并且其Schema属性采用**惰性推断(lazily-initialized)**策略:当未显式设置 schema 时,会基于ParameterType通过反射自动推断 JSON Schema。

这里有一个值得注意的实现细节:为了与 AOT/裁剪(trimming)场景兼容,这两个类在需要反射推断 schema 的构造路径上标注了[RequiresUnreferencedCode][RequiresDynamicCode],同时允许显式传入JsonSerializerOptions来生成 JSON Schema(见 KernelReturnParameterMetadata.cs)。这体现了 ADR-0035 中"本地函数由System.Type生成 JSON Schema"这一设计在工程落地时对现代 .NET 部署形态(AOT)的考量。

4.3 JsonSchemaFunctionView:决策文档中 Functions Manual 的直接实现

决策文档中展示的"JSON Schema 版 Functions Manual"在当前仓库中有一个非常直观的实现:内部类JsonSchemaFunctionView(位于 dotnet/src/InternalUtilities/planning/Schema/JsonSchemaFunctionView.cs)。它的字段与文档示例中的 JSON 结构几乎逐字段对应:

  • Name(函数名)
  • Description(函数描述)
  • ParametersJsonSchemaFunctionParameters,承载type: objectrequiredproperties
  • FunctionResponsesDictionary<string, JsonSchemaFunctionResponse>,以 HTTP 状态码为键的响应集合)

也就是说,决策文档里那两段 JSON 手册,正是由这类视图对象序列化而来。

4.4 从元数据到手册的转换逻辑

真正把KernelFunctionMetadata转换为JsonSchemaFunctionView的桥梁是扩展方法ToJsonSchemaFunctionView(位于 dotnet/src/InternalUtilities/planning/Extensions/KernelFunctionMetadataExtensions.cs)。其核心逻辑与决策文档的提案一一呼应:

  • 遍历function.Parameters,把每个参数的Schema塞进parameters.properties,并收集IsRequired参数进入required列表;
  • includeOutputSchematrue(默认开启)时,将function.ReturnParameter.Schema装配为状态码200、描述"Success"的响应内容——这与文档示例中"responses": { "200": { ... } }的结构完全一致;
  • 支持通过nameDelimiter参数控制插件名与函数名之间的连接符(默认-,而文档示例中使用.,说明分隔符是可配置的)。

同文件中的ToManualString方法则保留了旧版文本手册的生成能力:输出函数全名: description: ... inputs: - 参数名: 描述 (default value: ...)的纯文本格式,与文档第一节展示的旧版 Functions Manual 格式吻合。这从一个侧面说明:文本手册与 JSON Schema 手册两种形态在实现中并存,开发者可按需选择。

五、对开发者的实践启示

结合决策文档与源码,在为 Semantic Kernel 编写插件函数时,可以遵循以下实践来获得更好的 Planner / 函数调用效果:

  1. 为函数和参数提供准确、具体的描述Description会原样进入 Functions Manual,是 LLM 判断"何时调用该函数"的主要依据;
  2. 善用强类型签名。本地函数应使用具体的 .NET 类型(而非object/dynamic),这样ParameterType才能被用来推断出精确的 JSON Schema;对自定义对象类型,可进一步通过JsonSerializerOptions控制 schema 生成方式(参见 KernelParameterMetadata.cs 的构造重载);
  3. 为返回类型同样提供描述KernelReturnParameterMetadata.Description会进入responses节点的description,帮助 LLM 理解输出字段的语义,从而正确地从输出中提取下一个函数的输入(对应文档中dateResponse.date的 JSON Path 技巧);
  4. 远程插件(OpenAPI)场景:加载 OpenAPI 插件时,其 schema 会从规范中提取并以JsonDocument形式保存(相关实现散布在 dotnet/src/Functions/Functions.OpenApi 下,例如 OpenApiSchemaExtensions.cs),即使这些类型在 .NET 中不存在,LLM 依然能获得完整的结构信息;
  5. 接受 Token 成本:类型信息更丰富的手册意味着更大的提示词开销,但正如决策文档所论证的,这对于需要多函数串联的复杂规划是必要投资。

六、总结

ADR-0035 记录了一次影响深远的元数据设计决策:从"只描述函数名与参数文本"升级为"以 JSON Schema 完整描述输入结构、以 responses 描述输出结构"。它通过引入ReturnParameterView承载输出类型、为ParameterView增加NativeType(本地类型)与Schema(远程 schema)双通道,在不向核心抽象引入第三方依赖的前提下(最终选用JsonDocument),让 Planner 得以生成"从输出字段提取值并注入下一个输入"的精确计划。

从当前仓库源码可以看到,这套设计已经沉淀为KernelFunctionMetadata/KernelParameterMetadata/KernelReturnParameterMetadata系列类型,并被ToJsonSchemaFunctionView等转换逻辑用于生成现代 Functions Manual。对于任何希望让 LLM 可靠地串联多个插件函数的开发者而言,理解并善用这套类型描述机制,是解锁 Semantic Kernel 高级规划能力的关键一步。

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

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

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

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

立即咨询