文章摘要

最近,博主正在重构基于 ReAct 模式的 Agent 系统,计划将其升级为支持原生推理的新方案。为此,博主对 .NET 生态下的三种 Agent 框架进行了详细调研和实测:从 Semantic KernelMicrosoft.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+)支持返回内部推理过程,这个过程被称为 ReasoningThinking。如下表所示,这些不同的术语描述的其实是同一件事情:

术语含义
reasoning_content推理过程的实际文本内容(DeepSeek、OpenAI)
thinkingClaude 的推理区块,需配置 budget_tokens
thoughtGemini 的推理字段

主流模型推理能力对比

下表展示了目前主流模型在推理能力方面的差异对比:

厂商/模型开启方式推理字段推理可见工具调用价格
DeepSeek Reasonermodel: "deepseek-reasoner"reasoning_content便宜
OpenAI o1/o3选择 o1 模型reasoning_content✅ (mini)昂贵
Claude 4 (Extended Thinking)thinking.budget_tokenstype 为 thinking 的 content_block中等
Gemini 2.0 Thinkingthinking_budgetthought中等
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 ChatCompletionAgentMicrosoft.SemanticKernel.Agents
  • Microsoft.Extensions.AIMicrosoft.Extensions.AI 包 + Anthropic 兼容 API
  • Microsoft Agent FrameworkMicrosoft.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 演示 Semantic Kernel 中的 ChatCompletionAgent 演示

如果你尝试使用 deepseek-reasoner 这类推理模型,此时,你会得到类似下面的 Missing reasoning_content field 错误:

当 ChatCompletionAgent 遭遇推理模型世界的参差 当 ChatCompletionAgent 遭遇推理模型世界的参差

这个问题的根源在于,推理模型需要调用方将推理内容回传给 API,从而保证模型内部的思维链完整。

在 DeepSeek 的官方文档中,有一幅图形象而生动地描述了这个过程:模型一边思考一边调用工具,这不正是 ReAct 的核心思想吗?现在,它变成了模型的 Runtime/API 的一部分——交错思考(Interleaved Thinking):

DeepSeek-V3.2 中关于交错思考的图示 DeepSeek-V3.2 中关于交错思考的图示

结论:不太适合我的需求,我需要更全面的推理内容方面的支持。如果希望更灵活地处理推理内容,建议还是从原生 API 进行突破。这便解释了,为什么 Anthropic 曾建议初学者从 LLM API 而不是某个 AI 框架开始。

Microsoft.Extensions.AI - 意外惊喜

Microsoft.Extensions.AI(以下简称 MEAI)是微软重新设计的 AI 接口抽象,如同 HttpClient 之于 HTTP,IChatClient 在 AI 时代的地位丝毫不亚于 HttpClient。MEAI 的定位是 .NET 生态中的 AI 基础功能抽象层,它提供了诸如 IChatClientIEmbeddingGenerator 这样的接口,作为 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 中推理内容与工具调用演示-1 MEAI 中推理内容与工具调用演示-1

这恰恰印证了我们上面的观点: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 上面:

MEAI 中推理内容与工具调用演示-2 MEAI 中推理内容与工具调用演示-2

阿里的通义千问系列模型,其推理模式的开启方式与 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 中推理内容与工具调用演示 MAF 中推理内容与工具调用演示

目前,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 KernelMicrosoft.Extensions.AI 这两个偏底层的框架;如果你希望得到一个开箱即用的、对自由度要求不高的 Agent,Microsoft Agent Framework 基本可以覆盖主流场景。在模型的选择上,我个人更推荐国内的 Kimi K2.5 以及 Minimax 系列模型,这些模型的容性好、性价比高,搭配 MEAI 或者 SK,可以说是目前 .NET 生态下的最优解。如果大家有任何想法或者建议,欢迎在评论区留言,谢谢大家。

赞赏博主
相关推荐 随便逛逛
Semantic Kernel × MCP:智能体的上下文增强探索 本文深入探讨了 MCP(模型上下文协议),由 Anthropic 设计的开放协议,它如同 AI 领域的 USB 接口,旨在通过统一接口解决大模型连接不同数据源和工具的问题。文章详细介绍了 MCP 的架构、核心角色、工作原理以及如何与 Semantic Kernel 集成,为 .NET 开发者提供高效接入社区 MCP 服务器的方法,减少重复性平台对接工作。此外,还展示了 MCP 在操作浏览器、访问文件系统等场景中的应用效果,并探讨了其局限性及未来发展方向。在 AI 技术快速发展的背景下,MCP 的出现为实现 AI 模型的 “万物互联” 提供了可能,值得开发者关注与探索。
基于 Supabase 的 AI 应用开发探索 本文从 AI 产业兼具知识、资本、劳动和资源密集型的多元特性切入, 详细分析了 Supabase 作为理想 AI 开发平台的核心优势: 其开箱即用的 BaaS(后端即服务) 特性、与 AI 需求天然契合的向量数据库与集成能力, 以及活跃的开源社区支持。文章重点探讨了利用 Supabase PostgreSQL 的 pgvector 扩展构建 RAG(检索增强生成) 应用的具体实现, 包括数据表设计、向量索引优化和相似度检索函数编写; 介绍了如何通过 Supabase Edge Functions 与 LangGraph 框架构建复杂 AI 智能体 (Agent); 并进一步延伸讨论了构建轻量级 AI 网关以实现模型灵活切换与架构自由的重要性,旨在为开发者提供基于 Supabase 进行全栈 AI 开发的实用指南。
评论 隐私政策