程序员的AI体验(九):Semantic Kernel 流程框架
2025-06-07 15:58:26一、流程框架设计理念
- 探索性AI与企业级应用差异:探索性AI高度自主,适合创意生成;企业级应用需确定性、可重复性、可审计性。
- 流程框架关键转变:从“让AI决定做什么”转变为“让AI高效执行预定义流程”,开发者通过声明式语法控制业务流程。
- 应用场景与优势:插件数量多时,单纯依靠大语言模型编排函数调用不可靠,流程框架可解决此挑战。
- 架构基础:事件驱动架构
- 优势
- 高度解耦与模块化:每个步骤独立开发、测试和替换,提升系统可维护性。
- 异步与并发:步骤发布事件后无需等待响应,实现高性能并行处理。
- 灵活性与可扩展性:修改事件路由规则或增加新步骤即可适应业务变更。
- 云原生环境支持
- 支持本地与分布式部署:流程框架支持本地开发环境及Dapr、Orleans等分布式云原生环境。
- 无缝迁移与扩展:本地定义的流程可无缝迁移到生产环境,无需修改核心业务逻辑。
- 优势
- 核心概念详解
- 流程
- 定义与构成:流程是顶层容器,由步骤和事件路由规则构成,代表端到端业务目标。
- 生命周期与状态:每个流程实例有独立生命周期和状态。
- 步骤
- 基本执行单元:步骤是流程的原子性活动,封装具体业务逻辑,遵循单一职责原则。
- 内核函数:步骤类中用[KernelFunction]标记的方法,可被流程框架调用。
- 事件
- 信息传递与触发载体:事件由特定类表示,包含Id和Data属性,用于步骤间传递信息。
- 事件路由:事件路由基于Id,决定事件的流向。
- 上下文
- 运行时关键组件:上下文对象提供运行时信息和提交事件的方法,实现复杂流程控制逻辑。
- 流程
- SK的流程框架还在预发布版本,最终版代码可能会有偏差,但不影响我们学习。
二、构建第一个线性流程
1. 环境准备与项目设置
- 创建.NET控制台应用程序项目,安装Semantic Kernel流程框架核心依赖包。
- Process.Core是核心库
- Process.LocalRuntime提供本地运行组件。
<PropertyGroup>
<NoWarn>SKEXP0080</NoWarn> <!-- 关闭警告 -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.8" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.62.0" />
<PackageReference Include="Microsoft.SemanticKernel.Process.Core" Version="1.62.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Process.LocalRuntime" Version="1.62.0-alpha" />
</ItemGroup>
2. 定义流程步骤
- 信息收集步骤:收集产品信息,返回信息作为事件载荷传递给下一步。
// Steps/GatherProductInfoStep.cs
// 步骤1:信息收集步骤,用于收集产品信息
// 请注意,每个步骤类都需要继承自 KernelProcessStep
public class GatherProductInfoStep : KernelProcessStep
{
// 使用 [KernelFunction] 特性来标记这个方法是一个可被流程调用的内核函数
[KernelFunction]
// 定义一个内核函数,它接收产品名称作为输入,并返回收集到的产品信息字符串
public string GatherProductInformation(string productName)
{
// 在控制台打印日志,方便我们追踪流程的执行情况
Console.Write($"- 正在为产品 '{productName}' 收集信息...");
// 为了简化演示,这里我们返回一个硬编码的字符串作为产品信息
// 在实际应用中,这里可能会是调用API、查询数据库或执行网络爬虫等复杂操作
var productInfo = $"产品 '{productName}' 是一款AI开发平台,能够帮助开发者高效构建智能应用。";
Console.WriteLine($"信息收集完成。");
// 返回收集到的信息,这个返回值将作为事件的载荷,自动传递给下一个步骤
return productInfo;
}
}
- 文档生成步骤:根据产品信息生成文档内容。
// Steps/GenerateDocumentationStep.cs
// 步骤2: 文档生成,用于根据产品信息生成文档
public class GenerateDocumentationStep : KernelProcessStep
{
[KernelFunction] // 同样标记为内核函数
public string GenerateDocument(string productInfo)
{
// 在控制台打印日志
Console.Write("- 根据提供的信息生成文档...");
// 这里我们简单地通过字符串拼接来模拟文档生成过程
var generatedDocument = $"敏感词 # 用户手册\n\n## 简介\n{productInfo}\n\n## 功能特性\n- 特性A\n- 特性B\n- 特性C";
Console.WriteLine("文档生成成功。");
// 返回最终生成的文档内容
return generatedDocument;
}
}
- 文档发布步骤:接收文档内容并模拟发布操作。
// Steps/PublishDocumentationStep.cs
// 步骤3:文档发布,用于将生成的文档进行发布
public class PublishDocumentationStep : KernelProcessStep
{
// 定义一个内核函数,接收文档内容,并模拟发布操作
// 注意这个函数没有返回值,因为它是一个流程的终点
[KernelFunction]
public void PublishDocument(string document)
{
Console.WriteLine($"- 正在发布文档...");
// 为了直观展示,我们直接将文档内容打印到控制台
Console.WriteLine("-------------------- 文档内容 --------------------");
Console.WriteLine(document);
Console.WriteLine("-------------------------------------------------");
Console.WriteLine("- 文档发布成功!");
}
}
3. 编排工作流
流程构建器:使用ProcessBuilder定义事件流转路径,构建完整工作流。
事件流转逻辑:定义入口点、步骤间流转逻辑,形成线性执行链条。
// Program.cs
// 首先,创建一个流程构建器的实例,并为我们的流程起一个名字,例如 "DocumentationGenerationProcess"
var processBuilder = new ProcessBuilder("DocumentationGenerationProcess");
// 接下来,使用 AddStepFromType<T> 方法将我们刚才定义好的三个步骤类添加到流程中
// 这个方法会让流程框架来负责步骤实例的创建和生命周期管理
// 它会返回一个 ProcessStepBuilder 对象,我们稍后会用它来定义事件路由
var gatherProductInfoStep = processBuilder.AddStepFromType<GatherProductInfoStep>();
var generateDocumentationStep = processBuilder.AddStepFromType<GenerateDocumentationStep>();
var publishDocumentationStep = processBuilder.AddStepFromType<PublishDocumentationStep>();
// 现在,开始定义事件的编排逻辑,也就是我们工作流的执行顺序
// 1. 定义整个流程的入口点
processBuilder
// OnInputEvent("Start") 表示这个流程会监听一个来自外部、ID为 "Start" 的输入事件
// 我们可以把它理解为整个流程的启动信号
.OnInputEvent("Start")
// 当 "Start" 事件发生时,将该事件(及其携带的数据)发送到 "信息收集" 步骤进行处理
.SendEventTo(new ProcessFunctionTargetBuilder(gatherProductInfoStep));
// 2. 定义第一个步骤完成后的流转逻辑
gatherProductInfoStep
// OnFunctionResult() 是一个非常方便且常用的方法
// 它表示当 "信息收集" 步骤中的任何一个内核函数成功执行并返回结果后,就会自动触发一个内部事件
// 这个事件的 Data 属性会被自动填充为该函数的返回值
.OnFunctionResult()
// 将这个由函数结果触发的事件,发送到 "文档生成" 步骤
.SendEventTo(new ProcessFunctionTargetBuilder(generateDocumentationStep));
// 3. 定义第二个步骤完成后的流转逻辑
generateDocumentationStep
// 同样地,当 "文档生成" 步骤的内核函数执行完毕并返回结果后
.OnFunctionResult()
// 将其结果事件传递给最终的 "文档发布" 步骤
.SendEventTo(new ProcessFunctionTargetBuilder(publishDocumentationStep));
4. 启动与执行流程
- 创建Kernel实例:即使不调用大语言模型,流程框架运行也需要Kernel实例。
- 构建与启动流程:调用Build方法生成可执行流程对象,通过StartAsync方法启动流程。
// Program.cs
// 接上面代码
var kernel = builder.Build();
// 调用流程构建器的 Build() 方法,根据我们之前的定义,生成一个可执行的 KernelProcess 对象
var process = processBuilder.Build();
// 现在,定义一个初始事件,用于启动整个流程
var startEvent = new KernelProcessEvent
{
// 这个事件的ID必须与我们在编排中定义的入口事件ID "Start" 完全匹配
Id = "Start",
// 事件的 Data 属性将作为第一个步骤的输入参数
// 在这里,我们传入一个产品名称 "Semantic Kernel"
Data = "Semantic Kernel"
};
// 调用流程的 StartAsync 方法,异步启动整个流程
// 我们需要传入 Kernel 实例和我们创建的初始事件
Console.WriteLine("流程即将启动...");
await process.StartAsync(kernel, startEvent);
Console.WriteLine("流程执行完毕...");
5. 在流程中集成AI服务
- 通过用户机密配置AI服务的ApiKey。
- 注册OpenAI聊天补全服务,使Kernel具备与远端AI模型通信能力。
- 定义AI驱动步骤:创建AIGenerateDocumentationStep,调用AI模型生成文档。
// Steps/AIGenerateDocumentationStep.cs
// 定义一个由AI驱动的、全新的文档生成步骤
public class AIGenerateDocumentationStep : KernelProcessStep
{
// 依然是用 [KernelFunction] 标记内核函数
[KernelFunction]
// 请特别注意这个函数的签名:我们增加了一个 Kernel kernel 参数
// 流程框架非常智能,它会自动将我们配置好的 Kernel 实例注入到这里
public async Task<string> GenerateDocumentAsync(string productInfo, Kernel kernel)
{
Console.Write($"- 调用AI模型以生成文档...");
// 定义一个高质量的提示(Prompt)模板
const string prompt = """
你是一名专业的技术文档撰写人。
根据以下产品信息,为产品撰写一份简洁、专业的用户手册。
产品信息:
{{$productInfo}}
用户手册:
""";
// 使用注入的 Kernel 对象来调用AI模型
// InvokePromptAsync 方法会自动将 productInfo 变量填充到提示模板的占位符中,然后发送给AI
var result = await kernel.InvokePromptAsync(
prompt,
new KernelArguments { { "productInfo", productInfo } }
);
// 从AI的返回结果中,安全地获取生成的文本内容
var generatedDocument = result.GetValue<string>();
Console.WriteLine($"AI文档生成成功。");
// 将AI生成的结果作为函数的返回值,传递给流程的下一步
return generatedDocument;
}
}
- 替换模拟步骤:在流程编排中用AI步骤替换模拟步骤,无需改动事件路由逻辑。
var generateDocumentationStep = processBuilder.AddStepFromType<GenerateDocumentationStep>();
// 替换成
var generateDocumentationStep = processBuilder.AddStepFromType<AIGenerateDocumentationStep>();
三、高级流程编排功能
实现有状态的步骤
- 有状态步骤需求:一些复杂场景中,步骤需维护内部状态并在多次调用间持续存在。
- 状态管理机制:流程框架通过泛型步骤类和运行时自动管理状态的持久化与恢复。
// GenerateDocumentationState.cs
// 定义一个用于存储文档生成步骤状态的类
public class GenerateDocumentationState
{
// 使用一个列表来记录所有生成过的文档内容的历史版本
public List<string> DocumentHistory { get; set; } = new();
// 同时,记录一下最后一次生成的文档是什么
public string? LastGeneratedDocument { get; set; }
}
// Steps/StatefulGenerateDocumentationStep.cs
public class StatefulGenerateDocumentationStep : KernelProcessStep<GenerateDocumentationState>
{
// 1. 在步骤内部定义一个私有字段,用于在运行时持有当前的状态
private GenerateDocumentationState _state = new();
// 2. 重写 ActivateAsync 方法,这是框架注入状态的入口
public override ValueTask ActivateAsync(KernelProcessStepState<GenerateDocumentationState> state)
{
// 将框架从持久化存储中加载的状态,赋值给我们内部的私有字段
this._state = state.State!;
return base.ActivateAsync(state);
}
// 3. 在内核函数中,我们就可以自由地读取和修改这个状态字段了
[KernelFunction]
public string GenerateDocument(string productInfo)
{
Console.WriteLine($": 正在根据提供的信息生成文档...");
// 根据历史记录计算当前的版本号
var version = this._state.DocumentHistory.Count + 1;
var generatedDocument = $"# 产品手册 v{version}\n\n{productInfo}";
// 操作状态:更新最后一次生成的文档,并将其添加到历史记录中
this._state.LastGeneratedDocument = generatedDocument;
this._state.DocumentHistory.Add(generatedDocument);
Console.WriteLine($": 已生成第 {version} 版文档。历史版本数: {this._state.DocumentHistory.Count}。");
// 正常返回生成的文档
return generatedDocument;
}
}
自定义事件与条件分支
- 条件分支实现方式:步骤根据业务逻辑发布不同自定义事件,编排阶段定义不同事件的路由路径。
- 审核流程示例:构建内容审核流程,根据审核结果发布不同事件,实现条件路由。
// Steps/ReviewStep.cs
// 审核步骤
public class ReviewStep : KernelProcessStep
{
// 我们先用常量来定义两个自定义事件的ID,这是一个好习惯,可以避免拼写错误
public const string ReviewApprovedEvent = "ReviewApproved";
public const string ReviewRejectedEvent = "ReviewRejected";
// 定义内核函数,它将实现审核逻辑,并根据结果发布不同的事件
[KernelFunction]
public async Task ReviewContent(
string document,
// 关键点:在函数的参数列表中,声明一个 KernelProcessStepContext 类型的参数
// 流程框架的运行时会自动将这个上下文对象注入进来
KernelProcessStepContext context)
{
Console.Write($"- 正在审核文档内容...");
// 这是一个简化的审核逻辑,用于演示
// 在真实场景中,这里可能是调用一个专业的内容安全API或一个复杂的规则引擎
if (document.Contains("敏感词"))
{
Console.WriteLine($"- 发现敏感内容。发布 '{ReviewRejectedEvent}' 事件。");
// 使用上下文对象的 EmitEventAsync 方法来发布一个我们自定义的“拒绝”事件
await context.EmitEventAsync(new KernelProcessEvent
{
Id = ReviewRejectedEvent,
Data = "文档包含违规内容,已被拒绝。" // 我们还可以在事件中附带一些说明数据
});
}
else
{
Console.WriteLine($"- 内容合规。发布 '{ReviewApprovedEvent}' 事件。");
// 否则,就发布一个“通过”事件
await context.EmitEventAsync(new KernelProcessEvent
{
Id = ReviewApprovedEvent,
Data = document // 在这个事件中,我们将审核通过的原文内容继续传递下去
});
}
}
}
// 通知步骤
// 定义一个简单的通知步骤,用于处理被拒绝的情况
public class NotificationStep : KernelProcessStep
{
[KernelFunction]
public void SendNotification(string message)
{
Console.WriteLine($"- 发送通知:{message}");
}
}
- 调用
// Program.cs
// 1. 注入2个新的步骤
var notificationStep = processBuilder.AddStepFromType<NotificationStep>();
var reviewStep = processBuilder.AddStepFromType<ReviewStep>();
// 2. 把文档发布后的步骤链接到审核
generateDocumentationStep
// 同样地,当 "文档生成" 步骤的内核函数执行完毕并返回结果后
.OnFunctionResult()
// 将其结果事件传递给最终的 "文档发布" 步骤
.SendEventTo(new ProcessFunctionTargetBuilder(publishDocumentationStep));
// 改成
generateDocumentationStep
// 同样地,当 "文档生成" 步骤的内核函数执行完毕并返回结果后
.OnFunctionResult()
// 将其结果事件传递给最终的 "文档发布" 步骤
.SendEventTo(new ProcessFunctionTargetBuilder(reviewStep));
// 3. 为审核步骤添加新流程
// 这里是条件路由的关键定义
// 我们开始监听来自 "reviewStep" 步骤发出的事件
reviewStep
// OnEvent 方法允许我们精确地指定要监听的事件ID
.OnEvent(ReviewStep.ReviewApprovedEvent)
// 如果监听到 "审核通过" 事件,则将任务路由到 "发布" 步骤
.SendEventTo(new ProcessFunctionTargetBuilder(publishDocumentationStep));
// 同时,我们为同一个审核步骤定义另一条路由规则
reviewStep
// 监听 "审核拒绝" 事件
.OnEvent(ReviewStep.ReviewRejectedEvent)
// 如果监听到该事件,则将任务路由到 "通知" 步骤
.SendEventTo(new ProcessFunctionTargetBuilder(notificationStep));
- 测试
// 修改GenerateDocumentationStep内容,添加"敏感词"三个字,测试自动拒绝
var generatedDocument = $"敏感词 # 用户手册\n\n## 简介\n{productInfo}\n\n## 功能特性\n- 特性A\n- 特性B\n- 特性C";
人机协作模式
人机协作实现方式:利用步骤内核函数参数机制和外部消息通道实现人机协作。
一个步骤的内核函数只有在它所有参数都接收到数据之后,才会被框架调用。
函数(参数1,参数2)
让流程能够与外部世界双向通信
- 流程向外部发送信号:代理步骤
- 流程等待外部输入:空闲状态输入信息
- 外部流程输入信息:满足参数需求,恢复执行
审批流程示例:构建文档发布审批流程,实现流程暂停等待人工审批后再继续执行。
- 模拟一个外部通道
// Steps/InMemoryMessageChannel.cs
// 这是一个模拟外部消息系统的简单内存实现
public class InMemoryMessageChannel : IExternalKernelProcessMessageChannel
{
// 用一个布尔值来记录是否收到了需要用户审核的请求
public bool RequestUserReview;
// 当流程中的 ProxyStep 调用 EmitExternalEvent 时,此方法会被触发
public Task EmitExternalEventAsync(string eventName, KernelProcessProxyMessage message)
{
Console.WriteLine($"[外部通道]: 接收到流程发出的事件 '{eventName}',请求人工审核。");
// 根据事件名称设置状态,以便外部程序查询
RequestUserReview = eventName == "RequestUserReview";
return Task.CompletedTask;
}
// 在这个简单的示例中,我们不需要复杂的初始化和反初始化逻辑
public ValueTask Initialize() => ValueTask.CompletedTask;
public ValueTask Uninitialize() => ValueTask.CompletedTask;
}
- 添加审核步骤
// Steps/ApprovalPublishStep.cs
// 定义一个需要人工审批的发布步骤
public class ApprovalPublishStep : KernelProcessStep
{
[KernelFunction]
// 关键改动:在参数列表中增加了一个 'userApproval' 参数
// 这意味着,此函数现在需要两个输入:“document”和“userApproval”都满足后才能执行
public void PublishDocument(string document, bool userApproval)
{
Console.WriteLine($"- 接收到审批结果:{(userApproval? "批准" : "拒绝")}");
// 根据审批结果,决定是否执行真正的发布操作
if (userApproval)
{
Console.WriteLine($"- 正在发布文档...");
Console.WriteLine("-------------------- 文档内容 --------------------");
Console.WriteLine(document);
Console.WriteLine("-------------------------------------------------");
Console.WriteLine($"- 文档发布成功!");
}
else
{
Console.WriteLine($"- 文档被拒绝,不执行发布。");
}
}
}
- 编排
// 1. 把发布步骤改成审核步骤
var publishDocumentationStep = processBuilder.AddStepFromType<PublishDocumentationStep>();
// 改成
var publishDocumentationStep = processBuilder.AddStepFromType<ApprovalPublishStep>();
// 2. 添加一个代理步骤
var proxyStep = processBuilder.AddProxyStep("proxyStep", ["RequestUserReview"]);
// 3. 在审核通过步骤添加一个动作
reviewStep
// OnEvent 方法允许我们精确地指定要监听的事件ID
.OnEvent(ReviewStep.ReviewApprovedEvent)
// 动作1:通过 ProxyStep 向外部世界发出一个 "RequestUserReview" 事件,请求人工干预
.EmitExternalEvent(proxyStep, "RequestUserReview")
// 如果监听到 "审核通过" 事件,则将任务路由到 "发布" 步骤
.SendEventTo(new ProcessFunctionTargetBuilder(publishDocumentationStep, parameterName: "document"));
// 4. 为2个参数指定参数名
processBuilder
.OnInputEvent("UserApprovalCompleted")
// 将这个外部事件携带的数据,直接路由到“发布”步骤正在等待的“userApproval”参数上
.SendEventTo(new ProcessFunctionTargetBuilder(publishDocumentationStep, parameterName:"userApproval"));
// 5. 创建一个全局外的部事件监听器
// 新增一个全局的外部事件监听器,用于接收人工审批的结果
processBuilder
.OnInputEvent("UserApprovalCompleted")
// 将这个外部事件携带的数据,直接路由到“发布”步骤正在等待的“userApproval”参数上
.SendEventTo(new ProcessFunctionTargetBuilder(publishDocumentationStep, parameterName:"userApproval"));
// 6. 创建外部消息对象
var externalChannel = new InMemoryMessageChannel();
// 调用流程的 StartAsync 方法,异步启动整个流程
// 我们需要传入 Kernel 实例和我们创建的初始事件
Console.WriteLine("流程即将启动...");
await process.StartAsync(kernel, startEvent, externalChannel);
// 流程第一次执行完毕后,我们检查外部通道是否收到了需要审核的请求
if (externalChannel.RequestUserReview)
{
Console.WriteLine("--- 流程已暂停,等待人工操作... ---\n");
// 在这里,我们可以暂停程序,等待用户输入,或者通过API暴露给前端
// 为了演示,我们直接模拟用户批准了该请求
Console.WriteLine("--- 模拟用户批准 ---");
var approvalEvent = new KernelProcessEvent { Id = "UserApprovalCompleted", Data = true };
// 在同一个流程实例上,再次调用 StartAsync,并传入带有审批结果的恢复事件
await process.StartAsync(kernel, approvalEvent);
Console.WriteLine("--- 流程恢复执行并完毕 ---");
}
四、流程框架与智能体框架关系与区别
对比与区别
维度 | 流程框架(Process Framework) | 智能通框架(Agent Framework) |
---|---|---|
控制流 | 确定性的、预先定义的、由开发者硬编码 | 动态的、自主决策的、由LLM在运行时生成 |
自主性 | 低。严格遵循预设的脚本和规则 | 高。能够根据目标自主规划步骤和选择工具 |
可预测性 | 高。执行路径完全可预测,便于调试和审计 | 中/低。执行路径可能因LLM的推理而变化 |
核心优势 | 可靠性、可审计性、可重复性、企业级控制 | 灵活性、适应性、复杂问题解决能力 |
典型场景 | 员工入职理财、订单处理流程、财务审批流、文档自动化发布管道 | 动态研究助手、自动化任务规划器、交互式客服机器人、多工具协同任务 |
混合模式
- 混合模式优势:兼具智能体灵活性和流程可靠性,形成强大认知架构。
- 工作模式:
- 智能体接收任务并分解任务
- 规划与决策
- 任务派发:确定性、标准化子任务。发送一个初始事件,触发一个预定义的流程
- 流程执行
- 结果反馈
最佳实践
- 保持步骤单一职责:设计良好步骤应只关注一件事,提高复用性。
- 定义清晰事件契约:使用常量或枚举定义事件ID,考虑使用DTO类。
- 合理设计状态对象:状态对象应轻量化,避免存储冗余数据。
- 优先使用函数结果传递:简单线性流程优先使用OnFunctionResult()。
- 隔离AI与业务逻辑:分离确定性业务逻辑与非确定性AI生成逻辑。