大家好,我是Edison。
上一篇,我们了解了A2A的三大角色和四大对象,还知道了A2A的基本工作流程。本篇就来实践一个典型的A2A协议案例:Travel Planner。
Travel Planner介绍
本篇我们要开发的一个小助手暂且叫它为"Travel Planner",顾名思义,旅行规划助手。它的场景工作流程如下图所示:

让我们通过上一篇的知识,区分一下他们的角色:
- User 角色:用户
- Client 角色:主助手
- Remoet Agent 角色:航班智能体、酒店智能体、旅游智能体
在这个流程中,主助手Client负责接收用户发来的请求,并通过A2A协议从远端Agent获取必要的信息,最后整合这些信息发给大模型整理输出一个完整友好的旅游规划方案返回给用户。
那么,这也就意味着我们需要创建四个.NET项目,其中:
-
1个.NET控制台项目:主助手
-
3个ASP.NET Web项目:航班智能体、酒店智能体、旅游智能体
在VS解决方案中它们是这样子的:

那么,接下来我们就来一一实现,上干货!
注意:为了示例Code简单易懂,本案例并未涉及Task状态的相关设置。关于Task状态机相关案例,可以参考A2A .NET SDK的GitHub中的示例代码。
航班Agent实现
添加NuGet包,后续不再赘述:
A2A.AspNetCore "0.3.1-preview"
创建一个FlightAgent类,定义其能力 和 AgentCard:
public class FlightAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = QueryFlightsAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private Task<A2AResponse> QueryFlightsAsync(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 = $"Available Flilghts: CZ6250 - RMB380, MF1952 - RMB 390, GJ3156 - RMB410"
}]
};
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 = "Flight Agent",
Description = "Agent which will query and return available flights to user.",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [],
});
}
}
这里说明一下,我这里为了方便是直接返回了一个固定的输出内容,但在实际应用中往往需要进行具体的业务逻辑处理 或 调用大模型进行处理。下面的几个Agent也是类似的情况,就不再赘述。
然后,在Program.cs中进行注册:
using A2A;
using A2A.AspNetCore;
using FlightAgentServer;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var taskManager = new TaskManager();
var agent = new FlightAgent();
agent.Attach(taskManager);
// Add JSON-RPC endpoint for A2A
app.MapA2A(taskManager, "/flights");
// Add well-known agent card endpoint for A2A
app.MapWellKnownAgentCard(taskManager, "/flights");
// Add HTTP endpoint for A2A
app.MapHttpA2A(taskManager, "/flights");
app.Run();
这里需要注意的是:为了能够让Client端能够发现该Agent Card,请务必使用 MapWellKnownAgentCard 扩展方法进行注册 。
酒店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 = $"Available Hotels: JI Hotel - RMB350, Holiday In - RMB 320, Hilton - RMB610"
}]
};
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 Agent",
Description = "Agent which will query and return available hotels to user.",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [],
});
}
}
同样,请参考航班Agent完成Program.cs中的注册。
规划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 = $"Available Plans: The Muslim Quarter - 2 Hours, The Terracotta Warriors - 3 Hours, The Huaqing Palace - 2 Hours"
}]
};
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 = "Agent which will query and return travel plans to user.",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [],
});
}
}
主助手实现
这里我们暂且命名为TravelPlannerClient,在该项目中我们需要用到Semantic Kernel进行与大模型API的集成,因此我们先安装一下相关NuGet包:
A2A 0.3.1-preivew
Microsoft.SemanticKernel.Agents.Core 1.65.0
Microsoft.SemanticKernel.Agents.OpenAI 1.65.0-preview
然后,假设我们有一个配置文件appsettings.json定义了大模型供应商的信息:
{
"LLM": {
"BASE_URL": "https://api.siliconflow.cn",
"API_KEY": "******************************",
"MODEL_ID": "Qwen/Qwen2.5-32B-Instruct"
}
}
这里我们使用SiliconCloud提供的 Qwen2.5-32B-Instruct 模型,你可以通过这个URL注册账号:https://cloud.siliconflow.cn/i/DomqCefW 获取大量免费的Token来进行本次实验。
最后,就是完整的Client端代码实现,这里我分了几个主要的步骤写在注释中,方便你理解每个步骤是干什么的:
using A2A;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using TravelPlannerClient.Infrastructure;
Console.WriteLine("[Step1] Now loading the configuration...");
var config = new ConfigurationBuilder()
.AddJsonFile($"appsettings.json", optional: false, reloadOnChange: true)
.Build();
Console.WriteLine("[Step2] Now loading the chat client...");
var chattingApiConfiguration = new OpenAiConfiguration(
config.GetSection("LLM:MODEL_ID").Value,
config.GetSection("LLM:BASE_URL").Value,
config.GetSection("LLM:API_KEY").Value);
var openAiChattingClient = new HttpClient(new OpenAiHttpHandler(chattingApiConfiguration.EndPoint));
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(chattingApiConfiguration.ModelId, chattingApiConfiguration.ApiKey, httpClient: openAiChattingClient)
.Build();
Console.WriteLine("[Step3] Now loading the remote agent clients...");
// Remote Agent 1 : Flight Agent
var cardResolver = new A2ACardResolver(new Uri("https://localhost:7011/"));
var agentCard = await cardResolver.GetAgentCardAsync();
var flightAgentClient = new A2AClient(new Uri(agentCard.Url));
// Remote Agent 2 : Hotel Agent
cardResolver = new A2ACardResolver(new Uri("https://localhost:7021/"));
agentCard = await cardResolver.GetAgentCardAsync();
var hotelAgentClient = new A2AClient(new Uri(agentCard.Url));
// Remote Agent 3 : Plan Agent
cardResolver = new A2ACardResolver(new Uri("https://localhost:7031/"));
agentCard = await cardResolver.GetAgentCardAsync();
var planAgentClient = new A2AClient(new Uri(agentCard.Url));
Console.WriteLine("[Step4] Welcome to use Travel Planner!\n");
var task = "Hello, I'd like to have a travelling in Xian, please give me one travel plan.";
Console.WriteLine($"# User Message: \n{task}\n");
var userMessage = new AgentMessage
{
Role = MessageRole.User,
Parts = [new TextPart { Text = task }]
};
Console.WriteLine("[Step5] Now getting A2A Response from Remote Agents...");
var agentResponse = (AgentMessage)await flightAgentClient.SendMessageAsync(new MessageSendParams
{
Message = userMessage
});
var flightOptions = ((TextPart)agentResponse.Parts[0]).Text;
Console.WriteLine($"Flight-Agent Response: {flightOptions}");
agentResponse = (AgentMessage)await hotelAgentClient.SendMessageAsync(new MessageSendParams
{
Message = userMessage
});
var hotelOptions = ((TextPart)agentResponse.Parts[0]).Text;
Console.WriteLine($"Hotel-Agent Response: {hotelOptions}");
agentResponse = (AgentMessage)await planAgentClient.SendMessageAsync(new MessageSendParams
{
Message = userMessage
});
var planOptions = ((TextPart)agentResponse.Parts[0]).Text;
Console.WriteLine($"Plan-Agent Response: {planOptions}\n");
Console.WriteLine("[Step6] Now sumarizing the final response to user...");
var prompt = $"""
You are a travel planner assistant.
You have received the flight options, hotel options and travel plan from different agents.
You need to summarize the information and give a final travel plan to user.
Flight Options: {flightOptions}
Hotel Options: {hotelOptions}
Travel Plan: {planOptions}
Please provide a concise and clear travel plan to the user.
""";
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var response = await chatCompletionService.GetChatMessageContentAsync(prompt);
Console.WriteLine($"Your Plan: {response.Content}");
Console.ReadKey();
在代码中,我直接给出了用户的请求:"I'd like to have a travelling in Xian, please give me one travel plan.",即我想去西安旅游,给我一个旅游规划。
最终的执行结果如下所示,图中用红色矩形框标注的就是最终返回给用户的旅游规划:

可以看到,主助手通过A2A协议分别从多个远端Agent获取了部分信息,然后交由大模型进行了整合优化,一个用户友好的旅游规划方案就生成好了。
小结
本文介绍了一个典型的A2A应用案例 - Travel Planner 旅游规划助手,涉及一个Client 和 3个Remote Agent,是一个拿来练手增强理解的好案例,希望对你有所帮助!
示例源码
Github: https://github.com/EdisonTalk/MultiAgentOrchestration
参考资料
sing1ee,《2025年完整指南:A2A协议 - AI智能体协作的新标准》
黄佳,《MCP&A2A前沿实战》
圣杰:《.NET+AI | Semantic Kernel入门到精通》(推荐学习!!!)
作者:爱迪生
出处:https://edisontalk.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。