当前位置: 首页 > news >正文

A2A协议深度解析(流式返回以及多agent协同)

继续聊聊Google推出的A2A协议,也就是Agent to Agent协议,这个协议用于让多个Agent互相沟通交流,完成一项复杂的任务。
在上一篇文章里面讲述了A2A协议的基本使用场景,还通过两个Agent的同步调用梳理了协议的核心链路。这期来看看两个更进阶的内容:

• 1、A2A协议是如何处理流式返回的?

• 2、更多数量的Agent协作流程。

之前的链路都只涉及两个Agent,不够有代表性,因此把Agent的数量扩大,来看看三个甚至更多的Agent是如何协作的。
话不多说,直接开始。
鉴于可能有人不太清楚流式返回是什么,这里简单解释一下,已经了解的小伙伴可以直接跳过这一部分。
一般情况下使用HTTP访问一个网站的时候,浏览器会发送给目标服务器一个请求,比如说要访问一个HTML页面、一个图片、一个JS脚本之类的,目标服务器会返回对应的结果,一去一回,一次交互就完成了。
这个交互方式有个缺陷,它处理不了服务器连续发回多次响应的情况。比如说经常用的大模型聊天页面,模型的结果都是几个字几个字地返回,只是一去一回的话,显然无法做到这种效果。所以目前主流的大模型聊天页面用的都是流式返回。
它的特点是浏览器只需要请求一次,服务器接收到请求之后会连续发送多次响应,每次响应的内容都是几个字,而浏览器接收到几个字就显示几个字,这样用户就可以及时接收到模型的返回,出来几个字就看几个字,体验就会好很多。
等到所有的结果都返回完毕之后,服务器会发送一个完成的标识,浏览器接收到标识之后关闭连接,页面显示模型回答完毕,整个流程也就结束了。


了解了流式返回的概念之后,就来看一看A2A是如何定义流式返回协议的。这次用一个查询机票信息的场景举例,整个流程里面有三个角色,分别是用户、调度Agent和机票Agent。


用户询问:2025年5月1日从西雅图飞往纽约的航班中还有哪些航班有余票?用户的问题会首先发往调度Agent,调度Agent会转发给机票Agent,机票Agent返回航班信息,调度Agent接到这个信息之后稍作加工就返回给用户,这样用户也就得到了最终答案。
这个就是整个流程,可以看出这里面涉及到两个Agent,下面的任务就是去启动这两个Agent,然后跑通整个流程。

• 首先是调度Agent,这个Agent写在a2a-samples仓库里面,和平台放在一起,都是在demo/ui这个文件夹里面。在上个视频里面已经演示过启动方法了,所以这里面就不赘述了,直接就给它启动开了。可以看出它开放的接口是12000,直接打开这个地址,没问题。到这里平台和调度Agent就创建完成了。

• 下面来启动机票Agent,这个机票Agent是我自己写的,放在这个GitHub仓库下面。进入到这个GitHub仓库里面,然后进入到这个目录中,打开flight文件夹,然后再执行uv run .这个命令,就可以启动这个机票Agent了。这样两个Agent都启动完毕了。

• 下面打开Wireshark抓包软件,选择loopback,也就是说只抓本地的网络包。然后设定过滤条件为:http and tcp.dstport == 10001。为什么是10001?因为刚才开放的机票Agent的端口就是10001,所以就想抓发往机票Agent的各种网络包。

• 然后到这里Wireshark的操作也结束了,再回到平台这里,点击Agents,再点击这里,注册机票Agent,这样机票Agent就注册成功了。

• 然后新建一个对话,输入问题:2025年5月1日从西雅图飞往纽约的航班中还有哪些航班有余票?可以看出平台直接给出了答案。
有些观众可能会在这个时候产生一些疑惑,因为好像结果也没有流式返回,讲的不就是流式的场景吗,是不是搞错了?其实没有搞错,机票Agent确实是流式返回的,只不过调度Agent会等流式返回的所有结果都返回回来之后才整理出一份答案,然后再给到用户。
所以从用户的角度来看,好像这个过程并没有涉及到流式返回什么事情,但是机票Agent的返回确实是流式的。由于平台没有流式显示机票Agent的返回结果,所以才有了整个过程并没有流式进行的错觉。


不信就来看一下抓的网络包,验证一下想法。再回到Wireshark这里,可以看到平台一共是向机票Agent发送了三个请求,其中前两个是用来请求Agent Card的,这个是在注册Agent的时候发送的。这两个请求其实本质上都是一样的,随便点开一个来看一下。
这个没有格式化,我来给大家格式化一下Agent Card的结构,在上个视频里面已经聊过了,这里只重点强调其中的两个地方。

• 第一、机票Agent有个Skill可以用来查询机票信息,也就是这个地方。所以当问机票相关的问题的时候,调度Agent就可以去调用机票Agent,拿到相关的答案。

• 第二、机票Agent是支持流式返回的,对应的就是这里面的streaming,值为true,所以后面就可以看到调度Agent在请求机票Agent的时候会要求它流式返回结果。


Agent Card就看到这里,再来看发往机票Agent的问题,也就是这里面的第三个请求。这个请求是问完问题之后调度Agent发给机票Agent的,这整个界面分为两个部分,上面红色的是调度Agent的请求,下面紫色的这大片是机票Agent的返回。
可以看出机票Agent的返回有多个,每一个都包含一部分信息,这也就是流式返回了,一次返回一部分。先来一点点看,首先来看一下它的请求是什么样子的,我再来给大家格式化一下请求的结构,在上个视频里面也聊过了,这里面只重点强调其中的几个部分。

{ "id": "0a96020b-5876-4011-94d4-b8fc4743b911", "jsonrpc": "2.0", "method": "message/stream", "params": { "configuration": { "acceptedOutputModes": [ "text", "text/plain", "image/png" ] }, "message": { "contextId": "b5cdbc7f-504e-4174-a543-ba8aef6e85a9", "kind": "message", "messageId": "9a022c61-4948-4731-b0b0-2671c66b0b43", "parts": [ { "kind": "text", "text": "请帮我查询2025年5月1日从西雅图飞往纽约的航班有哪些?" } ], "role": "user" } } }

• 第一、要强调的就是这里面的method,请求中的method设置的是message/stream,这个跟上个文章里面聊的是不一样的。在上个文章里面调度Agent请求天气Agent的时候使用的method是message/send,这个就是期望天气Agent一次性返回所有的结果。而这里面所使用的这个message/stream就代表调度Agent希望得到流式的返回,因为机票Agent在它的Agent Card里面写明了它是支持流式返回的,所以调度Agent就优先请求流式结果了。

• 第二、请求剩余的内容就跟之前聊的差不多了,比如说这里面就是调度Agent要问机票Agent的问题。

data: {"id":"0a96020b-5876-4011-94d4-b8fc4743b911","jsonrpc":"2.0","result":{"contextId":"b5cdbc7f-504e-4174-a543-ba8aef6e85a9","history":[{"contextId":"b5cdbc7f-504e-4174-a543-ba8aef6e85a9","kind":"message","messageId":"9a022c61-4948-4731-b0b0-2671c66b0b43","parts":[{"kind":"text","text":"请帮我查询2025年5月1日从西雅图飞往纽约的航班有哪些?"}],"role":"user","taskId":"41469774-41a2-4b33-b0fb-1f12f6bbef6e"}],"id":"41469774-41a2-4b33-b0fb-1f12f6bbef6e","kind":"task","status":{"state":"submitted"}}} data: {"id":"0a96020b-5876-4011-94d4-b8fc4743b911","jsonrpc":"2.0","result":{"append":false,"artifact":{"artifactId":"6b457be7-8c24-4ca2-879d-cd03fe3528a1","parts":[{"kind":"text","text":"你要查询的机票"}]},"contextId":"b5cdbc7f-504e-4174-a543-ba8aef6e85a9","kind":"artifact-update","lastChunk":false,"taskId":"41469774-41a2-4b33-b0fb-1f12f6bbef6e"}} data: {"id":"0a96020b-5876-4011-94d4-b8fc4743b911","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"6b457be7-8c24-4ca2-879d-cd03fe3528a1","parts":[{"kind":"text","text":"如下:"}]},"contextId":"b5cdbc7f-504e-4174-a543-ba8aef6e85a9","kind":"artifact-update","lastChunk":false,"taskId":"41469774-41a2-4b33-b0fb-1f12f6bbef6e"}} data: {"id":"0a96020b-5876-4011-94d4-b8fc4743b911","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"6b457be7-8c24-4ca2-879d-cd03fe3528a1","parts":[{"kind":"text","text":"1. 航班号 FAKE-001,起飞时间 20:00,余票 30 张;2. 航班号 FAKE-002,起飞时间 23:00,余票 50 张"}]},"contextId":"b5cdbc7f-504e-4174-a543-ba8aef6e85a9","kind":"artifact-update","lastChunk":true,"taskId":"41469774-41a2-4b33-b0fb-1f12f6bbef6e"}} data: {"id":"0a96020b-5876-4011-94d4-b8fc4743b911","jsonrpc":"2.0","result":{"contextId":"b5cdbc7f-504e-4174-a543-ba8aef6e85a9","final":true,"kind":"status-update","status":{"state":"completed"},"taskId":"41469774-41a2-4b33-b0fb-1f12f6bbef6e"}}


下面来看一下机票Agent的返回,首先来看一下第一个,我来给大家复制粘贴,然后再格式化一下,格式化之后是这个样子。这个结构体的大部分属性在上个文章里面都已经聊过了,这里重点看一下status这个属性,它的state值被设成了submitted,意思就是说机票Agent接收到了调度Agent的请求了,它已经在内部提交了任务。如果任务状态有更新,那么机票Agent后面还会接着返回。
没错,第一个返回要表达的意思基本上就是这样了,它还没有回答调度Agent的问题,这里只是创建了一个任务,并且把任务的状态设置为了已提交。

• 然后再看一下第二个,第二个返回开始有了些实质性的内容。首先是机票Agent产出了一段文本内容,就是“你要查询的机票”,这里面的kind设置成为了artifact-update,artifact就是这一部分的内容了,它代表机票Agent的产物。我们的产物主要是文本,kind设置成为artifact-update,意思就是说当前的这个消息的功能是提供artifact的一部分内容,后面还会接到更多kind为artifact-update的消息,把这些消息连在一起才能够拿到一个完整的artifact。lastChunk的值设为了false,代表后面还会有更多kind为artifact-update的消息。

• 第二个返回到这里其实也就差不多了,其实也就是携带了这样的一部分的文本。

• 再看第三个返回,第三个返回跟第二个返回很像,它的文本换成了“如下:”,跟第二个返回的文本连起来就是“你要查询的机票如下:”,kind是artifact-update,lastChunk的值是false,这两个跟第二个返回也是一样的,说明后面还有别的内容。

• 再看第四个返回,第四个返回又追加了一些文本,也就是这里面的内容,这些就是具体的航班信息了,这里写的航班号是FAKE-001和FAKE-002,可以看出都是假的,因为这些数据都是机票Agent造的,并不是真的。为了简化链路,机票Agent的内部并没有真的去请求航空公司的API,它返回的都是固定文案,这个大家不必在意,只需要保证A2A协议的链路是真的就可以了。
另外lastChunk变成了true,代表artifact的所有信息都已经返回了,把前面第二个、第三个和这一个的文本连在一起就可以得到完整的文本内容了。不过大家注意,lastChunk为true只是代表了artifact的所有信息都返回完毕,这并不代表整个流式都结束了,后面还有一个消息,也就是这第五个消息,还需要第五个消息来更新整个任务的状态。


下面就一起来看一下第五个消息的具体内容,同样我给大家格式化一下,可以看到第五个返回的kind所对应的值是status-update,代表当前的这个消息的功能是更新任务状态,status里面的state值为completed,代表任务的状态已完成,到这里整个返回才算真正的结束了。

所以总结一下,机票Agent一共是返回了五条消息,其中第一条和最后一条是用来更新任务状态的,第一条用于把任务状态更新为已提交,最后一条用于把任务状态更新为已完成。中间的三个消息则是各自携带了artifact的一部分信息,先提交任务,再更新artifact,最后再完成任务,这个就是A2A协议里面流式返回的一般模式了。
之前聊的都是两个Agent交互的场景,这个代表性不够高,再来看看三个Agent之间是如何沟通的。还是照例先把平台和调度Agent启动开,它们都在demo/ui这个文件夹下面,按照之前的那种启动方法启动它们就可以了。
然后再把机票Agent也给启动开,除了这两个程序之外还需要再启动一个Agent,就是天气Agent了,这个Agent在之前文章里面提到过,把它启动开。
这些都启动完毕之后再打开Wireshark,选择这里面的loopback,然后填入筛选条件,这个筛选条件的意思是要抓取HTTP请求,并且目标端口是10000或者是10001,这两个端口就分别对应了天气Agent和机票Agent。


然后来到平台这里注册这两个Agents,首先是天气Agent,它的端口是10000,然后再注册机票Agent,它的端口是10001,都注册好了之后来问一个复杂点的问题,让这三个Agent都行动起来。
我们的问题是:我计划在5月1日至3日之间从西雅图飞往纽约,想选择出发当天阳光明媚的日子,请帮我查看这三天西雅图的天气,选择天气最好的那一天,并提供该日的机票信息。


这个问题是需要先调用天气Agent查询西雅图的天气,然后再调用机票Agent查询机票相关的信息。可以看出平台给出了答案,它从这三天里面挑选了5月1日,应该是因为天气Agent告诉它这天天气很好,然后它查询了5月1日这天的机票信息,并且在这里面都显示了出来。


来到Wireshark这里面大体看一下,可以看到一共是拿到了六个请求,其中前两个请求的是天气Agent的Agent Card,这两个请求其实都一样,随便打开一个来看一下,这个就是天气Agent的相关的信息了,中间的这两个则是请求了机票Agent的Agent Card,同样它们两个也是基本类似,所以跳看一下,这个就是机票Agent的相关信息了。


然后后面两个请求则分别是发往了天气Agent和机票Agent,一个一个看一下。

• 首先看看发往天气Agent的这个请求是什么样子的,这个是给天气Agent的问题“请提供5月1日至3日的西雅图天气预报”。可以看出这个问题与原始问题是不一样的,这个问题是调度Agent整理出来用于问天气Agent的,用户问题中的其他部分,比如说与机票相关的内容其实与天气Agent并没有什么关系,所以不必包含在天气Agent的请求里面。

• 再来看天气Agent的返回,天气Agent给我们的返回如下:“你要查询的天气信息是这样子的,5月1日晴天,5月2日小雨,5月3日大雨”,这就是为什么后面调度Agent会选择5月1日作为出发日期的一个原因,因为5月1日是这三天里面唯一一天是晴天的。


这个就是天气Agent的请求和返回了,再来看一下机票Agent的请求,调度Agent发往机票Agent的问题是“请提供5月1日从西雅图飞往纽约的航班信息”,因为之前天气Agent给出了5月1日是一个晴天,所以它这里面只查询了5月1日的机票信息。


然后机票Agent就按照刚才讨论的这五个消息的内容返回了结果,这个返回结果之前讨论过了,所以我这里也不再赘述了。

• 接收到了天气Agent和机票Agent的返回之后,调度Agent就根据这两个返回整理出了一份答案发给了平台,然后平台返回给了用户,整个流程到此也就结束了。
给这里面的问答部分画个流程图,整个流程图里面一共有五个角色,分别是用户、平台、调度Agent、天气Agent和机票Agent。

• 首先用户向平台发送了一个问题,这个问题简单来说就是查天气选机票,平台把问题转发给了调度Agent,调度Agent发现需要分别调用天气Agent和机票Agent才能够解决用户的问题,因此它就先问了天气Agent一个问题“获取5月1日到5月3日这三天的天气预报”,然后它从结果中获知5月1日天气比较好,那就选5月1日出发了。

• 后面查机票的时候也只查5月1日的就好了,所以紧接着它就问了机票Agent一个问题“获取5月1日那天的机票信息”,机票Agent的返回是流式的,一共是返回了五个消息,在机票Agent的五个消息都返回完毕之后,调度Agent就根据天气Agent和机票Agent的返回整理了一份答案发给了平台,平台又发给了用户,整个流程就到此结束了。
有些小伙伴可能会问,如果Agent的数量不止三个,有十个、二十个,这怎么处理?一样的,首先必须要有一个调度Agent,它负责寻找并下发任务给其他的Agents,并在最后根据其他Agent的回复总结答案发回给用户。所以只要Agent的数量不是特别夸张,这个方案就是可用的。

http://www.gsyq.cn/news/1457730.html

相关文章:

  • 把ESP32-CAM变成智能门铃:低成本实现局域网视频监控与人脸识别告警
  • 25级数应四班第六次实验
  • 从蓝牙到Wi-Fi:拆解FSK、PSK、QAM在常见物联网协议中的真实应用
  • 2026年靠谱的国产编码器/上海角度编码器/光电编码器/上海增量编码器公司对比推荐 - 行业平台推荐
  • AI工具如何真正驱动智能运营?揭秘头部企业已验证的7步整合方法论与数据看板搭建公式
  • 海德汉PWM21实战:手把手教你用它搞定伺服电机相位角校准(附西门子/力士乐案例)
  • 从MAX14920到LTC6804:两种AFE断线自检方案(电流源法 vs. 电阻分压法)的实战对比与选型建议
  • OpenCV findCirclesGrid实战:手把手教你搞定相机标定用的圆点棋盘(附参数调优心得)
  • NCWIT抱负奖与高校奖学金联动:如何系统培养女性计算机人才
  • 【Cursor】调整 Cursor 背景颜色
  • 从协议到代码:手把手实现一个简化的PLMN选网状态机(基于23.122 R9)
  • 别只盯着网络图了!深度解读VOSviewer三大视图(网络/覆盖/密度)的隐藏信息与实战选择
  • 2026年可靠的3PE防腐保温管/防腐螺旋钢管/3PE螺旋钢管深度厂家推荐 - 品牌宣传支持者
  • 告别系统设置界面:一份给Android App开发者的以太网自动配置指南(含静态IP/动态DHCP)
  • 避开这些坑,你的eCognition ESP2插件才算没白装:从LV图平滑曲线到成功出峰的实战复盘
  • 别让细节拖后腿:Nature Communications投稿中图片、表格与补充材料的‘隐形’要求详解
  • 从DPDK插件到完整协议栈:手把手带你拆解FD.io VPP的模块化设计
  • 6U CompactPCI系统板全套Altium设计文件:原理图、PCB、双格式BOM与线束定义
  • 手把手教你用ATmega4809读取BQ4050电量(附完整代码与波形分析)
  • Coturn服务器配置踩坑实录:从‘stun通了‘到真正高可用,我总结了这5个关键检查点
  • STM32 Bootloader跳转App总进HardFault?一个PSP指针引发的‘血案’与终极修复方案
  • 别再对着型号表发愁了!手把手教你解读DJ系列接插件命名规则(附AMP对照表)
  • 【Agent智能体18 | 构建AI工作流的技巧-评估】
  • MyBatis动态SQL中Integer=0被当成空字符串?一个条件判断引发的“血案”与避坑指南
  • HLA靶向效率:免疫系统如何进化出攻击病毒要害的智慧策略
  • DC NXT物理综合深度优化:如何利用SPG Flow与compile_ultra榨干芯片性能
  • Mojo 语言发布 1.0 版本:像 Python 编写、C++ 运行,还借鉴 Rust 理念!
  • 从一次线上HTTPS握手失败说起:深入理解JDK8的JCE加密限制与‘无限制’策略的来龙去脉
  • 从PEM到JKS:一份搞定K8s中Java应用(如Hadoop)HTTPS证书转换与配置的保姆级脚本
  • 从图像处理到量子计算:正交矩阵、酉矩阵这些‘特殊矩阵’到底有什么用?