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