mcpField
D
dotnet-semantic-kernel

dotnet-semantic-kernel

github.com/wshaddix开发8673 次安装35 周前更新
代码仓库

关于 dotnet-semantic-kernel

介绍如何在 .NET 应用中使用 Microsoft Semantic Kernel 编排 AI 与大模型。内容涵盖插件、提示词、向量存储检索、Agent,以及云端和本地 AI 服务连接。

主要功能

  1. 01配置 Kernel 与 AI 服务
  2. 02将 .NET 方法作为可调用插件
  3. 03创建 Handlebars 和 Liquid 提示词
  4. 04使用向量存储实现 RAG
  5. 05构建 Agent 与群聊
  6. 06流式传输聊天响应

适用场景

  1. 01让模型回答基于应用数据
  2. 02构建文档检索与问答
  3. 03协调专用 Agent 执行任务
  4. 04在聊天应用中流式返回响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-semantic-kernel

安装后在 Claude Code 中输入/dotnet-semantic-kernel确认已加载。

详细介绍

dotnet-semantic-kernel

用于在 .NET 应用程序中协调 AI 和大模型的 Microsoft Semantic Kernel。涵盖内核设置和配置、插件/函数调用、使用 Handlebars 和 Liquid 语法的提示词模板、记忆和向量存储集成、规划器、Agent 框架,以及与 Azure OpenAI、OpenAI 和本地模型的集成。

不在范围内: 通用 async/await 模式和取消令牌传递——请参阅 [skill:dotnet-csharp-async-patterns]。DI 容器机制和服务生命周期管理——请参阅 [skill:dotnet-csharp-dependency-injection]。HTTP 客户端弹性和重试策略——请参阅 [skill:dotnet-resilience]。配置绑定(选项模式、密钥)——请参阅 [skill:dotnet-csharp-configuration]。

交叉引用:[skill:dotnet-csharp-async-patterns] 介绍聊天补全中使用的异步流模式;[skill:dotnet-csharp-dependency-injection] 介绍 ASP.NET Core 中的内核服务注册;[skill:dotnet-resilience] 介绍 AI 服务调用的重试策略;[skill:dotnet-csharp-configuration] 介绍 API 密钥和模型配置的管理。


内核设置

Kernel 是 Semantic Kernel 的核心对象。它管理 AI 服务连接、插件和函数调用。

包概览
包用途
Microsoft.SemanticKernel核心内核、函数调用、提示词模板
Microsoft.SemanticKernel.Connectors.AzureOpenAIAzure OpenAI 聊天/嵌入/图像服务
Microsoft.SemanticKernel.Connectors.OpenAIOpenAI 聊天/嵌入/图像服务
Microsoft.SemanticKernel.Connectors.OllamaOllama 本地模型集成
Microsoft.SemanticKernel.Plugins.Core内置插件(时间、数学、文本)
Microsoft.SemanticKernel.Agents.CoreAgent 框架(聊天 Agent、群组聊天)
Microsoft.Extensions.VectorData.Abstractions向量存储抽象层
Microsoft.SemanticKernel.Connectors.QdrantQdrant 向量存储连接器
Microsoft.SemanticKernel.Connectors.AzureAISearchAzure AI Search 向量存储连接器
基本内核配置
using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();

// Azure OpenAI
builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o",
    endpoint: Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!,
    apiKey: Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY")!);

var kernel = builder.Build();
与 ASP.NET Core 集成 DI
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKernel();

builder.Services.AddAzureOpenAIChatCompletion(
    deploymentName: builder.Configuration["AI:DeploymentName"]!,
    endpoint: builder.Configuration["AI:Endpoint"]!,
    apiKey: builder.Configuration["AI:ApiKey"]!);

// Register plugins
builder.Services.AddSingleton<OrderPlugin>();
builder.Services.AddSingleton(sp =>
{
    var kernel = sp.GetRequiredService<Kernel>();
    kernel.Plugins.AddFromObject(sp.GetRequiredService<OrderPlugin>());
    return kernel;
});
多个 AI 服务

注册多个 AI 服务,并通过服务 ID 进行选择:

var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o",
    endpoint: endpoint,
    apiKey: apiKey,
    serviceId: "gpt4o");

builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o-mini",
    endpoint: endpoint,
    apiKey: apiKey,
    serviceId: "gpt4o-mini");

var kernel = builder.Build();

// Select service at invocation time
var settings = new PromptExecutionSettings { ServiceId = "gpt4o-mini" };
var result = await kernel.InvokePromptAsync("Summarize: {{$input}}", new(settings)
{
    ["input"] = longDocument
});
使用 Ollama 的本地模型
#pragma warning disable SKEXP0070  // Ollama connector is experimental

var builder = Kernel.CreateBuilder();

builder.AddOllamaChatCompletion(
    modelId: "llama3.2",
    endpoint: new Uri("http://localhost:11434"));

var kernel = builder.Build();

插件和函数调用

插件将 .NET 方法公开为 AI 模型可以调用的函数。这是让大模型响应基于真实数据和操作的主要机制。

定义插件
using Microsoft.SemanticKernel;
using System.ComponentModel;

public sealed class OrderPlugin
{
    private readonly IOrderRepository _repository;

    public OrderPlugin(IOrderRepository repository) => _repository = repository;

    [KernelFunction("get_order")]
    [Description("Retrieves an order by its ID")]
    public async Task<OrderSummary?> GetOrderAsync(
        [Description("The unique order identifier")] string orderId,
        CancellationToken ct = default)
    {
        var order = await _repository.GetByIdAsync(orderId, ct);
        return order is null ? null : new OrderSummary(order);
    }

    [KernelFunction("list_recent_orders")]
    [Description("Lists the most recent orders for a customer")]
    public async Task<IReadOnlyList<OrderSummary>> ListRecentOrdersAsync(
        [Description("The customer ID")] string customerId,
        [Description("Maximum number of orders to return")] int limit = 10,
        CancellationToken ct = default)
    {
        var orders = await _repository.GetRecentAsync(customerId, limit, ct);
        return orders.Select(o => new OrderSummary(o)).ToList();
    }
}
注册插件
var kernel = builder.Build();

// From an object instance (DI-friendly)
kernel.Plugins.AddFromObject(new OrderPlugin(orderRepo), "Orders");

// From a type (kernel creates the instance)
kernel.Plugins.AddFromType<TimePlugin>("Time");

// From functions directly
kernel.Plugins.AddFromFunctions("Math",
[
    KernelFunctionFactory.CreateFromMethod(
        ([Description("First number")] double a, [Description("Second number")] double b) => a + b,
        "Add",
        "Adds two numbers")
]);
自动函数调用

启用模型,使其在聊天过程中自动调用函数:

var settings = new AzureOpenAIPromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("What's the status of order ORD-12345?");

var result = await kernel.GetRequiredService<IChatCompletionService>()
    .GetChatMessageContentAsync(chatHistory, settings, kernel);

// The model calls get_order("ORD-12345") automatically and responds with the result
Console.WriteLine(result.Content);
函数筛选器

拦截函数调用以进行日志记录、授权或修改:

public sealed class AuthorizationFilter : IFunctionInvocationFilter
{
    public async Task OnFunctionInvocationAsync(
        FunctionInvocationContext context,
        Func<FunctionInvocationContext, Task> next)
    {
        // Check authorization before function execution
        if (context.Function.Name == "get_order")
        {
            var orderId = context.Arguments["orderId"]?.ToString();
            // Validate access...
        }

        await next(context);

        // Post-execution: log or modify result
    }
}

// Register the filter
builder.Services.AddSingleton<IFunctionInvocationFilter, AuthorizationFilter>();

提示词模板

提示词模板支持在结构化提示词中进行变量替换和函数调用。

内联提示词
var result = await kernel.InvokePromptAsync(
    "Summarize the following text in {{$style}} style:\n\n{{$input}}",
    new KernelArguments
    {
        ["input"] = articleText,
        ["style"] = "concise bullet points"
    });
Handlebars 模板

Handlebars 模板支持条件判断、循环和函数调用:

var templateString = """
    <message role="system">
    You are a helpful customer service agent.
    {{#if isVip}}You are speaking with a VIP customer. Be extra attentive.{{/if}}
    </message>
    <message role="user">
    Customer: {{customerName}}
    Query: {{query}}

    Recent orders:
    {{#each orders}}
    - Order {{this.Id}}: {{this.Status}} ({{this.Date}})
    {{/each}}
    </message>
    """;

var factory = new HandlebarsPromptTemplateFactory();
var template = factory.Create(new PromptTemplateConfig(templateString)
{
    TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat
});

var result = await template.RenderAsync(kernel, new KernelArguments
{
    ["customerName"] = "Alice",
    ["query"] = "Where is my order?",
    ["isVip"] = true,
    ["orders"] = recentOrders
});
YAML 提示词配置

将提示词定义为 YAML 文件,以分离关注点:

# prompts/summarize.yaml
name: Summarize
description: Summarizes text to a specified length
template_format: handlebars
template: |
  <message role="system">
  Summarize the following text in approximately {{maxWords}} words.
  Focus on key facts and actionable items.
  </message>
  <message role="user">{{input}}</message>
input_variables:
  - name: input
    description: The text to summarize
    is_required: true
  - name: maxWords
    description: Target word count
    default: "100"
execution_settings:
  default:
    temperature: 0.3
    max_tokens: 500
var yamlContent = File.ReadAllText("prompts/summarize.yaml");
var function = kernel.CreateFunctionFromPromptYaml(yamlContent);

var result = await kernel.InvokeAsync(function, new KernelArguments
{
    ["input"] = longText,
    ["maxWords"] = "50"
});

记忆和向量存储

Semantic Kernel 提供向量存储抽象,可用于实现检索增强生成(RAG)模式。

向量存储抽象
using Microsoft.Extensions.VectorData;

public sealed class DocumentRecord
{
    [VectorStoreRecordKey]
    public string Id { get; set; } = string.Empty;

    [VectorStoreRecordData(IsFilterable = true)]
    public string Source { get; set; } = string.Empty;

    [VectorStoreRecordData(IsFullTextSearchable = true)]
    public string Content { get; set; } = string.Empty;

    [VectorStoreRecordVector(Dimensions: 1536)]
    public ReadOnlyMemory<float> Embedding { get; set; }
}
注册向量存储
using Microsoft.SemanticKernel.Connectors.Qdrant;

var builder = Kernel.CreateBuilder();

// Register embedding generation
builder.AddAzureOpenAITextEmbeddingGeneration(
    deploymentName: "text-embedding-3-small",
    endpoint: endpoint,
    apiKey: apiKey);

// Register vector store
builder.Services.AddQdrantVectorStore("localhost", 6334);
RAG 模式
public sealed class RagService
{
    private readonly IVectorStoreRecordCollection<string, DocumentRecord> _collection;
    private readonly ITextEmbeddingGenerationService _embeddingService;
    private readonly IChatCompletionService _chatService;

    public RagService(
        IVectorStore vectorStore,
        ITextEmbeddingGenerationService embeddingService,
        IChatCompletionService chatService)
    {
        _collection = vectorStore.GetCollection<string, DocumentRecord>("documents");
        _embeddingService = embeddingService;
        _chatService = chatService;
    }

    public async Task<string> AskAsync(string question, CancellationToken ct = default)
    {
        // 1. Generate embedding for the question
        var questionEmbedding = await _embeddingService
            .GenerateEmbeddingAsync(question, cancellationToken: ct);

        // 2. Search for relevant documents
        var searchResults = _collection.VectorizedSearchAsync(
            questionEmbedding,
            new VectorSearchOptions { Top = 5 },
            ct);

        // 3. Build context from search results
        var contextBuilder = new StringBuilder();
        await foreach (var result in searchResults)
        {
            contextBuilder.AppendLine(result.Record.Content);
            contextBuilder.AppendLine("---");
        }

        // 4. Generate answer with context
        var chatHistory = new ChatHistory();
        chatHistory.AddSystemMessage(
            $"Answer based on the following context:\n\n{contextBuilder}");
        chatHistory.AddUserMessage(question);

        var response = await _chatService
            .GetChatMessageContentAsync(chatHistory, cancellationToken: ct);

        return response.Content ?? string.Empty;
    }
}
导入文档
public async Task IngestAsync(
    string documentId,
    string content,
    string source,
    CancellationToken ct = default)
{
    await _collection.CreateCollectionIfNotExistsAsync(ct);

    var embedding = await _embeddingService
        .GenerateEmbeddingAsync(content, cancellationToken: ct);

    await _collection.UpsertAsync(new DocumentRecord
    {
        Id = documentId,
        Content = content,
        Source = source,
        Embedding = embedding
    }, cancellationToken: ct);
}

Agent 框架

Semantic Kernel Agent 框架支持构建多 Agent 系统,让专用 Agent 协作完成任务。

聊天补全 Agent
#pragma warning disable SKEXP0110  // Agents framework is experimental

using Microsoft.SemanticKernel.Agents;

var agent = new ChatCompletionAgent
{
    Name = "OrderAssistant",
    Instructions = """
        You are an order management assistant. Help customers check order status,
        process returns, and answer questions about their orders.
        Always verify the customer's identity before sharing order details.
        """,
    Kernel = kernel,
    Arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings
    {
        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
    })
};

// Invoke via a thread (required -- agents do not accept bare strings)
var thread = new ChatHistoryAgentThread();
await foreach (var message in agent.InvokeAsync(
    "What's the status of my order ORD-12345?", thread))
{
    Console.WriteLine(message.Content);
}
Agent 群组聊天

多个 Agent 可以在群组聊天中协作,并设置终止条件:

var analyst = new ChatCompletionAgent
{
    Name = "DataAnalyst",
    Instructions = "You analyze data and provide insights. Present findings clearly.",
    Kernel = kernel
};

var writer = new ChatCompletionAgent
{
    Name = "ReportWriter",
    Instructions = "You take analytical findings and write clear, actionable reports.",
    Kernel = kernel
};

var chat = new AgentGroupChat(analyst, writer)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        TerminationStrategy = new ApprovalTerminationStrategy
        {
            MaximumIterations = 6
        }
    }
};

chat.AddChatMessage(
    new ChatMessageContent(AuthorRole.User, "Analyze Q4 sales trends and write a summary report."));

await foreach (var message in chat.InvokeAsync())
{
    Console.WriteLine($"[{message.AuthorName}]: {message.Content}");
}
OpenAI 助手 Agent

用于带有内置工具(代码解释器、文件搜索)的有状态对话:

#pragma warning disable SKEXP0110

// Create the assistant via the builder pattern
OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync(
    kernel,
    new OpenAIAssistantDefinition("gpt-4o")
    {
        Name = "DataProcessor",
        Instructions = "You process CSV data and generate insights.",
        EnableCodeInterpreter = true
    });

try
{
    // Assistant agents use threads for stateful conversations
    var thread = await agent.CreateThreadAsync();

    await foreach (var message in agent.InvokeAsync(
        "Analyze the attached sales data.", thread))
    {
        Console.WriteLine(message.Content);
    }
}
finally
{
    await agent.DeleteAsync();
}

注意:Agent 框架目前处于实验阶段(SKEXP0110)。Semantic Kernel 的各个版本之间 API 变化频繁。采用时,请对照最新示例核实方法签名。


流式响应

对于聊天应用,逐个 Token 流式传输响应:

var chatService = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory("You are a helpful assistant.");
chatHistory.AddUserMessage(userInput);

var settings = new AzureOpenAIPromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
    chatHistory, settings, kernel))
{
    Console.Write(chunk.Content);
}

核心原则

  • 优先使用函数调用,而不是在提示词中塞入大量内容——让模型调用插件获取实时数据,而不是将所有内容注入提示词
  • 让插件各司其职——每个插件应代表一个领域;在函数和参数上使用 [Description] 特性,让模型了解何时以及如何调用它们
  • 在生产环境中使用 YAML 提示词——将提示词内容与代码分离,便于迭代并允许非开发人员编辑
  • 不要在代码中存储 API 密钥——使用环境变量、Azure Key Vault 或 .NET 机密管理器(参见 [skill:dotnet-csharp-configuration])
  • 优先使用向量存储抽象——基于 IVectorStore 编写代码,以便在 Qdrant、Azure AI Search 和其他提供程序之间切换
  • 明确处理实验性 API——按调用抑制 SKEXP* 警告,而不要全局抑制,这样 API 转为稳定版时你能及时注意到

Agent 注意事项

  1. 不要在 Kernel 构建器调用中硬编码 API 密钥或端点——使用 builder.Configuration 或环境变量。硬编码的机密会泄漏到源代码管理中,并妨碍针对不同环境进行配置。
  2. 不要全局抑制所有 SKEXP* 警告——实验性 API 变化频繁。应按用法抑制(#pragma warning disable SKEXP0110),这样编译器就能标记新的实验性 API 使用位置。
  3. 不要在 ASP.NET Core 中为每个请求创建新的 Kernel 实例——将 Kernel 作为单例注册到 DI 中(它是线程安全的);如果需要每个请求的状态,请使用 kernel.Clone() 克隆。
  4. 不要在插件函数中忽略 CancellationToken——用户或超时策略可能会取消 AI 函数调用。始终通过插件方法签名传递 CancellationToken。
  5. 不要从插件函数返回大型对象——模型会将序列化结果作为上下文接收。应返回摘要 DTO,而不是完整的实体关系图,以免超出 Token 限制。
  6. 不要在没有 serviceId 的情况下混用 AddAzureOpenAIChatCompletion 和 AddOpenAIChatCompletion——如果没有服务 ID,最后注册的服务会生效。注册多个 AI 服务时,请使用显式的 serviceId。

前提条件

  • Microsoft.SemanticKernel NuGet 包(稳定版 1.x)
  • AI 服务端点(Azure OpenAI、OpenAI API 密钥或用于本地模型的 Ollama)
  • 对于向量存储:所选提供程序的运行实例(Qdrant、Azure AI Search 等)

参考资料

评论

支持 Markdown