.NET原生AI Agent框架:用C#构建可扩展工具调用智能体
在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中可以直接注册为服务,控制台程序则手动构建。
usingMicrosoft.SemanticKernel;varbuilder=Kernel.CreateBuilder();builder.AddOpenAIChatCompletion(modelId:"gpt-4o-mini",apiKey:"your-api-key");// 注册插件(后面定义)builder.Plugins.AddFromType<SystemToolPlugin>();builder.Plugins.AddFromType<WeatherToolPlugin>();Kernelkernel=builder.Build();Kernel对象本身是轻量级的,但内部持有的模型连接、插件实例建议复用。Web场景下按作用域创建,单例场景注意线程安全。
3.2 第二步:定义工具插件
插件就是普通C#类,通过KernelFunction和Description特性标注元数据。特性描述越精准,模型调用的准确率越高。
publicclassSystemToolPlugin{[KernelFunction][Description("获取当前系统时间,用于回答与日期时间相关的问题")]publicstringGetCurrentTime(){returnDateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");}[KernelFunction][Description("执行数学计算,支持加减乘除和基本表达式")]publicdoubleCalculate([Description("数学表达式字符串")]stringexpression){try{vartable=newSystem.Data.DataTable();varresult=table.Compute(expression,null);returnConvert.ToDouble(result);}catch{thrownewArgumentException("表达式格式无效");}}}插件方法支持同步、异步、泛型返回值等多种签名。参数支持基本类型、数组、自定义类,SK会自动完成JSON反序列化。
3.3 第三步:启用自动工具调用
配置FunctionChoiceBehavior为Auto模式,Kernel会自动处理工具调用的完整多轮循环,开发者只需拿到最终结果。
varsettings=newOpenAIPromptExecutionSettings{FunctionChoiceBehavior=FunctionChoiceBehavior.Auto()};varresult=awaitkernel.InvokePromptAsync("现在北京时间几点?帮我算一下256乘以1024等于多少",new(settings));Console.WriteLine(result);这一行调用背后,Kernel自动完成了:识别需要调用工具 → 选择合适的函数 → 解析参数 → 执行GetCurrentTime和Calculate→ 将结果返回模型 → 生成自然语言回答。整个过程无需手动干预。
四、进阶控制:手动编排工具调用
生产环境往往不能完全放权给AI自动执行,需要人工介入审批、审计日志、权限校验。这时可以切换为手动调用模式,精确控制每一步。
varsettings=newOpenAIPromptExecutionSettings{FunctionChoiceBehavior=FunctionChoiceBehavior.Auto(autoInvoke:false)};ChatHistoryhistory=[];history.AddUserMessage("查询北京明天的天气,并给出穿衣建议");while(true){varresponse=awaitkernel.GetRequiredService<IChatCompletionService>().GetChatMessageContentAsync(history,settings,kernel);// 模型返回文本回答,结束循环if(response.Contentisnotnull){Console.WriteLine(response.Content);break;}// 模型请求调用工具,人工审批后执行foreach(varfunctionCallinresponse.Items.OfType<FunctionCallContent>()){// 权限校验、审计日志、人工审批都可以加在这里Console.WriteLine($"AI请求调用:{functionCall.PluginName}.{functionCall.FunctionName}");varfunctionResult=awaitfunctionCall.InvokeAsync(kernel);history.Add(response);history.Add(functionResult.ToChatMessageContent());}}手动模式的价值在于:高危工具调用前增加人工确认、调用前后插入审计日志、对参数做安全校验、限制单轮最大调用次数防止死循环。
五、可扩展架构设计
真正的工业级Agent不会把所有工具写死在代码里,需要支持动态加载、热插拔、沙箱隔离。
5.1 插件动态发现与加载
基于反射扫描程序集,自动发现标注了KernelFunction的类,无需逐个注册:
publicstaticvoidAddAllPlugins(thisIKernelBuilderbuilder,Assemblyassembly){varpluginTypes=assembly.GetTypes().Where(t=>t.GetMethods().Any(m=>m.GetCustomAttribute<KernelFunctionAttribute>()!=null));foreach(vartypeinpluginTypes){builder.Plugins.AddFromType(type);}}配合.NET的AssemblyLoadContext,可以实现插件热加载,不重启主程序就能新增工具能力。
5.2 工具分级与权限控制
不同用户、不同场景下可用的工具集不同。通过特性标记工具安全等级,调用时动态过滤:
[AttributeUsage(AttributeTargets.Method)]publicclassToolSecurityLevelAttribute:Attribute{publicSecurityLevelLevel{get;}publicToolSecurityLevelAttribute(SecurityLevellevel)=>Level=level;}// 使用时按当前用户权限过滤插件varavailableFunctions=kernel.Plugins.GetFunctionsMetadata().Where(f=>GetSecurityLevel(f)<=userPermissionLevel);5.3 调用过滤器与审计
SK支持函数调用过滤器,类似ASP.NET的中间件,可以在工具执行前后插入统一逻辑:
publicclassAuditFilter:IFunctionInvocationFilter{publicasyncTaskOnFunctionInvocationAsync(FunctionInvocationContextcontext,Func<Task>next){varlog=new{context.Function.PluginName,context.Function.Name,context.Arguments,Timestamp=DateTime.Now};// 记录调用前审计awaitWriteAuditLogAsync(log);try{awaitnext();// 记录成功结果}catch(Exceptionex){// 记录失败异常throw;}}}// 注册过滤器builder.Services.AddSingleton<IFunctionInvocationFilter,AuditFilter>();六、工业级容错与稳定性保障
工具调用涉及外部系统,网络超时、接口异常、参数错误都可能发生。健壮的Agent必须有完善的容错机制。
6.1 自动重试与熔断
结合Polly策略,为插件方法增加重试、熔断、超时保护:
privatestaticreadonlyAsyncRetryPolicyRetryPolicy=Policy.Handle<HttpRequestException>().WaitAndRetryAsync(3,attempt=>TimeSpan.FromSeconds(attempt));[KernelFunction]publicasyncTask<string>QueryDatabaseAsync(stringsql){returnawaitRetryPolicy.ExecuteAsync(async()=>{// 实际数据库查询逻辑returnawaitExecuteQueryInternalAsync(sql);});}6.2 调用深度限制
防止模型陷入工具调用死循环,设置最大调用轮次上限,超过则强制终止并返回结果:
intmaxIterations=10;for(inti=0;i<maxIterations;i++){// 执行一轮推理varresponse=awaitGetModelResponseAsync(history);if(!response.HasFunctionCalls)break;// 执行工具调用awaitExecuteToolCallsAsync(response,history);}6.3 参数校验
不要完全信任模型生成的参数。工具方法入口处必须做合法性校验,防止SQL注入、路径穿越、越权访问等安全风险。
七、常见踩坑与最佳实践
坑一:插件描述写得太简略。Description是模型理解工具用途的唯一依据,写得越模糊,调用准确率越低。建议包含:功能说明、适用场景、参数含义、返回值格式。
坑二:返回纯文本而非结构化数据。工具返回的结果会被送回模型继续推理。纯自然语言返回会消耗大量token且容易产生歧义,优先返回JSON格式的结构化数据。
坑三:插件粒度过大。一个函数做太多事,模型难以决策何时调用。建议遵循单一职责,每个工具只做一件事,由模型负责组合编排。
坑四:忽略异常信息的反馈。工具执行失败时,不要吞掉异常直接返回空。将错误信息如实返回给模型,它通常能根据错误调整参数或换用其他工具。
最佳实践清单:
- 工具方法保持纯函数特性,减少外部状态依赖
- 输入输出使用基本类型,避免复杂类导致序列化问题
- 长耗时工具设置超时,避免阻塞整个推理链路
- 敏感操作增加二次确认,不要让AI直接执行高危动作
- 保留完整的调用链路日志,便于排查问题
八、总结与选型建议
Semantic Kernel作为.NET原生的AI Agent框架,最大的优势在于与.NET生态的深度融合。强类型插件、依赖注入、过滤器管道、异步编程模型,都是C#开发者熟悉的范式,学习成本远低于移植Python方案。
对于简单场景,几行代码开启Auto模式就能获得完整的工具调用能力;对于复杂企业应用,其插件化架构、过滤器机制、手动编排能力足以支撑生产级需求。配合记忆、规划器等模块,还可以进一步升级为具备规划、记忆、行动能力的完整智能体。
