☰
.NET搞AI的正确打开方式:ML.NET、ONNX Runtime与Semantic Kernel实战
2026/10/21 23:01:49 网站建设 项目流程

先说个结论:.NET 不是不能搞 AI,是你还没找对打开方式。

我经常在技术群里看到这样的论调:“搞机器学习必须用 Python,.NET 那套做做后端还行。”甚至有人直接下定论:“.NET 搞 AI 不行,想都不用想。”说的人多了,不少刚接触.NET 的朋友也就真的信了。前阵子我遇到一个做智能客服的团队,他们把一个文本分类模型从 Python 服务改写成 C# 推理服务,延迟反而降了百分之三十。这件事让我特别想好好聊一聊:到底是 .NET 干不了 AI,还是我们对“搞 AI”这件事的理解太窄了。

这篇文章主要适合这几类人。第一类是长期做 .NET 后端、耳朵里听惯了“AI 必须用 Python”的开发者;第二类是项目里要加一点视觉识别、文本分类或者模型部署能力,但不太想为了一个功能就重写整套服务的人;第三类是已经在用 Python 做模型训练,但推理上线时被依赖管理、版本兼容、高并发压得比较难受的人。我会把 .NET 生态里能干的 AI 事逐件梳理一遍,再给两个可以直接复现的实操例子,最后把我在生产环境里踩过的坑整理出来。

我先把态度放在前面:对于大多数企业的 AI 需求,.NET 不仅能做,而且很适合做。搞不定的那部分,其实是真正的前沿算法研究和超大模型训练,而那些本来就不应该让业务开发团队去碰。

1. 为什么会有“.NET 搞 AI 不行”的刻板印象

要破除一个偏见,先得知道偏见从哪来。这个问题的根源不在技术本身,而在生态的演进路径和人群的信息错位。

1.1 Python 太早占据了 AI 的内容话语权

Python 在人工智能领域的火爆,可以追溯到深度学习框架刚开始普及的时期。早期的主流深度学习框架纷纷把 Python 作为主要接口,研究社区里几乎所有论文代码、预训练模型、开源 repositories 都是用 Python 写的。搜索引擎、技术社区、在线课程里的“AI”或者“机器学习”相关内容,默认都是 Python 示例。一个新手想学 AI,搜出来的东西几乎全是 Python,久而久之脑子里就形成一条等式:AI 等于 Python,Python 等于 AI。

但我们需要把这个等式拆开看。模型训练阶段确实几乎所有框架的一手支持都在 Python,这是事实。问题是,训练只是 AI 真正落地的一部分,而且往往不是最耗时、最复杂的那部分。模型一旦训练完,后面还有一堆事:要把模型打包成一个稳定的服务接口、接入业务系统、做鉴权审计、跟现有数据链路打通、配监控指标、做灰度发布。这些事情才是 AI 功能实际上线时真正占工作量的部分,而它们恰恰不依赖 Python。用一个直白一点的比方:Python 像是研究阶段的实验室,.NET 像是生产车间。你可以因为实验室里到处写着 Python,就断定生产车间一定不能用。

1.2 “业务系统开发”的标签带来人群错位

另一边,.NET 多年以来在企业软件、后台管理系统、金融、办公自动化这些领域里占有率非常高。跑在 .NET 上的程序员大部分时间在和数据库、接口、权限、流程打交道,工作内容看起来离算法很远。所以大家天然有一个印象:这是两类完全不相关的东西。

但是做过一次完整项目上线的人都会明白,业务系统里真正需要算法的场景一点都不少。比如单据识别、合同抽取、风险评分、客服问答、质检分类。很多公司的做法是用 Python 团队单独做一个推理服务,再开一个 HTTP 接口给 .NET 调用,结果就是整套系统里多出一个依赖庞杂、部署困难的外部依赖。这种架构上的割裂会带来大量隐形沟通成本。反复看见这种场景以后我才意识到:如果 .NET 团队自己有能力把推理这件事接进来,很多人为的架构隔阂完全可以消除。

2. .NET 生态里到底有什么能用的 AI 组件

先别急着写代码,你来盘点一下现在手里的牌。今天的 .NET AI 生态不是空白一片,而是至少有三条清晰主线。

2.1 ML.NET:结构化数据场景里的原生选手

ML.NET 是 .NET 平台原生的机器学习框架,最适合处理的场景是结构化数据,比如预测用户流失、订单异常检测、销量回归分析。它不需要 GPU,不依赖外部 Python 环境,一个控制台项目就能跑起来。框架自带一套基于 IDataView 的数据管线抽象,把数据清洗、特征变换、模型训练、性能评估统一在一个流程里。

ML.NET 最大的优势在于和现有 .NET 代码的融合度。训练数据的数据模型可以直接用普通 C# class 定义,训练完的模型可以保存为 zip 文件,然后在服务里加载。很多做传统企业应用的团队,用几行代码就能在现有系统里加入一个简单的预测能力,这种“无缝嵌入”的体验是 Python 脚本很难给你的。当然它的短板也很明显:深度学习支持比较薄弱,如果目标是图像识别、生成式 AI 这些重任务,ML.NET 不是最合适的选择,这时候要看向下一个组件。

2.2 ONNX Runtime:把“Python 训练”和“.NET 推理”打通的关键桥梁

ONNX 是一种开放的模型交换格式,主流深度学习框架都支持把训练好的模型导出成 .onnx 文件。.NET 端通过 ONNX Runtime NuGet 包可以直接加载并执行这种模型文件,不需要安装 Python,不需要维护 conda 环境。这一下就把“训练在 Python、部署在 .NET”的路线彻底走通了。

实际落地时最常用的方式是:算法同事用 PyTorch 或 TensorFlow 训练模型,导出成 ONNX 文件交给 .NET 团队。.NET 团队把 ONNX Runtime 当作一个普通依赖引进来,通过 API 完成图像预处理、张量填充、模型推理和结果解析。整个过程对 .NET 工程师来说就是一个“加载模型文件 + 传数据进去 + 拿结果出来”的标准流程,把它装进现有微服务或者单体应用里没有任何心理负担。这也是我在生产环境里最推荐的一套组合,后面第三节我会演示具体操作。

2.3 Semantic Kernel:大模型时代 .NET 的入场券

大语言模型流行之后,.NET 生态也没有原地踏步。Semantic Kernel 是面向大模型应用场景的一套框架,把对话补全、提示词模板、函数调用、记忆等能力打包成了可编程接口。和直接用 HttpClient 调用大模型接口相比,SK 的核心价值在插件化抽象:开发者可以用 C# 写一个正常类,把方法标记成 KernelFunction,模型会在需要的时候自动调用这些方法。

这个机制带来的直接好处是,现有 .NET 业务服务里的能力可以被大模型复用,不需要重新学一套编程范式。假设你有一个查询订单状态的服务方法,把它注册为插件后,用户用自然语言问“我那个订单到哪了”,模型会在背后完成意图理解、触发对应方法、再把结果整理成自然语言返回。放在过去,这一套要拆成意图识别、实体抽取、流程编排好几个步骤,而现在靠一个清晰的抽象层就收拢了。

我把不同场景适合的技术路线整理成一张表,方便团队评估:

场景推荐方案适合团队典型路径
表格数据预测ML.NET纯 .NET 团队训练和预测都用 C#完成
图像、文本模型推理部署ONNX Runtime.NET 团队 + 少量 PythonPython 导出 ONNX,.NET 加载推理
大模型对话和工具调用Semantic Kernel熟悉业务的 .NET 团队用 C# 注册插件,模型自动调用
学术研究、自定义模型训练Python 生态算法团队训练留在 Python,产出模型落盘

3. 实操一:把图像分类模型部署成 .NET Web API

这一节是动手能力最强的部分。我以经典卷积图像分类模型为例,把从模型导出到接口上线的完整链路跑一遍。

3.1 在 Python 侧把模型导出为 ONNX

假设算法团队已经准备好一个图像分类模型,现在要交接给 .NET 团队。在 Python 侧,最简单的导出代码是这样的:

import torch from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "resnet18.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}} ) print("export ok")

这里有个值得注意的细节:dynamic_axes把 batch 维度标成了动态。如果你的服务只处理单张图片,固定 batch 为 1 也可以;但如果未来想要搞并发批处理,建议保留动态 batch,这样服务端可以根据实时负载决定一次送几张图进模型,吞吐量会更好。

导出来的 .onnx 文件一般几 MB 到几十 MB,放进 .NET 项目一个目录里即可。从这一刻起,新的部署环境完全不需要再安装 Python 运行时。

3.2 在 C# 中加载 ONNX 并构建预测管线

在 .NET 项目里,我建议直接用 ML.NET 的 ONNX 集成,因为它可以省掉大量手动拼接张量的操作。先在 NuGet 里引入这些包:

  • Microsoft.ML.OnnxRuntime
  • Microsoft.ML.ImageAnalytics
  • 图像解码库(System.Drawing.Common 或 ImageSharp,我后面会展开说)

然后定义一个模型封装类:

public class ImageInput { public byte[] Image { get; set; } } public class ImageOutput { public float[] Output { get; set; } } public class ImageClassifier { private readonly PredictionEngine<ImageInput, ImageOutput> _engine; public ImageClassifier(string modelPath) { var context = new MLContext(); var pipeline = context.Transforms.ResizeImages( inputColumnName: "image", outputColumnName: "resized", imageWidth: 224, imageHeight: 224) .Append(context.Transforms.ExtractPixels( outputColumnName: "input", offsetImage: 0f, scaleImage: 1f / 255f, interleavePixelColors: true)) .Append(context.Transforms.ApplyOnnxModel( modelFile: modelPath, outputColumnName: "output", inputColumnName: "input")); var emptyData = context.Data.LoadFromEnumerable( new List<ImageInput> { new ImageInput { Image = new byte[1] } }); var model = pipeline.Fit(emptyData); _engine = context.Model.CreatePredictionEngine<ImageInput, ImageOutput>(model); } public int Predict(byte[] imageBytes) { var prediction = _engine.Predict(new ImageInput { Image = imageBytes }); var scores = prediction.Output; var maxIndex = 0; for (var i = 1; i < scores.Length; i++) { if (scores[i] > scores[maxIndex]) maxIndex = i; } return maxIndex; } }

这个代码里最容易犯错的是预处理参数。机器人图像分类模型训练时如果用的是 PyTorch 的ToTensor和Normalize,像素值需要除以 255,通道顺序是 RGB,还可能有特定的均值和标准差。上面的示例只做了缩放和除以 255,属于最简情况。如果模型是用 BGR 通道训练的,或者归一化参数不同,预测结果会完全不靠谱。关于这个坑,我在第 5 节单独细说。

另外一个非常关键的点:PredictionEngine初始化时会加载模型文件,这个动作通常需要几百毫秒甚至几秒,千万不要放在请求处理函数里每次执行。应该做成单例,在服务启动时完成加载。

3.3 包装成 Web API 并做一次简单压测

用 ASP.NET Core 的最小 API 接口把上面这个类暴露出来,非常简洁:

var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(new ImageClassifier("resnet18.onnx")); builder.Services.AddCors(); var app = builder.Build(); app.MapPost("/classify", async (IFormFile file, ImageClassifier classifier) => { await using var stream = new MemoryStream(); await file.CopyToAsync(stream); var imageBytes = stream.ToArray(); var label = classifier.Predict(imageBytes); return Results.Json(new { label = label }); }); app.Run();

启动以后,用 curl 或者 Postman 传一张图片进去,接口就会返回模型预测的类别编号。我在一台普通 CPU 机器上测过这个接口,单张图片从请求进入到响应返回大约 80 到 120 毫秒,其中有一部分时间花在图片缩放和像素提取上。如果配上 GPU 推理,延迟还能继续下降。这里有一个很多人以为的误区:ONNX Runtime 不是只能排队跑,它支持并发推理,多个请求进来会在线程池上并行执行。只要你把模型封装成线程安全单例,吞吐量并不会差。

如果你在容器里压测发现吞吐量上不去,优先检查是不是每次请求都新建了模型对象,而不是去怀疑框架本身。

4. 实操二:用 Semantic Kernel 把大模型能力接进业务系统

传统模型推理说完了,再看大模型应用方向。很多 .NET 开发者觉得大模型应用只能指望 Python 生态,同样是个刻板印象。

4.1 编写一个最小可运行的对话应用

用 NuGet 引入 Semantic Kernel 之后,一个最简单的对话程序只有这么几行:

using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; var builder = Kernel.CreateBuilder(); builder.AddOpenAIChatCompletion( modelId: "gpt-4o-mini", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!); var kernel = builder.Build(); var chat = kernel.GetRequiredService<IChatCompletionService>(); var message = new ChatMessageContent(AuthorRole.User, "用三句话介绍你自己"); var reply = await chat.GetChatMessageContentAsync(message); Console.WriteLine(reply.Content);

这段代码里把 API Key 写在代码里只是演示,生产环境一定要从配置中心或环境变量读取。SK 本身接入了多种模型提供方。如果你的公司用的是兼容 OpenAI 协议的内部模型服务,只需要改一下客户端配置,业务代码几乎不用动。

4.2 注册业务函数,让模型决定何时调用

SK 对业务开发最实用的部分就是函数调用。假设系统里有一个查询订单状态的服务,把它包装进插件:

public class OrderPlugin { [KernelFunction("query_order_status")] public string QueryOrderStatus(string orderId) { // 这里实际应该查库或调用内部微服务 return $"订单 {orderId} 当前已发货,预计明天送达。"; } } builder.Plugins.AddFromType<OrderPlugin>();

注册完成后,当用户问“帮我看看订单 1024 到哪里了”,模型会在对话过程中发现自己缺少订单状态这个信息,于是生成一次函数调用请求,由 SK 调度执行OrderPlugin里的方法,再把方法返回值交给模型。模型最终结合这个工具结果,生成一句自然语言回答。对使用者来说,整个过程就是一次普通对话,压根感知不到背后有一个工具调用环节。

对开发团队来说,这个模式最大的吸引力是:你可以继续用 C# 写业务逻辑,完全不需要把业务逻辑翻成 Python 脚本。模型负责理解自然语言并判断该调用哪个函数,而你负责把函数写得可靠、可测、可监控。链路里的每个环节都能单独做单元测试,很符合 .NET 工程师的直觉。

4.3 生产化部署时的模型分级和成本控制

生成式模型调用是持续产生费用的,线上不能一上来就无脑配最强模型。我见过不少团队一上来就选了能力最高的模型,结果月底看账单才后悔。合理的做法是给不同技能配置不同模型:简单问答、意图识别用响应快、价格低的模型;复杂推理、长文档总结再升级到能力更强的模型。SK 里可以做这样的策略配置,但前提是团队内部要提前约定一个清晰的模型分级名单。

还有一个容易被忽略的问题是流式响应。对话场景尽量启用流式输出,不要等服务端把完整回答生成完再一次性返回。SK 提供了流式接口,ASP.NET Core 端可以用 SSE 或者 WebSocket 把消息一段段推给前端。这样首字延迟可以从几秒降到几百毫秒,用户体感完全不一样,在线对话场景尤其重要。

5. 生产环境里踩过的坑和排查笔记

这一节我想把自己实际踩过的一些坑记下来,没踩过的朋友可以少走弯路。

5.1 ONNX 模型加载失败、DLL 找不到

ONNX Runtime 是通过 NuGet 引进去的,里面带了不同平台的原生库。最容易出问题的地方是发布时没有指定运行时标识符,导致 Linux 容器里找不到 onnxruntime 原生库。看到类似于Unable to load DLL 'onnxruntime'的报错,优先检查发布时的 RuntimeIdentifier,以及目标镜像的平台架构是否和 NuGet 包里的 native 库一致。

5.2 图片预处理不一致导致结果完全离谱

这个坑我至少遇到过三次。模型训练时如果用的是 PyTorch 的ToTensor加Normalize,图像要除以 255,通道顺序是 RGB,并且还会用特定的 mean、std 做标准化。上面我演示的简化代码只做了除以 255,这时如果模型原本要求标准化,预测结果就会对不上。凡是遇到结果明显离谱的情况,第一步不是怀疑模型,而是回去核对模型开发者当年的预处理代码,把 Resize 尺寸、通道顺序、归一化系数逐一对照清楚。

5.3 Linux 容器里的图像解码库问题

在 Linux 容器里使用 System.Drawing.Common 处理图片,会依赖 GDI+ 相关库。很多精简版容器镜像没有这些基础库,跑起来直接报错。解决办法有两个:要么在 Dockerfile 里安装对应依赖,要么干脆换成 ImageSharp 或 SkiaSharp 这类跨平台库。从后续可维护性考虑,我更推荐 ImageSharp,它没有系统库依赖,跨平台行为也更可预期。所以图片预处理环节,最好一开始就设计成与平台库无关的实现。

5.4 PredictionEngine 的线程安全与并发

ML.NET 的PredictionEngine不是完全线程安全的。如果你在并发请求里共享同一个实例,有可能遇到状态混乱。实现上要么每次请求临时创建,但开销不小;要么维护一个对象池;要么就直接用 ONNX Runtime 自带的InferenceSession来管理模型会话。我的建议是:ML.NET 做原型非常顺手,但生产高并发场景里,直接上手 ONNX Runtime 原生接口更可控。

5.5 常见问题速查表

现象可能原因快速处理方式
模型加载报 DLL 缺失发布时未指定 RID 或原生依赖缺失指定 RuntimeIdentifier 后重新发布
推理结果全是同一个类别图片预处理和训练时不匹配核对尺寸、通道、归一化参数
Linux 容器图片读取异常System.Drawing.Common 缺库换用 ImageSharp 或 SkiaSharp
并发请求延迟明显变高PredictionEngine 争用或反复加载模型改为单例模型,必要时走原生会话
第一次请求特别慢模型文件在启动后才被加载服务启动时提前预热模型

6. 哪些场景该用 .NET 做 AI,哪些不该碰

最后这一节,我把自己的选择逻辑完整交代清楚。这不是为了证明“.NET 什么都能干”,而是把决策标准放在桌面上供大家参考。

6.1 训练研究归 Python,稳定交付归 .NET

如果你是一个在业务系统里长期写 C# 的工程师,大概率不会自己从头训练一个超大模型。线上绝大多数 AI 能力要么来自开源模型,要么来自团队内部算法同事训练好的模型。这类模型的推理阶段完全可以用 .NET 承载。反过来,如果你做的是算法结构创新、论文复现、新模型架构探索,那确实应该留在 Python,那边的生态迭代是最快的。两边各做各擅长的事,用 ONNX 文件或者肩简单口的 HTTP 协议作为交接点,是最稳的分工方式。

6.2 最适合 .NET 团队入手的 AI 方向

以我的经验看,这三类方向最值得用 .NET 去做。第一类是表格数据建模,比如用户流失预警、库存预测、异常检测,用 ML.NET 性价比很高。第二类是把现成的视觉模型、文本模型服务化,模型文件以 ONNX 或同类格式存在,部署进现有 .NET 应用几乎没有额外负担。第三类是构建大模型应用,用 Semantic Kernel 或者直接调用大模型接口,把业务能力封装成函数暴露出去,整体复杂度可控。

如果你现在正在纠结“要不要为一个小功能单独引一套 Python 微服务”,我的建议是没必要。当模型推理只是系统内部的一个普通依赖时,把它放进同一个进程反而更利于维护。你不需要维护一套额外的 Python 环境,不需要处理服务间不稳定的连接,不需要为了一个简单推理接口专门搭一套部署和监控链路。

6.3 几条实在话

第一,不要试图一步到位直接接大模型。先在现有业务里找一个最重复、最耗人力的环节,看看能不能用一个轻量分类器或小模型解决,把第一个成果做小做透。第二,优先打通 ONNX 这条链路。无论团队里有没有算法同事,你都可以找到合适的开源预训练模型转成 ONNX,在 .NET 里先跑通一次再谈复杂度。第三,要提前重视推理端的资源和稳定性。模型推理不是普通业务代码,它吃 CPU,也吃内存,更考验并发设计。上线前要做压测,重点看推理耗时的 P95 值,别只看平均指标。

从我个人的体会来说,早几年我也觉得 Python 才是 AI 世界的“母语”。后来在一个项目里被逼着用 .NET 去接一个文本分类模型,做完以后我的想法彻底变了。.NET 在工程化、类型安全、可观测性、部署便利性这些方面确实很扎实,而这些恰恰是 AI 功能从“能跑”走到“真正在生产里稳定跑”最稀缺的能力。今天如果有人再跟我说“.NET 搞 AI 不行”,我大概率会反问一句:你说的“搞 AI”,是指自己训练一个全新的模型,还是把一个成熟的模型真正落地?如果答案偏向后者,那我觉得,不是 .NET 不行,是观望的人还没真正动手试过。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询