Semantic Kernel 入门:用 C# 十分钟搭建第一个企业级 AI 智能体
摘要:很多 .NET 开发者对 AI Agent 的认知停留在“调 API + 拼 Prompt”的玩具阶段。Semantic Kernel (SK) 的核心价值,是将 LLM 能力以强类型、可测试、可观测的方式融入现有企业架构。本文不讲概念科普,直接用一个“智能工单分派助手”的最小完整示例,演示 SK 在企业场景下的正确打开方式。从 Plugin 定义、依赖注入、结构化输出到 OpenTelemetry 集成,全程 C# 原生体验。代码基于 Semantic Kernel 1.x + .NET 9,复制即可运行。
一、为什么企业级 Agent 不能用“脚本思维”写?
在动手之前,先明确 SK 与 LangChain/裸 API 调用的本质区别:
| 维度 | 脚本式 AI 调用 | Semantic Kernel 企业级 Agent |
|---|---|---|
| 工具定义 | 字符串 JSON Schema | 强类型 C# 方法 + Attribute |
| 依赖管理 | 全局变量 / 手动传递 | .NET DI 容器 |
| 返回值 | 自由文本,正则解析 | 结构化对象,编译器保障 |
| 可测试性 | 必须连真实 LLM | Plugin 可独立 Mock 单元测试 |
| 可观测性 | 手写日志 | 原生 OpenTelemetry Trace |
| 多模型切换 | if-else 硬编码 | Kernel 抽象 + 配置驱动 |
核心原则:把 LLM 当作一个“不确定性的服务调用”,而非“万能的黑盒”。SK 的所有设计都围绕这个原则展开。
二、项目准备(2 分钟)
dotnet new console -n TicketAgent --framework net9.0
cd TicketAgent
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package OpenTelemetry.Exporter.Console
appsettings.json:
{
"OpenAI": {
"ApiKey": "sk-your-key-here",
"ModelId": "gpt-4o-mini"
}
}
生产提示:企业环境应使用 Azure OpenAI + Managed Identity,避免密钥明文。本文用 OpenAI 直连仅为降低入门门槛。
三、定义业务服务:Agent 要调用的“真逻辑”(3 分钟)
关键认知:Plugin 不是 AI 代码,而是普通的 C# 业务服务。AI 只是它的调用者之一。
// ITicketService.cs — 纯业务接口,与 AI 完全解耦
public interface ITicketService
{
Task<TicketInfo?> GetTicketAsync(string ticketId, CancellationToken ct = default);
Task AssignTicketAsync(string ticketId, string assignee, string reason, CancellationToken ct = default);
Task<IReadOnlyList<string>> GetAvailableAgentsAsync(string category, CancellationToken ct = default);
}
public record TicketInfo(
string Id,
string Title,
string Category, // "network" | "database" | "application" | "security"
string Priority, // "P0" | "P1" | "P2" | "P3"
string Status, // "open" | "assigned" | "resolved"
string? CurrentAssignee
);
这里故意不展示实现——因为它可以是 EF Core 查数据库、HTTP 调 Jira、或内存字典。对 SK 来说完全不重要。这正是企业级集成的关键:AI 层不侵入业务层。
四、编写 Plugin:让 LLM “看见”业务能力(3 分钟)
Plugin 是 SK 的核心抽象。每个 [KernelFunction] 方法对应一个 LLM 可调用的工具。
using System.ComponentModel;
using Microsoft.SemanticKernel;
public class TicketPlugin(ITicketService _ticketService)
{
[KernelFunction("get_ticket")]
[Description("根据工单ID查询工单详情,包括标题、分类、优先级、状态和当前处理人")]
public async Task<TicketInfo?> GetTicketAsync(
[Description("工单编号,格式如 TK-20260704-001")] string ticketId,
CancellationToken ct = default)
{
return await _ticketService.GetTicketAsync(ticketId, ct);
}
[KernelFunction("list_available_agents")]
[Description("查询指定分类下当前可用的值班工程师列表")]
public async Task<IReadOnlyList<string>> GetAvailableAgentsAsync(
[Description("工单分类:network/database/application/security")] string category,
CancellationToken ct = default)
{
return await _ticketService.GetAvailableAgentsAsync(category, ct);
}
[KernelFunction("assign_ticket")]
[Description("将工单分配给指定工程师。仅在确认工程师可用且工单未关闭时调用")]
public async Task<string> AssignTicketAsync(
[Description("工单编号")] string ticketId,
[Description("目标工程师姓名")] string assignee,
[Description("分配理由,简述为何选择该工程师")] string reason,
CancellationToken ct = default)
{
await _ticketService.AssignTicketAsync(ticketId, assignee, reason, ct);
return $"✅ 工单 {ticketId} 已成功分配给 {assignee}";
}
}
⚠️ 三个容易踩的坑
- Description 是写给 LLM 看的,不是给人看的:LLM 靠它决定何时调用、传什么参。模糊描述 = 错误调用。
- 参数名要有语义:
string id不如string ticketId。LLM 会参考参数名推断含义。 - 返回值要自包含:LLM 无法访问上下文变量,返回的字符串/对象就是它推理的全部依据。
五、组装 Kernel + 结构化输出(2 分钟)
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
// ✅ 通过 DI 注册,Plugin 自动获得 ITicketService 实例
var services = new ServiceCollection();
services.AddSingleton<ITicketService, InMemoryTicketService>(); // 替换为你的实现
services.AddKernel()
.AddOpenAIChatCompletion(
modelId: config["OpenAI:ModelId"]!,
apiKey: config["OpenAI:ApiKey"]!);
var kernel = services.BuildServiceProvider().GetRequiredService<Kernel>();
// 注册 Plugin(从 DI 容器解析,享受生命周期管理)
kernel.Plugins.AddFromType<TicketPlugin>();
// ✅ 结构化输出:让 LLM 返回强类型对象,而非自由文本
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var response = await chatService.GetChatMessageContentAsync(
new ChatHistory
{
new(AuthorRole.System, """
你是IT运维工单分派助手。你的职责是:
1. 查询工单信息,判断分类和优先级
2. 查找该分类下可用的工程师
3. 选择最合适的工程师并分配工单
4. 如果工单已关闭或无可用人员,明确告知用户
始终先查询再操作,不要猜测工单状态。
"""),
new(AuthorRole.User, "请帮我把 TK-20260704-003 分配出去")
},
new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(), // 允许自动调用工具
ResponseFormat = typeof(AssignmentResult) // 🔑 结构化输出
},
kernel: kernel
);
// 反序列化为强类型对象,无需正则解析
var result = JsonSerializer.Deserialize<AssignmentResult>(response.Content!);
Console.WriteLine($"分配结果: {result?.Success}, 原因: {result?.Reason}");
// 结构化输出 DTO
public record AssignmentResult(
bool Success,
string? AssignedTo,
string Reason,
string? SuggestedNextAction
);
为什么结构化输出对企业至关重要?
- 下游代码可以
if (result.Success)分支处理,而非解析自然语言 - 序列化/存储/审计都有确定 schema
- LLM 被迫按格式思考,减少幻觉
六、可观测性:生产环境的必备能力(1 分钟)
企业级 Agent 不能是黑盒。SK 原生支持 OpenTelemetry:
using OpenTelemetry.Trace;
using OpenTelemetry.Resources;
services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("TicketAgent"))
.WithTracing(tracing => tracing
.AddSource("Microsoft.SemanticKernel*") // 🔑 SK 内置 Activity Source
.AddConsoleExporter() // 替换为 OTLP/Jaeger/Zipkin
);
启用后,每次 LLM 调用、Plugin 执行、Token 消耗都会自动生成 Trace Span:
[Trace] TicketAgent
└─ chat.completion gpt-4o-mini (1.2s, 850 tokens)
├─ tool_call: get_ticket (TK-20260704-003) → 45ms
├─ tool_call: list_available_agents (network) → 12ms
└─ tool_call: assign_ticket (TK-..., 张三, ...) → 38ms
这解决了企业 AI 最大的运维痛点:当 Agent 行为异常时,你能像排查微服务一样逐跳定位问题,而非对着聊天记录猜。
七、完整运行效果
用户: 请帮我把 TK-20260704-003 分配出去
[SK 内部流程]
→ get_ticket("TK-20260704-003")
← { Category: "network", Priority: "P1", Status: "open" }
→ list_available_agents("network")
← ["张三", "李四"]
→ assign_ticket("TK-20260704-003", "张三", "P1网络故障,张三为当日网络组值班负责人")
← "✅ 工单 TK-20260704-003 已成功分配给 张三"
输出: 分配结果: True, 原因: P1网络故障,张三为当日网络组值班负责人
整个过程 LLM 自主决策调用顺序和参数,但每一步都在强类型约束和可观测范围内。
八、从 Demo 到生产的检查清单
完成上述代码后,你有了一个可运行的原型。但要上生产,还需补齐:
| 检查项 | 说明 | 优先级 |
|---|---|---|
| ✅ 人机协同审批 | 高风险操作(如 P0 工单分配)增加 Human-in-the-loop 确认环节 | P0 |
| ✅ 速率限制 & 重试 | Plugin 内对下游服务做 Polly 熔断,防止 LLM 疯狂重试打垮后端 | P0 |
| ✅ Prompt 版本管理 | System Prompt 存入配置中心/Git,支持灰度回滚 | P1 |
| ✅ 评估体系 | 建立 Golden Dataset,CI 中自动跑回归测试 | P1 |
| ✅ 权限隔离 | 不同角色用户的 Plugin 可见性不同(SK 支持动态 Plugin 过滤) | P1 |
| ✅ Token 成本监控 | OpenTelemetry Metrics 追踪每请求 Token 消耗 | P2 |
| ✅ 流式输出 | 长回复启用 Streaming,改善用户体验 | P2 |
九、常见误区纠正
❌ “Plugin 越多越好”
正解:每个 Plugin 方法都是 LLM 的决策负担。超过 15 个工具时准确率显著下降。合并细粒度操作,只暴露业务语义完整的动作。
❌ “Description 随便写写就行”
正解:Description 的质量直接决定 Agent 可靠性。把它当作 API 文档来写,包含前置条件、副作用、返回值含义。建议团队 Review Plugin 时重点审查 Description。
❌ “用 SK 就不用管 Prompt 了”
正解:SK 降低了 Prompt 工程的频率,但没有消除它。System Prompt 仍然是 Agent 行为的“宪法”。好的 System Prompt + 好的 Plugin = 可靠 Agent;差的 System Prompt + 好的 Plugin = 偶尔可靠的 Agent。
❌ “C# 做 Agent 比 Python 慢”
正解:Agent 的瓶颈是 LLM API 延迟(秒级),不是本地代码执行(毫秒级)。SK 的 C# 实现与 Python SDK 调用同一后端 API,端到端延迟无差异。而强类型带来的开发效率和运行时稳定性,反而缩短了整体交付周期。
十、下一步学习路径
- 深入 Plugin 高级用法:动态 Plugin、Prompt Template as Plugin、gRPC/REST 自动导入
- RAG 集成:SK Memory + Vector Store(Azure AI Search / Qdrant / PostgreSQL pgvector)
- 多 Agent 协作:SK Agent Framework(Handoff、Group Chat、Delegation)
- 评估与 CI:Semantic Kernel Eval + xUnit 自动化回归
- 生产部署:.NET Aspire + SK + Azure Container Apps 全链路模板
参考资料
- Semantic Kernel 官方文档: https://learn.microsoft.com/en-us/semantic-kernel/
- SK C# Samples Repository: https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples
- OpenTelemetry .NET Integration: https://opentelemetry.io/docs/languages/net/
- Structured Outputs Guide: https://platform.openai.com/docs/guides/structured-outputs
更多推荐

所有评论(0)