在 .NET 生态里要把大模型真正用进业务,靠手写 HTTP 调用只能解决”能对话”,却搞不定”会调用工具、能记住上下文、可编排多步任务”。Semantic Kernel 是微软开源的 SDK,它把大模型、插件函数、记忆与规划器统一成一套可组合的运行时。本文用 C# 实战,带你从零跑通 Semantic Kernel 的插件式 AI 编排,并说清它和 Spring AI、LangChain4j 的取舍。
一、什么是 Semantic Kernel:.NET 原生的 AI 编排 SDK
Semantic Kernel(简称 SK)不是一个”框架套框架”的重型中间件,而是一层轻量的编排内核:你向它注册模型、函数和记忆,它负责在运行时把自然语言请求路由到合适的函数,再把结果回填给模型。对 .NET 团队最友好的点是——它原生支持 C# 特性,插件就是普通类加特性标注,无需额外 DSL。
相比直接调 OpenAI 的 chat/completions 接口,SK 的价值在三层:其一,函数即工具,本地 C# 方法可被模型自动调用;其二,连接器抽象,换 Azure OpenAI、OpenAI、Ollama 只改一行配置;其三,规划能力,复杂目标可拆成多个函数链式执行。
二、核心概念:Kernel、插件与连接器
理解三个核心对象就够了:
- Kernel:运行时容器,持有模型服务、插件与记忆。
- Plugin(插件):一组被模型可见的函数,可以是 C# 方法(原生函数),也可以是提示词模板(提示函数)。
- Connector(连接器):对接具体模型供应商的适配层,决定往哪发请求、用什么鉴权。
记忆(Memory)与向量连接器则负责 RAG:把文档切块、向量化后存起来,问答时先检索再生成,避免模型”一本正经胡说”。
三、五分钟跑通:用 Semantic Kernel 调用大模型
新建一个控制台项目,安装官方包后,下面这段就能让模型回答一句话。注意这里用 OpenAI 连接器,把 ApiKey 换成你自己的密钥即可。
// dotnet add package Microsoft.SemanticKernel
using Microsoft.SemanticKernel;
var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
modelId: "gpt-4o-mini",
apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!);
Kernel kernel = builder.Build();
string prompt = "用一句话解释什么是向量数据库。";
string answer = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(answer);
这就是最朴素的用法:InvokePromptAsync 直接把提示词发给模型。但要进入”编排”阶段,关键在插件。
四、插件即函数:用 [KernelFunction] 暴露能力
SK 的精髓是:把你的业务方法标注成模型可调用的函数。下面写一个”查天气”的插件,模型在回答时若需要天气数据,会自己决定调用它。
public class WeatherPlugin
{
[KernelFunction("get_weather")]
[Description("根据城市名称返回当前天气摘要")]
public string GetWeather(
[Description("城市名称,例如 上海")] string city)
{
// 实际项目里这里调气象 API,示例返回模拟数据
return $"{city} 今天多云,气温 22℃,东南风 3 级。";
}
}
// 注册插件到 Kernel
builder.Plugins.AddFromType<WeatherPlugin>();
[KernelFunction] 的函数名和 [Description] 描述会原样送给模型作为”工具清单”,所以描述要写清楚何时用、参数是什么,模型才能正确触发。
五、自动函数调用:让模型自己决定调哪个插件
注册好插件后,开启自动函数调用,模型就能在对话中自主选择并执行函数,再把结果融进最终回答。这和你用 Spring AI 的 Tool Calling 落地大模型 思路一致,区别在于 SK 用 C# 原生标注,无需额外配置类。也可参考 Function Calling 实战 理解底层协议。
var settings = new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};
var result = await kernel.InvokePromptAsync(
"上海现在天气怎么样?适合穿短袖吗?",
new(settings));
Console.WriteLine(result);
运行后你会看到:模型识别到需要天气,自动调用 get_weather("上海"),拿到结果后再用自然语言给出”建议穿薄外套”这类带推理的回答。整个过程我们没有写任何 if/else 去判断该调哪个函数。
六、函数流式输出与中文处理
生产环境里,让回答像打字机一样逐字流出能显著降低等待感。SK 支持用 await foreach 消费流式增量;中文要注意编码器显式指定 UTF-8,避免控制台乱码。
await foreach (var chunk in kernel.InvokePromptStreamingAsync<StreamingTextContent>(
"用三句话介绍 Semantic Kernel 的规划能力。"))
{
Console.Write(chunk.Text);
}
流式输出同样兼容函数调用:模型边思考边可能触发插件,SK 会在内部完成函数执行后继续流出最终文本。如果你还做 Java 后端,SSE 流式推送 一文讲透了服务端如何把这类流推到前端。
七、检索增强(RAG):接入向量与记忆
当模型需要”你公司内部的文档”,就得上 RAG。SK 提供内存向量存储与连接器,本地 demo 用 VolatileVectorStore 即可,生产可换成 PGVector 或 Azure AI Search。下面是把一段文本灌入记忆并检索的骨架:
// 使用 Microsoft.SemanticKernel.Connectors.OpenAI 的 text embedding
var memory = new KernelMemoryBuilder()
.WithOpenAITextEmbeddingGeneration("text-embedding-3-small", apiKey)
.WithOpenAIChatCompletion("gpt-4o-mini", apiKey)
.Build();
await memory.ImportTextAsync("Semantic Kernel 支持 C#、Python 与 Java。");
var ans = await memory.AskAsync("Semantic Kernel 支持哪些语言?");
Console.WriteLine(ans.Result);
这里借用了独立的 Kernel Memory 组件(与 SK 同源),它把切块、向量化、检索、生成封装成一步 AskAsync,比自己拼 HuggingFace + 向量库省心得多。想用 Ollama 本地模型跑这套流程,可参照 Dify + Ollama 私有化部署 的思路准备本地推理服务。
八、Semantic Kernel 与 Spring AI / LangChain4j 怎么选
三者定位相似——都是”给业务代码接大模型”的编排层,差别主要在语言栈与生态。一张表看清:
| 维度 | Semantic Kernel | Spring AI | LangChain4j |
|---|---|---|---|
| 主语言 | .NET / C# | Java / Spring | Java |
| 插件写法 | [KernelFunction] 标注 | @Tool 注解 | @ToolSpecification |
| 规划能力 | 内置 Planner | 需自行编排 | 中等 |
| 适合团队 | 已有 .NET 后端 | Spring 微服务 | 轻量 Java 项目 |
结论很直接:栈里是 C# 就选 SK,是 Java Spring 就选 Spring AI;若想要更贴近 LangChain Python 生态的 Java 体验,LangChain4j 实战 那篇给了完整示例。三者函数调用语义互通,迁移成本主要在标注语法。
九、生产落地清单
- 密钥放配置:ApiKey 走环境变量或 Key Vault,切勿硬编码进仓库。
- 函数描述要诚实:模型靠 [Description] 判断调用时机,含糊的描述会导致误调用。
- 加超时与重试:模型与向量库都可能抖动,套一层 Polly 之类的弹性策略。
- 控制函数数量:一次性暴露几十个函数会让模型”选择困难”,按场景拆成多个小插件。
- 日志可观测:记录每次函数调用的入参与返回,出问题才好复盘。
Semantic Kernel 的魅力在于”轻”:你不必引入整套 AI 平台,就能让现有 .NET 服务拥有调用工具、记忆上下文、编排多步任务的能力。把它当作连接业务代码与大模型的桥梁,先把一个真实插件跑通,再逐步往 RAG 和规划器扩展,是最稳的切入点。




