Semantic Kernel 内核详解
企业级 AI 应用真正的挑战在于 认知与行动:如何让 AI 理解复杂的业务上下文,如何让它自主调用外部工具,如何编排多步骤的推理任务?
Semantic Kernel (简称 SK)正是微软为 .NET 生态打造的大语言模型(LLM)编排框架。它不仅仅是 OpenAI 的 .NET SDK 封装,更是一个 内核------一个将 LLM 的推理能力与现有 C# 业务逻辑深度融合的桥梁。掌握 SK 的核心抽象,将其作为构建企业智能体的基石。
将深入 Semantic Kernel 的内核,从生命周期管理、提示词工程、函数调用到插件化架构,带你构建一个可扩展、可测试、可观测的 AI 编排层。
1 Kernel 对象生命周期与依赖注入集成
1.1 Kernel 是什么?
Kernel 是 Semantic Kernel 的核心容器,它聚合了三个关键组件:
- AI 服务(如聊天补全、文本嵌入)
- 插件(封装为业务能力的函数集合)
- 记忆(向量存储,用于 RAG)
每个 Kernel 实例代表一个独立的 AI 运行时环境。在 ASP.NET Core 应用中,我们需要谨慎管理 Kernel 的生命周期,确保线程安全、资源复用以及与依赖注入容器的集成。
1.2 构建 Kernel:从 Builder 模式开始
SK 推荐使用 KernelBuilder 来构建实例。一个典型的构建流程包括:
- 添加 AI 服务(OpenAI、Azure OpenAI、本地模型等)
- 注册插件(从类型、对象或目录加载)
- 配置日志和 HttpClient 工厂
csharp
using Microsoft.SemanticKernel;
var builder = Kernel.CreateBuilder();
// 添加 Azure OpenAI 服务
builder.AddAzureOpenAIChatCompletion(
deploymentName: "...",
endpoint: "https://openai.com/",
apiKey: "your-api-key");
// 添加 OpenAI 服务(可选,支持多模型)
builder.AddOpenAIChatCompletion(
modelId: "...",
endpoint: "...",
apiKey: "your-openai-key");
// 添加嵌入生成服务(用于语义内存)
builder.AddAzureOpenAITextEmbeddingGeneration(
deploymentName: "text-embedding-ada-002",
endpoint: "...",
apiKey: "...");
// 添加插件
builder.Plugins.AddFromType<TimePlugin>(); // 从类型加载插件
builder.Plugins.AddFromObject(new EmailPlugin()); // 从实例加载
// 配置日志
builder.Services.AddLogging(configure => configure.AddConsole());
// 使用 IHttpClientFactory(推荐)
builder.Services.AddHttpClient();
var kernel = builder.Build();
1.3 在 ASP.NET Core 中集成 Kernel
Kernel 对象是 线程安全 的,可以被多个请求共享。但考虑到不同的请求可能需要不同的 AI 配置(如不同的部署名称、插件集),更常见的做法是将 Kernel 注册为 Scoped 服务,并在每个请求中根据用户上下文动态构建。
方案一:注册为单例(简单场景)
如果所有请求共享相同的模型和插件,可以将 Kernel 注册为单例,减少构建开销。
csharp
// Program.cs
builder.Services.AddSingleton(sp =>
{
var kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddAzureOpenAIChatCompletion(
deploymentName: config["AI:LLM:DeploymentName"],
endpoint: config["AI:LLM:Endpoint"],
apiKey: config["AI:LLM:ApiKey"]);
kernelBuilder.Plugins.AddFromType<TimePlugin>();
kernelBuilder.Services.AddLogging();
return kernelBuilder.Build();
});
方案二:注册为 Scoped(动态配置)
当不同租户需要不同的模型或插件时,可以使用工厂模式,在请求范围内动态构建 Kernel。
csharp
// 注册一个 KernelFactory
builder.Services.AddScoped<IKernelFactory, KernelFactory>();
// 在服务中注入工厂,按需创建
public class ChatService
{
private readonly IKernelFactory _kernelFactory;
public ChatService(IKernelFactory kernelFactory) => _kernelFactory = kernelFactory;
public async Task<string> ChatAsync(string userInput, string tenantId)
{
var kernel = _kernelFactory.CreateForTenant(tenantId);
return await kernel.InvokePromptAsync(userInput);
}
}
1.4 资源清理与生命周期管理
Kernel 本身不持有非托管资源,但它内部使用的 HttpClient 等对象需要妥善管理。通过 IHttpClientFactory 注册的 HttpClient 会自动复用和清理,无需担心。但如果 Kernel 内部创建了需要释放的对象(如某些插件中的数据库连接),应实现 IDisposable 并通过 kernelBuilder.Services.AddSingleton<T>(...) 注册,由 DI 容器管理生命周期。
2 提示词模板引擎与函数调用(Function Calling)
2.1 提示词模板语法
Semantic Kernel 提供了一套强大的提示词模板引擎,支持变量、函数调用、循环等特性。模板语法以 {``{$variable}} 引用上下文变量,以 {``{namespace.functionName}} 调用插件函数。
基础示例:
csharp
var prompt = """
You are an AI assistant. Answer the user's question using the provided context.
Context: {{$context}}
Question: {{$userInput}}
Answer:
""";
var result = await kernel.InvokePromptAsync(prompt, new()
{
["context"] = "The user is asking about Semantic Kernel.",
["userInput"] = "What is SK?"
});
调用插件函数:在模板中直接调用插件函数,可以实现动态数据注入。
csharp
// 定义插件
public class WeatherPlugin
{
[KernelFunction]
[Description("Get the current weather for a city")]
public string GetWeather(string city)
{
return $"The weather in {city} is sunny, 25°C.";
}
}
// 注册插件
kernel.Plugins.AddFromType<WeatherPlugin>();
// 提示词中调用插件
var prompt = "What's the weather in {{$city}}? {{WeatherPlugin.GetWeather $city}}";
var result = await kernel.InvokePromptAsync(prompt, new() { ["city"] = "London" });
2.2 函数调用(Function Calling)原理
大语言模型原生支持 函数调用 (Function Calling)能力。模型可以根据用户输入,自主决定调用哪个函数,并生成符合函数签名的 JSON 参数。Semantic Kernel 将这个能力封装为 KernelFunction,并自动处理函数调用的往返过程。
手动定义函数:
csharp
// 使用 KernelFunction 特性标注方法
public class EmailPlugin
{
[KernelFunction]
[Description("Send an email to the specified recipient")]
public async Task<string> SendEmailAsync(
[Description("Recipient email address")] string to,
[Description("Email subject")] string subject,
[Description("Email body")] string body)
{
// 实际发送逻辑
await Task.Delay(100); // 模拟异步操作
return $"Email sent to {to}";
}
}
让模型自动调用 :当调用 InvokePromptAsync 时,如果提示词中包含了需要函数调用的逻辑,SK 会自动与 LLM 协商,执行函数并将结果返回给模型。
csharp
kernel.Plugins.AddFromType<EmailPlugin>();
var prompt = "Send an email to john@example.com with subject 'Hello' and body 'How are you?'";
var result = await kernel.InvokePromptAsync(prompt);
// 模型会决定调用 SendEmailAsync 函数,最终返回 "Email sent to john@example.com"
强制函数调用 :某些场景下,我们希望模型必须调用特定函数。可以通过 KernelArguments 的 ExecutionSettings 指定函数名称。
csharp
var arguments = new KernelArguments
{
["function_call"] = "EmailPlugin-SendEmailAsync"
};
var result = await kernel.InvokePromptAsync(prompt, arguments);
2.3 高级模板技巧
- 循环与条件:模板引擎支持简单的循环和条件语句(通过 Handlebars 风格的语法,需启用 Handlebars 插件)。
- 自动函数参数注入:SK 会自动将函数参数与上下文变量匹配,无需手动传递。
- 流式输出 :使用
InvokePromptStreamingAsync可以实时获取 LLM 的流式响应,适合聊天场景。
csharp
await foreach (var update in kernel.InvokePromptStreamingAsync(prompt))
{
Console.Write(update);
}
3 插件化架构:将现有 C# 业务逻辑封装为 AI 能力
插件(Plugin)是 Semantic Kernel 的核心扩展点。通过插件,我们可以将任何现有的 C# 方法、API、数据库操作等暴露给 AI,让 LLM 能够 调用 这些能力来完成复杂任务。
3.1 插件的三种定义方式
1. 从类加载(推荐)
使用 [KernelFunction] 特性标记方法,SK 会自动发现并注册。
csharp
public class OrderPlugin
{
private readonly IOrderRepository _orderRepository;
public OrderPlugin(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
[KernelFunction]
[Description("Get recent orders for a user")]
public async Task<List<Order>> GetRecentOrdersAsync(
[Description("User ID")] string userId,
[Description("Number of days to look back")] int days = 30)
{
return await _orderRepository.GetRecentOrdersAsync(userId, days);
}
}
注册时,需要将依赖的服务也注册到 DI 容器中。
2. 从对象加载
对于已有实例,可以直接添加到插件集合。
csharp
var orderPlugin = new OrderPlugin(orderRepository);
kernel.Plugins.AddFromObject(orderPlugin);
3. 从目录加载(OpenAPI/Swagger)
SK 支持从 OpenAPI 文档自动生成插件,可以快速集成第三方 REST API。
csharp
// 1. 注册 OpenAPI 插件(自动生成设备查询函数)
builder.Plugins.AddFromOpenApiAsync(
pluginName: "DeviceDataPlatform",
openApiDocumentPath: "https://factory-api/device-platform/swagger.json",
executionParameters: new OpenApiFunctionExecutionParameters
{
// 处理 API Key 认证
AuthCallback = async (request, cancellationToken) =>
{
request.Headers.Add("X-API-Key", GetApiKeyFromSecretStore());
await Task.CompletedTask;
},
// 只导入我们关心的操作,避免 AI 产生幻觉
OperationsToExclude = new List<string> { "POST_/write-data", "DELETE_/history" }
}
);
3.2 插件的描述与参数说明
为了让 LLM 正确选择并调用插件,我们必须提供清晰、详细的描述:
- 插件类 :可以添加
[Description]特性,说明插件的整体用途。 - 方法 :
[KernelFunction]的Description参数描述该方法的功能。 - 参数 :使用
[Description]描述每个参数的含义和格式。
良好的描述能够显著提升函数调用的准确性。例如:
csharp
[KernelFunction, Description("Calculates the total price including tax and shipping for a given product")]
public decimal CalculateTotalPrice(
[Description("Product identifier (SKU)")] string sku,
[Description("Quantity, must be positive integer")] int quantity,
[Description("Promo code, optional")] string promoCode = null)
{
// ...
}
3.3 依赖注入与插件实例化
在 ASP.NET Core 中,插件通常会依赖其他服务(如数据库、HttpClient)。我们可以利用 DI 容器来创建插件实例,确保依赖关系被正确解析。
方案一:手动解析(适用于简单场景)
csharp
var orderPlugin = sp.GetRequiredService<OrderPlugin>();
kernel.Plugins.AddFromObject(orderPlugin);
方案二:使用插件工厂(更优雅)
SK 提供了 IPlugin 接口,我们可以自定义一个 PluginLoader 从容器中获取插件实例。
csharp
public class KernelFactory
{
private readonly IServiceProvider _services;
public KernelFactory(IServiceProvider services) => _services = services;
public Kernel CreateKernel()
{
var builder = Kernel.CreateBuilder();
// 添加 AI 服务...
// 从 DI 容器中获取插件并添加
var orderPlugin = _services.GetRequiredService<OrderPlugin>();
builder.Plugins.AddFromObject(orderPlugin);
var emailPlugin = _services.GetRequiredService<EmailPlugin>();
builder.Plugins.AddFromObject(emailPlugin);
return builder.Build();
}
}
3.4 插件的最佳实践
- 单一职责:每个插件应该聚焦于一个明确的业务领域(如订单、用户、库存)。
- 异步友好 :插件方法应返回
Task或ValueTask,避免阻塞。 - 幂等性:如果可能,让插件方法具备幂等性,因为 LLM 可能会重试调用。
- 安全校验:插件方法内部必须进行权限校验,因为 LLM 可能被诱导执行危险操作(如删除数据)。
- 可观测性:在插件方法中添加日志和遥测,记录调用来源、参数和执行耗时。
3.5 插件与 RAG 的结合
插件不仅用于函数调用,还可以用于 检索增强生成(RAG)。通过插件,我们可以将向量数据库查询、搜索引擎结果等包装为函数,让 LLM 在回答问题时自主决定是否检索外部知识。
csharp
public class KnowledgeBasePlugin
{
private readonly IVectorDatabase _vectorDb;
public KnowledgeBasePlugin(IVectorDatabase vectorDb) => _vectorDb = vectorDb;
[KernelFunction]
[Description("Search the internal knowledge base for relevant information")]
public async Task<string> SearchKnowledgeBaseAsync(
[Description("Search query")] string query)
{
var embedding = await GenerateEmbeddingAsync(query);
var results = await _vectorDb.SimilaritySearchAsync(embedding, topK: 3);
return string.Join("\n", results.Select(r => r.Content));
}
}
然后在提示词中,让模型自主决定何时调用 SearchKnowledgeBaseAsync。这样,模型既能利用自身知识,又能按需获取最新信息。
总结
深入 Semantic Kernel 的内核,掌握了:
- Kernel 的生命周期管理:如何在 ASP.NET Core 中集成和复用 Kernel 实例。
- 提示词模板引擎:使用变量、函数调用和流式输出构建动态提示词。
- 函数调用机制:让 LLM 自主选择并调用 C# 方法,实现 AI 与业务逻辑的无缝对接。
- 插件化架构:将现有代码封装为插件,利用依赖注入和描述特性,构建可扩展的 AI 能力层。