动态

详情 返回 返回

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

Add a new 评论

Some HTML is okay.