Microsoft Agent Framework 接入DeepSeek的优雅姿势

一、前言

​ Microsoft Agent Framework 框架发布也有一阵子了,在观望(摸鱼)过后,也是果断(在老板的威胁下)将几个AI应用微服务完成了从Semantic Kernel 框架到Microsoft Agent Framework 框架中的迁移工作。

所以这篇文章,我想记录一下在开发过程中的我总结的一下工程化用法

二、Agent Framework是什么

简单来讲,Microsoft Agent Framework 是微软在 Semantic Kernel 之后推出的一个 新一代智能体(Agent)开发框架。它其实就是 SK 的“进化版”——思路差不多,但更直接、更好用,也更符合现在大家在做 多智能体(Multi-Agent)系统 的趋势。

如果你用过 Semantic Kernel,大概还记得那种层层嵌套的概念:KernelSkillFunctionContext…… 用起来就像在拼一堆乐高砖块。

三、对比Semantic Kernel

  1. 结构更加直观和优雅

    以前 SK 的 “Function” / “Skill” 概念太抽象。
    在 Agent Framework 里,你可以直接定义一个 Agent 类,然后给它挂上工具(Tool)、记忆(Memory)。

  2. Prompt 与逻辑分离更自然

    在 SK 里常常要写一堆 Template Function,还要用 YAML 或 JSON 去配置。
    在 Agent Framework 中,你直接在创建 Agent 时传入 instructions(提示词),框架会自动封装上下文调用,大幅减少模板样板代码。

  3. 内置的多 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);         }     } } 

内存上下文实现

[将内存添加到代理 | Microsoft Learn]

五、一些小坑

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 的
如有大佬,望告知解惑

发表评论

评论已关闭。

相关文章