最近,博主正在重构基于 ReAct 模式的 Agent 系统,计划将其升级为支持原生推理的新方案。为此,博主对 .NET 生态下的三种 Agent 框架进行了详细调研和实测:从 Semantic Kernel 到 Microsoft.Extensions.AI,再到 Microsoft Agent Framework。如果你了解过推理模型、扩展思考(Extended Thinking)、交错思考(Interleaved Thinking)等概念,相信会对这篇文章感兴趣。在过去的一个多月里,由"小龙虾" (OpenClaw) 引发的"全民养虾"热潮可谓风头无两。然而,在这 Agent 进化高歌猛进的历程中,关于模型思考本身的关注屈指可数。博主希望通过这篇文章,让大家看到 AI 光环下隐藏的"混乱"。在博主看来,这种混乱本身,与 LLM 基于概率预测下一个词元的不确定性同样迷人。
背景:为什么需要换方案?
首先,为什么需要更换方案?这是因为,此前的 Agent 基于 ReAct(Reasoning + Acting)模式工作。该模式的特点是 —— LLM 按照提示词模板生成"推理"内容,实际上是按照固定格式输出的文本,并且在调用工具时,只能一个一个来。这样就会带来两个非常明显的问题:
- 高延迟:每次工具调用都需要一次完整的 LLM 调用
- 假推理:模型只是在按格式输出,并不是真的在思考
新一代模型(比如 OpenAI o1、DeepSeek Reasoner、Claude 3.7+)支持原生推理,模型内部具备真正的推理能力。推理内容可以通过 API 获取,更重要的是工具可以并行执行。因此,博主在考虑让 Agent 系统支持原生推理,目标是——找到能获取这些信息的框架。
推理模式前置知识
在正式开始实验前,先了解一下当前主流大模型的推理模式差异。这有助于理解为什么不同框架的支持程度不同。
新一代推理模型(如 OpenAI o1、DeepSeek Reasoner、Claude 3.7+)支持返回内部推理过程,这个过程被称为 Reasoning 或 Thinking。如下表所示,这些不同的术语描述的其实是同一件事情:
| 术语 | 含义 |
|---|---|
| reasoning_content | 推理过程的实际文本内容(DeepSeek、OpenAI) |
| thinking | Claude 的推理区块,需配置 budget_tokens |
| thought | Gemini 的推理字段 |
主流模型推理能力对比
下表展示了目前主流模型在推理能力方面的差异对比:
| 厂商/模型 | 开启方式 | 推理字段 | 推理可见 | 工具调用 | 价格 |
|---|---|---|---|---|---|
| DeepSeek Reasoner | model: "deepseek-reasoner" | reasoning_content | ✅ | ✅ | 便宜 |
| OpenAI o1/o3 | 选择 o1 模型 | reasoning_content | ❌ | ✅ (mini) | 昂贵 |
| Claude 4 (Extended Thinking) | thinking.budget_tokens | type 为 thinking 的 content_block | ✅ | ✅ | 中等 |
| Gemini 2.0 Thinking | thinking_budget | thought | ✅ | ✅ | 中等 |
| Kimi K2.5 | 默认开启 | reasoning_content | ✅ | ✅ | 中等 |
| Qwen系列 | { "enable_thinking": true } | reasoning_content | ✅ | ✅ | 中等 |
关键差异:OpenAI o1/o3 的推理内容对用户不可见(用于内部优化),而 DeepSeek、Claude、Kimi 可以获取完整推理过程。此外,各模型在思维链回传方案上存在差异,例如:Anthropic 从 Claude 3.7 以后引入的 Messages API (Extended Thinking) 要求必须回传思维链,自带签名防止篡改;OpenAI 的 Response API 支持回传加密思维链;DeepSeek V3.2 提供的增强版 Chat Completions 允许输入端传入
reasoning_content等等。
推理 API 差异要点
下面的代码片段展示了不同模型在推理 API 方面的差异:
# DeepSeek - 独立字段返回
response = client.chat.completions.create(
model="deepseek-reasoner",
messages=[{"role": "user", "content": "你今天过得开心吗"}]
)
# 推理过程 & 最终答案
reasoning = response.choices[0].message.reasoning_content
answer = response.choices[0].message.content
# Claude - 通过 thinking 配置
response = client.messages.create(
model="claude-sonnet-4-20250514",
thinking={"type": "enabled", "budget_tokens": 1024},
messages=[{"role": "user", "content": "你今天过得开心吗"}]
)
# 通过 type=thinking 获取推理过程
# OpenAI o1 - 推理不可见
response = client.chat.completions.create(
model="o1",
messages=[{"role": "user", "content": "你今天过得开心吗"}]
)
# reasoning_content 对用户不可见
# Gemini - 通过 thinkingLevel/thinkingBudget
client = genai.Client()
response = client.models.generate_content(
model="gemini-3-flash-preview",
contents="你今天过得开心吗",
config=types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(
include_thoughts=True
)
)
)
# 返回的是思考总结/摘要,推理过程不可见
这就是为什么我们需要选择一个能获取推理内容的框架——否则大家都用 OpenAI o1 就好了。
实验设计
博主选择了三种框架进行测试:
- Semantic Kernel ChatCompletionAgent:
Microsoft.SemanticKernel.Agents包 - Microsoft.Extensions.AI:
Microsoft.Extensions.AI包 + Anthropic 兼容 API - Microsoft Agent Framework:
Microsoft.Agents.AI.OpenAI包
首先,让我们来定义两个简单的测试工具:
public static class TestTools
{
[Description("获取当前日期和时间")]
public static string GetCurrentTime()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
[Description("执行简单的数学运算,支持加减乘除")]
public static string Calculate(string expression)
{
expression = expression.Replace(" ", "");
var table = new System.Data.DataTable();
var result = table.Compute(expression, "");
return $"计算结果: {expression} = {result}";
}
}
重点观察三个方面:流式输出的内容、能否并行调用工具、推理内容如何获取。
实验结果
Semantic Kernel - 出师不利
先看最熟悉的 SK,配置 FunctionChoiceBehavior.Auto() 就能自动调用工具:
var agent = new ChatCompletionAgent {
Name = "TestAgent",
Instructions = "你是一个助手,请展示思考过程。",
Kernel = kernel,
Arguments = new KernelArguments(
new OpenAIPromptExecutionSettings {
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
})
};
await foreach (var message in agent.InvokeStreamingAsync(history)) {
Console.WriteLine(message.Content);
}
这个 ChatCompletionAgent 用起来确实方便,如果你熟悉 Semantic Kernel,上手难度为零。但是,我很快发现了一个致命问题:无法获取中间的 tool_call 事件。你只能拿到最终文本,模型在什么时候调用了什么工具、调用的参数以及返回值分别是什么,一概不知,更不用说推理内容了,完全没有!
当然,你可以添加 IFunctionInvocationFilter 或者 IAutoFunctionInvocationFilter 这两个过滤器来"曲线救国":
class KernelFunctionFilter : IAutoFunctionInvocationFilter {
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next) {
// 工具调用前
var args = JsonSerializer.Serialize(context.Arguments);
Console.WriteLine($"ToolUse -> {context.Function.PluginName}.{context.Function.Name}({args})");
await next(context);
// 工具调用后
Console.WriteLine($"ToolResult -> {context.Result.GetValue<string>()}");
}
}
Semantic Kernel 中的 ChatCompletionAgent 演示
如果你尝试使用 deepseek-reasoner 这类推理模型,此时,你会得到类似下面的 Missing reasoning_content field 错误:
当 ChatCompletionAgent 遭遇推理模型世界的参差
这个问题的根源在于,推理模型需要调用方将推理内容回传给 API,从而保证模型内部的思维链完整。
在 DeepSeek 的官方文档中,有一幅图形象而生动地描述了这个过程:模型一边思考一边调用工具,这不正是 ReAct 的核心思想吗?现在,它变成了模型的 Runtime/API 的一部分——交错思考(Interleaved Thinking):
结论:不太适合我的需求,我需要更全面的推理内容方面的支持。如果希望更灵活地处理推理内容,建议还是从原生 API 进行突破。这便解释了,为什么 Anthropic 曾建议初学者从 LLM API 而不是某个 AI 框架开始。
Microsoft.Extensions.AI - 意外惊喜
Microsoft.Extensions.AI(以下简称 MEAI)是微软重新设计的 AI 接口抽象,如同 HttpClient 之于 HTTP,IChatClient 在 AI 时代的地位丝毫不亚于 HttpClient。MEAI 的定位是 .NET 生态中的 AI 基础功能抽象层,它提供了诸如 IChatClient 和 IEmbeddingGenerator 这样的接口,作为 Semantic Kernel 和 AutoGen 的共同的底层框架。
在切换到 MEAI 以后,整个体验非常丝滑,你可以在 MEAI 的返回值中拿到完整的信息:
var chatClient = new ChatClientBuilder(openAIClient)
.UseFunctionInvocation()
.Build();
var chatOptions = new ChatOptions {
Tools = [
AIFunctionFactory.Create(TestTools.GetCurrentTime),
AIFunctionFactory.Create(TestTools.Calculate)
],
AllowMultipleToolCalls = true,
Reasoning = new ReasoningOptions { Effort = ReasoningEffort.High }
};
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, chatOptions)) {
foreach (var content in update.Contents) {
// FunctionCall
if (content is FunctionCallContent) {
var functionCallContent = content as FunctionCallContent;
Console.WriteLine($"[ToolCall(${functionCallContent.CallId})] {functionCallContent.Name}({JsonSerializer.Serialize(functionCallContent.Arguments)})");
}
// FunctionResult
if (content is FunctionResultContent) {
var functionResultContent = content as FunctionResultContent;
Console.WriteLine($"[ToolResult(${functionResultContent.CallId})] {functionResultContent.Result}");
}
// Reasoning
if (content is TextReasoningContent) {
var reasoningContent = content as TextReasoningContent;
Console.WriteLine($"[Reasoning] {reasoningContent.Text}");
}
// Answer
if (content is TextContent) {
var textContent = content as TextContent;
Console.WriteLine($"{textContent.Text ?? ""}");
}
}
}
可以看到——工具调用、工具结果、推理内容、最终答案——整个思维链全部都拿到了,并且支持并行工具调用,只需要设置 AllowMultipleToolCalls = true 就行。
这恰恰印证了我们上面的观点:MEAI 是比 Semantic Kernel 和 AutoGen 更为底层的 AI 基础功能抽象层。在 Agent Loop 的概念被提出以后,我们发现主流的 Agent 如 Claude Code、Codex、OpenClaw、PiAgent、Nanobot…背后都是这样一套相似的循环。如果你想设计一个 Agent,不出意外的话,最终,你大概率会收束到这个故事线上来。
那么,对于令 Semantic Kernel 中感到棘手的 reasoning_content,MEAI 是如何处理的呢?下面是具体的解决方案:
private static void SetDeepSeekReasoningCompatible(ChatOptions chatOptions) {
if (chatOptions.Reasoning == null || chatOptions.Reasoning.Effort == ReasoningEffort.None)
return;
// 等价于 extra_body={"thinking": {"type": "enabled"}}
chatOptions.RawRepresentationFactory = static (client) => {
var options = new ChatCompletionOptions();
options.Patch.Set("$.thinking.type"u8, "enabled");
return options;
};
chatOptions.Reasoning = null;
}
这一步的作用是开启 DeepSeek 系列模型的推理模式。当然,如果是 deepseek-reasoner 模型,这个功能默认是开启的。接下来,我们来处理 reasoning_content 报错的问题。这个问题的根本原因在于——MEAI 和 DeepSeek API 在设计上不兼容。MEAI 以及 OpenAI.NET SDK 的适配层没有把 ChatMessage.AdditionalProperties 展开为顶层 JSON 字段,这样就导致服务器端返回了 400 错误。详情请参考:
要修复这个问题,只需要处理一下 HttpClient 发出的请求体即可,这一步可以通过一个自定义的 HttpClientHandler 来实现:
if (!HasThinkingEnabled(rootObj))
return null;
if (rootObj.TryGetPropertyValue("messages", out var messagesNode) && messagesNode is JsonArray messagesArr) {
foreach (var node in messagesArr) {
if (node is JsonObject msgObj &&
string.Equals(msgObj["role"]?.GetValue<string>(), "assistant", StringComparison.OrdinalIgnoreCase) &&
(!msgObj.ContainsKey("reasoning_content") || msgObj["reasoning_content"] is null)) {
msgObj["reasoning_content"] = string.Empty;
}
}
}
那么,我们有没有办法拿到 DeepSeek 的推理内容呢?答案是 JSON Patch。这是 MEAI 中一个灵活到离谱的设计,通过这一特性,你不但可以控制 HttpClient 发送哪些字段,还可以从原始的响应中解析特定字段。
下面是一个非流式传输场景下,获取原始推理内容的方法:
private static ChatResponse ProcessChatResponse(ChatResponse chatResponse) {
var openaiChatCompletion = chatResponse.AsOpenAIChatCompletion();
openaiChatCompletion.Patch.TryGetValue("$.choices[0].message.reasoning_content"u8, out string reasoningContent);
if (string.IsNullOrEmpty(reasoningContent) || string.IsNullOrWhiteSpace(reasoningContent))
return chatResponse;
foreach (var message in chatResponse.Messages) {
if (message.Role != ChatRole.Assistant) continue;
if (message.AdditionalProperties != null && message.AdditionalProperties.ContainsKey("reasoning_content"))
continue;
message.AdditionalProperties = message.AdditionalProperties ?? new AdditionalPropertiesDictionary();
message.AdditionalProperties["reasoning_content"] = reasoningContent;
}
var assistantMessage = chatResponse.Messages.LastOrDefault(x => x.Role == ChatRole.Assistant);
assistantMessage.Contents.Insert(0, new TextReasoningContent(reasoningContent));
return chatResponse;
}
至此,DeepSeek 的推理模型就可以相对完美地运行在 MEAI 上面:
阿里的通义千问系列模型,其推理模式的开启方式与 DeepSeek 类似,更像是 DeepSeek 和 Claude 的"结合体":
{
"model": "qwen-plus",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "你是谁?"},
]
"enable_thinking": true,
"thinking_budget": 2046
}
相信大家现在可以感受到 AI 世界的参差。至少它并不像我们从外部看到的那样"祥和",就像微软的工程师试图用 JSON Patch 和 AdditionalProperties 来抹平不同模型间的差异,可在编写 MEAI 框架时,依然不可避免地要做出取舍。
对博主而言,这个原生推理能力最令我困惑的地方在于:它总是先调用工具、再给出思考。这一点和过去 ReAct 范式中的"三思而后行"完全不一样。可现在有大量的框架、产品都建立在这个机制上,Agent 开发者注定要被整个市场裹挟着向前。如果你打算做一个支持交错思考的 Agent,我的建议是:自己去实现一个 IChatClient,而不是像现在这样在 OpenAIChatClient 上"打补丁"。
结论:基本上都满足我的需求。但是,在推理内容获取和回传方面需要使用各种 Hack 技术。如果选择 Python 或者 JavaScript 这样的动态语言,在类似问题的处理上会更高效、灵活。
目前,微软正在将 SK 的底层迁移到 MEAI。因此,MEAI 中的这些问题,你可能会在 SK 和 MAF 中再次遇到。
Microsoft Agent Framework - 似曾相识
MAF 用法和 MEAI 几乎一样,区别在于:它直接建立在 OpenAI 的 .NET SDK 基础上,并没有像 MEAI 一样单独去设计一个 OpenAI 的客户端,它甚至可以直接运行 DeekSeep 的推理模型,是目前官方主要在推动的框架:
var openAIClient = new OpenAI.OpenAIClient(
new ApiKeyCredential(config.OpenAI_ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(config.OpenAI_BaseUrl) }
);
var agent = openAIClient.GetChatClient(modelId).AsAIAgent(
instructions: "你是一个助手,请展示思考过程。",
tools: [
AIFunctionFactory.Create(TestTools.GetCurrentTime),
AIFunctionFactory.Create(TestTools.Calculate)
]
);
await foreach (var update in agent.RunStreamingAsync(prompt))
{
foreach (var content in update.Contents)
{
// 和 MEAI 一样的处理方式
}
}
可以看到,运行效果和 MEAI 完全一致——流式输出、工具调用、工具结果都能正常获取。当然,这世上从来就没有什么救世主,想要获取推理内容还是要靠自己。我认为这从侧面反映了人们对于让模型“思考”这件事情的情绪变化,即:人们都知道推理能让 LLM 的答案相对准确,可是在获得答案以后,就不会再有人关心推理过程。当越来越多的事情变成结果导向,过程可能正在变得可有可无,这是一种幸运还是不幸呢?
目前,MAF 中提供的 Agent 主要是 ChatClientAgent,它可以将任何一个实现了 IChatClient 接口的客户端,通过 AsAIAgent() 扩展方法变成一个 Agent。从这里可以看出,MAF 和 MEAI 同宗同源,区别是 MAF 更贴近应用层,你可以使用 AIContextProvider 集成 Agent Skills、通过 ChatHistoryProvider 管理聊天历史等等。
结论:完整地继承了 MEAI 的功能以及 Bug,适合一般场景下的 Agent 开发,支持 Agent Skills、支持工具调用、支持推理内容获取(需要一点手段),吸收了 SK 以及 AutoGen 中的主要成果,是目前 .NET 生态中 AI 基础设施之集大成者。
总结
结合本次实验设定的三个目标:流式输出的内容、能否并行调用工具、推理内容如何获取,三大主流框架可谓是各擅胜场,差距主要体现在推理内容的获取方面。下面是一个简单的对比:
| 框架 | 工具调用 | Tool Call 输出 | Reasoning 输出 | Anthropic 支持 |
|---|---|---|---|---|
| Semantic Kernel | ✅ | ❌ | ❌ | ✅ |
| Microsoft.Extensions.AI | ✅ | ✅ | ✅ | ✅ (国内需重定向) |
| Microsoft Agent Framework | ✅ | ✅ | ✅ | ✅ (国内需重定向) |
模型推理能力对比
| 场景 | 推荐模型 | 理由 |
|---|---|---|
| 性价比优先 | DeepSeek Reasoner | 推理可见 + 便宜 + 工具调用 |
| 最强推理 | OpenAI o1 | 推理能力强(虽然不可见) |
| 国内生态 | Kimi K2.5 / DeepSeek / Minimax | 无需翻墙 |
| 多模态优先 | Claude / Gemini / Minimax | 图像+推理 |
.NET 开发者选型建议
如果你正在考虑技术选型,我的建议是:
- 如果只需要工具调用:Semantic Kernel 完全足够,简单易用,成熟稳重
- 如果需要获取推理内容:选 Microsoft.Extensions.AI
- 如果需要支持 Skills:选 Microsoft Agent Framework
综上所述,如果你打算开发一个 Agent 并且希望能够自由地编排内部流程,我推荐 Semantic Kernel 和 Microsoft.Extensions.AI 这两个偏底层的框架;如果你希望得到一个开箱即用的、对自由度要求不高的 Agent,Microsoft Agent Framework 基本可以覆盖主流场景。在模型的选择上,我个人更推荐国内的 Kimi K2.5 以及 Minimax 系列模型,这些模型的容性好、性价比高,搭配 MEAI 或者 SK,可以说是目前 .NET 生态下的最优解。如果大家有任何想法或者建议,欢迎在评论区留言,谢谢大家。

