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,大概还记得那种层层嵌套的概念: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 的
如有大佬,望告知解惑
2025-11-09 21:11:35【出处】:https://www.cnblogs.com/daibitx/p/19193204
=======================================================================================
如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的【关注我】。(●'◡'●)
因为,我的写作热情也离不开您的肯定与支持,感谢您的阅读,我是【Jack_孟】!
本文来自博客园,作者:jack_Meng,转载请注明原文链接:https://www.cnblogs.com/mq0036/p/19205301
【免责声明】本文来自源于网络,如涉及版权或侵权问题,请及时联系我们,我们将第一时间删除或更改!
浙公网安备 33010602011771号