
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內(nèi)核過濾器是 Semantic Kernel 在 .NET 平臺提供的函數(shù)調(diào)用與提示詞渲染攔截機制用于在函數(shù)執(zhí)行前/后、提示詞渲染前/后插入自定義邏輯支持通過依賴注入DI注冊、按注冊順序組成管道并隨時調(diào)整執(zhí)行順序。本指南以 0033-kernel-filters.md 這份架構(gòu)決策記錄ADR為主體結(jié)合當(dāng)前倉庫 dotnet/src 中的接口實現(xiàn)、Kernel.cs 的過濾器管線代碼以及 Concepts/Filtering 下的官方示例完整還原其設(shè)計動機、接口形態(tài)、注冊方式、執(zhí)行順序與實戰(zhàn)用法讀完后你將能在自己的 Semantic Kernel 應(yīng)用中獨立實現(xiàn)函數(shù)過濾器、提示詞過濾器與自動函數(shù)調(diào)用過濾器。一、背景事件處理器方案的局限Semantic Kernel 早期通過 Kernel Events 與事件處理器來攔截函數(shù)執(zhí)行過程中的事件。其典型寫法如下摘自原 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.)該方案雖然可用但存在三個明顯問題不支持依賴注入處理器難以訪問應(yīng)用中注冊的特定服務(wù)如ILoggerFactory除非處理器定義在服務(wù)實例恰好可用的同一作用域內(nèi)這限制了處理器在解決方案中的定義位置。生命周期不明確不清楚處理器應(yīng)在應(yīng)用運行的哪個階段掛載到 Kernel也不清楚是否需要以及何時摘除。機制不通用.NET 的事件event與事件處理器模式對未接觸過事件的開發(fā)者并不友好。這些痛點構(gòu)成了本 ADR 的決策驅(qū)動因素。二、決策引入類似 ASP.NET Action Filters 的 Kernel Filters設(shè)計團隊最終決定引入Kernel Filters——一種與 ASP.NET 中 Action Filters 類似的接收內(nèi)核事件的機制。這一決策滿足以下要求對應(yīng) ADR 的 Decision Drivers處理器支持依賴注入便于訪問應(yīng)用內(nèi)注冊的服務(wù)處理器可在解決方案任意位置定義Startup.cs或獨立文件均可不受位置限制在應(yīng)用運行期可以清晰地注冊與移除處理器接收和處理內(nèi)核事件的機制在 .NET 生態(tài)中應(yīng)簡單且通用新方案需支持 Kernel Events 已有的全部能力——取消函數(shù)執(zhí)行、修改 Kernel 參數(shù)arguments、在發(fā)送給 AI 之前修改渲染后的提示詞等。三、兩個核心抽象函數(shù)過濾器與提示詞過濾器ADR 提出兩個新的抽象接口開發(fā)者需要按需實現(xiàn)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的初版設(shè)計隨著 Semantic Kernel 演進當(dāng)前倉庫中的接口已升級為異步中綴async middleware風(fēng)格即把next委托傳入方法由過濾器自行決定何時調(diào)用下一個過濾器或真正的函數(shù)/渲染操作。當(dāng)前的實際接口位于 dotnet/src/SemanticKernel.Abstractions/Filters函數(shù)過濾器 IFunctionInvocationFilter.cspublic interface IFunctionInvocationFilter { Task OnFunctionInvocationAsync(FunctionInvocationContext context, FuncFunctionInvocationContext, Task next); }提示詞過濾器 IPromptRenderFilter.cspublic interface IPromptRenderFilter { Task OnPromptRenderAsync(PromptRenderContext context, FuncPromptRenderContext, Task next); }next委托指向管道中的下一個過濾器如果過濾器不調(diào)用next則后續(xù)過濾器與真正的函數(shù)/渲染操作都不會執(zhí)行——這正是過濾器可以短路short-circuit調(diào)用鏈、實現(xiàn)提前返回或拒絕執(zhí)行的關(guān)鍵機制兩個接口的 XML 文檔均明確注明此行為。上下文對象接口方法攜帶的上下文對象承載了過濾器可讀寫的全部數(shù)據(jù)FunctionInvocationContext.cs暴露Kernel、FunctionKernelFunction、ArgumentsKernelArguments可修改、ResultFunctionResult可讀寫賦值即可覆蓋真實函數(shù)結(jié)果、CancellationToken以及IsStreaming標(biāo)識當(dāng)前是流式還是非流式調(diào)用。PromptRenderContext.cs暴露Kernel、Function、Arguments、ExecutionSettingsPromptExecutionSettings以及核心的RenderedPrompt屬性——過濾器可以查看渲染后的提示詞并修改它最終值就是真正發(fā)送給 AI 的提示詞此外還支持直接設(shè)置Result來跳過后續(xù)函數(shù)調(diào)用并返回結(jié)果。四、實戰(zhàn)一用過濾器重寫事件處理器邏輯ADR 給出了將上文事件處理器等價改寫為過濾器類的示例。把相同邏輯封裝成獨立類并通過構(gòu)造函數(shù)注入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 當(dāng)時的初版接口形態(tài)用于說明設(shè)計意圖對照當(dāng)前倉庫應(yīng)實現(xiàn)IFunctionInvocationFilter.OnFunctionInvocationAsync并在調(diào)用await next(context)之后讀取context.Result.Metadata來記錄 Token 用量。使用依賴注入注冊時寫法完全一致見下節(jié)因此把處理器改造成可注入過濾器的思路沒有變化。五、實戰(zhàn)二過濾器的注冊與生命周期管理過濾器定義好后可在 Kernel 構(gòu)建前后兩個階段進行配置。方式一構(gòu)建前通過依賴注入注冊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構(gòu)造函數(shù)在構(gòu)建時會調(diào)用AddFilters()方法見 Kernel.cs該方法通過this.Services.GetServicesIFunctionInvocationFilter()、GetServicesIPromptRenderFilter()、GetServicesIAutoFunctionInvocationFilter()枚舉 DI 容器中注冊的全部過濾器并裝載進內(nèi)核集合因此只要在 builder 的 Services 中注冊構(gòu)建出的 Kernel 即自動生效。實際示例可參考 Concepts/Filtering/FunctionInvocationFiltering.cs。方式二構(gòu)建后直接添加post-construction// Adding filter after Kernel initialization (post-construction) kernel.FunctionFilters.Add(new MyAwesomeFilter());對應(yīng)到當(dāng)前接口構(gòu)建后添加的寫法為kernel.FunctionInvocationFilters.Add(new MyAwesomeFilter()); kernel.PromptRenderFilters.Add(new FirstPromptFilter(...));官方示例 PromptRenderFiltering.cs 正是采用這種方式在kernel.PromptRenderFilters上直接Add。三種過濾器集合屬性均定義在 Kernel.csFunctionInvocationFiltersIListIFunctionInvocationFilterPromptRenderFiltersIListIPromptRenderFilterAutoFunctionInvocationFiltersIListIAutoFunctionInvocationFilter多過濾器與運行時調(diào)整順序注冊多個過濾器時它們按注冊順序依次觸發(fā)kernelBuilder.Services.AddSingletonIFunctionFilter, Filter1(); kernelBuilder.Services.AddSingletonIFunctionFilter, Filter2(); kernelBuilder.Services.AddSingletonIFunctionFilter, Filter3();由于過濾器集合本身是IListT也可以在運行期改變執(zhí)行順序或移除某個過濾器kernel.FunctionFilters.Insert(0, new InitialFilter()); kernel.FunctionFilters.RemoveAt(1);對應(yīng)當(dāng)前接口的寫法即kernel.FunctionInvocationFilters.Insert(0, ...)/kernel.FunctionInvocationFilters.RemoveAt(...)。過濾器的管道式執(zhí)行原理過濾器之所以按順序觸發(fā)是因為 Kernel.cs 中的InvokeFilterOrFunctionAsync采用遞歸 委托實現(xiàn)了經(jīng)典中間件管道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); } }即執(zhí)行index0的過濾器時傳入的next委托會遞歸調(diào)用index1的過濾器以此類推當(dāng)index越界時執(zhí)行真正的函數(shù)調(diào)用。因此在await next(context)之前寫的代碼在函數(shù)調(diào)用前執(zhí)行相當(dāng)于OnFunctionInvoking在await next(context)之后寫的代碼在函數(shù)調(diào)用后執(zhí)行相當(dāng)于OnFunctionInvoked不調(diào)用next即可終止管道跳過函數(shù)執(zhí)行。提示詞渲染的InvokeFilterOrPromptRenderAsync遵循完全相同的模式Kernel.cs自動函數(shù)調(diào)用過濾器的管道則實現(xiàn)在 KernelFunctionInvokingChatClient.cs 的InvokeFilterOrFunctionAsync中。六、實戰(zhàn)三覆蓋結(jié)果與修改渲染提示詞過濾器最有價值的實戰(zhàn)能力是覆蓋執(zhí)行結(jié)果和改寫發(fā)送給 AI 的提示詞。覆蓋函數(shù)執(zhí)行結(jié)果在函數(shù)過濾器中調(diào)用next后直接替換context.Result即可讓下游拿到過濾器給出的結(jié)果。官方示例 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寫入自定義元數(shù)據(jù)Token 用量、成本等供調(diào)用方或后續(xù)管道讀取。覆蓋渲染后的提示詞在提示詞過濾器中await next(context)之后設(shè)置context.RenderedPrompt即可替換真正發(fā)送給模型的內(nèi)容。官方示例 PromptRenderFiltering.cspublic async Task OnPromptRenderAsync(PromptRenderContext context, FuncPromptRenderContext, Task next) { var functionName context.Function.Name; // 讀取函數(shù)信息 await next(context); // 覆蓋渲染后的提示詞再發(fā)送給 AI context.RenderedPrompt Respond with following text: Prompt from filter.; }調(diào)用await kernel.InvokePromptAsync(Hi, how can you help me?)時模型實際收到的將是Respond with following text: Prompt from filter.而非原始提示詞。這正是 ADR 決策要求中在發(fā)送給 AI 之前修改渲染后的提示詞這一能力的落地實現(xiàn)。流式與非流式調(diào)用FunctionInvocationContext與PromptRenderContext都攜帶IsStreaming標(biāo)志過濾器可據(jù)此區(qū)分處理kernel.InvokeAsync非流式與kernel.InvokeStreamingAsync流式兩種調(diào)用模式。官方示例 FunctionInvocationFiltering.cs 展示了同一過濾器同時兼容兩種模式的寫法DualModeFilter以及流式場景下逐 chunk 改寫輸出內(nèi)容的StreamingFunctionFilterExample。七、延伸自動函數(shù)調(diào)用過濾器IAutoFunctionInvocationFilter在函數(shù)過濾器和提示詞過濾器之外當(dāng)前倉庫還提供第三類過濾器——自動函數(shù)調(diào)用過濾器用于攔截 LLM 在 Function Calling工具調(diào)用流程中對函數(shù)的自動執(zhí)行。接口定義見 IAutoFunctionInvocationFilter.cspublic interface IAutoFunctionInvocationFilter { Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, FuncAutoFunctionInvocationContext, Task next); }上下文對象 AutoFunctionInvocationContext.cs 額外暴露了ChatHistory對話歷史可修改、ChatMessageContent、ToolCallId、RequestSequenceIndex與FunctionSequenceIndex定位當(dāng)前處于第幾輪請求、第幾個函數(shù)調(diào)用以及ExecutionSettings等自動調(diào)用專屬信息其Result同樣是可讀寫的賦值即可替換自動調(diào)用得到的函數(shù)結(jié)果。官方示例 AutoFunctionInvocationFiltering.cs 展示了典型用法注冊過濾器、啟用FunctionChoiceBehavior.Required([function], autoInvoke: true)觸發(fā)自動調(diào)用過濾器在await next(context)前后輸出請求序號、函數(shù)序號與函數(shù)總數(shù)并可返回覆蓋后的結(jié)果示例輸出為Result from auto function invocation filter.。同樣地它支持在kernel.AutoFunctionInvocationFilters上直接 Addpost-construction或通過builder.Services.AddSingletonIAutoFunctionInvocationFilter(...)注入pre-construction。八、Kernel Events 與 Kernel Filters 的取舍最后回到 ADR 的原始決策將兩者的適用場景梳理如下維度Kernel Events事件處理器Kernel Filters過濾器依賴注入不支持受限于定義位置完全支持可在獨立類中注入任意服務(wù)定義位置需在服務(wù)可用處定義任意位置Startup.cs或獨立文件均可生命周期掛載/摘除時機不明確構(gòu)建前 DI 注冊 構(gòu)建后 Add/Insert/RemoveAt 明確管理機制通用性.NET 事件機制對新手不友好類似 ASP.NET Action Filters / 中間件生態(tài)內(nèi)通用攔截能力取消執(zhí)行、改參數(shù)、改提示詞同等能力 結(jié)果覆蓋 管道短路從當(dāng)前倉庫實現(xiàn)看過濾器已全面接管事件方案的可擴展能力Kernel 在構(gòu)建期自動裝載 DI 中注冊的過濾器運行期通過遞歸中間件管道按注冊順序執(zhí)行并提供結(jié)果覆蓋、提示詞改寫與短路控制。若你的應(yīng)用需要可注入、可排序、可移除的橫切關(guān)注點日志、鑒權(quán)、敏感信息過濾、Token 用量統(tǒng)計、提示詞審計等Kernel Filters 是比事件處理器更契合的機制官方全部用法示例集中在 dotnet/samples/Concepts/Filtering包含 PIIDetection.cs、RetryWithFilters.cs、MaxTokensWithFilters.cs、TelemetryWithFilters.cs 等進階場景可繼續(xù)深入閱讀。參考資料架構(gòu)決策記錄docs/decisions/0033-kernel-filters.md過濾器接口與上下文dotnet/src/SemanticKernel.Abstractions/Filters內(nèi)核過濾器集合與管道實現(xiàn)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創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考