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

浙公网安备 33010602011771号