MAF快速入门(14)快速集成A2A Agent

大家好,我是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

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

相关推荐
gentle coder8 小时前
【langchain】AI应用开发框架
langchain·llm·rag
重生之我要成为代码大佬11 小时前
LangChain-多任务应用开发
langchain·大模型·agent
doll ~CJ12 小时前
Large Language Model(LLM)应用开发学习实践(三)
langchain·llm·提示词工程·ai应用
Rolei_zl13 小时前
(AI生成) openClaw 的前世今生
llm·aigc
人工智能培训13 小时前
具身智能如何在保证安全的前提下高效探索学习?
语言模型·llm·数据采集·模型量化·多模态学习·具身智能·环境感知
Elwin Wong14 小时前
浅析DeepSeek-OCR v1&v2
人工智能·大模型·llm·ocr·deepseek
CoderJia程序员甲14 小时前
GitHub 热榜项目 - 日榜(2026-02-03)
git·ai·开源·llm·github
时光追逐者15 小时前
一个基于 .NET + Vue 实现的通用权限管理平台(RBAC模式),前后端分离模式,开箱即用!
前端·vue.js·c#·.net·.net core
中杯可乐多加冰1 天前
RAG 深度实践系列(七):从“能用”到“好用”——RAG 系统优化与效果评估
人工智能·大模型·llm·大语言模型·rag·检索增强生成