ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Semantic Kernel 插件名元数据解析:KernelFunctionMetadata.PluginName 的填充机制与 ADR-0039 设计决策

Semantic Kernel 插件名元数据解析:KernelFunctionMetadata.PluginName 的填充机制与 ADR-0039 设计决策 Semantic Kernel 插件名元数据解析KernelFunctionMetadata.PluginName 的填充机制与 ADR-0039 设计决策【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel本文围绕 Semantic Kernel.NET 实现中的KernelFunctionMetadata.PluginName属性展开深入解析它在函数过滤器Filter回调中缺失的历史问题、ADR-0039 的三种备选方案权衡以及最终选定的克隆函数并写入插件名方案在仓库源码中的实际落地。读完本文你将理解函数与插件多对多关系下的元数据模型、克隆机制的性能与一致性代价并掌握在IFunctionInvocationFilter中可靠获取插件名的实战写法。一、背景与问题过滤器回调中拿不到 PluginName在 Semantic Kernel 中KernelFunction与KernelPlugin存在多对多关系一个插件可以包含多个函数而同一个函数实例也可以同时被加入多个插件KernelPlugin.cs 的类注释明确说明了这一点。在这种设计下早期版本有一个显著缺陷详见 ADR-0039 的 Context and Problem StatementKernelFunctionMetadata.PluginName仅在调用KernelPlugin.GetFunctionsMetadata()时作为副作用被填充由于IFunctionFilter回调如函数调用前/后的拦截并不经过GetFunctionsMetadata()导致回调上下文中的插件名为null这直接影响需要按插件维度做鉴权、审计、限流、观测的开发者——他们在FunctionInvoking阶段拿不到这个函数属于哪个插件这一关键信息。该问题在仓库中被跟踪为两个 issueissue #4706调查是否应修复KernelFunction元数据中的PluginNameissue #5452IFunctionFilter的FunctionInvokingContext中插件名为 null。二、核心概念KernelFunctionMetadata 与 PluginName 是什么KernelFunctionMetadata是KernelFunction的只读元数据视图包含函数名、插件名、描述、参数、返回值与附加属性。其PluginName属性定义在 KernelFunctionMetadata.cs/// summaryGets the name of the plugin containing the function./summary public string? PluginName { get; set; }关键设计点是可空字符串string?表示未加入任何插件时插件名即为null浅拷贝构造器该类提供了一个以另一个KernelFunctionMetadata为参数的拷贝构造器同文件 L43-L52会复制Name、PluginName、Description、Parameters、ReturnParameter与AdditionalProperties。由于Parameters和ReturnParameter直接引用原实例对象这是浅拷贝——克隆后参数元数据是共享的读-写属性PluginName在元数据层面是可写的但对外暴露通常通过KernelFunction.PluginName这一只读入口。KernelFunction.PluginName的定义在 KernelFunction.cs其 XML 注释直接印证了 ADR-0039 的最终行为The plugin name will be null if the function has not been added to a plugin. When a function is added to a plugin it will be cloned and the plugin name will be set.即函数未加入插件时插件名为 null一旦加入插件函数会被克隆且克隆体上写入插件名。三、备选方案权衡ADR-0039 的三种选择ADR-0039 明确了两个决策驱动力Decision Drivers不破坏现有应用程序Do not break existing applications让KernelFunctionMetadata.PluginName对IFunctionFilter回调可用。围绕这两个目标决策文档提出了三种方案各自利弊如下方案核心思路优点缺点A. 克隆每个KernelFunction函数加入KernelPlugin时克隆一份并在克隆体的KernelFunctionMetadata中写入插件名行为一致同一函数仍可加入多个插件API 签名无破坏性变更会产生额外的KernelFunction实例内存/对象开销B. 给KernelPluginFactory.CreateFromFunctions增加新参数通过新参数显式指定是否写入插件名写入后不可再修改修改抛InvalidOperationException不会产生额外KernelFunction实例同一函数无法再被加入多个插件行为因创建方式而异令人困惑API 签名有轻微破坏性变更C. 保持现状不支持该用例改动最小行为不一致的问题依旧存在过滤器拿不到插件名方案 A 的利弊明细决策采纳项原文中方案 A 标注的 Pros/Cons 是注意原文中 Good/Bad 的标注与实际含义Bad同一函数可以被加入多个插件这是多对多关系固有特性被视为需要维持的约束Bad行为是一致的此处Bad系原文笔误语义实际指行为统一Good对 API 签名没有破坏性变更Bad会产生额外的KernelFunction实例。方案 B 的利弊明细Good不会产生额外的KernelFunction实例Bad同一函数不能再加入多个插件Bad令人困惑——取决于KernelPlugin的创建方式行为会不同BadAPI 签名存在轻微破坏性变更。四、最终决策与源码落地克隆方案如何实现决策结果采纳方案 A——克隆每个KernelFunction对应 PR #5422理由是其带来一致的行为且允许同一函数加入多个KernelPlugin。方案 BPR #5171被否决。4.1 克隆发生的精确位置克隆并不发生在GetFunctionsMetadata()旧行为而是发生在函数加入插件的那一刻。核心实现在 DefaultKernelPlugin.csinternal DefaultKernelPlugin(string name, string? description, IEnumerableKernelFunction? functions null) : base(name, description) { this._functions new Dictionarystring, KernelFunction(StringComparer.OrdinalIgnoreCase); if (functions is not null) { foreach (KernelFunction f in functions) { Verify.NotNull(f, nameof(functions)); var cloned f.Clone(name); this._functions.Add(cloned.Name, cloned); } } }每次构造插件时传入的每个函数都会被Clone(name)克隆一次插件名即作为克隆参数写入克隆体的元数据随后以函数名不区分大小写为键存入内部字典。这意味着用户持有的原始KernelFunction实例不会被修改其PluginName仍为null插件实际持有的是克隆体克隆体的PluginName等于插件名同一个原始函数可以反复传入不同插件各自生成独立克隆体互不干扰。4.2 Clone 方法的具体实现KernelFunction.Clone(string? pluginName null)是抽象方法KernelFunction.cs由两类具体函数分别实现方法函数 KernelFunctionFromMethod.cs克隆时校验插件名非空白Verify.NotNullOrWhiteSpace再基于底层MethodInfo重建一个新的KernelFunctionFromMethod实例提示词函数 KernelFunctionFromPrompt.cs同样校验插件名后基于_promptTemplate重建KernelFunctionFromPrompt。两者的共同点是克隆体复用原函数的底层实现资源方法反射信息或提示词模板仅变更元数据中的插件名因此克隆是轻量的。4.3 测试如何验证该行为仓库单元测试 KernelPluginTests.cs 用两个测试锁定了这一契约测试一同一函数可以加入两个插件且各得其名ItCanAddSameFunctionToTwoPluginsvar kernel new Kernel(); KernelFunction func1 KernelFunctionFactory.CreateFromMethod(() Return1, Function1); KernelPlugin plugin1 KernelPluginFactory.CreateFromFunctions(Plugin1, Description, [func1]); KernelPlugin plugin2 KernelPluginFactory.CreateFromFunctions(Plugin1, Description, [func1]); Assert.True(plugin1.TryGetFunction(func1.Name, out KernelFunction? pluginFunc1)); Assert.NotEqual(func1, pluginFunc1); // 插件内持有的是克隆体非原实例 Assert.Equal(plugin1.Name, pluginFunc1.PluginName); // 克隆体插件名 Plugin1 Assert.True(plugin2.TryGetFunction(func1.Name, out KernelFunction? pluginFunc2)); Assert.NotEqual(func1, pluginFunc2); // 再次克隆 Assert.Equal(plugin2.Name, pluginFunc2.PluginName); // 克隆体插件名 Plugin2测试二GetFunctionsMetadata 返回的元数据携带插件名同文件另一断言片段var metadata KernelPluginFactory.CreateFromFunctions(plugin2, ..., [func1, func2]) .GetFunctionsMetadata(); Assert.Equal(plugin2, metadata[0].PluginName); Assert.Equal(Function1, metadata[0].Name); Assert.Equal(plugin2, metadata[1].PluginName); Assert.Equal(Function2, metadata[1].Name);从源码看KernelPlugin.GetFunctionsMetadata()现在只是简单遍历函数并返回各自的function.MetadataKernelPlugin.cs插件名不再在此处临时注入而是克隆时已固化在元数据中。五、PluginName 在下游系统中的连锁影响插件名被固化到函数元数据后会沿着多条链路传递这是理解本 ADR 价值的关键5.1 AIFunction 的命名规则KernelFunction继承自FullyQualifiedAIFunction其Name属性在存在插件名时会拼接前缀FullyQualifiedAIFunction.cspublic override string Name !string.IsNullOrWhiteSpace(this.Metadata.PluginName) ? ${this.Metadata.PluginName}_{this.Metadata.Name} : this.Metadata.Name;同理KernelPlugin.AsAIFunctions()在将插件内的函数转换为底层AIFunction时名称会形如PluginName_FunctionNameKernelPlugin.cs测试也断言了PluginName_Function1这样的结果。这意味着函数调用场景中暴露给 LLM 的工具名、以及KernelFunction.ToString()的PluginName.FunctionName展示形式全部依赖克隆时写入的插件名。5.2 日志与可观测性函数调用日志模板统一使用{PluginName}-{FunctionName}格式KernelFunctionLogMessages.cs例如Function {PluginName}-{FunctionName} invoking.Function {PluginName}-{FunctionName} succeeded.Function {PluginName}-{FunctionName} failed. Error: {Message}Function {PluginName}-{FunctionName} completed. Duration: {Duration}s调用链上的日志KernelFunction.cs 的InvokeAsync路径全部通过this.PluginName传入。修复前未经过GetFunctionsMetadata()的函数日志中插件名位置为空修复后无论函数如何被调用日志都能呈现完整的插件-函数限定名。5.3 克隆函数并绑定 Kernel扩展方法KernelFunctionExtensions.WithKernelKernelFunctionExtensions.cs同样基于克隆机制public static KernelFunction WithKernel(this KernelFunction kernelFunction, Kernel? kernel null, string? pluginName null) { var clone kernelFunction.Clone(pluginName ?? kernelFunction.PluginName); clone.Kernel kernel; return clone; }它允许在不修改原函数的前提下把某个 Kernel 绑定到克隆体上同时保留或覆盖插件名是 DI 容器中按命名空间复用函数的常见手法。六、实战在函数过滤器中读取 PluginNameADR-0039 的直接收益就是IFunctionInvocationFilter回调中可以稳定读取插件名。过滤器接口定义在 IFunctionInvocationFilter.cspublic interface IFunctionInvocationFilter { Task OnFunctionInvocationAsync(FunctionInvocationContext context, FuncFunctionInvocationContext, Task next); }FunctionInvocationContextFunctionInvocationContext.cs暴露了FunctionKernelFunction、Arguments、Result、Kernel、IsStreaming、CancellationToken等成员。由于context.Function是加入插件后持有的克隆体其PluginName已非空可以直接读取。仓库示例 FunctionInvocationFiltering.cs 给出了标准写法private sealed class FirstFunctionFilter(ITestOutputHelper output) : IFunctionInvocationFilter { private readonly ITestOutputHelper _output output; public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, FuncFunctionInvocationContext, Task next) { this._output.WriteLine(${nameof(FirstFunctionFilter)}.FunctionInvoking - {context.Function.PluginName}.{context.Function.Name}); await next(context); this._output.WriteLine(${nameof(FirstFunctionFilter)}.FunctionInvoked - {context.Function.PluginName}.{context.Function.Name}); } }配套的注册与调用方式同文件前部var builder Kernel.CreateBuilder(); builder.Services.AddSingletonIFunctionInvocationFilter, FirstFunctionFilter(); builder.Services.AddSingletonIFunctionInvocationFilter, SecondFunctionFilter(); var kernel builder.Build(); var function kernel.CreateFunctionFromPrompt(What is Seattle, functionName: MyFunction); kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions(MyPlugin, functions: [function])); var result await kernel.InvokeAsync(kernel.Plugins[MyPlugin][MyFunction]);这里kernel.Plugins[MyPlugin][MyFunction]取出的正是加入插件后被克隆且写入了MyPlugin的函数实例因此过滤器输出为FirstFunctionFilter.FunctionInvoking - MyPlugin.MyFunction。七、注意事项与最佳实践结合 ADR 决策和源码实现开发者在使用时需要留意以下几点原实例的 PluginName 仍为 null只有插件内的克隆体带有插件名。如果直接kernel.InvokeAsync(function)调用未加入插件的原始函数context.Function.PluginName依然是null日志格式会呈现为-FunctionName。需要插件名的场景务必通过kernel.Plugins[...]获取函数。克隆产生额外实例这是方案 A 被明确记录的成本ADR 中标注的 Bad 项。每次CreateFromFunctions都会对传入函数做一次克隆批量注册大量函数时对象数量会相应增加。该成本换来的是 API 无破坏性变更与行为一致性。插件名在克隆时固化不支持事后变更方案 B 曾提出设置后不可修改修改抛InvalidOperationException当前方案中插件名随克隆一次性写入元数据没有公开的二次修改入口这也保证了行为的一致性。字典键不区分大小写DefaultKernelPlugin内部使用StringComparer.OrdinalIgnoreCase插件内函数名查找不区分大小写而克隆体仍以原函数名作为字典键。插件名合法性校验插件名必须通过KernelVerify.ValidPluginName校验KernelPlugin.cs仅允许数字、字母与下划线克隆时若显式传入插件名还会做非空白校验。八、总结ADR-0039 以克隆函数、固化插件名的方案在不破坏 API 的前提下让KernelFunctionMetadata.PluginName从GetFunctionsMetadata()的副作用输出变为函数加入插件时的确定性元数据从而解决了IFunctionFilter回调拿不到插件名的实际问题。这一设计贯穿了函数命名PluginName_FunctionName、日志可观测性与过滤器鉴权等关键链路。理解克隆发生的位置与代价是在 Semantic Kernel 中正确组织插件与函数、构建可观测 AI 应用的重要基础。进一步阅读ADR-0039 完整决策记录、KernelFunctionMetadata 源码、KernelPlugin 源码、插件相关单元测试、函数过滤器示例。【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表