摘要:很多 .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}";
    }
}

⚠️ 三个容易踩的坑

  1. Description 是写给 LLM 看的,不是给人看的:LLM 靠它决定何时调用、传什么参。模糊描述 = 错误调用。
  2. 参数名要有语义string id 不如 string ticketId。LLM 会参考参数名推断含义。
  3. 返回值要自包含: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,端到端延迟无差异。而强类型带来的开发效率和运行时稳定性,反而缩短了整体交付周期。


十、下一步学习路径

  1. 深入 Plugin 高级用法:动态 Plugin、Prompt Template as Plugin、gRPC/REST 自动导入
  2. RAG 集成:SK Memory + Vector Store(Azure AI Search / Qdrant / PostgreSQL pgvector)
  3. 多 Agent 协作:SK Agent Framework(Handoff、Group Chat、Delegation)
  4. 评估与 CI:Semantic Kernel Eval + xUnit 自动化回归
  5. 生产部署:.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
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐