Semantic Kernel Kernel Filters 深度指南:从事件处理器到可注入式过滤器管线的演进与实践
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
Kernel Filters(内核过滤器)是 Semantic Kernel 在 .NET 平台提供的函数调用与提示词渲染拦截机制,用于在函数执行前/后、提示词渲染前/后插入自定义逻辑,支持通过依赖注入(DI)注册、按注册顺序组成管道并随时调整执行顺序。本指南以 0033-kernel-filters.md 这份架构决策记录(ADR)为主体,结合当前仓库 dotnet/src 中的接口实现、Kernel.cs 的过滤器管线代码以及 Concepts/Filtering 下的官方示例,完整还原其设计动机、接口形态、注册方式、执行顺序与实战用法,读完后你将能在自己的 Semantic Kernel 应用中独立实现函数过滤器、提示词过滤器与自动函数调用过滤器。
一、背景:事件处理器方案的局限
Semantic Kernel 早期通过 Kernel Events 与事件处理器来拦截函数执行过程中的事件。其典型写法如下(摘自原 ADR):
ILogger 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.cs:
public interface IFunctionInvocationFilter { Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next); }- 提示词过滤器 IPromptRenderFilter.cs:
public interface IPromptRenderFilter { Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, Task> next); }next委托指向管道中的下一个过滤器;如果过滤器不调用next,则后续过滤器与真正的函数/渲染操作都不会执行——这正是过滤器可以"短路"(short-circuit)调用链、实现提前返回或拒绝执行的关键机制(两个接口的 XML 文档均明确注明此行为)。
上下文对象
接口方法携带的上下文对象承载了过滤器可读写的全部数据:
- FunctionInvocationContext.cs:暴露
Kernel、Function(KernelFunction)、Arguments(KernelArguments,可修改)、Result(FunctionResult,可读写,赋值即可覆盖真实函数结果)、CancellationToken以及IsStreaming(标识当前是流式还是非流式调用)。 - PromptRenderContext.cs:暴露
Kernel、Function、Arguments、ExecutionSettings(PromptExecutionSettings),以及核心的RenderedPrompt属性——过滤器可以查看渲染后的提示词并修改它,最终值就是真正发送给 AI 的提示词;此外还支持直接设置Result来跳过后续函数调用并返回结果。
四、实战一:用过滤器重写事件处理器逻辑
ADR 给出了将上文事件处理器等价改写为过滤器类的示例。把相同逻辑封装成独立类,并通过构造函数注入ILoggerFactory:
public 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-construction)
IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); kernelBuilder.AddOpenAIChatCompletion( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey); // Adding filter with DI (pre-construction) kernelBuilder.Services.AddSingleton<IFunctionFilter, MyFunctionFilter>(); Kernel kernel = kernelBuilder.Build(); var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.");从源码看,Kernel构造函数在构建时会调用AddFilters()方法(见 Kernel.cs),该方法通过this.Services.GetServices<IFunctionInvocationFilter>()、GetServices<IPromptRenderFilter>()、GetServices<IAutoFunctionInvocationFilter>()枚举 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.cs:
FunctionInvocationFilters(IList<IFunctionInvocationFilter>)PromptRenderFilters(IList<IPromptRenderFilter>)AutoFunctionInvocationFilters(IList<IAutoFunctionInvocationFilter>)
多过滤器与运行时调整顺序
注册多个过滤器时,它们按注册顺序依次触发:
kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter1>(); kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter2>(); kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter3>();由于过滤器集合本身是IList<T>,也可以在运行期改变执行顺序或移除某个过滤器:
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( NonNullCollection<IFunctionInvocationFilter>? functionFilters, Func<FunctionInvocationContext, 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); } }即:执行index=0的过滤器时,传入的next委托会递归调用index=1的过滤器,以此类推;当index越界时执行真正的函数调用。因此:
- 在
await next(context)之前写的代码,在函数调用前执行(相当于OnFunctionInvoking); - 在
await next(context)之后写的代码,在函数调用后执行(相当于OnFunctionInvoked); - 不调用
next即可终止管道,跳过函数执行。
提示词渲染的InvokeFilterOrPromptRenderAsync遵循完全相同的模式(Kernel.cs),自动函数调用过滤器的管道则实现在 KernelFunctionInvokingChatClient.cs 的InvokeFilterOrFunctionAsync中。
六、实战三:覆盖结果与修改渲染提示词
过滤器最有价值的实战能力是覆盖执行结果和改写发送给 AI 的提示词。
覆盖函数执行结果
在函数过滤器中,调用next后直接替换context.Result即可让下游拿到过滤器给出的结果。官方示例 FunctionInvocationFiltering.cs 展示了:
builder.Services.AddSingleton<IFunctionInvocationFilter, 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.cs:
public async Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, 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.cs:
public interface IAutoFunctionInvocationFilter { Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, 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上直接 Add(post-construction),或通过builder.Services.AddSingleton<IAutoFunctionInvocationFilter>(...)注入(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),仅供参考