深入 .NET AI Agent 开发:利用 Microsoft.Agents.AI 提取思考、调用工具与执行脚本

📅 2026/6/23 11:49:18
深入 .NET AI Agent 开发:利用 Microsoft.Agents.AI 提取思考、调用工具与执行脚本
效果图一、整体架构概览示例代码中AIAgentService是核心服务负责创建 OpenAI 兼容的聊天客户端并将其包装为AIAgent。关键步骤如下配置 OpenAI 客户端支持自定义端点、超时、重试策略。启用 / 禁用工具与技能通过AISetting控制是否注入工具列表Tools和技能上下文提供者AIContextProviders。发送消息并获取响应支持流式RunStreamingAsync和非流式RunAsync两种模式。在流式输出中解析结构化内容通过判断update.Contents中的具体类型来捕获工具调用、工具结果以及普通文本同时从原始响应中提取模型的思考过程。二、提取思考过程Reasoning许多大语言模型如 OpenAI o1 系列会在生成最终答案前输出一段内部的“思考链”reasoning tokens。Microsoft.Agents.AI的AgentResponseUpdate对象在流式模式下提供了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 ReadOnlySpanbyte(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中查找reasoning或reasoning_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表示模型请求调用某个函数其中包含Name、CallId和ArgumentsJSON 对象。我们将其序列化后通过回调通知外部系统以便记录或展示。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框架的灵活性和扩展性