通过A2A协议与实现-09:.NET SDK客户端的设计与实现的介绍,我们知道可以利用IA2AClient对象远程调用A2A Server,根据采用的协议不同,我们可以选择A2AClient和A2AHttpJsonClient。A2AAgent则提供一种更加方便的交互方式:将承载于A2A Server的远程Agent视为本地Agent一样调用。
1. 连接A2A Server创建A2AAgent
如下演示的是一个ASP.NET Core程序,我们通过调用WebApplication的扩展方法MapA2AHttpJson以HTTP-JSON协议,基于创建的AIAgent构建了一个A2A Server。启动之后,我们根据通信协议创建了对应的A2AHttpJsonClient客户端,并据此创建了A2AAgent。
usingA2A;usingAzure.AI.Projects;usingDotNetEnv;usingMicrosoft.Agents.AI.A2A;usingMicrosoft.Agents.AI.Hosting;usingMicrosoft.Extensions.AI;usingOpenAI;usingOpenAI.Responses;usingSystem.ClientModel;Env.Load();varapiKey=Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!;varagent=newOpenAIClient(credential:newApiKeyCredential(key:apiKey),options:newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetResponsesClient().AsAIAgent(model:"gpt-5.4-mini",name:"a2a-agent",instructions:""" 你是一个深谙中国古代历史的专家,善于根据正史,以公正客观的态度于人交流历史问题。 对于用户提出的问题,请以简介概括性的语言予以答复,字数尽量保持在200字以内。""");varbuidler=WebApplication.CreateBuilder();buidler.Services.AddA2AServer(agent.Name!).AddAIAgent(agent.Name!,(services,anme)=>agent);varurl="http://localhost:3721/";buidler.WebHost.UseUrls(url);varapp=buidler.Build();app.MapA2AHttpJson(agent,"/");awaitapp.StartAsync();usingvarclient=newA2AHttpJsonClient(newUri(url));vara2aAgent=newA2AAgent(client);varsession=awaita2aAgent.CreateSessionAsync();while(true){Console.Write("\n$ (:q or quit to exit): ");varmessage=Console.ReadLine();if(messageis":q"or"quit"){break;}awaitforeach(varupdateina2aAgent.RunStreamingAsync(message!,session)){Console.Write(update);}}我们构建了一个循环,在同一个Session中利用A2AAgent作为代理,完成与远程Agent的对话。如下所示的就是我与AI进行的一段简单的针对齐鲁两个不同命运的交流。
$ (:q or quit to exit): 齐国和鲁国,位置相邻,首任国君地位相当,却有不同的发展轨迹,你如何评价? 齐鲁同处山东,开国皆受封于周,但走向不同: 齐以姜太公治国,重视“因俗简礼”、发展工商与军政,地理又临海负山,便于吸纳人才、积蓄实力,最终成为春秋首霸。 鲁则承周礼最深,尊礼重教,政治较稳,但礼制繁密也使其机动性不足,国力渐弱。 可以说,齐偏务实开拓,鲁偏守礼保传统;一强于变,一长于守,正体现两种不同的国家道路。 $ (:q or quit to exit): 但是最终的结局是:齐国由田氏代姜,鲁国却国祚绵绵 大体可以这样理解:齐国在春秋末年国势强盛,但内部卿大夫坐大,最终发生“田氏代齐”; 鲁国虽国弱,却因三桓势力彼此牵制,君权虽衰而宗周旧礼保存较多,且地处列国夹缝中,外部压力相对较大而不至于被某一家彻底吞并。 不过“国祚绵绵”也要分阶段看,鲁国并非始终强盛,只是存续时间较长,直到战国末才为楚所灭。 $ (:q or quit to exit): 这是否看成是周公和太公不同的政治理念导致两国不同的走向? 可以这样理解,但需稍作限定。 若指西周初年,周公更重“制礼作乐”、以宗法封建和礼制整合天下; 太公(姜尚)在齐更偏重因地制宜、务实尚功,重农商、尚武备。 两者理念确有差异,但两国走向不仅由个人政治理念决定,还受地理、族群基础、资源禀赋与后世君主施政影响。 也就是说,周公与太公的不同是重要起点,但不是唯一原因。2. A2AAgentSession
在正式介绍A2AAgent的实现原理之前,我们先介绍它的一些周边。首先介绍的是通过调用其CreateSessionAsync方法创建的A2AAgentSession。如下面的代码所示,A2AAgentSession继承自AgentSession。A2A语境下的Context其实就是Session的概念,所以ContextId和SessionId、ThreadId与ConversationId表达同一个概念。如果采用后台任务来处理Agent调用,后台任务的ID和状态分别通过TaskId和TaskState表示。
publicsealedclassA2AAgentSession:AgentSession{publicstring?ContextId{get;}publicstring?TaskId{get;}publicTaskState?TaskState{get;}}publicenumTaskState{Unspecified,Submitted,Working,Completed,Failed,Canceled,InputRequired,Rejected,AuthRequired}3. A2AContinuationToken
如果调用A2AAgent返回的AgentResponse携带一个ResponseContinuationToken,意味着Agent的执行尚未结束,还需要携带这个Token发起后续的调用。对于A2A来说,只有采用后台任务处理Agent调用请求才会出现这样的情况,所以这样的ResponseContinuationToken应该携带后台任务的信息。此Token对应的类型为ResponseContinuationToken,它利用TaskId返回尚未结束的后台任务的ID。
internalclassA2AContinuationToken:ResponseContinuationToken{internalA2AContinuationToken(stringtaskId);internalstringTaskId{get;}}4. A2AAgent
由于A2AAgent使用IA2AClient对象远程调用A2A Server,所以我们在初始化A2AAgent对象时必须执行此对象。除此之外,我们可以采用如下两种方式(分别指定或者利用A2AAgentOptions配置选项)指定Agent的Id、名称和描述。
publicsealedclassA2AAgent:AIAgent{publicA2AAgent(IA2AClienta2aClient,string?id=null,string?name=null,string?description=null,ILoggerFactory?loggerFactory=null);publicA2AAgent(IA2AClienta2aClient,A2AAgentOptionsoptions,ILoggerFactory?loggerFactory=null);}publicsealedclassA2AAgentOptions{publicstring?Id{get;set;}publicstring?Name{get;set;}publicstring?Description{get;set;}}4.1 阻塞式调用
在重写的RunCoreAsync方法中,如果指定的AgentSession为null,会创建一个空的A2AAgentSession对象。如果AgentRunOptions的ContinuationToken不为null,会采用如下的流程处理请求:
- 如果
ContinuationToken类型不是A2AContinuationToken,则从中解析出TaskId并创建对应的A2AContinuationToken; - 根据
TaskId调用IA2AClient对象的GetTaskAsync方法得到描述后台任务的AgentTask象; - 将
TaskId和表示后台任务状态的TaskState写入当前的A2AAgentSession; - 采用如下的方式根据
AgentTask创建一个AgentResponse,并将其作为返回值。
privateAgentResponseConvertToAgentResponse(AgentTasktask){returnnewAgentResponse{AgentId=this.Id,ResponseId=task.Id,FinishReason=MapTaskStateToFinishReason(task.Status.State),RawRepresentation=task,Messages=task.ToChatMessages()??[],ContinuationToken=CreateContinuationToken(task.Id,task.Status.State),AdditionalProperties=task.Metadata?.ToAdditionalProperties(),};}否则会根据当前的A2AAgentSession、输入的消息列表和AgentRunOptions创建SendMessageRequest对象,具体的构建逻辑如下:
- 将传入的
ChatMessag列表转换成A2A.Message(角色为User,所有的AIContent转换成A2A.Part),并作为SendMessageRequest的Message属性。如果A2AAgentSession携带TaskId:- 如果任务状态为
InputRequired(表示任务正在等待输入),则将TaskId赋值给Message的TaskId属性, - 否则添加到
ReferenceTaskIds列表中;
- 如果任务状态为
- 将
AgentRunOptions的AdditionalProperties转换成元数据赋值给SendMessageRequest的Metadata属性; - 如果
AgentRunOptions的AllowBackgroundResponses为true(运行后台响应),将Message的SendMessageConfiguration的ReturnImmediately设置为true。
然后将SendMessageRequest作为输入调用IA2AClient对象的SendMessageAsync方法,并按照如下的方式处理返回的SendMessageResponse:
- 如果
PayloadCase为SendMessageResponseCase.Message(返回代表执行结果的消息):- 将响应携带的
ContextId写入当前的A2AAgentSession; - 调用如下这个
ConvertToAgentResponse方法将消息转换成返回的AgentResponse。
- 将响应携带的
privateAgentResponseConvertToAgentResponse(Messagemessage){returnnewAgentResponse{AgentId=this.Id,ResponseId=message.MessageId,FinishReason=ChatFinishReason.Stop,RawRepresentation=message,Messages=[message.ToChatMessage()],AdditionalProperties=message.Metadata?.ToAdditionalProperties(),};}- 如果
PayloadCase为SendMessageResponseCase.Task(返回处理请求的后台任务):- 将响应返回的
ContextId、TaskId和TaskState写入当前的A2AAgentSession; - 调用上面定义的
ConvertToAgentResponse方法将代表后台任务的AgentTask转换成返回的AgentResponse。
- 将响应返回的
4.2 流式调用
在重写的RunCoreStreamingAsync方法中,如果指定的AgentSession为null,会创建一个空的A2AAgentSession对象。
- 如果
AgentRunOptions的ContinuationToken不为null,则将TaskId作为参数调用IA2AClient对象SubscribeToTaskAsync接入指定的后台任务,返回代表响应流的IAsyncEnumerable<StreamResponse>对象; - 否则,按照上述的方式将输入的
ChatMessage列表转换成A2A.Message,并据此创建SendMessageRequest对象。最后调用IA2AClient对象的SendStreamingMessageAsync方法得到代表响应流的IAsyncEnumerable<StreamResponse>对象。
接下来异步遍历响应流的每个StreamResponse,针对StreamResponseCase的不同调用对应的方法创建实时输出的AgentResponseUpdate,并将StreamResponse携带的ContextId、TaskId和TaskState写入当前的A2AAgentSession。
//StreamResponseCase.MessageprivateAgentResponseUpdateConvertToAgentResponseUpdate(Messagemessage){returnnewAgentResponseUpdate{AgentId=this.Id,ResponseId=message.MessageId,FinishReason=ChatFinishReason.Stop,RawRepresentation=message,Role=ChatRole.Assistant,MessageId=message.MessageId,Contents=message.Parts.ConvertAll(part=>part.ToAIContent()),AdditionalProperties=message.Metadata?.ToAdditionalProperties(),};}//StreamResponseCase.TaskprivateAgentResponseUpdateConvertToAgentResponseUpdate(AgentTasktask){returnnewAgentResponseUpdate{AgentId=this.Id,ResponseId=task.Id,FinishReason=MapTaskStateToFinishReason(task.Status.State),RawRepresentation=task,Role=ChatRole.Assistant,Contents=task.ToAIContents(),ContinuationToken=CreateContinuationToken(task.Id,task.Status.State),AdditionalProperties=task.Metadata?.ToAdditionalProperties(),};}//StreamResponseCase.StatusUpdateprivateAgentResponseUpdateConvertToAgentResponseUpdate(TaskStatusUpdateEventstatusUpdateEvent){returnnewAgentResponseUpdate{AgentId=this.Id,ResponseId=statusUpdateEvent.TaskId,RawRepresentation=statusUpdateEvent,Role=ChatRole.Assistant,MessageId=statusUpdateEvent.Status.Message?.MessageId,FinishReason=MapTaskStateToFinishReason(statusUpdateEvent.Status.State),AdditionalProperties=statusUpdateEvent.Metadata?.ToAdditionalProperties()??[],Contents=statusUpdateEvent.Status.GetUserInputRequests(),};}//StreamResponseCase.ArtifactUpdateprivateAgentResponseUpdateConvertToAgentResponseUpdate(TaskArtifactUpdateEventartifactUpdateEvent){returnnewAgentResponseUpdate{AgentId=this.Id,ResponseId=artifactUpdateEvent.TaskId,RawRepresentation=artifactUpdateEvent,Role=ChatRole.Assistant,Contents=artifactUpdateEvent.Artifact.ToAIContents(),AdditionalProperties=artifactUpdateEvent.Metadata?.ToAdditionalProperties()??[],};}