一、前言
Microsoft Agent Framework 框架发布也有一阵子了,在观望(摸鱼)过后,也是果断(在老板的威胁下)将几个AI应用微服务完成了从Semantic Kernel 框架到Microsoft Agent Framework 框架中的迁移工作。
所以这篇文章,我想记录一下在开发过程中的我总结的一下工程化用法。
二、Agent Framework是什么
简单来讲,Microsoft Agent Framework 是微软在 Semantic Kernel 之后推出的一个 新一代智能体(Agent)开发框架。它其实就是 SK 的“进化版”——思路差不多,但更直接、更好用,也更符合现在大家在做 多智能体(Multi-Agent)系统 的趋势。
如果你用过 Semantic Kernel,大概还记得那种层层嵌套的概念:Kernel、Skill、Function、Context…… 用起来就像在拼一堆乐高砖块。
三、对比Semantic Kernel
-
结构更加直观和优雅
以前 SK 的 “Function” / “Skill” 概念太抽象。
在 Agent Framework 里,你可以直接定义一个Agent类,然后给它挂上工具(Tool)、记忆(Memory)。 -
Prompt 与逻辑分离更自然
在 SK 里常常要写一堆 Template Function,还要用 YAML 或 JSON 去配置。
在 Agent Framework 中,你直接在创建 Agent 时传入instructions(提示词),框架会自动封装上下文调用,大幅减少模板样板代码。 -
内置的多 Agent 协作更顺手
它原生支持多个 Agent 之间的消息传递,你可以像写微服务一样写“智能体服务”。
四、使用姿势
在使用SK框架的时候我就很讨厌构建一个“kernel”,什么都找他实现,那种方法一开始很方便和简洁,但是复用和调试就是灾难。所以我的做法是:每个任务一个 Agent,职责单一、结构清晰、方便测试。然后再把这些 Agent 都注册进 IOC 容器里,像注入普通服务一样调用。
4.1 Agent任务分配
以一个从文档上解析公司名做示例:
namespace STD.AI.Implementations { public class CompanyExtractionAgent : BaseAgentFunction, ICompanyExtractionAgent { private readonly AIAgent _agent; public CompanyExtractionAgent(IOptions<LLMConfiguration> config) { var openAIClient = new OpenAIClient(new ApiKeyCredential(config.Value.ApiKey), new OpenAIClientOptions { Endpoint = new Uri(config.Value.Endpoint), }); var responseClient = openAIClient.GetChatClient(config.Value.Model); _agent = responseClient.CreateAIAgent(instructions: "你是一个信息抽取助手,请从文本中提取所有公司名称,必须返回合法 JSON 数组,如 ["公司A", "公司B"]。不要输出解释或额外内容。"); } public async Task<List<string>> ExtractCompanyNamesAsync(string filePath) { if (!File.Exists(filePath)) throw new FileNotFoundException("找不到指定文件", filePath); string content = DocumentReader.ExtractText(filePath); if (string.IsNullOrWhiteSpace(content)) return new List<string>(); var thread = _agent.GetNewThread(); var chunks = SplitDocumentIntoChunks(content); var allCompanies = new HashSet<string>(); foreach (var chunk in chunks) { string prompt = @$" 请从以下文本片段中中提取所有公司名称,并严格以 JSON 数组形式输出: 示例输出: [""阿里巴巴集团"", ""腾讯科技有限公司"", ""百度公司""] 以下是正文:{chunk}"; try { var response = await _agent.RunAsync(prompt, thread); string raw = response.Text ?? string.Empty; string cleaned = CleanJsonResponseList(raw); var companies = JsonSerializer.Deserialize<List<string>>(cleaned) ?? new List<string>(); LogProvider.Info(raw); foreach (var c in companies) allCompanies.Add(c); } catch (JsonException ex) { LogProvider.Warning($"解析失败: {ex.Message}"); } } return allCompanies.ToList(); } } }
4.2 给Agent 装上手和眼
4.2.1 添加MCP服务
namespace STD.AI { public static class MCPConfigExtension { public static string _pluginPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); public static string _userDataDir = Path.Combine(_pluginPath, "browser-data"); public static async Task AddMcpClientAsync(this IServiceCollection services, bool Headless) { try { var config = new List<string> { "@playwright/mcp", "--caps", "pdf", "--output-dir",Path.GetTempPath(), "--user-data-dir", _userDataDir, }; if (Headless) { config.Add("--headless"); } var transport = new StdioClientTransport(new StdioClientTransportOptions { Name = "PlaywrightMCP", Command = "npx", Arguments = config }); var mcpClient = await McpClient.CreateAsync(transport); services.AddSingleton(mcpClient); } catch (Exception ex) { LogProvider.Error($"AddMcpClientfail:{ex.ToString()}"); } } } }
4.2.2 注册MCP工具
namespace STD.AI.Implementations { /// <summary> /// 公司信息查询 Agent,使用 MCP 浏览器工具自动查询公司信息 /// </summary> public class CompanyInfoQueryAgent : BaseAgentFunction, ICompanyInfoQueryAgent { private readonly AIAgent _agent; private readonly McpClient _mcpClient; public CompanyInfoQueryAgent(IOptions<LLMConfiguration> config, McpClient mcpClient) { _mcpClient = mcpClient ?? throw new ArgumentNullException(nameof(mcpClient)); var openAIClient = new OpenAIClient(new ApiKeyCredential(config.Value.ApiKey), new OpenAIClientOptions { Endpoint = new Uri(config.Value.Endpoint) }); var responseClient = openAIClient.GetChatClient(config.Value.Model); // 获取 MCP 工具并注册到 Agent var tools = _mcpClient.ListToolsAsync().GetAwaiter().GetResult(); _agent = responseClient.CreateAIAgent( instructions: @" 你是一个专业的商业信息采集 AI 助手,拥有网络访问能力 (MCP 浏览器工具)。 你的任务是:自动访问多个公开来源(如企业官网、天眼查、企查查、维基百科、新闻报道等), 提取公司相关信息,并输出为严格 JSON 格式,映射到以下 CompanyInfo 结构。 请严格返回合法 JSON(不包含解释性文字或 Markdown)。 ### CompanyInfo 字段定义与说明: { ""companyName"": ""公司中文名称(必须字段)"", ""englishName"": ""英文名称,如有"", ""officialWebsite"": ""公司官网 URL,如未知可留空"", ""contactPhone"": ""公司主要联系电话"", ""email"": ""公司官方邮箱"", ""address"": ""公司总部地址"", ""businessScope"": ""经营范围,描述主营业务及服务"", ""registrationNumber"": ""工商注册号(如可获得)"", ""unifiedSocialCreditCode"": ""统一社会信用代码(如可获得)"", ""companyType"": ""公司类型(如有限责任公司、股份有限公司等)"", ""legalRepresentative"": ""法定代表人姓名"", ""registeredCapital"": ""注册资本(含币种)"", ""establishedDate"": ""公司成立日期(ISO格式,如 2020-05-12)"", ""industry"": ""所属行业(如互联网、制造业等)"", ""mainBusiness"": ""主营产品或服务"", ""employeeCount"": ""员工数量(大约范围,如 '100-500人')"", ""stockCode"": ""股票代码(如上市公司)"", ""stockExchange"": ""交易所(如上交所、纳斯达克)"", ""lastUpdated"": ""数据最后处理时间(ISO 8601 格式)"" } 返回的 JSON 必须能直接被 C# System.Text.Json 反序列化为 CompanyInfo 对象。 ", name: "mcpAgent", description: "调用 MCP 工具实现公司数据查询", tools: tools.Cast<AITool>().ToList() ); } public async Task<CompanyInfo?> QueryCompanyInfoAsync(string companyName) { if (string.IsNullOrWhiteSpace(companyName)) throw new ArgumentException("公司名称不能为空", nameof(companyName)); var thread = _agent.GetNewThread(); string userPrompt = $@" 请使用 MCP 浏览器工具搜索并访问多个网页, 综合提取公司 “{companyName}” 的完整工商及公开资料。 请整合不同来源的数据,确保字段尽量完整,并返回合法 JSON。 "; var response = await _agent.RunAsync(userPrompt, thread); string raw = response.Text ?? string.Empty; raw = CleanJsonResponse(raw); return JsonSerializer.Deserialize<CompanyInfo>(raw); } } }
4.3 注册函数工具
4.3.1 编写函数工具
using Microsoft.Extensions.AI; namespace STD.AI.Tools { public class CompanyInfoTool : AITool { private readonly HttpClient _httpClient; public CompanyInfoTool(HttpClient httpClient) { _httpClient = httpClient; } public async Task<string> QueryCompanyInfoAsync(string companyName) { var response = await _httpClient.GetAsync($"https://api.example.com/company/{companyName}"); return await response.Content.ReadAsStringAsync(); } } }
4.3.2 注册函数工具
namespace STD.AI.Implementations { public class CompanyInfoAgent : BaseAgentFunction, ICompanyInfoAgent { private readonly AIAgent _agent; private readonly CompanyInfoTool _companyInfoTool; public CompanyInfoAgent(IOptions<LLMConfiguration> config, CompanyInfoTool companyInfoTool) { _companyInfoTool = companyInfoTool ?? throw new ArgumentNullException(nameof(companyInfoTool)); var openAIClient = new OpenAIClient(new ApiKeyCredential(config.Value.ApiKey), new OpenAIClientOptions { Endpoint = new Uri(config.Value.Endpoint) }); var responseClient = openAIClient.GetChatClient(config.Value.Model); // 创建 Agent,并注册工具 _agent = responseClient.CreateAIAgent( instructions: "你是一个公司信息查询助手,请使用工具查询公司相关信息。", name: "companyInfoAgent", description: "使用公司信息查询工具来获取公司资料", tools: new List<AITool> { _companyInfoTool } ); } public async Task<string> GetCompanyInfoAsync(string companyName) { var thread = _agent.GetNewThread(); // AI 通过工具查询公司信息 var response = await _agent.RunAsync($"请查询公司 {companyName} 的详细信息", thread); return response.Text; } } }
4.4 记忆功能
namespace STD.AI.Implementations { public class CompanyInfoAgentWithMemory : BaseAgentFunction { private readonly AIAgent _agent; private readonly CompanyInfoTool _companyInfoTool; public CompanyInfoAgentWithMemory(IOptions<LLMConfiguration> config, CompanyInfoTool companyInfoTool) { _companyInfoTool = companyInfoTool ?? throw new ArgumentNullException(nameof(companyInfoTool)); var openAIClient = new OpenAIClient(new ApiKeyCredential(config.Value.ApiKey), new OpenAIClientOptions { Endpoint = new Uri(config.Value.Endpoint) }); var responseClient = openAIClient.GetChatClient(config.Value.Model); // 创建代理 _agent = responseClient.CreateAIAgent( instructions: "你是一个公司信息查询助手,请使用工具查询公司相关信息。", name: "companyInfoAgentWithMemory", description: "使用公司信息查询工具,并且记住用户的历史对话。", tools: new List<AITool> { _companyInfoTool } ); } // 查询公司信息并使用记忆存储对话内容 public async Task<string> GetCompanyInfoAsync(string companyName) { var thread = _agent.GetNewThread(); // AI 通过工具查询公司信息 var response = await _agent.RunAsync($"请查询公司 {companyName} 的详细信息", thread); // 序列化并保存当前对话状态到持久存储(例如文件、数据库等) var serializedThread = thread.Serialize(JsonSerializerOptions.Web).GetRawText(); await SaveThreadStateAsync(serializedThread); return response.Text; } // 恢复之前的对话上下文并继续对话 public async Task<string> ResumePreviousConversationAsync(string companyName) { var thread = _agent.GetNewThread(); // 从存储中加载之前的对话状态 var previousThread = await LoadThreadStateAsync(); // 反序列化并恢复对话 var reloadedThread = _agent.DeserializeThread(JsonSerializer.Deserialize<JsonElement>(previousThread)); // 使用恢复的上下文继续对话 var response = await _agent.RunAsync($"继续查询公司 {companyName} 的信息", reloadedThread); return response.Text; } // 模拟保存线程状态到持久存储 private async Task SaveThreadStateAsync(string serializedThread) { // 示例:保存到文件(可以替换为数据库或其他存储介质) var filePath = Path.Combine(Path.GetTempPath(), "agent_thread.json"); await File.WriteAllTextAsync(filePath, serializedThread); } // 模拟加载存储的线程状态 private async Task<string> LoadThreadStateAsync() { // 示例:从文件加载(可以替换为数据库或其他存储介质) var filePath = Path.Combine(Path.GetTempPath(), "agent_thread.json"); return await File.ReadAllTextAsync(filePath); } } }
内存上下文实现:
五、一些小坑
5.1 API地址配置
namespace STD.Model { public class LLMConfiguration { public string Model { get; set; } public string Endpoint { get; set; } public string ApiKey { get; set; } public bool IsValid() { return !string.IsNullOrWhiteSpace(Model) && !string.IsNullOrWhiteSpace(Endpoint) && Uri.IsWellFormedUriString(Endpoint, UriKind.Absolute) && !string.IsNullOrWhiteSpace(ApiKey); } } }
填写Endpoint(OpenAI规范):
SK框架:https://api.deepseek.com/v1 AgentFramework框架:https://api.deepseek.com
5.2 结构化输出
namespace STD.AI.Implementations { public class CompanyInfoQueryAgent : BaseAgentFunction, ICompanyInfoQueryAgent { private readonly AIAgent _agent; private readonly McpClient _mcpClient; public CompanyInfoQueryAgent(IOptions<LLMConfiguration> config, McpClient mcpClient) { _mcpClient = mcpClient ?? throw new ArgumentNullException(nameof(mcpClient)); var openAIClient = new OpenAIClient( new ApiKeyCredential(config.Value.ApiKey), new OpenAIClientOptions { Endpoint = new Uri(config.Value.Endpoint ?? "https://api.deepseek.com") } ); // 获取 chat client(DeepSeek/Azure/OpenAI 的封装) var chatClient = openAIClient.GetChatClient(config.Value.Model); // 从你的 MCP client 获取工具列表(假设返回 IList<AITool> 或 可转换的集合) var tools = _mcpClient.ListToolsAsync().GetAwaiter().GetResult() .Cast<AITool>() .ToList(); JsonElement companySchema = AIJsonUtilities.CreateJsonSchema(typeof(CompanyInfo)); //定义规范输出 ChatOptions chatOptions = new() { ResponseFormat = ChatResponseFormat.ForJsonSchema( schema: companySchema, schemaName: nameof(CompanyInfo), schemaDescription: "Structured CompanyInfo output"), }; chatOptions.Tools = tools; var agentOptions = new ChatClientAgentOptions { Name = "CompanyInfoAgent", Instructions = @"你是商业信息采集助手。请使用已注册的浏览器/网页工具搜索并整合公司信息,严格返回符合 CompanyInfo JSON schema 的对象。", Description = "使用 MCP 工具检索公司公开信息,返回结构化 CompanyInfo。", ChatOptions = chatOptions }; // 创建 Agent(使用 chatClient 的 CreateAIAgent 重载) _agent = chatClient.CreateAIAgent(agentOptions); } public async Task<CompanyInfo?> QueryCompanyInfoAsync(string companyName) { if (string.IsNullOrWhiteSpace(companyName)) throw new ArgumentException("公司名称不能为空", nameof(companyName)); var thread = _agent.GetNewThread(); string prompt = $@" 请使用已注册的网页/浏览器工具(MCP 工具集合),访问多个来源(官网、企查查/天眼查、维基/百科、相关新闻等), 综合提取公司 ""{companyName}"" 的信息并严格返回符合 CompanyInfo 模型的 JSON 对象。"; var response = await _agent.RunAsync(prompt, thread); // 框架内置反序列化(结构化输出),使用 System.Text.Json Web 选项 var company = response.Deserialize<CompanyInfo>(JsonSerializerOptions.Web); return company; } } }
RunAsync报错,经排查DeepseekAPI不支持,但官方文档是支持JsonFormat type:jsonobject 的
如有大佬,望告知解惑