语义内核入门:C# 中的 AI 编排
如果您一直在构建 .NET 应用程序并关注 AI 领域的发展,您可能想知道:将大型语言模型集成到我的 C# 项目中而不会将我的代码库变成意大利面条的最佳方法是什么? 这正是 Microsoft 的语义内核解决的问题,在去年用它构建生产应用程序之后,我可以告诉您它已成为我的开发人员工具包中最重要的工具之一。
在这篇文章中,我将引导您完成开始使用语义内核所需的一切 - 从理解核心概念到构建现实世界的人工智能助手。无论您是刚刚涉足 AI 开发,还是正在寻找一种结构化方法来编排现有 .NET 应用程序中的 LLM 调用,本指南都能满足您的需求。
什么是语义内核?
Semantic Kernel (SK) 是 Microsoft 的开源 SDK,充当应用程序代码和大型语言模型(如 GPT-4o、Azure OpenAI 或其他 AI 服务)之间的编排层。将其视为一种轻量级中间件,可让您以干净、可组合的方式将传统 C# 代码与 AI 功能结合起来。
但为什么不直接调用 OpenAI API 呢?你绝对可以——对于简单的用例来说,这很好。但此时你需要:
- 让人工智能根据用户输入决定调用哪些函数
- 将多个人工智能调用与管道中的传统代码相结合
- 添加记忆和上下文,以便人工智能记住之前的交互
- 构建多步骤代理,通过复杂的任务进行推理
…你会发现自己在重新发明轮子。 Semantic Kernel 为您提供开箱即用的所有功能,具有一流的 .NET 支持、依赖项注入集成以及对任何 C# 开发人员来说都很自然的插件架构。
该项目位于 GitHub 上的 microsoft/semantic-kernel 存储库下,并具有适用于 C#、Python 和 Java 的 SDK。 C# SDK 是最成熟的,也是我们在此重点关注的。
核心概念
在编写任何代码之前,让我们先了解一下构建块。
内核
Kernel 是语义内核中的中心对象。它是协调器——将你的人工智能服务、插件和配置联系在一起的东西。您创建一个,在其上注册您的服务和插件,然后使用它来执行提示或调用函数。如果您熟悉 ASP.NET Core 中的依赖注入,那么内核会感觉非常熟悉——它本质上是一个具有 AI 超能力的服务容器。
插件和功能
插件是内核可以调用的相关函数的集合。函数有两种类型:
- 提示函数 — 定义为发送到 LLM 的自然语言模板
- 本机函数 — 用内核可以发现和调用的属性修饰的常规 C# 方法
例如,您可能有一个带有本机函数 GetCurrentWeather(string city) 的 WeatherPlugin 和一个以友好方式汇总天气数据的提示函数。### 人工智能连接器
连接器是语义内核与人工智能服务对话的方式。最常见的是:
AzureOpenAIChatCompletion— 用于 Azure OpenAI 服务OpenAIChatCompletion— 直接用于 OpenAI 的 API- 用于矢量搜索和存储的嵌入连接器
您在启动时将这些注册到内核上,其他一切都可以正常工作。
设置您的项目
让我们动手吧。首先创建一个新的控制台应用程序:
dotnet new console -n SemanticKernelDemo
cd SemanticKernelDemo
现在添加语义内核 NuGet 包:
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
如果您直接使用 OpenAI 而不是 Azure OpenAI:
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI
对于内存和嵌入支持(我们稍后将使用它):
dotnet add package Microsoft.SemanticKernel.Plugins.Memory
dotnet add package Microsoft.Extensions.VectorData.Abstractions
您的 .csproj 应面向 .NET 8 或更高版本。 Semantic Kernel 的最新版本充分利用了现代 .NET 功能。
你的第一个内核
让我们从最简单的示例开始 - 创建一个内核,将其连接到人工智能服务,并向其提出问题。
using Microsoft.SemanticKernel;
// Build the kernel with Azure OpenAI
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key"
);
var kernel = builder.Build();
// Invoke a simple prompt
var result = await kernel.InvokePromptAsync(
"Explain dependency injection in C# in three sentences."
);
Console.WriteLine(result);
如果您直接使用 OpenAI,请交换服务注册:
builder.AddOpenAIChatCompletion(
modelId: "gpt-4o",
apiKey: "your-openai-api-key"
);
就是这样。运行它,您将得到依赖注入的简明解释。但这只是表面现象。
使用提示模板
提示模板允许您使用 Handlebars 样式语法通过变量参数化提示:
var prompt = """
You are a technical writer. Write a brief summary of {{$topic}}
aimed at developers with {{$experienceLevel}} experience.
Keep it under 200 words.
""";
var function = kernel.CreateFunctionFromPrompt(prompt);
var arguments = new KernelArguments
{
["topic"] = "gRPC in .NET",
["experienceLevel"] = "intermediate"
};
var result = await kernel.InvokeAsync(function, arguments);
Console.WriteLine(result);
这就是语义内核开始发挥作用的地方 - 您可以定义可重用的提示模板,对其进行版本控制,并将它们组合成更大的工作流程。
插件和本机函数
插件是 Semantic Kernel 弥合 AI 和现有 C# 代码之间差距的地方。本机函数只是您向内核公开的常规方法。
using Microsoft.SemanticKernel;
using System.ComponentModel;
public class TimePlugin
{
[KernelFunction("get_current_time")]
[Description("Gets the current date and time in UTC")]
public string GetCurrentTime()
{
return DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC");
}
[KernelFunction("get_time_in_timezone")]
[Description("Gets the current time in a specific timezone")]
public string GetTimeInTimezone(
[Description("The IANA timezone identifier, e.g. 'America/New_York'")] string timezone)
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
var time = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
return time.ToString("yyyy-MM-dd HH:mm:ss");
}
}
请注意 [KernelFunction] 和 [Description] 属性。这些很重要——人工智能会读取描述来了解何时以及如何调用您的函数。良好的描述可以区分有效使用你的工具的人工智能和混乱的人工智能。
在您的内核上注册插件:
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key"
);
builder.Plugins.AddFromType<TimePlugin>();
var kernel = builder.Build();
您还可以构建更复杂的插件来注入服务。由于语义内核与 Microsoft.Extensions.DependencyInjection 集成,您的插件可以像任何其他服务一样接收构造函数依赖项:
public class OrderPlugin
{
private readonly IOrderRepository _repository;
public OrderPlugin(IOrderRepository repository)
{
_repository = repository;
}
[KernelFunction("get_order_status")]
[Description("Retrieves the status of an order by its ID")]
public async Task<string> GetOrderStatus(
[Description("The order ID to look up")] string orderId)
{
var order = await _repository.GetByIdAsync(orderId);
return order is null
? $"No order found with ID {orderId}"
: $"Order {orderId}: {order.Status}, placed on {order.CreatedAt:d}";
}
}
函数调用和自动调用
这就是事情变得非常有趣的地方。通过函数调用(也称为工具调用),您可以让 AI 模型根据对话上下文决定要调用哪些已注册的函数。该模型不执行代码 - 它返回一个结构化请求,表示“我想使用这些参数调用函数 X”,并且内核处理实际的调用。
以下是启用自动函数调用的方法:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key"
);
builder.Plugins.AddFromType<TimePlugin>();
builder.Plugins.AddFromType<WeatherPlugin>();
var kernel = builder.Build();
// Enable automatic function calling
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var result = await kernel.InvokePromptAsync(
"What time is it in Tokyo and what's the weather like there?",
new KernelArguments(settings)
);
Console.WriteLine(result);
使用 FunctionChoiceBehavior.Auto(),内核将:
- 将您的提示以及所有可用功能的描述发送给 AI
- AI 决定需要调用
get_time_in_timezone和get_weather - 内核自动执行那些函数 4.结果回传给AI
- AI 使用函数结果组成自然语言响应这个循环可以在一次调用中发生多次——人工智能可能会按顺序调用多个函数来收集它需要的所有信息。您还可以使用
FunctionChoiceBehavior.Required()强制 AI 调用至少一个函数,或提供允许使用的特定函数列表。
聊天完成与历史记录
对于会话应用程序,您需要直接将 ChatCompletionService 与 ChatHistory 对象一起使用:
using Microsoft.SemanticKernel.ChatCompletion;
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();
history.AddSystemMessage("""
You are a helpful developer assistant. You have access to tools
for checking the time and weather. Be concise and friendly.
""");
while (true)
{
Console.Write("You: ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) break;
history.AddUserMessage(input);
var response = await chatService.GetChatMessageContentAsync(
history,
executionSettings: settings,
kernel: kernel
);
history.AddAssistantMessage(response.Content ?? "");
Console.WriteLine($"Assistant: {response.Content}");
}
这为您提供了一个完全交互式的聊天机器人,它可以维护对话历史记录并可以根据需要调用您的插件。
内存和嵌入
人工智能应用程序中最强大的模式之一是检索增强生成(RAG) - 通过将数据嵌入向量空间并在查询时检索相关块,让人工智能访问您自己的数据。
语义内核提供了用于向量存储和嵌入的抽象。以下是如何设置内存中向量存储以进行开发:
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using Microsoft.SemanticKernel.Embeddings;
#pragma warning disable SKEXP0010
// Create an embedding generation service
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAITextEmbeddingGeneration(
deploymentName: "text-embedding-ada-002",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key"
);
var kernel = builder.Build();
var embeddingService = kernel.GetRequiredService<ITextEmbeddingGenerationService>();
// Generate embeddings for your documents
var documents = new[]
{
"Semantic Kernel is an open-source SDK for AI orchestration.",
"Azure OpenAI provides enterprise-grade AI models.",
"Plugins in SK allow you to expose C# methods to AI models."
};
var embeddings = await embeddingService.GenerateEmbeddingsAsync(documents);
对于生产场景,您可以将这些嵌入存储在专用矢量数据库中,例如 Azure AI Search、Qdrant 或 Pinecone。语义内核通过 Microsoft.Extensions.VectorData 抽象提供了所有这些的连接器。
典型的 RAG 流程如下所示:
- 摄取:对文档进行分块,生成嵌入,将它们存储在矢量数据库中
- 检索:当用户提出问题时,嵌入查询并找到最相似的文档
- 生成:将检索到的文档作为上下文以及用户的问题传递给 LLM
[KernelFunction("search_knowledge_base")]
[Description("Searches the internal knowledge base for relevant information")]
public async Task<string> SearchKnowledgeBase(
[Description("The search query")] string query)
{
var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query);
var results = await _vectorStore.SearchAsync(queryEmbedding, limit: 3);
return string.Join("\n\n", results.Select(r => r.Text));
}
通过将 RAG 管道公开为内核函数,人工智能可以自动决定何时需要搜索您的知识库 - 保持编排干净并让模型做它最擅长的事情。
策划者和代理人
随着您的人工智能应用程序变得越来越复杂,您将需要人工智能规划和执行多步骤任务。这就是语义内核代理框架的用武之地。
基础知识:聊天完成代理
最简单的代理类型使用指令和插件包装聊天完成模型:
using Microsoft.SemanticKernel.Agents;
#pragma warning disable SKEXP0110
var agent = new ChatCompletionAgent
{
Name = "DevAssistant",
Instructions = """
You are a senior .NET developer assistant. Help users with code
reviews, architecture decisions, and debugging. Always provide
code examples when relevant. Use your available tools to look up
current information when needed.
""",
Kernel = kernel,
Arguments = new KernelArguments(settings)
};
var history = new ChatHistory();
history.AddUserMessage("How should I structure a clean architecture project in .NET 8?");
await foreach (var message in agent.InvokeAsync(history))
{
Console.WriteLine(message.Content);
history.Add(message);
}
多代理协作
当您有多个代理一起工作时,事情就会变得真正强大。语义内核支持群聊模式,其中不同专业的代理进行协作:
#pragma warning disable SKEXP0110
var codeReviewer = new ChatCompletionAgent
{
Name = "CodeReviewer",
Instructions = """
You review C# code for bugs, performance issues, and best practices.
Be specific about what you find and suggest concrete fixes.
""",
Kernel = kernel
};
var securityAuditor = new ChatCompletionAgent
{
Name = "SecurityAuditor",
Instructions = """
You focus exclusively on security vulnerabilities in code.
Look for injection attacks, authentication issues, data exposure,
and OWASP Top 10 violations.
""",
Kernel = kernel
};
var groupChat = new AgentGroupChat(codeReviewer, securityAuditor)
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = new MaximumIterationTerminationStrategy(4)
}
};
groupChat.AddChatMessage(
new ChatMessageContent(AuthorRole.User, "Review this code: ...")
);
await foreach (var message in groupChat.InvokeAsync())
{
Console.WriteLine($"[{message.AuthorName}]: {message.Content}");
}
这种模式对于需要权衡不同观点或专业领域的复杂任务非常有用。每个代理都使用自己的系统提示进行操作,并且可以拥有自己的一组插件。
真实示例:构建项目文档助手
让我们用一个实际的例子将所有内容联系在一起——一个人工智能助手,可以通过阅读文件和回答问题来帮助开发人员理解代码库。
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;
// Define our plugins
public class FileSystemPlugin
{
private readonly string _rootPath;
public FileSystemPlugin(string rootPath)
{
_rootPath = rootPath;
}
[KernelFunction("read_file")]
[Description("Reads the contents of a file from the project directory")]
public async Task<string> ReadFile(
[Description("Relative path to the file")] string path)
{
var fullPath = Path.Combine(_rootPath, path);
if (!File.Exists(fullPath))
return $"File not found: {path}";
var content = await File.ReadAllTextAsync(fullPath);
// Truncate very large files
if (content.Length > 8000)
content = content[..8000] + "\n... [truncated]";
return content;
}
[KernelFunction("list_files")]
[Description("Lists files in a directory, optionally filtered by extension")]
public string ListFiles(
[Description("Relative directory path")] string directory,
[Description("File extension filter like '.cs' or '.json'")] string? extension = null)
{
var fullPath = Path.Combine(_rootPath, directory);
if (!Directory.Exists(fullPath))
return $"Directory not found: {directory}";
var files = Directory.GetFiles(fullPath, "*.*", SearchOption.AllDirectories)
.Select(f => Path.GetRelativePath(_rootPath, f))
.Where(f => extension is null || f.EndsWith(extension))
.Take(50);
return string.Join("\n", files);
}
}
public class DocumentationPlugin
{
[KernelFunction("generate_summary")]
[Description("Generates a structured markdown summary for documentation")]
public string GenerateSummaryTemplate(
[Description("Name of the component")] string componentName,
[Description("Brief description")] string description,
[Description("Key responsibilities as comma-separated values")] string responsibilities)
{
var items = responsibilities.Split(',', StringSplitOptions.TrimEntries);
var bullets = string.Join("\n", items.Select(r => $"- {r}"));
return $"""
## {componentName}
{description}
### Responsibilities
{bullets}
---
*Generated documentation — review and expand as needed.*
""";
}
}
// Wire it all up
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key"
);
builder.Plugins.AddFromObject(new FileSystemPlugin("./src"));
builder.Plugins.AddFromType<DocumentationPlugin>();
var kernel = builder.Build();
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();
history.AddSystemMessage("""
You are a codebase documentation assistant. You help developers understand
projects by reading source files and explaining architecture, patterns,
and design decisions.
When asked about code, use your tools to read the actual files rather
than guessing. Be specific and reference actual code when possible.
Generate documentation artifacts when asked.
""");
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
Console.WriteLine("Documentation Assistant ready. Ask me about your codebase!");
Console.WriteLine("Type 'exit' to quit.\n");
while (true)
{
Console.Write("You: ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
break;
history.AddUserMessage(input);
var response = await chatService.GetChatMessageContentAsync(
history,
executionSettings: settings,
kernel: kernel
);
Console.WriteLine($"\nAssistant: {response.Content}\n");
history.AddAssistantMessage(response.Content ?? "");
}
该助理可以:- 从项目目录中列出并读取文件
- 通过阅读实际的源文件来回答有关代码库的问题
- 生成 Markdown 格式的文档
- 维持对话上下文,以便后续问题自然有效
AI 根据您的要求自动决定何时调用 read_file、list_files 或 generate_summary。问它“OrderService 是做什么的?”它会读取文件,分析它并解释它。要求它“为身份验证模块生成文档”,它将探索文件、了解结构并生成格式化摘要。
制作技巧
在发布语义内核应用程序之前,我通过艰难的方式学到了一些东西:
正确使用依赖注入。 在 ASP.NET Core 应用程序中,在 DI 容器中注册内核和服务,而不是内联创建它们:
builder.Services.AddKernel()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: configuration["AzureOpenAI:Endpoint"]!,
apiKey: configuration["AzureOpenAI:ApiKey"]!
)
.Plugins.AddFromType<TimePlugin>()
.AddFromType<OrderPlugin>();
妥善处理错误。 LLM 调用可能会失败、超时或返回意外结果。将您的调用包装在 try-catch 块中,并使用 Polly 或内置弹性功能实施重试策略。
监控token使用情况。 每一个提示、每一个功能描述、每一条聊天记录都会消耗token。使用过滤器来记录和跟踪使用情况:
kernel.FunctionInvocationFilters.Add(new LoggingFilter());
**保持函数描述准确。**模糊的描述会导致AI错误地调用函数。通过询问以下问题来测试您的描述:“如果我只阅读描述,我能准确地知道何时以及如何使用此功能吗?”
结论
语义内核是从根本上改变您构建应用程序的想法的库之一。它不仅仅是一个 API 包装器,它还是一个编排框架,可让您以可维护、可测试和生产就绪的方式将 AI 功能与传统代码组合在一起。
我最喜欢它的是它尊重 .NET 生态系统。它使用您已经知道的模式——依赖注入、属性、异步/等待、接口——并将它们扩展到人工智能世界。您不必学习全新的范式;您只需将人工智能添加为工具包中的另一项功能即可。
如果您正在构建 .NET 应用程序并且尚未探索语义内核,那么现在正是时候。 SDK 稳定,社区活跃,其支持的模式(从简单的提示编排到多代理协作)正在成为现代开发人员的基本技能。
从小处开始。创建一个内核,注册一个插件,然后观看 AI 调用您的代码。一旦点击,您将开始看到在应用程序中随处添加智能的机会。
官方文档 和 GitHub 存储库 是继续您的旅程的优秀资源。快乐建设!