一、为什么我开始研究 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;
    }
}

几个踩坑要点:

  1. 函数命名用 snake_case——哪怕你写的是 C#!这不是 Bug,是设计决策。因为 LLM 的训练数据以 Python 为主,snake_case 的函数名 AI 理解起来更准确。我在 C# 项目里第一次用 PascalCase 命名函数,AI 时不时调错或者理解偏差,改成 snake_case 之后明显好了。

  2. Description 很重要——@kernel_function 标记只是让函数"可见",Description 才是让 AI "理解"的关键。写好 Description,比写好代码更重要。我后来养成了习惯:先写 Description,再写代码逻辑。

  3. 只有标记了的函数才会暴露给 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 不是做关键词匹配,而是通过语义相似度找到"用户喜欢简洁的技术文档"这条记录——哪怕两者没有一个共同的关键词。

我的使用心得:

  1. 短期记忆是必须的——没有对话历史,AI 就是金鱼,每轮对话都从零开始。
  2. 长期记忆看场景——如果你只是做单次问答,不需要向量存储;但如果你的 Agent 需要跨会话记住用户偏好或检索知识库,那这就是核心能力。
  3. 向量数据库的选择很重要——我实际项目中用的是 PostgreSQL(pgvector),因为我们已经有 PG 集群,不想再引入新的基础设施。Azure AI Search 在云原生场景很方便,但成本需要注意。
  4. 关于 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 会自动完成四步:

  1. 调用 get_pizza_menu() ——看看菜单上有没有夏威夷披萨
  2. 调用 add_pizza_to_cart(size="large", toppings=["ham", "pineapple"]) ——下单
  3. 调用 get_cart() ——确认购物车内容
  4. 调用 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 代码,微软提供了迁移指南:

说白了就是:你以前写在 Planner 里的步骤逻辑,现在应该拆成一个个独立的 Plugin 函数,让 AI 自己组合调用。这确实更灵活,但迁移工作量不小——我花了两周时间重构旧代码。

六、踩坑记录:那些文档里不会告诉你的事

讲完了三大核心,接下来是这篇文章最有价值的部分——我踩过的坑。这些都是实打实的教训,官方文档不会告诉你这些。

6.1 版本更新太快,API 稳定性是最大痛点

SK 的更新频率极高——NuGet 上最新版已经到了 1.77.0。这意味着几乎每隔几天就有新版本,API 变动频繁。

我最惨的一次经历:项目用的是 v0.x 的 Planner,某天例行更新依赖,发现整个编排系统不工作了。查了一圈才知道 Planner 废弃了。这种"更新一下依赖,代码就挂了"的体验,真的让人怀疑人生。

教训:锁定版本号。 不要用 latest 或不指定版本。在 requirements.txtcsproj 里写明确版本号,只在充分测试后才升级。

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 为什么选择了这条路径"。

我的调试方法:

  1. 开启 SK 的日志输出(设置 logging 到 DEBUG 级别)
  2. 每次函数调用前后都打印 ChatHistory
  3. FunctionChoiceBehavior.None() 先关掉自动调用,手动模拟每一步,确认每个函数单独工作正常
  4. 再开回 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 的实现更原生。

来源:Microsoft Learn — AI Integrations

7.4 各框架的不足

  • LangChain:链式架构在复杂场景维护成本高,Python 限定限制了跨团队复用
  • LlamaIndex:编排能力偏弱,做 Agent 还需要搭别的框架
  • SK:版本更新太快导致 API 不稳定,文档有时滞后于实际代码,向量存储还在实验期

八、总结:Semantic Kernel 适合什么场景

写了这么多,最后总结一下我的判断:

SK 最适合的场景

  1. 企业级 Agent 系统——跨语言团队、需要长期维护、微软生态绑定
  2. 多工具自动编排——你有大量内部 API,需要 AI 根据意图自动组合调用
  3. 已有 REST API 快速 AI 化——OpenAPI 插件让你的微服务秒变 AI 能力
  4. 需要 MCP 协议集成——SK 是目前原生支持 MCP 的少数框架之一

SK 不太适合的场景

  1. 简单的 RAG 系统——LlamaIndex 更专业
  2. 快速原型验证——LangChain 更快上手
  3. 对 API 稳定性极度敏感的项目——SK 的更新频率会让你头疼
  4. 纯 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 全支持。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐