Semantic Kernel Kernel Filters 深度指南:从事件处理器到可注入式过滤器管线的演进与实践 📅 发布时间:2026/9/10 21:40:33 👁 浏览次数: Semantic Kernel Kernel Filters 深度指南从事件处理器到可注入式过滤器管线的演进与实践【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernelKernel Filters内核过滤器是 Semantic Kernel 在 .NET 平台提供的函数调用与提示词渲染拦截机制用于在函数执行前/后、提示词渲染前/后插入自定义逻辑支持通过依赖注入DI注册、按注册顺序组成管道并随时调整执行顺序。本指南以 0033-kernel-filters.md 这份架构决策记录ADR为主体结合当前仓库 dotnet/src 中的接口实现、Kernel.cs 的过滤器管线代码以及 Concepts/Filtering 下的官方示例完整还原其设计动机、接口形态、注册方式、执行顺序与实战用法读完后你将能在自己的 Semantic Kernel 应用中独立实现函数过滤器、提示词过滤器与自动函数调用过滤器。一、背景事件处理器方案的局限Semantic Kernel 早期通过 Kernel Events 与事件处理器来拦截函数执行过程中的事件。其典型写法如下摘自原 ADRILogger logger loggerFactory.CreateLogger(MyLogger); var kernel Kernel.CreateBuilder() .AddOpenAIChatCompletion( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey) .Build(); void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) { logger.LogInformation(Invoking: {FunctionName}, e.Function.Name) } void MyInvokedHandler(object? sender, FunctionInvokedEventArgs e) { if (e.Result.Metadata is not null e.Result.Metadata.ContainsKey(Usage)) { logger.LogInformation(Token usage: {TokenUsage}, e.Result.Metadata?[Usage]?.AsJson()); } } kernel.FunctionInvoking MyInvokingHandler; kernel.FunctionInvoked MyInvokedHandler; var result await kernel.InvokePromptAsync(How many days until Christmas? Explain your thinking.)该方案虽然可用但存在三个明显问题不支持依赖注入处理器难以访问应用中注册的特定服务如ILoggerFactory除非处理器定义在服务实例恰好可用的同一作用域内这限制了处理器在解决方案中的定义位置。生命周期不明确不清楚处理器应在应用运行的哪个阶段挂载到 Kernel也不清楚是否需要以及何时摘除。机制不通用.NET 的事件event与事件处理器模式对未接触过事件的开发者并不友好。这些痛点构成了本 ADR 的决策驱动因素。二、决策引入类似 ASP.NET Action Filters 的 Kernel Filters设计团队最终决定引入Kernel Filters——一种与 ASP.NET 中 Action Filters 类似的接收内核事件的机制。这一决策满足以下要求对应 ADR 的 Decision Drivers处理器支持依赖注入便于访问应用内注册的服务处理器可在解决方案任意位置定义Startup.cs或独立文件均可不受位置限制在应用运行期可以清晰地注册与移除处理器接收和处理内核事件的机制在 .NET 生态中应简单且通用新方案需支持 Kernel Events 已有的全部能力——取消函数执行、修改 Kernel 参数arguments、在发送给 AI 之前修改渲染后的提示词等。三、两个核心抽象函数过滤器与提示词过滤器ADR 提出两个新的抽象接口开发者需要按需实现public interface IFunctionFilter { void OnFunctionInvoking(FunctionInvokingContext context); void OnFunctionInvoked(FunctionInvokedContext context); } public interface IPromptFilter { void OnPromptRendering(PromptRenderingContext context); void OnPromptRendered(PromptRenderedContext context); }需要特别说明的是ADR 文档2023 年制定中给出的是IFunctionFilter/IPromptFilter的初版设计随着 Semantic Kernel 演进当前仓库中的接口已升级为异步中缀async middleware风格即把next委托传入方法由过滤器自行决定何时调用下一个过滤器或真正的函数/渲染操作。当前的实际接口位于 dotnet/src/SemanticKernel.Abstractions/Filters函数过滤器 IFunctionInvocationFilter.cspublic interface IFunctionInvocationFilter { Task OnFunctionInvocationAsync(FunctionInvocationContext context, FuncFunctionInvocationContext, Task next); }提示词过滤器 IPromptRenderFilter.cspublic interface IPromptRenderFilter { Task OnPromptRenderAsync(PromptRenderContext context, FuncPromptRenderContext, Task next); }next委托指向管道中的下一个过滤器如果过滤器不调用next则后续过滤器与真正的函数/渲染操作都不会执行——这正是过滤器可以短路short-circuit调用链、实现提前返回或拒绝执行的关键机制两个接口的 XML 文档均明确注明此行为。上下文对象接口方法携带的上下文对象承载了过滤器可读写的全部数据FunctionInvocationContext.cs暴露Kernel、FunctionKernelFunction、ArgumentsKernelArguments可修改、ResultFunctionResult可读写赋值即可覆盖真实函数结果、CancellationToken以及IsStreaming标识当前是流式还是非流式调用。PromptRenderContext.cs暴露Kernel、Function、Arguments、ExecutionSettingsPromptExecutionSettings以及核心的RenderedPrompt属性——过滤器可以查看渲染后的提示词并修改它最终值就是真正发送给 AI 的提示词此外还支持直接设置Result来跳过后续函数调用并返回结果。四、实战一用过滤器重写事件处理器逻辑ADR 给出了将上文事件处理器等价改写为过滤器类的示例。把相同逻辑封装成独立类并通过构造函数注入ILoggerFactorypublic sealed class MyFunctionFilter : IFunctionFilter { private readonly ILogger _logger; public MyFunctionFilter(ILoggerFactory loggerFactory) { this._logger loggerFactory.CreateLogger(MyLogger); } public void OnFunctionInvoking(FunctionInvokingContext context) { this._logger.LogInformation(Invoking {FunctionName}, context.Function.Name); } public void OnFunctionInvoked(FunctionInvokedContext context) { var metadata context.Result.Metadata; if (metadata is not null metadata.ContainsKey(Usage)) { this._logger.LogInformation(Token usage: {TokenUsage}, metadata[Usage]?.AsJson()); } } }注意上述代码片段保留了 ADR 当时的初版接口形态用于说明设计意图对照当前仓库应实现IFunctionInvocationFilter.OnFunctionInvocationAsync并在调用await next(context)之后读取context.Result.Metadata来记录 Token 用量。使用依赖注入注册时写法完全一致见下节因此把处理器改造成可注入过滤器的思路没有变化。五、实战二过滤器的注册与生命周期管理过滤器定义好后可在 Kernel 构建前后两个阶段进行配置。方式一构建前通过依赖注入注册pre-constructionIKernelBuilder kernelBuilder Kernel.CreateBuilder(); kernelBuilder.AddOpenAIChatCompletion( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey); // Adding filter with DI (pre-construction) kernelBuilder.Services.AddSingletonIFunctionFilter, MyFunctionFilter(); Kernel kernel kernelBuilder.Build(); var result await kernel.InvokePromptAsync(How many days until Christmas? Explain your thinking.);从源码看Kernel构造函数在构建时会调用AddFilters()方法见 Kernel.cs该方法通过this.Services.GetServicesIFunctionInvocationFilter()、GetServicesIPromptRenderFilter()、GetServicesIAutoFunctionInvocationFilter()枚举 DI 容器中注册的全部过滤器并装载进内核集合因此只要在 builder 的 Services 中注册构建出的 Kernel 即自动生效。实际示例可参考 Concepts/Filtering/FunctionInvocationFiltering.cs。方式二构建后直接添加post-construction// Adding filter after Kernel initialization (post-construction) kernel.FunctionFilters.Add(new MyAwesomeFilter());对应到当前接口构建后添加的写法为kernel.FunctionInvocationFilters.Add(new MyAwesomeFilter()); kernel.PromptRenderFilters.Add(new FirstPromptFilter(...));官方示例 PromptRenderFiltering.cs 正是采用这种方式在kernel.PromptRenderFilters上直接Add。三种过滤器集合属性均定义在 Kernel.csFunctionInvocationFiltersIListIFunctionInvocationFilterPromptRenderFiltersIListIPromptRenderFilterAutoFunctionInvocationFiltersIListIAutoFunctionInvocationFilter多过滤器与运行时调整顺序注册多个过滤器时它们按注册顺序依次触发kernelBuilder.Services.AddSingletonIFunctionFilter, Filter1(); kernelBuilder.Services.AddSingletonIFunctionFilter, Filter2(); kernelBuilder.Services.AddSingletonIFunctionFilter, Filter3();由于过滤器集合本身是IListT也可以在运行期改变执行顺序或移除某个过滤器kernel.FunctionFilters.Insert(0, new InitialFilter()); kernel.FunctionFilters.RemoveAt(1);对应当前接口的写法即kernel.FunctionInvocationFilters.Insert(0, ...)/kernel.FunctionInvocationFilters.RemoveAt(...)。过滤器的管道式执行原理过滤器之所以按顺序触发是因为 Kernel.cs 中的InvokeFilterOrFunctionAsync采用递归 委托实现了经典中间件管道private static async Task InvokeFilterOrFunctionAsync( NonNullCollectionIFunctionInvocationFilter? functionFilters, FuncFunctionInvocationContext, Task functionCallback, FunctionInvocationContext context, int index 0) { if (functionFilters is { Count: 0 } index functionFilters.Count) { await functionFilters[index].OnFunctionInvocationAsync(context, (context) InvokeFilterOrFunctionAsync(functionFilters, functionCallback, context, index 1)).ConfigureAwait(false); } else { await functionCallback(context).ConfigureAwait(false); } }即执行index0的过滤器时传入的next委托会递归调用index1的过滤器以此类推当index越界时执行真正的函数调用。因此在await next(context)之前写的代码在函数调用前执行相当于OnFunctionInvoking在await next(context)之后写的代码在函数调用后执行相当于OnFunctionInvoked不调用next即可终止管道跳过函数执行。提示词渲染的InvokeFilterOrPromptRenderAsync遵循完全相同的模式Kernel.cs自动函数调用过滤器的管道则实现在 KernelFunctionInvokingChatClient.cs 的InvokeFilterOrFunctionAsync中。六、实战三覆盖结果与修改渲染提示词过滤器最有价值的实战能力是覆盖执行结果和改写发送给 AI 的提示词。覆盖函数执行结果在函数过滤器中调用next后直接替换context.Result即可让下游拿到过滤器给出的结果。官方示例 FunctionInvocationFiltering.cs 展示了builder.Services.AddSingletonIFunctionInvocationFilter, FunctionFilterExample(); var kernel builder.Build(); var function KernelFunctionFactory.CreateFromMethod(() Result from method); var result await kernel.InvokeAsync(function); // 实际输出 // Result from filter. // Metadata: metadata_key: metadata_value该示例同时证明过滤器还能向FunctionResult.Metadata写入自定义元数据Token 用量、成本等供调用方或后续管道读取。覆盖渲染后的提示词在提示词过滤器中await next(context)之后设置context.RenderedPrompt即可替换真正发送给模型的内容。官方示例 PromptRenderFiltering.cspublic async Task OnPromptRenderAsync(PromptRenderContext context, FuncPromptRenderContext, Task next) { var functionName context.Function.Name; // 读取函数信息 await next(context); // 覆盖渲染后的提示词再发送给 AI context.RenderedPrompt Respond with following text: Prompt from filter.; }调用await kernel.InvokePromptAsync(Hi, how can you help me?)时模型实际收到的将是Respond with following text: Prompt from filter.而非原始提示词。这正是 ADR 决策要求中在发送给 AI 之前修改渲染后的提示词这一能力的落地实现。流式与非流式调用FunctionInvocationContext与PromptRenderContext都携带IsStreaming标志过滤器可据此区分处理kernel.InvokeAsync非流式与kernel.InvokeStreamingAsync流式两种调用模式。官方示例 FunctionInvocationFiltering.cs 展示了同一过滤器同时兼容两种模式的写法DualModeFilter以及流式场景下逐 chunk 改写输出内容的StreamingFunctionFilterExample。七、延伸自动函数调用过滤器IAutoFunctionInvocationFilter在函数过滤器和提示词过滤器之外当前仓库还提供第三类过滤器——自动函数调用过滤器用于拦截 LLM 在 Function Calling工具调用流程中对函数的自动执行。接口定义见 IAutoFunctionInvocationFilter.cspublic interface IAutoFunctionInvocationFilter { Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, FuncAutoFunctionInvocationContext, Task next); }上下文对象 AutoFunctionInvocationContext.cs 额外暴露了ChatHistory对话历史可修改、ChatMessageContent、ToolCallId、RequestSequenceIndex与FunctionSequenceIndex定位当前处于第几轮请求、第几个函数调用以及ExecutionSettings等自动调用专属信息其Result同样是可读写的赋值即可替换自动调用得到的函数结果。官方示例 AutoFunctionInvocationFiltering.cs 展示了典型用法注册过滤器、启用FunctionChoiceBehavior.Required([function], autoInvoke: true)触发自动调用过滤器在await next(context)前后输出请求序号、函数序号与函数总数并可返回覆盖后的结果示例输出为Result from auto function invocation filter.。同样地它支持在kernel.AutoFunctionInvocationFilters上直接 Addpost-construction或通过builder.Services.AddSingletonIAutoFunctionInvocationFilter(...)注入pre-construction。八、Kernel Events 与 Kernel Filters 的取舍最后回到 ADR 的原始决策将两者的适用场景梳理如下维度Kernel Events事件处理器Kernel Filters过滤器依赖注入不支持受限于定义位置完全支持可在独立类中注入任意服务定义位置需在服务可用处定义任意位置Startup.cs或独立文件均可生命周期挂载/摘除时机不明确构建前 DI 注册 构建后 Add/Insert/RemoveAt 明确管理机制通用性.NET 事件机制对新手不友好类似 ASP.NET Action Filters / 中间件生态内通用拦截能力取消执行、改参数、改提示词同等能力 结果覆盖 管道短路从当前仓库实现看过滤器已全面接管事件方案的可扩展能力Kernel 在构建期自动装载 DI 中注册的过滤器运行期通过递归中间件管道按注册顺序执行并提供结果覆盖、提示词改写与短路控制。若你的应用需要可注入、可排序、可移除的横切关注点日志、鉴权、敏感信息过滤、Token 用量统计、提示词审计等Kernel Filters 是比事件处理器更契合的机制官方全部用法示例集中在 dotnet/samples/Concepts/Filtering包含 PIIDetection.cs、RetryWithFilters.cs、MaxTokensWithFilters.cs、TelemetryWithFilters.cs 等进阶场景可继续深入阅读。参考资料架构决策记录docs/decisions/0033-kernel-filters.md过滤器接口与上下文dotnet/src/SemanticKernel.Abstractions/Filters内核过滤器集合与管道实现dotnet/src/SemanticKernel.Abstractions/Kernel.cs官方示例dotnet/samples/Concepts/Filtering【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考