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

posted @ 2025-11-05 11:55  daibitx  阅读(498)  评论(3)    收藏  举报