AutoGen .NET 实战:用 KernelPluginMiddleware 把 Semantic Kernel 插件能力挂载到 OpenAIChatAgent 等任意 Agent
本文基于 AutoGen .NET 官方文档《Use kernel plugin in other agents》(原文)展开,讲解 AutoGen.SemanticKernel 包提供的 KernelPluginMiddleware 中间件:它让 Semantic Kernel 的 Kernel 插件(内置插件或自定义插件)可以直接服务于 OpenAIChatAgent 等非 Semantic Kernel 的 AutoGen Agent。读完本篇,你将能够亲手编写带 GetWeather 函数的自定义插件,并将其注册到 OpenAIChatAgent 上完成一次真实的工具调用对话,同时从源码层面理解参数如何被解析、函数如何被拦截与执行。
什么是 Semantic Kernel 插件
在 Semantic Kernel 中,kernel plugin(内核插件)是一组可以在 LLM 调用期间被调用的 kernel function 的集合。Semantic Kernel 提供了丰富的内置插件,例如核心插件(core plugins)、Web 搜索插件(web search plugin)等,你也可以创建自己的插件。Kernel 插件极大扩展了 Semantic Kernel 的能力,使其可以执行 Web 搜索、图像搜索、文本摘要等任务。
而在 AutoGen .NET 体系中,插件能力不必局限于 Semantic Kernel 自家的 Agent。AutoGen.SemanticKernel 包提供了一个名为 KernelPluginMiddleware 的中间件,允许你在 OpenAIChatAgent 等其他 AutoGen Agent 中使用 Semantic Kernel 插件。这正是本篇的实战主题。
AutoGen.SemanticKernel 包提供了什么
结合 AutoGen.SemanticKernel 概览文档,该包提供两类 Agent 和两个中间件:
| 组件 | 说明 |
|---|---|
SemanticKernelAgent |
对 Kernel 的轻量封装 Agent,仅支持通过 IMessage<ChatMessageContent> 使用原始 ChatMessageContent 类型;要支持更多 AutoGen 内置消息类型,需注册 SemanticKernelChatMessageContentConnector |
SemanticKernelChatCompletionAgent |
对 Microsoft.SemanticKernel.Agents.ChatCompletionAgent 的轻量封装 Agent |
SemanticKernelChatMessageContentConnector |
在 AutoGen 内置消息类型与 ChatMessageContent 之间互转的连接器。当前阶段仅支持 TextMessage、ImageMessage 与 MultiModalMessage 三类消息;尚不支持 ToolCallMessage、ToolCallResultMessage 等函数调用消息类型 |
KernelPluginMiddleware |
允许在其他 AutoGen Agent(如 OpenAIChatAgent)中使用 Semantic Kernel 插件的中间件 |
安装方式:先按 安装指南 配置好 AutoGen 包源,然后在项目文件中添加包引用:
<ItemGroup>
<PackageReference Include="AutoGen.SemanticKernel" Version="AUTOGEN_VERSION" />
</ItemGroup>
官方示例工程 AutoGen.SemanticKernel.Sample.csproj 中的实际依赖可作参考:它引用了 AutoGen.OpenAI(提供 OpenAIChatAgent)、AutoGen.SemanticKernel(提供 KernelPluginMiddleware)、AutoGen.SourceGenerator(作为 Analyzer,用于源码生成式的函数契约),以及 NuGet 包 Microsoft.SemanticKernel.Plugins.Web。
实战:四步把 GetWeather 插件挂到 OpenAIChatAgent 上
下面的完整示例来自官方示例文件 Use_Kernel_Functions_With_Other_Agent.cs,定义一个只含 GetWeather 函数的简单插件,并在 OpenAIChatAgent 中使用它。
Step 1:添加 using 语句
using AutoGen.Core;
using AutoGen.OpenAI;
using AutoGen.OpenAI.Extension;
using Microsoft.SemanticKernel;
using OpenAI;
要点:AutoGen.SemanticKernel 命名空间下的 KernelPluginMiddleware 所在 using 由全局 using 或项目配置提供(示例中通过 GlobalUsing/隐式 using 生效),而 Microsoft.SemanticKernel 用于构建 Kernel 与创建插件。
Step 2:创建插件
这一步创建一个包含单个 GetWeather 函数的简单插件:该函数接收一个 location 作为输入,返回该地点的天气信息。
var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable.");
var modelId = "gpt-4o-mini";
var kernelBuilder = Kernel.CreateBuilder();
var kernel = kernelBuilder.Build();
var getWeatherFunction = KernelFunctionFactory.CreateFromMethod(
method: (string location) => $"The weather in {location} is 75 degrees Fahrenheit.",
functionName: "GetWeather",
description: "Get the weather for a location.");
var plugin = kernel.CreatePluginFromFunctions("my_plugin", [getWeatherFunction]);
参数说明:
KernelFunctionFactory.CreateFromMethod:把一个 .NET 委托/方法包装为 kernel function。functionName(GetWeather)是 LLM 发起工具调用时使用的函数名,description会进入函数契约描述,直接影响 LLM 判断“何时调用该函数”;- 委托的形参
string location会被 Semantic Kernel 元数据机制识别为函数参数,后续中间件正是依据这些元数据来做参数解析(见后文源码分析); kernel.CreatePluginFromFunctions("my_plugin", ...):将函数聚合为名为my_plugin的插件实例,一个插件可以承载多个函数;OPENAI_API_KEY环境变量与模型名gpt-4o-mini是运行前提。
Step 3:创建 OpenAIChatAgent 并注册插件
这一步先创建 KernelPluginMiddleware 并把上一步的插件注册给它——KernelPluginMiddleware 会加载该插件,让其中的函数可以被其他 Agent 调用;随后创建 OpenAIChatAgent 并把该中间件注册上去。
// Create a middleware to handle the plugin functions
var kernelPluginMiddleware = new KernelPluginMiddleware(kernel, plugin);
var openAIClient = new OpenAIClient(openAIKey);
var openAIAgent = new OpenAIChatAgent(
chatClient: openAIClient.GetChatClient(modelId),
name: "assistant")
.RegisterMessageConnector() // register message connector so it support AutoGen built-in message types like TextMessage.
.RegisterMiddleware(kernelPluginMiddleware) // register the middleware to handle the plugin functions
.RegisterPrintMessage(); // pretty print the message to the console
三个链式注册的分工:
RegisterMessageConnector():注册消息连接器,使 Agent 支持TextMessage等 AutoGen 内置消息类型(连接器负责 AutoGen 消息与 OpenAI SDK 消息格式之间的互转);RegisterMiddleware(kernelPluginMiddleware):注册插件中间件,这是插件能力接入的关键一步——中间件会把插件函数注入 LLM 请求的函数列表,并拦截 LLM 返回的工具调用消息代为执行;RegisterPrintMessage():把消息漂亮地打印到控制台,方便观察多轮工具调用过程。
Step 4:与 OpenAIChatAgent 对话
最后向 OpenAIChatAgent 提问西雅图的天气。OpenAIChatAgent 会使用插件中的 GetWeather 函数来获取西雅图的天气信息:
var toolAggregateMessage = await openAIAgent.SendAsync("Tell me the weather in Seattle");
// The aggregate message will be converted to [ToolCallMessage, ToolCallResultMessage] when flowing into the agent
// send the aggregated message to llm to generate the final response
var finalReply = await openAIAgent.SendAsync(toolAggregateMessage);
注意这里需要两次 SendAsync 调用,原因在于中间件的工作方式:
- 第一次
SendAsync中,LLM 判定需要调用GetWeather,返回ToolCallMessage;中间件检测到该函数在插件的函数映射中,立即本地执行函数,并把“调用 + 结果”打包成ToolCallAggregateMessage(内部对应[ToolCallMessage, ToolCallResultMessage]的组合)直接返回,LLM 并未看到函数结果; - 第二次
SendAsync(toolAggregateMessage)把这个聚合消息送回 Agent/LLM,LLM 基于真实的函数执行结果生成最终自然语言回答。
这种“一次调用拿到工具结果、二次调用生成最终答复”的模式,是该示例代码的固有流程,读者复制运行时应保留两次调用。
源码解析:KernelPluginMiddleware 如何把插件变成函数调用能力
下面结合源码说明这个中间件在幕后做了什么。
构造阶段:把插件函数转换为 AutoGen 的函数契约
KernelPluginMiddleware 的实现在 KernelPluginMiddleware.cs。其构造函数做了三件事:
public KernelPluginMiddleware(Kernel kernel, KernelPlugin kernelPlugin)
{
_kernelPlugin = kernelPlugin;
var functionContracts = kernelPlugin.Select(k => k.Metadata.ToFunctionContract());
var functionMap = kernelPlugin.ToDictionary(kv => kv.Metadata.Name, kv => InvokeFunctionPartial(kernel, kv));
_functionCallMiddleware = new FunctionCallMiddleware(functionContracts, functionMap, Name);
}
- 契约转换:对插件中每个
KernelFunction的元数据调用扩展方法ToFunctionContract()(定义在 KernelExtension.cs),把KernelFunctionMetadata映射为 AutoGen 核心的FunctionContract——包括函数名、描述、参数列表(名称、描述、默认值、是否必填、参数类型)以及返回值类型。这些契约随后会被合并进 LLM 请求的Functions列表,相当于“告诉 LLM 有哪些工具可用”; - 执行映射:为每个函数名建立到
InvokeFunctionPartial(kernel, function)的映射,即“函数名 → 可执行的委托”; - 委托复用:自身不重复实现函数调用逻辑,而是构造一个
FunctionCallMiddleware并把自己的InvokeAsync直接转发给它。KernelPluginMiddleware的InvokeAsync实现只有一行:
public Task<IMessage> InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default)
{
return _functionCallMiddleware.InvokeAsync(context, agent, cancellationToken);
}
也就是说,插件中间件本质上是一个由 kernel plugin 驱动的 FunctionCallMiddleware 工厂,真正的拦截与执行逻辑在 AutoGen 核心的 FunctionCallMiddleware.cs 中。
参数解析:LLM 的 JSON 参数如何变成 .NET 方法实参
当 LLM 返回工具调用时,函数参数是一段 JSON 字符串。InvokeFunctionAsync(KernelPluginMiddleware.cs)负责把这段 JSON 解析为 Semantic Kernel 能识别的 KernelArguments:
var kernelArguments = new KernelArguments();
var parameters = function.Metadata.Parameters;
var jsonObject = JsonSerializer.Deserialize<JsonObject>(arguments) ?? new JsonObject();
foreach (var parameter in parameters)
{
var parameterName = parameter.Name;
if (jsonObject.ContainsKey(parameterName))
{
var parameterType = parameter.ParameterType ?? throw new ArgumentException($"Missing parameter type for {parameterName}");
var parameterValue = jsonObject[parameterName];
var parameterObject = parameterValue.Deserialize(parameterType);
kernelArguments.Add(parameterName, parameterObject);
}
else
{
if (parameter.DefaultValue != null)
{
kernelArguments.Add(parameterName, parameter.DefaultValue);
}
else if (parameter.IsRequired)
{
throw new ArgumentException($"Missing required parameter: {parameterName}");
}
}
}
var result = await function.InvokeAsync(kernel, kernelArguments);
return result.ToString();
由此可以得出几个行为事实:
- 按类型反序列化:LLM 给出的 JSON 值会按函数元数据中声明的
ParameterType反序列化为对应 .NET 对象,因此插件方法签名(如string location)决定了参数类型约束; - 默认值兜底:LLM 未提供某参数、但函数元数据有默认值时,使用默认值;
- 必填参数缺失会抛异常:既无传入值又无默认值的必填参数会导致
ArgumentException,调试工具调用失败时这是常见排查点; - 执行入口是
function.InvokeAsync(kernel, ...):真正运行的是 Semantic Kernel 的函数调用管线,结果以ToString()字符串形式回传给ToolCallResultMessage。
执行阶段:FunctionCallMiddleware 的拦截逻辑
FunctionCallMiddleware(FunctionCallMiddleware.cs)的核心流程在 InvokeAsync 中:
- 先检查入站消息:若上下文最后一条消息本身就是
ToolCallMessage(例如 Step 4 中第二次SendAsync传入的聚合消息被拆解后的场景),则直接执行映射中对应的函数并返回ToolCallResultMessage,短路内部 Agent,不再请求 LLM; - 合并函数列表:把中间件自带的
functions(即插件契约)与GenerateReplyOptions.Functions中已有的函数拼接,写入options.Functions后再调用agent.GenerateReplyAsync(...)——这就是插件函数“出现在 LLM 可用工具列表”的机制; - 拦截出站工具调用:若 Agent 的回复是
ToolCallMessage且函数名在映射中,则执行函数并返回ToolCallAggregateMessage(包含ToolCallMessage与ToolCallResultMessage,即 Step 4 两次调用之间传递的聚合消息);若函数不在映射中或回复不是工具调用消息,则原样返回 Agent 的回复; - 对不在映射中的函数名,
InvokeToolCallMessagesBeforeInvokingAgentAsync会把错误信息"Function {name} is not available. Available functions are: ..."作为该次工具调用的结果写回,帮助 LLM 自我纠正。
该中间件同时实现了 IStreamingMiddleware,支持流式场景下对 ToolCallMessageUpdate 的合并与执行,因此插件能力在流式对话中同样可用(从源码结构看,流式路径与非流式路径共用同一套函数映射与拦截逻辑)。
适用边界与注意事项
- 消息类型支持范围:
KernelPluginMiddleware走的是 AutoGen 核心消息体系(TextMessage/ToolCallMessage等),与SemanticKernelChatMessageContentConnector的限制不同——后者的概览文档明确说明当前仅支持TextMessage、ImageMessage、MultiModalMessage之间的转换,尚不支持ToolCallMessage/ToolCallResultMessage。用插件中间件时消息流由FunctionCallMiddleware处理,不受该连接器限制; - 模型与密钥前提:示例硬编码
gpt-4o-mini且要求OPENAI_API_KEY环境变量;OpenAIChatAgent依赖AutoGen.OpenAI包提供的OpenAIChatAgent与消息连接器扩展(AutoGen.OpenAI.Extension命名空间); - 两次 SendAsync 是流程设计使然:工具执行发生在中间件层,最终答复需要把工具结果送回 LLM,因此对话代码要包含第二次调用;
- 插件函数名即契约名:
functionName(如GetWeather)同时出现在 LLM 的工具列表与执行映射字典的键中,二者天然一致,无需额外对齐;若自定义插件包含多个函数,每个函数都会被转换为契约并进入映射。
小结
KernelPluginMiddleware 是 AutoGen .NET 打通“Semantic Kernel 插件生态 → 任意 AutoGen Agent”的桥梁:构造时把插件函数元数据转换为 AutoGen 的 FunctionContract 并建立“函数名 → 执行委托”映射,运行时由核心的 FunctionCallMiddleware 将函数注入 LLM 请求、拦截工具调用消息并代为执行,返回 ToolCallAggregateMessage 供第二轮生成最终答复。完整可运行的示例代码位于 dotnet/samples/AgentChat/AutoGen.SemanticKernel.Sample/Use_Kernel_Functions_With_Other_Agent.cs,配套入口为 Program.cs;中间件源码见 KernelPluginMiddleware.cs,契约转换扩展见 KernelExtension.cs。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00