第2系列_第3期_.NET AI 插件热加载机制_DLL扫描到Kernel注册 .NET AI 插件热加载机制:从 DLL 扫描到 Kernel 注册系列:.NET C# Semantic Kernel 集成深度解析 ③/6难度:⭐⭐⭐(进阶源码,需.NET基础)源码仓库:github.com/zongge0812/zonggeAgent适用环境:.NET 9.0 + WPF一、引言在 AI 桌面应用中,插件的意义远超传统软件——SK 的插件系统直接赋予 AI “动手能力”。AI 不是光说不练,而是可以通过插件读取文件、分析代码、生成文档。zonggeAgent 的插件系统实现了3 个关键能力:┌──────────────────────────────────────────────────────────────┐ │ ① 热加载:新插件编译后直接放入目录,应用无需重启即可生效 │ │ ② 自动发现:DLL 扫描 + 反射 + ITools 接口自动匹配 │ │ ③ 权限过滤:聊天模式只读,开发模式全读写(安全隔离) │ └──────────────────────────────────────────────────────────────┘二、整体架构插件系统的核心是PluginLoader静态类,位于Tools/Implementations/目录下:PluginLoader(静态类) │ ├── 缓存:_pluginsCache(ListPluginsCache) │ ├── RegisterPlugin(Kernel) → 入口:将插件注册到SK Kernel │ ├── LoadPluginsFromDirectories() → 核心:多目录扫描 + 反射加载 │ ├── ScanDirectoryForPlugins(baseDir) → 主目录 │ └── ScanDirectoryForPlugins("plugins/") → 热更新目录 │ ├── ClearPluginCache() → 清空缓存,支持热替换时重新加载 │ └── RegisterPluginDependencies() → 注册插件依赖到SK的IServiceCollection调用位置在BaseAIProviderService.SendMessageAsync()和SendMessageStreamAsync()中调用:// 每次对话都会执行varchatService=awaitCreateChatServiceAsync(config);varkernel=CreateKernel(chatService);// ⚡ 热加载注册插件工具PluginLoader.RegisterPlugin(kernel);// 💬 聊天模式过滤kernel=ApplyChatModeFilter(kernel,chatService);这意味着:每次对话都会重新检查插件,热更新后的 DLL 在下一次对话立即生效。三、DLL 扫描策略3.1 双目录扫描privatestaticvoidLoadPluginsFromDirectories(Kernelkernel,stringpluginDir="plugins",stringsearchPattern="*.dll"){varscannedAssemblies=newListAssembly();varbaseDir=AppDomain.CurrentDomain.BaseDirectory;// 1️⃣ 扫描程序主目录(bin/Debug/net9.0-windows/)// 编译后的插件DLL所在位置ScanDirectoryForPlugins(baseDir,searchPattern,scannedAssemblies);// 2️⃣ 扫描 plugins/ 子目录(热加载扩展点)varpluginDirectory=Path.Combine(baseDir,pluginDir);if(Directory.Exists(pluginDirectory)){ScanDirectoryForPlugins(pluginDirectory,searchPattern,scannedAssemblies);}else{Directory.CreateDirectory(pluginDirectory);// 创建空目录}}扫描目录用途更新方式主目录bin/Debug/net9.0-windows/项目编译时自动输出的插件DLL重新编译项目plugins/外部开发者/社区贡献的热加载DLL直接放入DLL文件,无需编译3.2 程序集过滤不是所有 DLL 都是插件——ScanDirectoryForPlugins内部实现了严格的过滤逻辑:privatestaticvoidScanDirectoryForPlugins(stringdirectory,stringsearchPattern,ListAssemblyscannedAssemblies){vardllFiles=Directory.GetFiles(directory,searchPattern,SearchOption.TopDirectoryOnly);foreach(vardllPathindllFiles){varassemblyName=AssemblyName.GetAssemblyName(dllPath);varname=assemblyName.Name??"";// 🚫 跳过系统/框架程序集if(name.StartsWith("Microsoft.")||name.StartsWith(