Semantic Kernel Process 与 Dapr 集成实战:在 ASP.NET Core 中构建可持久化的有状态流程
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
本指南以仓库中的 ProcessWithDapr 示例为主体,讲解如何将 Semantic Kernel Process(SK Process)框架托管到 Dapr 运行时中,通过 Dapr Actor 模型获得进程状态的持久化与弹性扩展能力。读完本文,你将掌握在 ASP.NET Core Web API 中注册 Process 相关 Actor、构建带初始状态与循环退出条件的有状态流程、以及通过 HTTP 接口重复调用同一流程实例并观察状态复用的完整实战方法。
背景:为什么用 Dapr 托管 Semantic Kernel Process
Semantic Kernel Process 是一个面向业务编排的框架,允许开发者把复杂的任务拆解为一系列相互连接、通过事件通信的步骤(Step)。当流程需要跨请求、跨进程长期运行时,状态的管理与恢复就成为了关键问题。
Dapr(Distributed Application Runtime)是一个可移植、事件驱动的运行时,官方定位是简化构建运行在云和边缘环境中、具备弹性与状态保持能力的应用。它天然适合承载 Semantic Kernel Process:
- 流程中的每个步骤都可以映射为 Dapr Actor,借助 Actor 的持久化能力保存步骤状态;
- 流程实例可以通过稳定的进程 ID 反复启动、查询与停止,状态不会因请求结束而丢失;
- 无需牺牲性能或可靠性,即可在规模与数量上对流程进行水平扩展。
从仓库源码看,Dapr 运行时(Microsoft.SemanticKernel.Process.Runtime.Dapr)内部正是通过一组 Dapr Actor 来承载流程的各个组成部分,这一点将在下文"理解代码"部分结合源码详细展开。
Demo 架构总览
示例描述的是一个带循环与条件退出的流程:Kickoff 启动后并行触发 A、B 两个步骤,二者完成后汇聚到 C 步骤;C 步骤带有一个"循环计数"状态,每运行一次计数加一,当计数达到 3 时请求退出,否则重新回到 Kickoff 重新开始一轮。其事件流转如下:
这个拓扑对应了 Process 框架的几类核心能力:扇出(fan-out)(Kickoff 同时触发 A 与 B)、汇聚(fan-in)(A、B 的结果都进入 C)、循环(C 未达退出条件时回到 Kickoff)以及条件退出(计数达到阈值后结束流程)。
运行前准备:本地 Dapr 环境
Demo 应用本身是普通的 ASP.NET Core Web API,但流程运行时依赖 Dapr 提供的 Actor 服务。因此在运行之前,必须先在本机完成 Dapr 的本地开发配置,并确保 Dapr 容器处于运行状态,否则示例应用无法正常工作。
准备步骤要点如下:
- 安装 Dapr CLI,并执行 Dapr 本地自托管初始化(该步骤会拉起运行 Actor 所需的 sidecar 容器);
- 确认 Dapr 容器正在运行,可通过
dapr list或docker ps检查; - 选择启动方式:既可以使用 Dapr CLI,也可以使用 Dapr 的 VS Code 扩展。如果希望在代码运行时调试,推荐使用 VS Code 扩展方式。
构建并运行示例
在完成 Dapr 本地环境配置后,按以下步骤运行:
- 构建并运行示例。本地运行 Dapr 服务可通过 Dapr CLI 或 VS Code 扩展完成;
- 服务启动后,会暴露一个监听在 localhost 5000 端口的 API。
第一次调用:初始化流程实例
打开浏览器访问http://localhost:5000/processes/1234,即发起一个Id = "1234"的新流程实例。此时控制台会输出类似下面的日志:
##### Kickoff ran. ##### AStep ran. ##### BStep ran. ##### CStep activated with Cycle = '1'. ##### CStep run cycle 2. ##### Kickoff ran. ##### AStep ran. ##### BStep ran. ##### CStep run cycle 3 - exiting.第一次运行时的关键行为:
CState以Cycle = 1初始化,这正是构建流程时为 CStep 指定的初始状态;CState总共被调用两次,才达到终止条件Cycle >= 3。
第二次调用:状态被持久化复用
保持浏览器停留在该页面并刷新,再次运行同一个流程实例(Id = "1234"),日志变为:
##### Kickoff ran. ##### AStep ran. ##### BStep ran. ##### CStep run cycle 3 - exiting.两次运行的日志并不相同,原因在于:
- 第一次运行时流程尚未执行过,其初始状态来自构建流程时显式指定的状态;
- 第二次运行时,流程从上一次运行结束时的持久化状态继续——
CState初始化即为Cycle = 3(第一次运行的最终状态),因此只被调用一次,且立即命中Cycle >= 3的终止条件。
这正是 Dapr Actor 持久化能力在流程层面的体现:流程实例的状态并不随 HTTP 请求的结束而消失。
新实例:独立状态
若改用http://localhost:5000/processes/ABCD创建一个Id = "ABCD"的新流程实例,它会如预期从初始状态重新开始执行(即与第一次运行 1234 时的日志一致)。这验证了流程状态是以实例 ID 为粒度隔离存储的。
理解代码:从项目结构到 Actor 注册
Demo 的完整代码位于 ProcessWithDapr 示例目录,包含Program.cs、Controllers/ProcessController.cs、ProcessWithDapr.csproj与appsettings.json四个文件。下面按集成流程拆解关键环节。
第一步:项目依赖
在新建 ASP.NET Core Web API 项目后,需要引入两类包。
Semantic Kernel 相关包(版本号以 README 编写时的建议为准):
dotnet add package Microsoft.SemanticKernel --version 1.24.0 dotnet add package Microsoft.SemanticKernel.Process.Core --version 1.24.0-alpha dotnet add package Microsoft.SemanticKernel.Process.Runtime.Dapr --version 1.24.0-alphaDapr 相关包:
dotnet add package Dapr.Actors.AspNetCore --version 1.14.0需要说明的是,当前仓库中的 ProcessWithDapr.csproj 并未使用上述 NuGet 包版本,而是直接通过ProjectReference引用仓库内的三个 Experimental 工程(Process.Abstractions、Process.Core、Process.Runtime.Dapr),并仅以PackageReference引入Dapr.Actors与Dapr.Actors.AspNetCore。此外项目目标框架为net10.0,并在NoWarn中显式忽略了 SKEXP 系列的实验性 API 警告(SKEXP0001、SKEXP0010、SKEXP0040、SKEXP0050、SKEXP0060、SKEXP0080、SKEXP0110),因为 Process 相关组件当前仍属于实验性模块。
第二步:配置 Dapr 与 Kernel
在 Program.cs 中完成三件关键配置:
// Configure the Kernel with DI. This is required for dependency injection to work with processes. builder.Services.AddKernel(); // Configure Dapr builder.Services.AddActors(static options => { // Register the actors required to run Processes options.AddProcessActors(); }); builder.Services.AddControllers(); var app = builder.Build(); // ... app.MapControllers(); app.MapActorsHandlers(); app.Run();逐行解读:
AddKernel():将 Kernel 注册到依赖注入容器。文档明确指出这是流程与依赖注入协同工作的必要条件,ProcessController正是通过构造函数注入Kernel实例的;AddActors(...)与options.AddProcessActors():注册流程运行时所需的全部 Actor 类型。AddProcessActors是 KernelProcessDaprExtensions.cs 提供的扩展方法,其实现注册了 8 个 Actor:
actorOptions.Actors.RegisterActor<ProcessActor>(); actorOptions.Actors.RegisterActor<StepActor>(); actorOptions.Actors.RegisterActor<MapActor>(); actorOptions.Actors.RegisterActor<ProxyActor>(); actorOptions.Actors.RegisterActor<EventBufferActor>(); actorOptions.Actors.RegisterActor<MessageBufferActor>(); actorOptions.Actors.RegisterActor<ExternalEventBufferActor>(); actorOptions.Actors.RegisterActor<ExternalMessageBufferActor>();其中ProcessActor对应整个流程实例,StepActor对应流程中的步骤,EventBufferActor/MessageBufferActor等负责流程内部事件与消息的缓冲与路由。从源码结构看(Process.Runtime.Dapr 目录),该运行时还包含Interfaces(IProcess、IStep、IEventBuffer、IMessageBuffer等 actor 接口)与Serialization(KernelProcessEventSerializer、ProcessMessageSerializer等)两个子目录,分别承载 Actor 间的调用契约与流程状态/消息的序列化逻辑;
MapActorsHandlers():将 Dapr Actor 的 HTTP 处理端点映射到应用,使 sidecar 能够把 Actor 方法调用转发到本服务中的 Actor 实现。
第三步:在 Controller 中构建并启动流程
ProcessController.cs 是 Demo 的核心:它在一个 GET 请求的 action 方法内完成流程的构建、启动与状态查询。
[HttpGet("processes/{processId}")] public async Task<IActionResult> PostAsync(string processId) { var process = this.GetProcess(); var processContext = await process.StartAsync( new KernelProcessEvent() { Id = CommonEvents.StartProcess }, processId: processId); var finalState = await processContext.GetStateAsync(); return this.Ok(processId); }process.StartAsync最终落到 DaprKernelProcessFactory.StartAsync:当传入的processId非空且流程尚未持有 ID 时,会先为流程状态赋上该 ID,再创建DaprKernelProcessContext并调用StartWithEventAsync。从StartAsync的文档注释可以确认:如果流程已存在 ID,传入的processId不会覆盖原有值。
DaprKernelProcessContext(见 DaprKernelProcessContext.cs)以流程 ID 构造ActorId,并通过IActorProxyFactory(注入场景)或静态ActorProxy.Create(非注入场景)创建指向ProcessActor的代理;随后依次执行InitializeProcessAsync(将流程定义下发到 actor)与RunOnceAsync(投递初始事件触发执行)。GetStateAsync则通过 actor 的GetProcessInfoAsync拉取当前流程状态的快照并反序列化为KernelProcess。
流程构建与事件路由
GetProcess()方法展示了完整的 ProcessBuilder 用法:
ProcessBuilder processBuilder = new("ProcessWithDapr"); var kickoffStep = processBuilder.AddStepFromType<KickoffStep>(); var myAStep = processBuilder.AddStepFromType<AStep>(); var myBStep = processBuilder.AddStepFromType<BStep>(); // 为 CStep 指定初始状态 CurrentCycle = 1 var myCStep = processBuilder.AddStepFromType<CStep, CStepState>( initialState: new() { CurrentCycle = 1 }); processBuilder .OnInputEvent(CommonEvents.StartProcess) .SendEventTo(new ProcessFunctionTargetBuilder(kickoffStep)); kickoffStep .OnEvent(CommonEvents.StartARequested) .SendEventTo(new ProcessFunctionTargetBuilder(myAStep)) .SendEventTo(new ProcessFunctionTargetBuilder(myBStep)); myAStep .OnEvent(CommonEvents.AStepDone) .SendEventTo(new ProcessFunctionTargetBuilder(myCStep, parameterName: "astepdata")); myBStep .OnEvent(CommonEvents.BStepDone) .SendEventTo(new ProcessFunctionTargetBuilder(myCStep, parameterName: "bstepdata")); myCStep .OnEvent(CommonEvents.CStepDone) .SendEventTo(new ProcessFunctionTargetBuilder(kickoffStep)); myCStep .OnEvent(CommonEvents.ExitRequested) .StopProcess(); var process = processBuilder.Build();这段代码对应了前文架构图中的全部连接关系:
OnInputEvent(CommonEvents.StartProcess)把外部输入事件路由到 KickoffStep;- KickoffStep 完成后发出
StartARequested,同时扇出到 AStep 与 BStep; - A、B 完成后分别以
astepdata、bstepdata参数名将结果送入 CStep(汇聚); - CStep 发出
CStepDone时回到 KickoffStep 形成循环;发出ExitRequested时调用StopProcess()终止流程。
有状态步骤:状态持久化的关键
CStep是 Demo 中演示状态持久化的核心。它继承自KernelProcessStep<CStepState>,通过重写ActivateAsync在每个执行周期加载持久化或配置的状态:
private sealed class CStep : KernelProcessStep<CStepState> { private CStepState? _state; public override ValueTask ActivateAsync(KernelProcessStepState<CStepState> state) { this._state = state.State!; Console.WriteLine($"##### CStep activated with Cycle = '{state.State?.CurrentCycle}'."); return base.ActivateAsync(state); } [KernelFunction] public async ValueTask DoItAsync(KernelProcessStepContext context, string astepdata, string bstepdata) { this._state!.CurrentCycle++; if (this._state.CurrentCycle >= 3) { Console.WriteLine("##### CStep run cycle 3 - exiting."); await context.EmitEventAsync(new() { Id = CommonEvents.ExitRequested }); return; } Console.WriteLine($"##### CStep run cycle {this._state.CurrentCycle}."); await context.EmitEventAsync(new() { Id = CommonEvents.CStepDone }); } }CStepState是标注了[DataContract]/[DataMember]的可序列化状态对象,字段为int CurrentCycle。注释明确指出:为了让步骤始终从"上一次持久化或配置的状态"开始,必须重写ActivateAsync并使用其提供的状态对象——这正是第二次调用 1234 实例时Cycle = 3能直接生效的原因:状态由 Dapr Actor 持久化,步骤每次激活时从 Actor 状态存储中恢复。
从 Process 框架的通用概念看(参考 GettingStartedWithProcesses 示例),流程步骤分为两类:
- 无状态步骤(Stateless Steps):执行之间不保留任何信息,如 Demo 中的 KickoffStep、AStep、BStep;
- 有状态步骤(Stateful Steps):维护可持久化、可序列化的状态,在流程的后续运行中复用与更新,如 Demo 中的 CStep。
状态版本化的注意事项
一旦有状态的步骤/流程部署上线,版本化就变得至关重要,因为它决定了你能否在改进流程的同时继续读取旧版本步骤产生的状态。相关决策文档收录于 docs/decisions(如 0054-processes.md),示例层面的建议如下:
- 小幅改进:仅涉及步骤改名、步骤版本升级时,应保证旧步骤名到新步骤名的映射,并验证新版本能读取旧版本生成的状态;
- 大幅变更:涉及步骤重构替换时,可能需要自定义新旧状态的等价映射,并配套实现与测试以保证数据完整性与流程连续性。
总结与扩展路径
本文通过 ProcessWithDapr 示例完整走通了"Dapr + Semantic Kernel Process"的集成链路:环境准备、API 调用、状态复用验证,再到Program.cs的 Actor 注册、Controller 中的流程构建与DaprKernelProcessContext的底层状态存取。核心要点可归纳为三条:
- 配置是前提:
AddKernel()、AddActors()+AddProcessActors()、MapActorsHandlers()三者缺一不可,且本地 Dapr 容器必须处于运行状态; - 状态按实例 ID 持久化:同一 ID 的重复调用会从上次的终止状态继续,新 ID 则从初始状态开始;
- 有状态步骤需重写
ActivateAsync:通过KernelProcessStep<TState>与[DataContract]状态对象实现可序列化、可恢复的步骤状态。
想进一步深入 Process 框架,可以继续阅读仓库中的 GettingStartedWithProcesses 系列示例(涵盖循环、扇入扇出、子流程复用、有状态步骤与 Agent 编排等进阶场景)、Process 框架决策文档,以及 Process.Runtime.Dapr 运行时源码(其中 KernelProcessDaprExtensions.cs 的 Actor 注册清单可作为理解运行时内部结构的最佳入口)。
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考