在Python生态中,LangChain几乎成了AI Agent的代名词。而在.NET阵营,很多开发者还停留在"直接调SDK发HTTP请求"的原始阶段,手动解析function_call、拼接对话历史、处理多轮工具调用,代码冗余且容易出错。

事实上,微软官方早已为.NET生态打造了完整的智能体开发框架——Semantic Kernel。它不是简单的SDK封装,而是一套完整的内核架构,原生支持插件化工具调用、多步推理编排、记忆检索与计划生成,与C#强类型体系深度融合,编译期就能发现大量问题。

本文从工程落地视角出发,拆解工具调用智能体的核心架构,给出可直接复用的插件化实现方案,并覆盖权限控制、异常容错、动态扩展等工业级特性。

一、核心架构:智能体的三层能力模型

一个完整的工具调用智能体,本质上由三层能力构成:模型推理层负责理解与决策,工具执行层负责对接外部系统,编排内核层负责调度两者的多轮交互。Semantic Kernel正是中间那层编排内核。

Kernel是整个框架的核心容器,它聚合了三类资源:

  • AI服务:统一抽象聊天补全、文本嵌入等模型能力,支持OpenAI、Azure OpenAI、国产大模型等多种后端
  • 插件集:以.NET类形式封装的工具函数,通过特性标注元数据,模型可自主发现并调用
  • 记忆与规划器:提供向量检索、任务拆解等高级能力,支撑复杂Agent场景

工具调用的完整执行链路是:Kernel将所有插件函数序列化为JSON Schema → 随对话历史一同发送给LLM → LLM决策是否调用工具 → Kernel解析参数并执行本地函数 → 将执行结果回填上下文 → 再次送入LLM生成最终回答。整个多轮往返过程对上层透明,开发者只需专注写业务函数。

二、前期准备

创建控制台或Web项目,安装核心NuGet包:

Install-Package Microsoft.SemanticKernel
Install-Package Microsoft.SemanticKernel.ChatCompletion

如果使用OpenAI服务,还需安装对应提供商包:

Install-Package Microsoft.SemanticKernel.Connectors.OpenAI

建议使用.NET 8及以上版本,SK 1.1x系列API已趋于稳定,适合生产环境使用。

三、基础实现:三步搭建可调用工具的智能体

3.1 第一步:构建Kernel内核

Kernel采用建造者模式配置,支持依赖注入集成。在ASP.NET Core中可以直接注册为服务,控制台程序则手动构建。

using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "gpt-4o-mini",
    apiKey: "your-api-key");

// 注册插件(后面定义)
builder.Plugins.AddFromType<SystemToolPlugin>();
builder.Plugins.AddFromType<WeatherToolPlugin>();

Kernel kernel = builder.Build();

Kernel对象本身是轻量级的,但内部持有的模型连接、插件实例建议复用。Web场景下按作用域创建,单例场景注意线程安全。

3.2 第二步:定义工具插件

插件就是普通C#类,通过KernelFunctionDescription特性标注元数据。特性描述越精准,模型调用的准确率越高。

public class SystemToolPlugin
{
    [KernelFunction]
    [Description("获取当前系统时间,用于回答与日期时间相关的问题")]
    public string GetCurrentTime()
    {
        return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
    }

    [KernelFunction]
    [Description("执行数学计算,支持加减乘除和基本表达式")]
    public double Calculate([Description("数学表达式字符串")] string expression)
    {
        try
        {
            var table = new System.Data.DataTable();
            var result = table.Compute(expression, null);
            return Convert.ToDouble(result);
        }
        catch
        {
            throw new ArgumentException("表达式格式无效");
        }
    }
}

插件方法支持同步、异步、泛型返回值等多种签名。参数支持基本类型、数组、自定义类,SK会自动完成JSON反序列化。

3.3 第三步:启用自动工具调用

配置FunctionChoiceBehavior为Auto模式,Kernel会自动处理工具调用的完整多轮循环,开发者只需拿到最终结果。

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

var result = await kernel.InvokePromptAsync(
    "现在北京时间几点?帮我算一下256乘以1024等于多少",
    new(settings));

Console.WriteLine(result);

这一行调用背后,Kernel自动完成了:识别需要调用工具 → 选择合适的函数 → 解析参数 → 执行GetCurrentTimeCalculate → 将结果返回模型 → 生成自然语言回答。整个过程无需手动干预。

四、进阶控制:手动编排工具调用

生产环境往往不能完全放权给AI自动执行,需要人工介入审批、审计日志、权限校验。这时可以切换为手动调用模式,精确控制每一步。

var settings = new OpenAIPromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
};

ChatHistory history = [];
history.AddUserMessage("查询北京明天的天气,并给出穿衣建议");

while (true)
{
    var response = await kernel.GetRequiredService<IChatCompletionService>()
        .GetChatMessageContentAsync(history, settings, kernel);
    
    // 模型返回文本回答,结束循环
    if (response.Content is not null)
    {
        Console.WriteLine(response.Content);
        break;
    }
    
    // 模型请求调用工具,人工审批后执行
    foreach (var functionCall in response.Items.OfType<FunctionCallContent>())
    {
        // 权限校验、审计日志、人工审批都可以加在这里
        Console.WriteLine($"AI请求调用: {functionCall.PluginName}.{functionCall.FunctionName}");
        
        var functionResult = await functionCall.InvokeAsync(kernel);
        history.Add(response);
        history.Add(functionResult.ToChatMessageContent());
    }
}

手动模式的价值在于:高危工具调用前增加人工确认、调用前后插入审计日志、对参数做安全校验、限制单轮最大调用次数防止死循环。

五、可扩展架构设计

真正的工业级Agent不会把所有工具写死在代码里,需要支持动态加载、热插拔、沙箱隔离。

5.1 插件动态发现与加载

基于反射扫描程序集,自动发现标注了KernelFunction的类,无需逐个注册:

public static void AddAllPlugins(this IKernelBuilder builder, Assembly assembly)
{
    var pluginTypes = assembly.GetTypes()
        .Where(t => t.GetMethods()
            .Any(m => m.GetCustomAttribute<KernelFunctionAttribute>() != null));
    
    foreach (var type in pluginTypes)
    {
        builder.Plugins.AddFromType(type);
    }
}

配合.NET的AssemblyLoadContext,可以实现插件热加载,不重启主程序就能新增工具能力。

5.2 工具分级与权限控制

不同用户、不同场景下可用的工具集不同。通过特性标记工具安全等级,调用时动态过滤:

[AttributeUsage(AttributeTargets.Method)]
public class ToolSecurityLevelAttribute : Attribute
{
    public SecurityLevel Level { get; }
    public ToolSecurityLevelAttribute(SecurityLevel level) => Level = level;
}

// 使用时按当前用户权限过滤插件
var availableFunctions = kernel.Plugins.GetFunctionsMetadata()
    .Where(f => GetSecurityLevel(f) <= userPermissionLevel);

5.3 调用过滤器与审计

SK支持函数调用过滤器,类似ASP.NET的中间件,可以在工具执行前后插入统一逻辑:

public class AuditFilter : IFunctionInvocationFilter
{
    public async Task OnFunctionInvocationAsync(
        FunctionInvocationContext context, Func<Task> next)
    {
        var log = new 
        {
            context.Function.PluginName,
            context.Function.Name,
            context.Arguments,
            Timestamp = DateTime.Now
        };
        
        // 记录调用前审计
        await WriteAuditLogAsync(log);
        
        try
        {
            await next();
            // 记录成功结果
        }
        catch (Exception ex)
        {
            // 记录失败异常
            throw;
        }
    }
}

// 注册过滤器
builder.Services.AddSingleton<IFunctionInvocationFilter, AuditFilter>();

六、工业级容错与稳定性保障

工具调用涉及外部系统,网络超时、接口异常、参数错误都可能发生。健壮的Agent必须有完善的容错机制。

6.1 自动重试与熔断

结合Polly策略,为插件方法增加重试、熔断、超时保护:

private static readonly AsyncRetryPolicy RetryPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(attempt));

[KernelFunction]
public async Task<string> QueryDatabaseAsync(string sql)
{
    return await RetryPolicy.ExecuteAsync(async () =>
    {
        // 实际数据库查询逻辑
        return await ExecuteQueryInternalAsync(sql);
    });
}

6.2 调用深度限制

防止模型陷入工具调用死循环,设置最大调用轮次上限,超过则强制终止并返回结果:

int maxIterations = 10;
for (int i = 0; i < maxIterations; i++)
{
    // 执行一轮推理
    var response = await GetModelResponseAsync(history);
    
    if (!response.HasFunctionCalls) break;
    
    // 执行工具调用
    await ExecuteToolCallsAsync(response, history);
}

6.3 参数校验

不要完全信任模型生成的参数。工具方法入口处必须做合法性校验,防止SQL注入、路径穿越、越权访问等安全风险。

七、常见踩坑与最佳实践

坑一:插件描述写得太简略。Description是模型理解工具用途的唯一依据,写得越模糊,调用准确率越低。建议包含:功能说明、适用场景、参数含义、返回值格式。

坑二:返回纯文本而非结构化数据。工具返回的结果会被送回模型继续推理。纯自然语言返回会消耗大量token且容易产生歧义,优先返回JSON格式的结构化数据。

坑三:插件粒度过大。一个函数做太多事,模型难以决策何时调用。建议遵循单一职责,每个工具只做一件事,由模型负责组合编排。

坑四:忽略异常信息的反馈。工具执行失败时,不要吞掉异常直接返回空。将错误信息如实返回给模型,它通常能根据错误调整参数或换用其他工具。

最佳实践清单:

  • 工具方法保持纯函数特性,减少外部状态依赖
  • 输入输出使用基本类型,避免复杂类导致序列化问题
  • 长耗时工具设置超时,避免阻塞整个推理链路
  • 敏感操作增加二次确认,不要让AI直接执行高危动作
  • 保留完整的调用链路日志,便于排查问题

八、总结与选型建议

Semantic Kernel作为.NET原生的AI Agent框架,最大的优势在于与.NET生态的深度融合。强类型插件、依赖注入、过滤器管道、异步编程模型,都是C#开发者熟悉的范式,学习成本远低于移植Python方案。

对于简单场景,几行代码开启Auto模式就能获得完整的工具调用能力;对于复杂企业应用,其插件化架构、过滤器机制、手动编排能力足以支撑生产级需求。配合记忆、规划器等模块,还可以进一步升级为具备规划、记忆、行动能力的完整智能体。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐