大家好,我是Edison。
最近我一直在跟着圣杰的《.NET+AI智能体开发进阶》课程学习MAF开发多智能体工作流,我强烈推荐你也上车跟我一起出发!
上一篇,我们学习了MAF中常见的多智能体编排模式。本篇,我们来了解下在MAF中如何快速集成A2A (Agent to Agent)。
1 A2A协议介绍
在之前的系列文章中我们其实已经介绍过A2A协议了,这里我们快速温习一下。A2A 即 Agent-to-Agent,翻译过来就是"智能代理之间的协议",我们可以理解为它就是一个大模型Agent们用来"聊天"的"通用语言"。

A2A定义了一套清晰、标准的沟通方式,让Agent们可以顺畅地交流,让不同平台和框架下的Agent都能够说"同一种话",实现无障碍的信息交换和协作。
更多关于A2A协议的内容:
2 将A2A Agent封装为Tool
在MAF集成A2A Agent,最主要的操作就是:将A2A Agent封装为一个Tool ,这个Tool对应到MAF中就是一个AIFunction对象。
前面我们提到可以将MCP服务也封装为一个Tool(AIFunction)让Agent调用,这里A2A Agent也是一样的道理。
这样做的好处是:让MAF中的Agent像调用本地函数一样调用远程A2A Agent 或 MCP Server。
下面的代码展示了在MAF中将A2A Card转换为Agent,然后再将Agent转换为AIFunction:
......
var functionTools = new List<AIFunction>();
foreach (var endpoint in agentEndpoints)
{
var resolver = new A2ACardResolver(new Uri(endpoint));
var card = await resolver.GetAgentCardAsync();
var agent = card.AsAIAgent(); // Convert A2A Agent to AIAgent instance
functionTools.AddRange(AgentFunctionHelper.CreateFunctionTools(agent, card));
}
......
下面是AgentFunctionHelper类的代码实现:
public class AgentFunctionHelper
{
public static IEnumerable<AIFunction> CreateFunctionTools(AIAgent a2aAgent, AgentCard agentCard)
{
foreach (var skill in agentCard.Skills)
{
AIFunctionFactoryOptions options = new()
{
Name = Sanitize(skill.Id),
Description = $$"""
{
"description": "{{skill.Description}}",
"tags": "[{{string.Join(", ", skill.Tags ?? [])}}]",
"examples": "[{{string.Join(", ", skill.Examples ?? [])}}]",
"inputModes": "[{{string.Join(", ", skill.InputModes ?? [])}}]",
"outputModes": "[{{string.Join(", ", skill.OutputModes ?? [])}}]"
}
""",
};
yield return AIFunctionFactory.Create(RunAgentAsync, options);
}
async Task<string> RunAgentAsync(string input, CancellationToken cancellationToken)
{
var response = await a2aAgent.RunAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
return response.Text;
}
}
private static readonly Regex InvalidNameCharsRegex = new Regex("[^0-9A-Za-z]+", RegexOptions.Compiled);
public staticstringSanitize(string name)
{
return InvalidNameCharsRegex.Replace(name, "_");
}
}
其中的CreateFunctionTools方法实现了将A2A Agent的所有公开技能转换为AIFunction工具。
而Sanitize方法则实现了函数名称的规范化,因为AIFunction的名称必须符合一定规范(仅限字母、数字和下划线),因此需要主动对技能名称进行规范化。
3 完整集成示例
这次我们还是使用上次文章中的案例,即一个旅游助手,它可以通过A2A协议调用多个Agent的技能。

我们需要创建四个.NET项目,其中:
-
1个.NET控制台项目:主助手
-
3个ASP.NET Web项目:天气智能体、酒店智能体、路线智能体
在VS中的项目结构如下:

本次案例我们希望实现主助手可以回答用户关于不同主题(景点,酒店,天气)的问题,它可以根据问题自主选择需要调用一个或多个Agent去获取必要的信息后进行整合优化后再回复用户。
3.1 天气Agent
添加NuGet包,后续A2A Agent项目都需要安装此包,不再赘述:
A2A.AspNetCore
创建一个 WeatherAgent类,定义其能力 和 AgentCard,这里我们需要公开一个AgentSkill即天气查询的能力:
public class WeatherAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = QueryWeatherAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private Task<A2AResponse> QueryWeatherAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<A2AResponse>(cancellationToken);
}
// Process the message
var messageText = messageSendParams.Message.Parts.OfType<TextPart>().First().Text;
// Create and return an artifact
var message = new AgentMessage()
{
Role = MessageRole.Agent,
MessageId = Guid.NewGuid().ToString(),
ContextId = messageSendParams.Message.ContextId,
Parts = [new TextPart() {
Text = $"""
🌤️ **天气查询结果**
查询时间:{DateTime.Now:yyyy-MM-dd HH:mm}
**北京天气**
- 今日:晴转多云,气温 -2°C ~ 8°C
- 明日:多云,气温 0°C ~ 10°C
- 后日:阴,气温 2°C ~ 9°C
**上海天气**
- 今日:多云,气温 5°C ~ 12°C
- 明日:小雨,气温 6°C ~ 10°C
- 后日:阴转晴,气温 4°C ~ 11°C
👔 穿衣建议:北京较冷,建议穿羽绒服;上海温和,建议穿夹克外套,带好雨具。
"""
}]
};
return Task.FromResult<A2AResponse>(message);
}
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<AgentCard>(cancellationToken);
}
var capabilities = new AgentCapabilities()
{
Streaming = true,
PushNotifications = false,
};
return Task.FromResult(new AgentCard()
{
Name = "weather agent",
Description = "weather information agent",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [
new AgentSkill
{
Id = "weather-query",
Name = "天气查询",
Description = "查询指定城市的天气预报,包括温度、降水概率、穿衣建议等",
Tags = ["weather", "forecast", "climate"],
Examples = ["上海明天天气怎么样", "成都这周的天气预报", "杭州下雨吗"],
InputModes = ["text"],
OutputModes = ["text"]
}],
});
}
}
这里说明一下,这里为了方便是直接返回了一个固定的天气信息输出内容,但在实际应用中往往需要进行具体的业务逻辑处理 或 调用大模型进行处理。
下面的几个Agent也是类似的情况,就不再赘述。然后,在Program.cs中进行注册,完成端口映射:
using A2A;
using A2A.AspNetCore;
using WeatherAgentServer;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var taskManager = new TaskManager();
var agent = new WeatherAgent();
agent.Attach(taskManager);
// Add JSON-RPC endpoint for A2A
app.MapA2A(taskManager, "/weather");
// Add well-known agent card endpoint for A2A
app.MapWellKnownAgentCard(taskManager, "/weather");
// Add HTTP endpoint for A2A
app.MapHttpA2A(taskManager, "/weather");
app.Run();
3.2 酒店Agent
创建一个HotelAgent类,定义其能力 和 AgentCard:
public class HotelAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = QueryHotelsAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private Task<A2AResponse> QueryHotelsAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<A2AResponse>(cancellationToken);
}
// Process the message
var messageText = messageSendParams.Message.Parts.OfType<TextPart>().First().Text;
// Create and return an artifact
var message = new AgentMessage()
{
Role = MessageRole.Agent,
MessageId = Guid.NewGuid().ToString(),
ContextId = messageSendParams.Message.ContextId,
Parts = [new TextPart() {
Text = $"""
🏨 **酒店推荐**
根据您的需求,为您推荐以下酒店:
**豪华型 ⭐⭐⭐⭐⭐**
1. 上海外滩华尔道夫酒店
- 📍 外滩核心位置,江景房
- 💰 ¥2,500/晚起
- ⭐ 评分 4.9/5.0
**舒适型 ⭐⭐⭐⭐**
2. 上海静安香格里拉大酒店
- 📍 静安寺商圈,交通便利
- 💰 ¥1,200/晚起
- ⭐ 评分 4.7/5.0
**经济型 ⭐⭐⭐**
3. 全季酒店(上海南京路店)
- 📍 南京路步行街旁
- 💰 ¥380/晚起
- ⭐ 评分 4.5/5.0
💡 提示:建议提前预订,周末和节假日价格可能上涨 20-50%。
"""
}]
};
return Task.FromResult<A2AResponse>(message);
}
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<AgentCard>(cancellationToken);
}
var capabilities = new AgentCapabilities()
{
Streaming = true,
PushNotifications = false,
};
return Task.FromResult(new AgentCard()
{
Name = "hotel-a2a-agent",
Description = "hotel information agent",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [
new AgentSkill
{
Id = "hotel-recommendation",
Name = "酒店推荐",
Description = "根据目的地和预算推荐合适的酒店,包括豪华型、舒适型、经济型",
Tags = ["hotel", "accommodation", "booking", "travel"],
Examples = ["推荐上海的酒店", "上海外滩附近有什么好酒店", "预算500以内的北京酒店"],
InputModes = ["text"],
OutputModes = ["text"]
}
],
});
}
}
同样,请参考天气Agent完成Program.cs中的注册。
3.3 景点Agent
创建一个PlanAgent类,定义其能力 和 AgentCard:
public class PlanAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = QueryPlansAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private Task<A2AResponse> QueryPlansAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<A2AResponse>(cancellationToken);
}
// Process the message
var messageText = messageSendParams.Message.Parts.OfType<TextPart>().First().Text;
// Create and return an artifact
var message = new AgentMessage()
{
Role = MessageRole.Agent,
MessageId = Guid.NewGuid().ToString(),
ContextId = messageSendParams.Message.ContextId,
Parts = [new TextPart() {
Text = $"""
🎡 **景点推荐**
为您推荐上海必游景点:
**历史文化类**
1. 🏛️ 外滩 - 欣赏万国建筑博览群
2. 🏯 豫园 - 江南古典园林代表
3. 🕌 城隍庙 - 品尝地道上海小吃
**现代都市类**
4. 🗼 东方明珠塔 - 上海地标,俯瞰浦江两岸
5. 🌆 陆家嘴 - 金融中心,上海之巅
6. 🛍️ 南京路步行街 - 购物天堂
**文艺休闲类**
7. 🎨 田子坊 - 文艺小店聚集地
8. 📚 武康路 - 梧桐树下的法式风情
9. 🌳 世纪公园 - 城市绿肺,亲子游首选
📅 建议游玩时间:3-4 天可覆盖主要景点
"""
}]
};
return Task.FromResult<A2AResponse>(message);
}
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<AgentCard>(cancellationToken);
}
var capabilities = new AgentCapabilities()
{
Streaming = true,
PushNotifications = false,
};
return Task.FromResult(new AgentCard()
{
Name = "plan agent",
Description = "travel plan & attraction agent",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [
new AgentSkill
{
Id = "attraction-recommendation",
Name = "景点推荐",
Description = "推荐目的地的热门景点和游玩路线,包括历史文化、现代都市、文艺休闲等类型",
Tags = ["attraction", "sightseeing", "tourism", "travel"],
Examples = ["上海有什么好玩的", "北京必去的景点", "杭州西湖怎么玩"],
InputModes = ["text"],
OutputModes = ["text"]
}
],
});
}
}
同样,请参考天气Agent完成Program.cs中的注册。
3.4 主助手
这里我们暂且命名为TravelPlannerClient,在该项目中我们需要用到MAF,因此我们先安装一下相关NuGet包:
Microsoft.Extensions.AI.OpenAI
Microsoft.Agents.AI.A2A
Microsoft.Agents.AI.Abstractions
Microsoft.Extensions.AI.Abstractions
首先,创建一个ChatClient供主助手使用:
var chatClient = new OpenAIClient(
new ApiKeyCredential(openAIProvider.ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) })
.GetChatClient(openAIProvider.ModelId)
.AsIChatClient();
其次,将远程A2A Agents转换为AIFunction Tools:
var agentEndpoints = new[]
{
"https://localhost:7021/a2a", // hotel agent
"https://localhost:7011/a2a", // weather agent
"https://localhost:7031/a2a" // plan agent
};
// Collecting all AI Tools
var functionTools = new List<AIFunction>();
foreach (var endpoint in agentEndpoints)
{
var resolver = new A2ACardResolver(new Uri(endpoint));
var card = await resolver.GetAgentCardAsync();
var agent = card.AsAIAgent(); // Convert A2A Agent to AIAgent instance
functionTools.AddRange(AgentFunctionHelper.CreateFunctionTools(agent, card));
}
然后,创建一个可以调用A2A Agents的主Agent,这一步是核心所在:
var mainAgent = new ChatClientAgent(
chatClient: chatClient,
instructions: """
你是一个智能旅行规划助手。你可以利用可用的工具来帮助用户完成任务。
当用户询问时,请使用合适的工具获取信息,然后回复用户。
""",
tools: [.. functionTools]
);
最后,我们可以做下测试:
// 用户请求 - 测试不同的技能调用
var userRequests = new[]
{
"查询一下上海的天气情况",
"推荐一下上海的酒店",
"帮我规划一下今日上海的一日游景点,并告诉我该如何穿衣服",
};
foreach (var userRequest in userRequests)
{
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"👤 用户请求: {userRequest}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 执行 Agent
Console.WriteLine("⏱️ 主 Agent 处理中...");
var response = await mainAgent.RunAsync(userRequest);
Console.WriteLine($"💬 回答:\n{response.Text}");
Console.WriteLine();
}
现在,我们来看看测试结果:
case1:查询一下上海的天气情况(简单任务 )

可以看到,主助手通过调用天气Agent获取天气信息完成了回答。
case2:推荐上海的酒店(简单任务)

可以看到,主助手通过调用酒店Agent获取酒店信息完成了回答
case3:帮我规划一下今日上海的一日游景点,并告诉我该如何穿衣服(复杂任务)

可以看到,主助手调用了多个Agent(景点Agent 和 天气Agent)获取信息,还在此之上进行了整合优化,最后输出了完善的回复。
4 小结
本文介绍了MAF中集成A2A Agent的核心操作:将A2A Agent转换为AIFunction工具,然后由主Agent自主选择调用最终生成回答,希望本文的案例对你有所帮助。
示例源码
GitHub: https://github.com/EdisonTalk/MAFD
参考资料
圣杰,《.NET + AI 智能体开发进阶》(推荐指数:★★★★★)
Microsoft Learn,《Agent Framework Tutorials》

作者:爱迪生
出处:https://edisontalk.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。