Semantic Kernel 实战笔记:插件、记忆与编排——真实踩坑记录
一、为什么我开始研究 Semantic Kernel
故事要从去年说起。
那时候我们团队在做一个企业知识库的智能问答系统,用的 LangChain。说实话,LangChain 做原型很快,但随着业务复杂度上升,链条越来越长,调试越来越痛苦——你懂的,就是那种"链式调用像意大利面条,出错了根本不知道是哪一环断的"的感觉。
然后某天在技术圈子里看到有人推荐 Semantic Kernel(以下简称 SK),说是微软出的,“企业级 AI 编排 SDK”。作为一个在微软生态里泡了十多年的老兵,我本能地想试试——毕竟微软的东西,虽说有时候更新快得让人骂娘,但工程质量和文档水准通常不会太差。
于是我就这么开始了。从第一次 pip install semantic-kernel 到现在踩完各种坑,断断续续搞了大半年。这篇文章就是把过程中的关键知识点和真实踩坑经验整理出来,希望能帮到同样在探索 AI Agent 开发的朋友。
二、初印象:和 LangChain 到底有什么不同
第一次打开 SK 的 GitHub 仓库,我就注意到一个很微软风格的决策:支持多语言。C#、Python、Java,三套 SDK,同一套核心概念。这跟 LangChain 的纯 Python 路线完全不同。
对于我这种主力写 C# 但偶尔也要写 Python 的人来说,这简直是福音。同一套插件概念,同一套编排逻辑,跨语言复用——这在企业项目里太重要了。你不可能让后端团队(写 C#)和数据团队(写 Python)用两套完全不同的框架。
但更核心的差异在于设计哲学:
- LangChain 的思路是"链"(Chain)——把多个步骤像链条一样串起来,每一步的输出是下一步的输入。这在简单的流水线场景很直观,但复杂场景下,调试和维护成本会飙升。
- SK 的思路是"插件+编排"——你把能力封装成插件,然后让 LLM 通过 Function Calling 自动决定调用什么、怎么调用。开发者不需要手动编排执行顺序,AI 自己规划。
这个差异乍一看可能觉得"不就是换了个名字吗",但实际用下来感受很不一样。LangChain 的链是你自己写的,每一步都要自己设计;SK 的插件是你注册的,怎么组合由 AI 决定。前者更像传统编程,后者更像给 AI 一堆工具然后说"你自己想办法"。
我的直觉是:简单场景 LangChain 更快上手,复杂 Agent 场景 SK 更适合长期维护。当然,这只是个人感受,后面我会更详细地对比。
三、插件系统:从 Function 到 Plugin
3.1 插件是什么?不是你以为的那种"插件"
在 SK 里,“插件”(Plugin)不是 WordPress 那种可以装装卸卸的模块,而是AI 可以调用的能力的封装单元。简单说:你有一堆函数,你把它们按功能分组打包,注册到 Kernel 里,AI 就能通过 Function Calling 来调用它们。
一个插件可以包含:
- Native Function——你用 C# 或 Python 写的普通函数,标记一下就能让 AI 调用
- OpenAPI 导入的函数——你已有的 REST API,直接从 Swagger 规范导入
- MCP 服务——Model Context Protocol 的第三方服务
最常用的是 Native Function,下面重点讲。
3.2 定义一个原生插件
核心就一件事:用 @kernel_function(Python)或 [KernelFunction](C#)标记你的方法。
来个实际例子——我做的灯控系统插件(这是 SK 官方文档的入门案例,但我会加点自己的注释):
Python 版:
from typing import Annotated
from semantic_kernel.functions import kernel_function
class LightsPlugin:
"""灯光控制插件——让 AI 能控制你家里的灯"""
def __init__(self, lights: list[dict]):
self._lights = lights
@kernel_function
async def get_lights(self) -> list[dict]:
"""获取所有灯的列表和当前状态"""
return self._lights
@kernel_function
async def change_state(
self,
change_state: dict
) -> dict | None:
"""修改灯的状态(开关、亮度、颜色)"""
for light in self._lights:
if light["id"] == change_state["id"]:
light["is_on"] = change_state.get("is_on", light["is_on"])
light["brightness"] = change_state.get("brightness", light["brightness"])
light["hex"] = change_state.get("hex", light["hex"])
return light
return None
C# 版:
using System.ComponentModel;
using Microsoft.SemanticKernel;
public class LightsPlugin
{
private readonly List<LightModel> _lights;
public LightsPlugin(List<LightModel> lights)
{
_lights = lights;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
return _lights;
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
var light = _lights.FirstOrDefault(l => l.Id == changeState.Id);
if (light == null) return null;
light.IsOn = changeState.IsOn;
light.Brightness = changeState.Brightness;
light.Color = changeState.Color;
return light;
}
}
几个踩坑要点:
-
函数命名用 snake_case——哪怕你写的是 C#!这不是 Bug,是设计决策。因为 LLM 的训练数据以 Python 为主,snake_case 的函数名 AI 理解起来更准确。我在 C# 项目里第一次用 PascalCase 命名函数,AI 时不时调错或者理解偏差,改成 snake_case 之后明显好了。
-
Description 很重要——
@kernel_function标记只是让函数"可见",Description才是让 AI "理解"的关键。写好 Description,比写好代码更重要。我后来养成了习惯:先写 Description,再写代码逻辑。 -
只有标记了的函数才会暴露给 AI——这意味着你可以在同一个类里放辅助方法,只要不加标记,AI 就不会调用它。这个设计很聪明,避免了暴露不需要的内部逻辑。
3.3 注册插件并让它工作
注册很简单:
from semantic_kernel import Kernel
kernel = Kernel()
kernel.add_plugin(LightsPlugin(lights), plugin_name="Lights")
var builder = Kernel.CreateBuilder();
builder.Plugins.AddFromObject(new LightsPlugin(lights));
Kernel kernel = builder.Build();
注册之后,当你开启了自动 Function Calling(下一节详讲),AI 就会根据用户的问题自动决定是否调用这些插件函数。你不需要写任何调度逻辑——这是 SK 最优雅的地方。
3.4 OpenAPI 和 MCP 插件:让现有服务秒变 AI 能力
除了自己写函数,你还可以直接把已有的 API 接入。这在企业场景特别有用——你几十个内部微服务,不可能一个个重写。
OpenAPI 插件——从 Swagger 规范直接导入:
from semantic_kernel.connectors.openapi import OpenAPIPlugin
plugin = await OpenAPIPlugin.from_openapi(
"LightAPI",
uri="https://example.com/openapi.json"
)
kernel.add_plugin(plugin)
一条命令,你的 REST API 就变成了 AI 可以调用的插件。参数类型、请求格式,全部从 OpenAPI 规范自动解析。这在实际项目里省了我不少时间。
MCP 插件——接入 Model Context Protocol 服务:
from semantic_kernel.connectors.mcp import MCPStdioPlugin
async with MCPStdioPlugin(
name="Github",
description="Github Plugin",
command="docker",
args=["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"],
env={"GITHUB_PERSONAL_ACCESS_TOKEN": os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN")},
) as github_plugin:
kernel = Kernel()
kernel.add_plugin(github_plugin)
MCP 是 Anthropic 推出的协议标准,SK 从 v1.6 开始原生支持。这意味着你不仅可以接入 OpenAI 生态的 API,还能接入更广泛的 MCP 服务网络——这在我看来是 SK v1.x 最有远见的一个功能。
3.5 v1.x 的重要变化
如果你看过 SK 的早期版本(v0.x),可能会记得一个叫"Semantic Function"的概念——用 .skprompt.txt 文件定义纯提示词函数。这个概念在 v1.x 已经不再作为独立概念存在了,取而代之的是更统一的 KernelFunction 标记方式。
也就是说,v1.x 的插件系统做了全面重构:旧的 ISKFunction 接口被移除,API 更简洁了,MCP 和 OpenAPI 的集成也增强了。
这个变化对新手来说其实是好事——概念更少了,上手更简单。但对从旧版本迁移过来的人来说,代码要改不少(后面踩坑部分我会详细讲)。
四、记忆机制:让 AI 不只是"金鱼"
4.1 从 SemanticMemory 到 Vector Store——又一次架构演进
SK 的记忆系统经历了和编排一样的大改。
早期版本里,SK 有一个叫 SemanticMemory 的核心模块,提供了 ISemanticTextMemory 接口,用来做向量存储和语义搜索。听起来很美好,但实际用起来有几个问题:
- 抽象层级太高,和具体向量数据库的交互不够灵活
- 接口设计偏向"万能存储",但实际场景千差万别
- 性能调优空间有限
所以在 v1.x 中,微软把这套体系重构为Vector Store(向量存储)集成能力,用专门的 Connector 对接各家向量数据库。旧的 ISemanticTextMemory 接口被标记为实验性/旧版,新项目应该用 Vector Store connectors。
4.2 短期记忆 vs 长期记忆
理解 SK 的记忆机制,最简单的框架就是两分法:
| 维度 | 短期记忆(Chat History) | 长期记忆(Vector Store) |
|---|---|---|
| 实现方式 | ChatHistory 对象管理对话上下文 |
向量数据库 + 嵌入模型 |
| 存储位置 | 内存 | 持久化存储(Azure AI Search、Qdrant 等) |
| 生命周期 | 单次对话 | 跨会话持久保存 |
| 用途 | 对话上下文理解 | 知识和偏好长期存储 |
短期记忆很简单——就是对话历史。SK 用 ChatHistory 对象管理,每轮对话的消息都追加进去,作为下一轮的上下文传给 LLM。这和任何聊天系统的做法一样。
from semantic_kernel.contents import ChatHistory
history = ChatHistory(system_message="You are a helpful assistant.")
history.add_user_message("今天天气怎么样?")
history.add_assistant_message("北京今天晴,25°C。")
history.add_user_message("明天呢?") # AI 能理解"明天"指的是北京的明天
长期记忆才是有意思的部分——用向量数据库存储跨会话的知识和偏好。
4.3 向量存储实战
SK 支持的向量数据库阵容相当豪华:Azure AI Search、Chroma、Elasticsearch、Qdrant、PostgreSQL(pgvector)、Redis、Pinecone、Milvus、Weaviate、DuckDB、CosmosDB(MongoDB vCore)……基本主流的都覆盖了。
⚠️ 重要提示: SK 的向量存储 API 目前正处于新旧过渡期。v1.x 推荐使用新的 Vector Store Connector,但旧的 ISemanticTextMemory 接口(AzureCognitiveSearchMemoryStore 等)仍可用于过渡。下面先用旧接口做一个示例,帮助理解概念——但新项目请务必查最新文档确认 Connector 的用法。
用 Azure AI Search 作为长期记忆的例子(旧接口版,仅供概念演示):
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import AzureTextEmbedding
from semantic_kernel.connectors.memory.azure_cognitive_search import (
AzureCognitiveSearchMemoryStore
)
kernel = Kernel()
# 配置嵌入服务——把文本变成向量
kernel.add_service(
AzureTextEmbedding(
deployment_name="text-embedding-ada-002",
endpoint="https://your-endpoint.openai.azure.com",
api_key="your-api-key"
)
)
# 配置向量存储——Azure AI Search
memory_store = AzureCognitiveSearchMemoryStore(
search_endpoint="https://your-search.search.windows.net",
admin_key="your-admin-key"
)
# 保存信息到长期记忆
await memory_store.save_information_async(
collection="user-preferences",
text="用户喜欢简洁的技术文档",
id="user1_001"
)
# 语义搜索——AI 能"理解"你在找什么
results = await memory_store.search_async(
collection="user-preferences",
query="用户喜欢什么样的文档风格?",
limit=5
)
for result in results:
print(f"Found: {result.text} (relevance: {result.relevance})")
⚠️ 上面这段代码用的是旧版
ISemanticTextMemory接口(AzureCognitiveSearchMemoryStore)。SK v1.x 正在将向量存储重构为统一的 Vector Store Connector 体系,新接口的 API 可能与上述代码不同。如果你的项目要上线,请查阅 最新官方文档 确认当前版本的用法。
这段代码的实际效果是:当你搜索"用户喜欢什么样的文档风格"时,AI 不是做关键词匹配,而是通过语义相似度找到"用户喜欢简洁的技术文档"这条记录——哪怕两者没有一个共同的关键词。
我的使用心得:
- 短期记忆是必须的——没有对话历史,AI 就是金鱼,每轮对话都从零开始。
- 长期记忆看场景——如果你只是做单次问答,不需要向量存储;但如果你的 Agent 需要跨会话记住用户偏好或检索知识库,那这就是核心能力。
- 向量数据库的选择很重要——我实际项目中用的是 PostgreSQL(pgvector),因为我们已经有 PG 集群,不想再引入新的基础设施。Azure AI Search 在云原生场景很方便,但成本需要注意。
- 关于 RAG(检索增强生成)——如果你要做知识库问答,长期记忆 + LLM 就是 RAG 的核心思路:先把知识存到向量数据库,用户提问时检索相关片段,再让 LLM 基于检索结果生成回答。SK 的 Vector Store Connector 就是为此而生的。
⚠️ 注意: 以上 Vector Store API 目前标记为实验性(Experimental),具体用法可能随版本变化。一定要查最新文档,别直接抄我这段代码就上线。
五、编排策略:Planner 的墓碑与 Function Calling 的崛起
5.1 坟墓里的三种 Planner
这是 SK 变化最大的领域,也是我踩坑最惨的地方。
早期 SK(v1.0 之前)提供了三种 Planner:
- Sequential Planner——顺序执行,一步步来
- Action Planner——只执行一个动作
- Stepwise Planner——逐步推理,更像 ReAct 模式
我最初用的就是 Stepwise Planner,当时觉得很酷——给 AI 一堆工具,AI 自己规划怎么一步步完成任务。代码写起来是这样的:
# ⚠️ 这段代码已经不能用了!仅作历史参考
from semantic_kernel.planners import StepwisePlanner
planner = StepwisePlanner(kernel)
plan = await planner.create_plan("帮我订一张明天去上海的机票")
result = await plan.invoke()
然后有一天我更新了 SK 版本,发现这代码直接报错了。查了文档才知道——三种 Planner 全部废弃移除。
是的,微软在文档里直接写了:这些 Planner 已不再支持,现在完全使用 Function Calling 作为主要编排方式。
当时我的心情……怎么说呢,就像你精心装修了一套房子,然后房东告诉你这房子要拆迁了。
5.2 Function Calling:新的编排范式
但冷静下来仔细看,Function Calling 的方案其实比 Planner 更优雅。
工作原理是一个自动循环:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 序列化函数 | 把所有注册的 KernelFunction(含参数 Schema)序列化为 JSON Schema(一种描述数据结构的标准格式) |
| 2 | 发送给模型 | 函数描述 + ChatHistory 一并发给 LLM |
| 3 | 模型处理 | LLM 决定是返回文本消息还是调用某个函数 |
| 4 | 解析函数调用 | SK 解析函数名和参数 |
| 5 | 执行函数 | SK 调用对应的 KernelFunction |
| 6 | 返回结果 | 函数结果返回给 LLM,让它继续推理 |
| 7 | 重复 2-6 | 直到 LLM 返回最终文本响应或达到最大迭代次数 |
关键洞察:你不需要告诉 AI 该怎么做,AI 自己决定。你只需要提供足够的插件函数和清晰的 Description,AI 就能像人类一样"想办法"。
5.3 开启自动 Function Calling
核心概念其实很简单——开启自动 Function Calling 只需一个关键配置 function_choice_behavior="auto":
Python 版完整示例:
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import (
AzureChatCompletion,
OpenAIChatPromptExecutionSettings
)
from semantic_kernel.contents import ChatHistory
# 灯数据
lights = [
{"id": 1, "name": "客厅灯", "is_on": False, "brightness": 0, "hex": "#000000"},
{"id": 2, "name": "卧室灯", "is_on": True, "brightness": 80, "hex": "#FFD700"},
{"id": 3, "name": "厨房灯", "is_on": False, "brightness": 0, "hex": "#000000"},
]
async def main():
kernel = Kernel()
# 1. 添加 AI 服务
kernel.add_service(
AzureChatCompletion(
deployment_name="gpt-4o-deployment",
endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key"
)
)
# 2. 注册插件
kernel.add_plugin(LightsPlugin(lights), plugin_name="Lights")
# 3. 开启自动 Function Calling——这是关键!
settings = OpenAIChatPromptExecutionSettings(
function_choice_behavior="auto"
)
# 4. 开始对话
history = ChatHistory(system_message="You are a helpful assistant that can control lights.")
history.add_user_message("请打开客厅灯和厨房灯")
chat_service = kernel.get_service()
result = await chat_service.get_chat_message_content(
chat_history=history,
settings=settings,
kernel=kernel
)
print(f"AI 回复: {result}")
# AI 会自动调用 Lights.change_state 来打开两盏灯
asyncio.run(main())
C# 版完整示例:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var builder = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o-deployment",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key"
);
builder.Plugins.AddFromObject(new LightsPlugin(lights), "Lights");
Kernel kernel = builder.Build();
var chatService = kernel.GetRequiredService<IChatCompletionService>();
// 开启自动 Function Calling
OpenAIPromptExecutionSettings settings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var history = new ChatHistory("You are a helpful assistant that can control lights.");
history.AddUserMessage("请打开客厅灯和厨房灯");
var result = await chatService.GetChatMessageContentAsync(
history,
executionSettings: settings,
kernel: kernel
);
Console.WriteLine($"AI 回复: {result.Content}");
运行这段代码,当你说"请打开客厅灯和厨房灯"时,AI 会自动识别意图,调用 Lights.change_state 函数两次——分别把客厅灯和厨房灯的状态改为 is_on: True。你不需要写任何调度逻辑。
5.4 多步任务编排:披萨点单的例子
这是官方文档里的经典案例,我第一次看到时确实被惊艳了:
public class OrderPizzaPlugin(
IPizzaService pizzaService,
IUserContext userContext,
IPaymentService paymentService)
{
[KernelFunction("get_pizza_menu")]
public async Task<Menu> GetPizzaMenuAsync()
=> await pizzaService.GetMenu();
[KernelFunction("add_pizza_to_cart")]
[Description("Add a pizza to the user's cart")]
public async Task<CartDelta> AddPizzaToCart(
PizzaSize size,
List<PizzaToppings> toppings,
int quantity = 1,
string specialInstructions = "")
{
// 添加披萨到购物车的逻辑
}
[KernelFunction("remove_pizza_from_cart")]
public async Task<RemovePizzaResponse> RemovePizzaFromCart(int pizzaId)
{
// 从购物车移除披萨的逻辑
}
[KernelFunction("get_cart")]
[Description("Returns the user's current cart")]
public async Task<Cart> GetCart()
{
// 返回当前购物车
}
[KernelFunction("checkout")]
[Description("Checkouts the user's cart")]
public async Task<CheckoutResponse> Checkout()
{
// 结账逻辑
}
}
当用户说"帮我点一个大份的夏威夷披萨"时,LLM 会自动完成四步:
- 调用
get_pizza_menu()——看看菜单上有没有夏威夷披萨 - 调用
add_pizza_to_cart(size="large", toppings=["ham", "pineapple"])——下单 - 调用
get_cart()——确认购物车内容 - 调用
checkout()——完成订单
全部自动完成,不需要你写任何流程控制代码。 这就是 Function Calling 编排的核心魅力——你只需要定义好每一步能做什么,AI 自己决定怎么组合。
5.5 Function Choice Behavior 的三种模式
除了 Auto(),SK 还提供另外两种模式:
| 模式 | 说明 | 用途 |
|---|---|---|
Auto() |
AI 自动决定是否调用函数 | 默认推荐——大多数场景用这个 |
Required() |
强制每次响应都必须调用函数 | 需要确保 AI 一定会执行某个操作的场景 |
None() |
禁止调用函数 | 纯对话场景,不需要任何工具调用 |
我实际项目中 90% 的时间用 Auto()。Required() 只在必须执行操作的场景使用(比如强制查询数据库)。None() 主要在测试时用——先关掉 Function Calling 看看纯对话效果,再打开看差异。
5.6 旧 Planner 的迁移
如果你和我一样,手上有旧的 Stepwise Planner 或 Handlebars Planner 代码,微软提供了迁移指南:
- 迁移文档:Stepwise Planner Migration Guide
- 核心策略:把 Planner 逻辑重构为 Plugin,通过 Function Choice Behavior 自动编排
说白了就是:你以前写在 Planner 里的步骤逻辑,现在应该拆成一个个独立的 Plugin 函数,让 AI 自己组合调用。这确实更灵活,但迁移工作量不小——我花了两周时间重构旧代码。
六、踩坑记录:那些文档里不会告诉你的事
讲完了三大核心,接下来是这篇文章最有价值的部分——我踩过的坑。这些都是实打实的教训,官方文档不会告诉你这些。
6.1 版本更新太快,API 稳定性是最大痛点
SK 的更新频率极高——NuGet 上最新版已经到了 1.77.0。这意味着几乎每隔几天就有新版本,API 变动频繁。
我最惨的一次经历:项目用的是 v0.x 的 Planner,某天例行更新依赖,发现整个编排系统不工作了。查了一圈才知道 Planner 废弃了。这种"更新一下依赖,代码就挂了"的体验,真的让人怀疑人生。
教训:锁定版本号。 不要用 latest 或不指定版本。在 requirements.txt 或 csproj 里写明确版本号,只在充分测试后才升级。
6.2 Description 写不好,AI 就会乱调用
这是新手最容易忽略的问题。SK 的 Function Calling 完全依赖 Description 来让 AI 理解函数的作用。如果你写了一个模糊的 Description,AI 就可能在不该调用的时候调用,或者该调用的时候不调用。
反面教材:
@kernel_function
async def process(self, data: dict) -> dict:
"""Process the data""" # ← 这 Description 有什么用?AI 根本不知道这函数干嘛
...
正面教材:
@kernel_function
async def lookup_order(self, order_id: str) -> Order:
"""根据订单号查询订单详情,包含状态、金额和物流信息""" # ← 清晰、具体
...
我的经验法则: Description 要回答三个问题——这个函数做什么?什么情况下该调用?返回什么?三个都写清楚,AI 的调用准确率会大幅提升。
6.3 函数参数类型要简单
AI 在生成函数调用参数时,是根据 JSON Schema 来构造的。如果你的参数类型太复杂(嵌套对象、泛型集合),AI 构造参数出错的概率会显著增加。
建议:
- 尽量用基本类型(string、int、bool)作为参数
- 需要传复杂对象时,用 dict/Dictionary 作为中间层
- 枚举类型很好用——AI 只需要选一个值,比构造复杂对象容易得多
6.4 并行函数调用的陷阱
OpenAI 1106+ 模型支持并行函数调用——比如你说"把灯1打开、灯2关闭",AI 可以同时调用两个 change_state 函数。
这听起来很高效,但有个陷阱:如果你的函数有副作用(修改共享状态),并行调用可能导致竞态条件。
比如两个并行调用都修改同一个购物车,后执行的可能会覆盖前一个的结果。SK 框架层面没有自动处理这个问题,你需要在插件逻辑里自己保证幂等性或者加锁。
6.5 向量存储 API 还在实验期
前面说过,Vector Store 的 Python API 目前标记为实验性。这意味着:
- 方法名可能改
- 参数可能变
- 甚至整个 Connector 的用法都可能重构
如果你现在要上线 RAG 系统,务必做好版本锁定和抽象层——别让业务代码直接依赖 SK 的 Vector Store API,中间加一层自己的 Repository 接口,这样 SK API 变了,你只需要改 Repository 实现,业务逻辑不用动。
6.6 调试 Function Calling 很痛苦
当 AI 自动调用了一堆函数,但最终结果不对时,调试起来很头疼。因为整个过程是 AI 自己决定的,你很难复现"AI 为什么选择了这条路径"。
我的调试方法:
- 开启 SK 的日志输出(设置
logging到 DEBUG 级别) - 每次函数调用前后都打印 ChatHistory
- 用
FunctionChoiceBehavior.None()先关掉自动调用,手动模拟每一步,确认每个函数单独工作正常 - 再开回
Auto()看整体效果
这不是优雅的方案,但确实有效。希望未来 SK 能提供更好的调用链追踪工具。
七、对比思考:SK vs LangChain vs LlamaIndex
7.1 定位差异
- LangChain——“链式编排”,擅长把多个步骤串成流水线。生态最丰富,社区最活跃,Python 优先。适合快速原型和简单流程。
- LlamaIndex——“数据索引”,擅长把外部数据(文档、数据库、网页)接入 LLM。RAG 场景最专业,数据处理能力强。适合知识库和检索场景。
- Semantic Kernel——“插件+AI编排”,擅长让 AI 自主调用工具完成复杂任务。跨语言支持,企业级设计,微软生态深度绑定。适合 Agent 和自动化场景。
7.2 我的实际选择
坦白说,没有一个框架是万能的。我在不同项目里用了不同的方案:
- 简单的 RAG 系统——用 LlamaIndex,它的索引和检索能力最专业
- 快速的对话原型——用 LangChain,上手最快
- 需要长期维护的 Agent 系统——用 SK,插件架构更清晰,跨语言复用更方便
但如果你只有精力学一个,我的建议是:看你的主力语言。写 Python 的,LangChain 或 LlamaIndex 起步更快;写 C# 的,SK 是唯一靠谱的选择。
7.3 AI 模型支持对比
这可能是很多人忽略的维度。SK 对 AI 模型的支持范围远比想象中宽:
| 模型提供商 | SK 支持 | LangChain 支持 | LlamaIndex 支持 |
|---|---|---|---|
| OpenAI / Azure OpenAI | ✅ | ✅ | ✅ |
| Google Gemini | ✅ | ✅ | ✅ |
| Mistral AI | ✅ | ✅ | 部分 |
| Hugging Face | ✅ | ✅ | 部分 |
| Ollama(本地部署) | ✅ | ✅ | ✅ |
| ONNX(本地推理) | ✅ | ❌ | ❌ |
| Amazon Bedrock | ✅ | ✅ | ✅ |
| NVIDIA NIM | ✅ | ✅ | ❌ |
SK 几乎覆盖了所有主流提供商,这在企业项目里很重要——你不可能只绑一家。LangChain 覆盖面也很广,但 ONNX 这类本地推理场景是 SK 的独到之处,而 NVIDIA NIM 两家都有集成,但 SK 的实现更原生。
7.4 各框架的不足
- LangChain:链式架构在复杂场景维护成本高,Python 限定限制了跨团队复用
- LlamaIndex:编排能力偏弱,做 Agent 还需要搭别的框架
- SK:版本更新太快导致 API 不稳定,文档有时滞后于实际代码,向量存储还在实验期
八、总结:Semantic Kernel 适合什么场景
写了这么多,最后总结一下我的判断:
SK 最适合的场景
- 企业级 Agent 系统——跨语言团队、需要长期维护、微软生态绑定
- 多工具自动编排——你有大量内部 API,需要 AI 根据意图自动组合调用
- 已有 REST API 快速 AI 化——OpenAPI 插件让你的微服务秒变 AI 能力
- 需要 MCP 协议集成——SK 是目前原生支持 MCP 的少数框架之一
SK 不太适合的场景
- 简单的 RAG 系统——LlamaIndex 更专业
- 快速原型验证——LangChain 更快上手
- 对 API 稳定性极度敏感的项目——SK 的更新频率会让你头疼
- 纯 Python 小团队——SK 的跨语言优势在纯 Python 团队里体现不出来
关于 Microsoft Agent Framework (MAF)
最后必须提一个重要信息:微软已经发布了 Microsoft Agent Framework (MAF),这是微软在 Agent 开发领域的最新框架,与 Semantic Kernel 有着密切的演进关系。
根据 官方博客 的说明:
- MAF 并非简单地在 SK 版本号上加个 “v2.0”,而是微软重新设计的 Agent 开发框架,其核心理念延续了 SK 的插件编排思路,但架构有所不同
- SK v1.x 将持续维护至少到 MAF 正式发布后的 1 年
- 新项目如果侧重 Agent 开发,可以考虑使用 MAF
- 已有 SK 项目可以继续使用 v1.x,但要做好长期迁移的准备
这意味着如果你现在开始新项目,需要根据项目性质做选择:侧重 Agent 开发的,考虑 MAF;侧重工具编排和 Function Calling 的,SK v1.x 仍然成熟可靠。已有 SK 项目短期内不用担心——微软承诺了维护期。
写在最后
折腾 Semantic Kernel 这大半年,最大的感触是:AI 编排框架还在快速演化期,没有"最佳实践",只有"当前最合适的实践"。Planner 废弃了,Memory 重构了,MAF 又来了——每一步变化都在修正上一版的设计缺陷,但也给开发者带来了迁移成本。
但这恰恰是做 AI 开发的常态。底层模型在变,编排方式在变,最佳实践在变。与其追求一个"永远不过时"的框架,不如培养一种"快速适应变化"的能力。
SK 的插件架构和 Function Calling 编排,在我看来是当前 Agent 开发最清晰的设计范式之一。即便 MAF 会取代它,这些核心概念(插件封装、AI 自主编排、向量记忆)大概率会延续下去。
学概念比学 API 更重要。 API 会变,概念会长存。
版本与环境要求
最后补一个实用信息——SK 的运行环境要求:
| 语言 | 最低版本 | 备注 |
|---|---|---|
| C# (.NET) | .NET 8.0+ | 也支持 .NET Standard 2.0 |
| Python | 3.10+ | 必须支持 async/await |
| Java | JDK 17+ | 功能覆盖相对较少 |
操作系统:Windows、macOS、Linux 全支持。
更多推荐

所有评论(0)