news 2026/9/11 1:57:30

Semantic Kernel Kernel Hooks(钩子)设计解读:Phase 1 决策记录与源码落地分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Semantic Kernel Kernel Hooks(钩子)设计解读:Phase 1 决策记录与源码落地分析

Semantic Kernel Kernel Hooks(钩子)设计解读:Phase 1 决策记录与源码落地分析

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

本篇文章围绕 Semantic Kernel 仓库中docs/decisions/0005-kernel-hooks-phase1.md这份 ADR(架构决策记录)展开,系统讲解"函数执行前/后拦截"这一核心机制的设计背景、候选方案权衡、最终决策(事件式注册),并结合当前仓库源码验证其落地形态与演进方向(过滤器 filters)。读完本文,你将理解 Semantic Kernel 中FunctionInvoking/FunctionInvoked事件的设计动机、CancelKernelEventArgs的语义,以及为何新代码推荐使用IFunctionInvocationFilter替代事件。

一、问题背景:为什么需要 Kernel Hooks

在 LLM 应用开发中,一个 Kernel 通常会编排多个函数的执行,例如先查询上下文、再渲染 Prompt、最后调用 LLM 并返回结果。开发者常常需要在函数执行之前执行之后两个时间点介入,以实现:

  • 前置拦截(Pre-Execution / Function Invoking):
    • 获取当前SKContext(即当前上下文变量);
    • 修改将要传给函数的输入参数;
    • 中止/取消整个流水线(pipeline)的执行;
    • 跳过某个函数的执行;
  • 后置处理(Post-Execution / Function Invoked):
    • 获取 LLM 模型结果(Token 用量、停止序列等);
    • 获取SKContext与输出参数;
    • 修改输出参数内容(在返回值返回给调用方之前);
    • 取消流水线执行;
    • 重复执行函数。

这份 ADR 记录了 Semantic Kernel(当时 .NET 侧)为满足上述场景而做的第一阶段设计决策。决策驱动因素很明确:架构变更过程对社区透明、决策记录存放在仓库中便于各语言移植团队查阅,并且方案要"简单、可扩展、易于理解"。

值得注意的是,ADR 同时划定了 Phase 1 的范围边界——以下能力被明确放入Phase 2

  • 前置阶段:获取已渲染的 Prompt、获取当前使用的 settings(执行设置)、修改已渲染的 Prompt;
  • 后置阶段:获取已渲染的 Prompt、获取当前使用的 settings。

也就是说,Phase 1 只解决"执行前改参数 / 执行后改结果"的问题,Prompt 内容级别的拦截留待下一阶段。

二、五个候选方案的系统性比较

决策记录完整列出了五种可选实现,并逐一给出了 Pros/Cons:

1. Callback Registration + Recursive(递归回调注册,作用于 Kernel/Plan/Function 三级)

在 plan(计划)和 function(函数)级别以配置方式指定将被触发的回调处理器。

  • 优点:观察/修改数据的常见模式;注册回调时会返回注册对象,可用于将来取消函数执行;递归方式允许对同一事件注册多个回调,也允许在既有回调之上叠加新回调。
  • 缺点:递归方式可能占用更多内存,且在函数或 plan 被释放之前回调可能无法被垃圾回收。

2. Single Callback(单一回调委托)

在 Kernel 级别配置;也可以放在函数构造函数或函数调用参数中。

  • 优点:同样具备"通过委托签名中的参数观察/修改数据"的通用模式。
  • 缺点:一个特定事件(Pre/Post/InExecution)只能注册一个观察方法;如果作为函数调用参数传入,函数签名需要额外增加三个参数。

3. Event Based Registration(基于事件的注册,仅 Kernel 级)——最终选定方案

IKernelISKFunction上暴露事件,调用方可以订阅这些事件进行交互。

  • 优点:同一事件可注册多个监听器;监听器可随时注册/注销;EventArgs是观察和修改数据的通用模式。
  • 缺点:事件处理器是void返回类型,修改数据只能靠引用传递EventArgs;对异步模式/多线程的支撑程度不明确;不支持ISKFunction.InvokeAsync

4. Middleware(中间件,仅 Kernel 级)

在 Kernel 级别配置,仅用于IKernel.RunAsync操作,模式类似 ASP.NET Core 中间件:用 context 和requestDelegate next控制前置/后置条件。

  • 优点:处理前置/后置设置与过滤数据的常见模式。
  • 缺点:函数可以在自己的实例上独立运行,中间件暗示了更高的复杂度,并且需要一个外部容器/管理器(Kernel)来拦截/观察函数调用。

5. ISKFunction Event Support Interfaces(ISKFunction 事件支持接口)

ISKFunction增加可选的事件支撑接口,让函数实现者决定是否支持某个具体事件。ADR 中给出了原型代码:Kernel.RunAsync在执行函数前后分别触发FunctionInvokingFunctionInvoked事件,SemanticFunction通过实现ISKFunctionEventSupport<TEventArgs>来准备事件参数(例如在触发前把已渲染的 Prompt 写入上下文字典)。

  • 优点:Kernel不感知SemanticFunction的具体实现细节;可为自定义ISKFunction扩展专属的EventArgs(包括语义函数的 Prompt);未来新事件可通过ISKFunctionEventSupport<NewEvent>扩展;接口可选,自定义函数可选择不实现。
  • 缺点:自定义函数若要支持事件,必须承担实现ISKFunctionEventSupport接口的责任;Kernel需要检查函数是否实现该接口,否则要抛出异常或忽略事件;原本只需实现InvokeAsync的函数实现者,现在需要把状态管理分散到多个位置。

三、决策结果:采用事件式注册(Event Based Registration)

ADR 最终选择方案 3:Event Base Registration(Kernel only),理由是最简单,并且能直接利用标准 .NET 事件实现的全部能力。

在决策记录中,针对社区常见疑问给出了明确的结论,这些结论也构成了该机制的语义契约:

问题决策结论
后置处理器应在 LLM 结果返回后立即执行,还是函数整个执行结束前执行?当前实现中,后置处理器在函数执行完成之后执行
前置/后置处理器是否应支持多注册(pub/sub,可注册/注销)?标准 .NET 事件天然支持多注册与调用方管理的注销
在既有处理器之上再叠加处理器,应允许还是抛错?标准 .NET 事件行为:不抛错,且会依次执行所有已注册的处理器
在 Plan 上设置的处理器是否应级联到所有内部步骤并覆盖既有处理器?处理器会在每一步执行前后被触发,与 KernelRunAsync流水线的工作方式一致
前置处理器意图取消执行时,链上后续处理器是否还要被调用?标准 .NET 行为:会调用所有已注册的处理器;函数是否执行仅取决于所有处理器都执行完后Cancellation Request的最终状态

其中最后一条尤其关键:它明确了"取消"是一个协作式的最终状态判定——每个处理器都可以设置/改写取消标志,最终结果由事件触发方(Kernel)读取。这一语义在当前的CancelKernelEventArgs.Cancel属性注释中仍能找到一致描述(见 CancelKernelEventArgs.cs)。

四、源码落地:事件机制在仓库中的实际形态

虽然 ADR 写于 2023 年(status: accepted,date: 2023-05-29),但其决策在今天仓库的源码中依然可追溯。在 Kernel.cs 的 Obsolete 区域中,Kernel类暴露了四个事件:

public event EventHandler<FunctionInvokingEventArgs>? FunctionInvoking; public event EventHandler<FunctionInvokedEventArgs>? FunctionInvoked; public event EventHandler<PromptRenderingEventArgs>? PromptRendering; public event EventHandler<PromptRenderedEventArgs>? PromptRendered;

其中FunctionInvoking/FunctionInvoked正是 ADR Phase 1 的核心产出,而PromptRendering/PromptRendered对应 ADR 中规划到 Phase 2 的"渲染后的 Prompt"能力——说明后续版本确实补齐了 Phase 2 的事件。

4.1 事件参数类型体系

当前事件参数类型已从 ADR 原型中的SKContext演进为更通用的KernelArguments

  • KernelEventArgs.cs:抽象基类,继承自EventArgs,携带Function(关联的KernelFunction)、Arguments(调用参数)、Metadata(元数据字典);
  • CancelKernelEventArgs.cs:在基类之上增加bool Cancel属性,语义为"事件触发方(Kernel)在考虑最终结果时读取该值",与 ADR 中"最终状态决定执行与否"的结论一致;
  • FunctionInvokingEventArgs.cs:sealed密封类,在函数即将被调用前触发,可用于修改参数或设置Cancel
  • FunctionInvokedEventArgs.cs:sealed密封类,在函数调用完成之后触发,额外暴露ResultFunctionResult)与内部只读的ResultValue,并提供SetResultValue(object?)方法用于覆盖函数的返回值

SetResultValue的实现验证了 ADR 中"修改输出参数内容(在返回输出之前)"的后置场景确实落地。而 FunctionInvokedEventArgsTests.cs 中的单元测试也印证了这一点:

  • ResultValuePropertyShouldBeInitializedByOriginalOne:构造FunctionInvokedEventArgsResultValue初始化为原始函数结果(测试中用整数 36 验证);
  • ResultValuePropertyShouldBeUpdated:调用SetResultValue(72)后,ResultValue被更新为 72。

4.2 当前的弃用状态:事件 → 过滤器(Filters)演进

需要特别向读者说明的是:在当前仓库版本中,这套事件机制已被标记为[Obsolete],编译警告信息明确写道:

Events are deprecated in favor of filters. Example in dotnet/samples/GettingStarted/Step7_Observability.cs of Semantic Kernel repository.

也就是说,FunctionInvoking/FunctionInvoked事件在现行版本中保留但不再推荐使用,官方推荐的新机制是Kernel Filters(过滤器),对应接口 IFunctionInvocationFilter.cs:

public interface IFunctionInvocationFilter { Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next); }

OnFunctionInvocationAsync在函数调用前被调用,next委托指向过滤器管道中的下一个过滤器或函数本身——如果不调用next,后续过滤器与函数本体都不会执行。这正是 ADR 中"Middleware(方案 4)"思想在 Filter 形态下的回归:既保留了事件式注册"同一事件多处理器、按注册顺序依次执行"的灵活性,又通过next委托提供了显式的中断控制,同时规避了事件机制不支持异步、只能靠引用修改EventArgs的短板。

从演进脉络看,Phase 1 的"执行前改参数、执行后改结果"、"多处理器协作式取消"等核心能力,在过滤器机制中均有对应物:FunctionInvocationContext提供ArgumentsResult等属性,处理器可以修改参数与结果,也可以通过不调用next短路整个调用链。

五、对开发者的实战指引

5.1 如果使用旧版本(事件机制)

在仍使用事件机制的 Semantic Kernel 版本中,可按下述方式订阅:

kernel.FunctionInvoking += (sender, e) => { // 修改输入参数 e.Arguments["prompt"] = "修改后的输入"; // 或取消执行:e.Cancel = true; }; kernel.FunctionInvoked += (sender, e) => { // 读取结果 Console.WriteLine(e.Result); // 覆盖结果:e.SetResultValue("新的返回值"); };

注意:Cancel的协作式语义意味着多个处理器会全部执行,最终是否取消取决于最后一个设置的值。

5.2 新代码推荐使用过滤器

在当前仓库版本中,推荐通过实现IFunctionInvocationFilter并在 DI 容器中注册(AddFunctionInvocationFilter一类扩展方法)来拦截函数调用。可参考仓库中的 Step7_Observability.cs 示例,了解官方推荐的过滤器用法。

5.3 事件与过滤器的能力对照

能力维度事件(Phase 1,已废弃)过滤器(现行推荐)
前置修改参数✅ 通过Arguments✅ 通过FunctionInvocationContext.Arguments
取消执行Cancel = true(协作式最终状态)✅ 不调用next短路
后置读取/修改结果Result/SetResultValue✅ 通过context.Result
多处理器✅ 标准 .NET 事件多订阅✅ 管道式多过滤器
异步支持⚠️ 事件处理器 void,需靠引用传递✅ 原生 async 管道

六、总结

0005-kernel-hooks-phase1.md这份 ADR 完整记录了 Semantic Kernel 函数钩子机制的设计取舍:从五个候选方案中选定"基于事件的注册"作为 Phase 1 方案,明确划定了 Phase 2 的范围(渲染 Prompt 与 settings 的访问/修改),并给出了多处理器取消、级联触发等关键语义的权威解答。这些设计决策在仓库源码中均有迹可循——FunctionInvoking/FunctionInvoked事件及其EventArgs体系是 Phase 1 的直接落地,PromptRendering/PromptRendered事件补齐了 Phase 2 能力,而当前版本中事件被标记为弃用、由IFunctionInvocationFilter过滤器接管,则是这一设计在新架构下的自然演进。对于想要理解 Semantic Kernel 拦截机制的开发者而言,这份 ADR 加上 Kernel.cs 与 Filters 目录 下的实现,构成了从"设计意图"到"代码现实"的完整学习路径。

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 1:56:24

MySQL数据库与表操作完全指南

1. MySQL数据库与表操作完全指南作为关系型数据库的标杆产品&#xff0c;MySQL在Web应用、企业系统和数据分析领域占据着不可替代的地位。我使用MySQL已有八年时间&#xff0c;从最初的简单CRUD操作到现在的复杂架构设计&#xff0c;积累了大量实战经验。本文将系统梳理数据库和…

作者头像 李华
网站建设 2026/9/11 1:55:42

C语言字符串排序实现与PTA题目解析

1. PTA字符串排序题目解析与实现思路这道来自PTA&#xff08;Programming Teaching Assistant&#xff09;平台的经典题目&#xff0c;考察的是对C语言字符串处理能力的掌握程度。题目要求编写程序&#xff0c;实现对多个字符串按字典序进行排序的功能。作为高校编程教学中常见…

作者头像 李华
网站建设 2026/9/11 1:51:18

WorkBuddy开放平台Agent应用开发全流程实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华