![[A2A协议与实现-10]利用A2AAgent作为远程调用Agent的本地代理](http://pic.xiahunao.cn/yaotu/[A2A协议与实现-10]利用A2AAgent作为远程调用Agent的本地代理)
通过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();varapiKeyEnvironment.GetEnvironmentVariable(OPENAI_API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_BASE_URL)!;varagentnewOpenAIClient(credential:newApiKeyCredential(key:apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetResponsesClient().AsAIAgent(model:gpt-5.4-mini,name:a2a-agent,instructions: 你是一个深谙中国古代历史的专家善于根据正史以公正客观的态度于人交流历史问题。 对于用户提出的问题请以简介概括性的语言予以答复字数尽量保持在200字以内。);varbuidlerWebApplication.CreateBuilder();buidler.Services.AddA2AServer(agent.Name!).AddAIAgent(agent.Name!,(services,anme)agent);varurlhttp://localhost:3721/;buidler.WebHost.UseUrls(url);varappbuidler.Build();app.MapA2AHttpJson(agent,/);awaitapp.StartAsync();usingvarclientnewA2AHttpJsonClient(newUri(url));vara2aAgentnewA2AAgent(client);varsessionawaita2aAgent.CreateSessionAsync();while(true){Console.Write(\n$ (:q or quit to exit): );varmessageConsole.ReadLine();if(messageis:qorquit){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?idnull,string?namenull,string?descriptionnull,ILoggerFactory?loggerFactorynull);publicA2AAgent(IA2AClienta2aClient,A2AAgentOptionsoptions,ILoggerFactory?loggerFactorynull);}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{AgentIdthis.Id,ResponseIdtask.Id,FinishReasonMapTaskStateToFinishReason(task.Status.State),RawRepresentationtask,Messagestask.ToChatMessages()??[],ContinuationTokenCreateContinuationToken(task.Id,task.Status.State),AdditionalPropertiestask.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{AgentIdthis.Id,ResponseIdmessage.MessageId,FinishReasonChatFinishReason.Stop,RawRepresentationmessage,Messages[message.ToChatMessage()],AdditionalPropertiesmessage.Metadata?.ToAdditionalProperties(),};}如果PayloadCase为SendMessageResponseCase.Task返回处理请求的后台任务将响应返回的ContextId、TaskId和TaskState写入当前的A2AAgentSession调用上面定义的ConvertToAgentResponse方法将代表后台任务的AgentTask转换成返回的AgentResponse。4.2 流式调用在重写的RunCoreStreamingAsync方法中如果指定的AgentSession为null会创建一个空的A2AAgentSession对象。如果AgentRunOptions的ContinuationToken不为null则将TaskId作为参数调用IA2AClient对象SubscribeToTaskAsync接入指定的后台任务返回代表响应流的IAsyncEnumerableStreamResponse对象否则按照上述的方式将输入的ChatMessage列表转换成A2A.Message,并据此创建SendMessageRequest对象。最后调用IA2AClient对象的SendStreamingMessageAsync方法得到代表响应流的IAsyncEnumerableStreamResponse对象。接下来异步遍历响应流的每个StreamResponse针对StreamResponseCase的不同调用对应的方法创建实时输出的AgentResponseUpdate并将StreamResponse携带的ContextId、TaskId和TaskState写入当前的A2AAgentSession。//StreamResponseCase.MessageprivateAgentResponseUpdateConvertToAgentResponseUpdate(Messagemessage){returnnewAgentResponseUpdate{AgentIdthis.Id,ResponseIdmessage.MessageId,FinishReasonChatFinishReason.Stop,RawRepresentationmessage,RoleChatRole.Assistant,MessageIdmessage.MessageId,Contentsmessage.Parts.ConvertAll(partpart.ToAIContent()),AdditionalPropertiesmessage.Metadata?.ToAdditionalProperties(),};}//StreamResponseCase.TaskprivateAgentResponseUpdateConvertToAgentResponseUpdate(AgentTasktask){returnnewAgentResponseUpdate{AgentIdthis.Id,ResponseIdtask.Id,FinishReasonMapTaskStateToFinishReason(task.Status.State),RawRepresentationtask,RoleChatRole.Assistant,Contentstask.ToAIContents(),ContinuationTokenCreateContinuationToken(task.Id,task.Status.State),AdditionalPropertiestask.Metadata?.ToAdditionalProperties(),};}//StreamResponseCase.StatusUpdateprivateAgentResponseUpdateConvertToAgentResponseUpdate(TaskStatusUpdateEventstatusUpdateEvent){returnnewAgentResponseUpdate{AgentIdthis.Id,ResponseIdstatusUpdateEvent.TaskId,RawRepresentationstatusUpdateEvent,RoleChatRole.Assistant,MessageIdstatusUpdateEvent.Status.Message?.MessageId,FinishReasonMapTaskStateToFinishReason(statusUpdateEvent.Status.State),AdditionalPropertiesstatusUpdateEvent.Metadata?.ToAdditionalProperties()??[],ContentsstatusUpdateEvent.Status.GetUserInputRequests(),};}//StreamResponseCase.ArtifactUpdateprivateAgentResponseUpdateConvertToAgentResponseUpdate(TaskArtifactUpdateEventartifactUpdateEvent){returnnewAgentResponseUpdate{AgentIdthis.Id,ResponseIdartifactUpdateEvent.TaskId,RawRepresentationartifactUpdateEvent,RoleChatRole.Assistant,ContentsartifactUpdateEvent.Artifact.ToAIContents(),AdditionalPropertiesartifactUpdateEvent.Metadata?.ToAdditionalProperties()??[],};}