在基于 .NET 的 AI Agent 开发中,Microsoft.Agents.AI 提供了强大的抽象,使得构建能够调用工具、理解思考过程并执行自定义脚本的智能体变得直观。本文将基于实际代码片段,深入讲解如何利用该框架提取模型的思考过程(Reasoning)、处理工具调用(Tool Calls),以及如何安全地执行外部脚本作为 Agent 的 Skill。


效果图

在这里插入图片描述

一、整体架构概览

示例代码中,AIAgentService 是核心服务,负责创建 OpenAI 兼容的聊天客户端并将其包装为 AIAgent。关键步骤如下:

  1. 配置 OpenAI 客户端:支持自定义端点、超时、重试策略。
  2. 启用 / 禁用工具与技能:通过 AISetting 控制是否注入工具列表(Tools)和技能上下文提供者(AIContextProviders)。
  3. 发送消息并获取响应:支持流式(RunStreamingAsync)和非流式(RunAsync)两种模式。
  4. 在流式输出中解析结构化内容:通过判断 update.Contents 中的具体类型来捕获工具调用、工具结果以及普通文本,同时从原始响应中提取模型的思考过程。

二、提取思考过程(Reasoning)

许多大语言模型(如 OpenAI o1 系列)会在生成最终答案前输出一段内部的“思考链”(reasoning tokens)。Microsoft.Agents.AIAgentResponseUpdate 对象在流式模式下提供了 RawRepresentation,允许我们访问底层模型的原始更新。

代码中 GetReasoningTextAsync 方法展示了完整提取逻辑:

if (update.RawRepresentation is Microsoft.Extensions.AI.ChatResponseUpdate streamingChatCompletionUpdate
    && streamingChatCompletionUpdate.RawRepresentation is OpenAI.Chat.StreamingChatCompletionUpdate chatCompletionUpdate)
{
    ref JsonPatch patch = ref chatCompletionUpdate.Patch;
    var jsonPathBytes = Encoding.UTF8.GetBytes("$.choices");
    var jsonPathSpan = new ReadOnlySpan<byte>(jsonPathBytes);
    if (patch.TryGetJson(jsonPathSpan, out var data))
    {
        var jsonString = Encoding.UTF8.GetString(data.ToArray());
        using var doc = JsonDocument.Parse(jsonString);
        // 遍历 choices[].delta 查找 reasoning 或 reasoning_content 字段
    }
}

原理

  • StreamingChatCompletionUpdate.Patch 是一个 JsonPatch 对象,包含了本次增量更新的原始 JSON 数据。
  • 通过 TryGetJson 并指定 JSONPath $.choices,可以获取 choices 数组的完整片段。
  • 遍历数组中的每个 choice,在 delta 中查找 reasoningreasoning_content 字段(兼容不同模型),将字符串值拼接起来即为模型的思考过程。
  • 注意处理 null 值,用 Replace("null", "\n") 清理无关内容。

这样,即使框架的上层接口未直接暴露 reasoning,我们依然可以通过原始数据获取并单独回调,实现思考过程的实时展示。


三、处理工具调用(Tool Calls)

AIAgent 的流式响应循环中,update.Contents 是一系列 AIContent 派生对象,我们可以根据具体类型区分工具调用请求和工具执行结果。

代码片段:

await foreach (var update in aiAgent.RunStreamingAsync(msg))
{
    foreach (var content in update.Contents)
    {
        switch (content)
        {
            case FunctionCallContent funcCall:
                // 模型决定调用工具,输出工具名称、参数
                aISetting.ToolStreameCallback.Invoke($"\n [工具调用] 名称:{funcCall.Name},调用ID:{funcCall.CallId},参数:{JsonConvert.SerializeObject(funcCall.Arguments)}");
                break;
            case FunctionResultContent funcResult:
                // 工具执行完毕返回结果
                aISetting.ToolStreameCallback.Invoke($"\n [工具返回] 调用ID:{funcResult.CallId},结果:{funcResult.Result}");
                break;
            case TextContent textContent:
                // 普通文本输出(通常已由 update.Text 处理)
                break;
        }
        // 处理可读文本
        if (!string.IsNullOrEmpty(update.Text))
        {
            aISetting.StreameCallback.Invoke(update.Text);
            resultText += update.Text;
        }
    }
}

关键点

  • FunctionCallContent:表示模型请求调用某个函数,其中包含 NameCallIdArguments(JSON 对象)。我们将其序列化后通过回调通知外部系统,以便记录或展示。
  • FunctionResultContent:当工具执行完成后,Agent 会收到一个包含调用 ID 和结果的更新。同样通过回调传递结果,便于构建完整的对话记录。
  • TextContent:通常与普通文本输出对应,但框架通常会将文本聚合到 update.Text 属性中,所以此处主要针对非文本类型做处理。

利用这种模式,我们可以在 Agent 执行过程中实时监控工具调用状态,进行日志记录或界面更新。


四、执行 Skill 脚本

Microsoft.Agents.AI 中,AgentFileSkill 允许我们将外部脚本(如 Python、Shell、PowerShell)注册为 Agent 的技能。PySubprocessScriptRunner 类展示了一个通用的脚本执行器,其核心方法是 StaticRunAsync

1. 脚本类型与解释器选择

根据脚本文件扩展名动态决定启动进程的命令:

switch (Path.GetExtension(scriptFullPath).ToLowerInvariant())
{
    case ".py":
        startInfo = CreateStartInfo("python", $"\"{scriptFullPath}\"");
        break;
    case ".sh":
        startInfo = CreateStartInfo("bash", $"\"{scriptFullPath}\"");
        break;
    case ".ps1":
        // 根据操作系统选择 powershell 或 pwsh
        break;
    default:
        startInfo = CreateStartInfo(scriptFullPath, string.Empty);
        break;
}

2. 进程配置与编码处理

为避免跨平台输出乱码,CreateStartInfo 方法根据运行平台设置编码:

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding outputEncoding = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Encoding.Default : Encoding.UTF8;

同时启用输入输出重定向,并禁用 Shell 执行,确保安全。

3. 向脚本传递参数

Agent 调用技能时会传入 argumentsJsonElement? 类型),脚本执行器将其序列化为 JSON 字符串,通过标准输入(stdin)传递给脚本:

string inputJson = JsonSerializer.Serialize(arguments);
await process.StandardInput.WriteAsync(inputJson);
process.StandardInput.Close();

这要求脚本必须能够从 stdin 读取 JSON 数据并自行解析。

4. 异步执行与结果处理

进程启动后立即开始异步读取标准输出和标准错误,并等待进程结束或取消:

process.OutputDataReceived += (s, e) => outputBuilder.AppendLine(e.Data);
process.ErrorDataReceived += (s, e) => errorBuilder.AppendLine(e.Data);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken);

若退出码非零,抛出异常并附带错误信息;否则,尝试将输出反序列化为 JSON 对象,失败时返回原始字符串。这样 Agent 就可以将脚本结果直接作为工具返回值参与后续对话。


五、总结

通过以上代码实践,我们可以看到 Microsoft.Agents.AI 框架的灵活性和扩展性:

  • 思考过程提取:利用原始响应中的 JSON Patch 数据,轻松穿透抽象层获取模型内部推理细节。
  • 工具调用监控:通过 update.Contents 的多态类型判断,实现对工具调用请求和结果的精确拦截与展示。
  • Skill 脚本执行:将外部脚本无缝集成为 Agent 的工具,通过子进程调用并传递 JSON 参数,实现了语言模型与本地代码的协同工作。

这些技术结合在一起,使得开发者能够构建出功能强大、可观测性高、且能充分利用现有代码资产的 AI Agent 系统。无论是要增强对话体验,还是整合复杂的业务逻辑,Microsoft.Agents.AI 都提供了坚实的基础。

六、代码开源地址

NetCoreKevin框架下的kevin.AI.AgentFramework模块,基于.NET构建的企业级SaaSAI智能体应用架构
项目地址:github:https://github.com/junkai-li/NetCoreKevin
Gitee: https://gitee.com/netkevin-li/NetCoreKevin

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐