前言
学习 UP 主 马克的技术工作坊 的 A2A协议深度解析 - 第 2 部分:流式返回 + 多Agent场景 视频,了解下 A2A 协议是如何处理流式结果返回的,记录下个人学习笔记,仅供自己参考😄
video:A2A协议深度解析 - 第 2 部分:流式返回 + 多Agent场景
code:https://github.com/MarkTechStation/VideoCode
1. 简述
这节我们来继续聊聊 Google 推出的 A2A 协议,也就是 Agent to Agent 的协议,这个协议用于让多个 Agent 互相沟通交流完成一项复杂的任务,在上篇文章中,我们讲述了 A2A 协议的基本使用场景,还通过两个 Agent 的同步调用梳理了协议的核心链路
那这篇文章我们来看看两个更进阶的内容,A2A 协议是如何处理流式返回的,更多数量的 Agent 协作流程,我们之前的链路都只涉及两个 Agent,不够有代表性,因此我们把 Agent 的数量扩大来看看三个甚至更多的 Agent 是如何协作的
OK,下面我们直接开始
2. 什么是流式返回
在正式开始之前,我们还是来简单解释一下流式返回是什么
一般情况下我们使用 http 访问一个网站的时候,我们的浏览器会发送给目标服务器一个请求,比如说要访问一个 HTML 页面,一张图片,一个 JS 脚本之类的
目标服务器会返回对应的结果,一去一回一次交互就完成了,这种交互方式有个缺陷,它处理不了服务器连续发回多次响应的情况,比如说我们经常用的大模型聊天页面,模型的结果都是几个字几个字的返回,只是一去一回的话显然无法做到这种效果
所以目前主流的大模型聊天页面用的都是流式返回,它的特点是浏览器只需要请求一次,服务器接收到请求之后会连续发送多次响应,每次响应的内容都是几个字,而浏览器接收到几个字就显示几个字,这样用户就可以及时接收到模型的返回,出来几个字就看几个字,体验就会好很多
等到所有的结果都返回完毕之后,服务器会发送一个完成的标识,浏览器接收到标识之后关闭连接,页面显示模型回答完毕,整个流程也就结束了
3. 抓包分析 A2A 的流式响应规范
那了解了流式返回的概念之后,我们就来看一看 A2A 是如何定义流式返回协议的,这次我们用一个查询机票信息的场景举例:
整个流程里有三个角色,分别是用户、调度 Agent 和机票 Agent,用户询问 2025 年 7 月 6 日从西雅图飞往纽约的航班中,还有哪些航班有余票?
用户的问题会首先发往调度 Agent,调度 Agent 会转发给机票 Agent,机票 Agent 返回航班信息,调度 Agent 接到这个消息之后稍作加工就返回给用户,这样用户也就得到了最终答案,这个就是整个流程
可以看出这里面涉及到两个 Agents,我们下面的任务就是去启动这两个 Agent 然后跑通整个流程。首先是调度 Agent,这个 Agent 写在 a2a-sample 仓库里面和平台放在一起,都是在 demo/ui 这个文件夹里面,在上篇文章中我们已经演示过启动方法了,所有这里就不再赘述了
可以看出它开放的接口是 12000,我们来直接打开这个地址:
没问题,到这里平台和调度 Agent 就创建完成了,下面我们来启动机票 Agent
这个机票 Agent 是 UP 自己写的,放在 https://github.com/MarkTechStation/VideoCode 这个 GitHub 仓库下面,我们进入到这个 GitHub 仓库里面,然后进入到 A2A协议深度解析(2) 目录中,打开 flight 文件夹,然后再执行 uv run .
这个命令就可以启动这个机票 Agent 了,如下图所示:
这样的话我们两个 Agent 都启动完毕了
我们下面打开 WireShark 抓包软件,选择 loopback 也就是说我们只抓本地的网络包,然后呢我们设定过滤条件为 http and tcp.dstport == 10001
为什么是 10001 呢,因为我们刚才开放的这个机票 Agent 的端口就是 10001,所以呢我们就想抓发往这个机票 Agent 的各种网络包,然后到这里 WireShark 的操作也就结束了
我们再回到平台这里,注册一下我们的机票 Agent,
点击 save 保存,OK,这样机票 Agent 就注册成功了
然后我们新建一个对话输入问题:2025 年 7 月 6 日从西雅图飞往纽约的航班中,还有哪些航班有余票?可以看出平台直接给出了答案:
大家可能会在这个时候产生一些疑惑,因为好像这个结果也没有流式返回啊,我们讲的不是流式的这个场景吗,是不是搞错了呢
其实没有搞错,机票 Agent 的结果确实是流式返回的,只不过调度 Agent 会等这个流式返回的所有结果都返回回来之后才整理出一份答案,然后呢再给到我们,所有呢从我们用户的角度来看,好像这个过程并没有涉及到流式返回什么事情
但是机票 Agent 的返回确实是流式的,由于平台没有流式显示机票 Agent 的返回结果,所有呢你才有了整个过程并非没有流式进行的错觉,不信的话,我们就来看一下我们抓的网络包验证一下我们的想法
再回到 WireShark 这里,可以看到平台一共是向机票 Agent 发送了三个请求,其中前两个是用来请求 Agent Card 的,这个呢是在注册 Agent 的时候发送的,这两个请求本质上都是一样的,我们随便点开一个来看一下:
我们来格式化一下,内容如下:
{
"capabilities": {
"streaming": true
},
"defaultInputModes": [
"text"
],
"defaultOutputModes": [
"text"
],
"description": "提供机票查询和预订功能",
"name": "机票 Agent",
"skills": [
{
"description": "给定时间,查询对应的机票信息",
"examples": [
"给我查询5月1日的机票信息"
],
"id": "查询机票信息",
"name": "查询机票信息",
"tags": [
"查询",
"机票"
]
},
{
"description": "预定机票",
"examples": [
"给我预定5月1日从纽约飞往旧金山的机票"
],
"id": "预定机票",
"name": "预定机票",
"tags": [
"预定",
"机票"
]
}
],
"url": "http://127.0.0.1:10001",
"version": "1.0.0"
}
Agent Card 的结构我们在上篇文章中已经聊过了,这里只重点强调其中的两个地方,第一机票 Agent 有个 skills
可以用来查询机票信息,所以当我们问机票相关的问题的时候调度 Agent 就可以去调用这个机票 Agent 拿到相关的答案
第二点是这个机票 Agent 是支持流式返回的,对应的就是上面的 streaming
值为 true,所以你后面就可以看到调度 Agent 在请求机票 Agent 的时候会要求它流式返回结果,那 Agent Card 就看到这里
我们再来看看发往机票 Agent 的问题,也就是 WireShark 里面的第三个请求:
我们来看一下,这个请求是我们问完问题之后调度 Agent 发给机票 Agent 的,整个界面分为两个部分,上面红色的是调度 Agent 的请求,下面紫色的是机票 Agent 的返回
可以看出机票 Agent 的返回有多个,每一个都包含一部分信息,这也就是流式返回了,一次返回一部分,我们来一点点看,首先我们来看一下它的请求是什么样子的,我们来格式化下:
{
"id": "f552f085-252b-4862-b7a8-34a6c7a58944",
"jsonrpc": "2.0",
"method": "message/stream",
"params": {
"configuration": {
"acceptedOutputModes": [
"text",
"text/plain",
"image/png"
]
},
"message": {
"contextId": "08cb5d0e-6cce-419a-a460-97dd8b1c4a16",
"kind": "message",
"messageId": "b8954814-48ab-448e-b37a-2a5440552b4c",
"parts": [
{
"kind": "text",
"text": "2025年7月6日从西雅图飞往纽约的航班中,还有哪些航班有余票?"
}
],
"role": "user"
}
}
}
请求的结构我们在上篇文章中也提到过,这里只重点强调其中的几个部分,第一个要强调的就是这里面的 method
,请求中的 method 设置的 message/stream,这个跟我们上篇文章中提到的是不一样的,在上篇文章中调度 Agent 请求天气 Agent 的时候使用的 method 是 message/send,这个就是期望天气 Agent 一次性返回所有的结果
而这里面所使用的这个 message/stream 就代表调度 Agent 希望得到流式的返回,因为机票 Agent 在它的 Agent Card 里面写明了它是支持流式返回的,所有调度 Agent 就优先请求流式结果了
然后这个请求剩余的内容就跟我们之前聊的差不多了,比如说 text
里面就是调度 Agent 要问机票 Agent 的问题,那请求我们就看到这里,下面我们来看一下机票 Agent 的返回,首先我们来看下第一个:
{
"id": "f552f085-252b-4862-b7a8-34a6c7a58944",
"jsonrpc": "2.0",
"result": {
"contextId": "08cb5d0e-6cce-419a-a460-97dd8b1c4a16",
"history": [
{
"contextId": "08cb5d0e-6cce-419a-a460-97dd8b1c4a16",
"kind": "message",
"messageId": "b8954814-48ab-448e-b37a-2a5440552b4c",
"parts": [
{
"kind": "text",
"text": "2025年7月6日从西雅图飞往纽约的航班中,还有哪些航班有余票?"
}
],
"role": "user",
"taskId": "7cc113de-c09d-4857-bd2f-e81ee3b19424"
}
],
"id": "7cc113de-c09d-4857-bd2f-e81ee3b19424",
"kind": "task",
"status": {
"state": "submitted"
}
}
}
这个结构体的大部分属性我们在上篇文章里面都已经聊过了,这里我们重点看一下 status
这个属性,它的 state 值被设成了 submitted,意思就是说机票 Agent 接收到了调度 Agent 的请求了,它已经在内部提交了任务,如果任务状态有更新的话,那么机票 Agent 的后面还会接着返回,没错,第一个返回要表达的意思呢基本上就只是这样了
它还没有回答调度 Agent 的问题,这里只是创建了一个任务并且把任务的状态设置为了已提交,然后我们再看一下第二个:
{
"id": "f552f085-252b-4862-b7a8-34a6c7a58944",
"jsonrpc": "2.0",
"result": {
"append": false,
"artifact": {
"artifactId": "51235694-f1eb-47ca-a7b3-848c14afefd5",
"parts": [
{
"kind": "text",
"text": "你要查询的机票"
}
]
},
"contextId": "08cb5d0e-6cce-419a-a460-97dd8b1c4a16",
"kind": "artifact-update",
"lastChunk": false,
"taskId": "7cc113de-c09d-4857-bd2f-e81ee3b19424"
}
}
第二个返回开始有了些实质性的内容,首先是机票 Agent 产出了一段文本内容就是:你要查询的机票,这里面的 kind
设置成了 artifact-update,artifact 代表机票 Agent 的产物,我们的产物主要是文本,kind 设置成了 artifact-update,意思就是说当前的这个消息的功能是提供 artifact 的一部分内容
我们后面还会接到更多 kind 为 artifact-update 的消息,把这些消息连在一起才能够拿到一个完整的 artifact,lastChunk
的值设为了 false,代表后面还会有更多 kind 为 artifact-update 的消息,第二个返回到这里其实也就差不多了,其实也就是携带了这样的一部分的文本
我们再看第三个返回:
{
"id": "f552f085-252b-4862-b7a8-34a6c7a58944",
"jsonrpc": "2.0",
"result": {
"append": true,
"artifact": {
"artifactId": "51235694-f1eb-47ca-a7b3-848c14afefd5",
"parts": [
{
"kind": "text",
"text": "如下:"
}
]
},
"contextId": "08cb5d0e-6cce-419a-a460-97dd8b1c4a16",
"kind": "artifact-update",
"lastChunk": false,
"taskId": "7cc113de-c09d-4857-bd2f-e81ee3b19424"
}
}
第三个返回跟第二个返回很像,它的文本换成了上述内容,跟第二个返回的文本连起来就是:你要查询的机票如下:,kind
是 artifact-update,lastChunk
的值是 false,这两个跟第二个返回也是一样的,说明后面还有别的内容
那我们再来看第四个返回:
{
"id": "f552f085-252b-4862-b7a8-34a6c7a58944",
"jsonrpc": "2.0",
"result": {
"append": true,
"artifact": {
"artifactId": "51235694-f1eb-47ca-a7b3-848c14afefd5",
"parts": [
{
"kind": "text",
"text": "1. 航班号 FAKE-001,起飞时间 20:00,余票 30 张;2. 航班号 FAKE-002,起飞时间 23:00,余票 50 张"
}
]
},
"contextId": "08cb5d0e-6cce-419a-a460-97dd8b1c4a16",
"kind": "artifact-update",
"lastChunk": true,
"taskId": "7cc113de-c09d-4857-bd2f-e81ee3b19424"
}
}
第四个返回又追加了一些文本,也就是上面的内容,其中包含具体的航班信息,这里写的航班号是 FAKE-001 和 FAKE-002,可以看出都是假的,因为这些数据都是机票 Agent 造的并不是真的
为了简化链路,机票 Agent 的内部并没有真的去请求航空公司的 API,它返回的都是固定文案,这个大家不必在意,我们只需要保证 A2A 协议的链路是真的就可以了
另外 lastChunk
变成了 true,代表 artifact 的所有信息都已经返回了,把前面第二个第三个和这一个的文本连在一起就可以得到完整的文本内容了。不过大家需要注意 lastChunk 为 true 只是代表了 artifact 的所有消息都返回完成了,这并不代表整个流式都结束了
大家可以看到后面还有一个消息,也就是第五个消息,具体内容如下:
{
"id": "f552f085-252b-4862-b7a8-34a6c7a58944",
"jsonrpc": "2.0",
"result": {
"contextId": "08cb5d0e-6cce-419a-a460-97dd8b1c4a16",
"final": true,
"kind": "status-update",
"status": {
"state": "completed"
},
"taskId": "7cc113de-c09d-4857-bd2f-e81ee3b19424"
}
}
可以看到第五个返回的 kind
所对应的值是 status-update,代表当前的这个消息的功能是更新任务状态,status
里面的 state 值为 completed 代表任务状态已完成,到这里,整个返回才算是真的结束了
所以总结一下机票 Agent 一共是返回了五条信息,其中第一条和最后一条也就是图示的蓝色部分是用来更新任务状态的,第一条用于把任务状态更新为已提交,最后一条用于把任务状态更新为已完成,中间的三个消息则是携带了 artifact 的一部分信息,先提交任务,再更新 artifact,最后再完成任务
这个就是 A2A 协议里面流式返回的一般模式了
4. 更多数量的 Agent 协作流程
我们之前聊的都是两个 Agent 的交互场景,这个代表性不够高,我们再来看看三个 Agent 之间是如何沟通的,还是照例我们先把平台和调度 Agent 的启动开,它们都在 demo/ui 这个文件夹下面,我们按照之前的那种启动方法启动它们就可以了
然后我们再把这个机票 Agent 也给启动开,除了这两个程序之外呢,我们还需要再启动一个 Agent,那就是天气 Agent 了,这个 Agent 我们在上篇文章里面提到过,我们把它也启动开
那这些都启动完毕之后我们再打开 WireShark 选择这里面的 loopback,然后填入我们的筛选条件 http and (tcp.dstport==10000 or tcp.dstport==10001)
:
这个筛选条件的意思是我们要抓取 http 请求,并且目标端口是 10000 或者 10001,这两个端口就分别对应了天气 Agent 和机票 Agent,然后我们来到平台这里注册这两个 Agent,首先是天气 Agent,它的端口呢是 10000,然后再注册机票 Agent,它的端口是 10001
都注册好了之后,我们来问一个复杂点的问题,让这三个 Agent 都行动起来,我们的问题是:我计划在 7 月 6 日至 8 日之间从西雅图飞往纽约,想选择出发当天阳光明媚的日志,请帮我查看这三天西雅图的天气,选出天气最好的那一天,并提供该日的机票信息
这个问题是需要先调用天气 Agent 查询西雅图的天气,然后呢再调用机票 Agent 查询机票相关的信息,可以看出平台给出了答案,它从这三天里面天选了 7 月 6 日,应该是因为天气 Agent 告诉它这天天气很好,然后它查询了 7 月 6 日这天的机票信息,并且在这里面都显示出来了
我们来到 WireShark 这里大体看一下:
Note:理论上只有六个请求才对,博主这里多了一个天气 Agent 的请求,可能是调度 Agent 在请求时出了一些问题
可以看到我们一共是拿到了七个请求,其中前两个请求的是天气 Agent 的 Agent Card,这两个请求其实都一样,我们随便打开一个来看一下:
这个就是天气 Agent 的相关的信息了
中间的这两个则是请求了机票 Agent 的 Agent Card,同样它们两个也是基本类似,所以我们挑一个看一下:
这个呢就是机票 Agent 的相关信息了,然后后面两个请求呢则分别是发往了天气 Agent 和机票 Agent,我们来一个一个看一下,首先看看发往天气 Agent 的这个请求是什么样子的:
给天气 Agent 的问题是:请提供 7 月 6 日至 8 日的西雅图天气预报,可以看出这个问题与我们的原始问题是不一样的,这个问题是调度 Agent 整理出来用于问天气 Agent 的
用户问题中的其他部分比如说与机票相关的内容其实与天气 Agent 并没有什么关系,所以呢不必包含在天气 Agent 的请求里面,那再来看看天气 Agent 的返回,如下所示:
{
"id": "eb50e9d9-6dc6-4319-bb96-8852816c6827",
"jsonrpc": "2.0",
"result": {
"artifacts": [
{
"artifactId": "9335111f-2ccd-42fb-85e4-1fec45ae4474",
"description": "",
"name": "天气查询结果",
"parts": [
{
"kind": "text",
"text": "您要查询的天气信息如下:5 月 1 日:晴天;5 月 2 日:小雨;5 月 3 日:大雨。"
}
]
}
],
"contextId": "d0e63dc4-4735-475b-8f26-d0a0dd5ec7a0",
"history": [
{
"contextId": "d0e63dc4-4735-475b-8f26-d0a0dd5ec7a0",
"kind": "message",
"messageId": "0503f276-e71f-475b-8542-4c467521f731",
"parts": [
{
"kind": "text",
"text": "请提供 7 月 6 日至 8 日西雅图的天气预报"
}
],
"role": "user",
"taskId": "9d681d48-9541-4bfd-bcad-724fc6c7f453"
}
],
"id": "9d681d48-9541-4bfd-bcad-724fc6c7f453",
"kind": "task",
"status": {
"state": "completed"
}
}
}
Note:由于天气 Agent 返回的是固定内容,所以大家当作提供的是 7 月 6 日至 8 日的天气就行
这就是为什么后面调度 Agent 会选择 7 月 6 日作为出发日期的一个原因,因为 7 月 6 日是这三天里面唯一一天是晴天的,这个就是天气 Agent 的请求和返回了
我们再来看一下机票 Agent 的请求,调度 Agent 发往机票 Agent 的问题是:请帮我查询 7 月 6 日从西雅图飞往纽约的机票信息,因为之前天气 Agent 给出了 7 月 6 日是一个晴天,所以它这里只查询了 7 月 6 日的机票信息,然后我们的机票 Agent 就按照我们刚才讨论的这五个消息的内容返回了结果,这个返回结果我们之前讨论过了,所以这里就不再赘述了
接收到了天气 Agent 和机票 Agent 的返回之后调度 Agent 就根据这两个返回整理出了一份答案发回给了平台,然后平台发送给了用户,整个流程到此也就结束了
5. 流程图 (问答部分)
我们来给这里面的问答部分画个流程图,如下图所示:
整个流程图里面一共有五个角色,分别是用户、平台、调度 Agent、天气 Agent 和机票 Agent,首先用户向平台发送了一个问题,这个问题简单来说就是查天气、选机票,平台把问题转发给了调度 Agent,调度 Agent 发现需要分别调用天气 Agent 和机票 Agent 才能够解决用户的问题
因此呢它就先问了天气 Agent 一个问题:获取 7 月 6 日到 8 日这三天的天气预告,然后呢它从结果中获知 7 月 6 日天气比较好,那就选 7 月 6 日出发了,后面查机票的时候也只查 7 月 6 日就好了,所以紧接着它就问了机票 Agent 的一个问题:获取 7 月 6 日那天的机票信息
机票 Agent 的返回是流式的,一共是返回了五个信息,在机票 Agent 的五个消息都返回完毕之后,调度 Agent 就根据天气 Agent 和机票 Agent 的返回整理了一份答案,发给了平台,平台呢又发给了用户,整个流程到此就结束了
那大家可能有所疑惑,如果 Agent 的数量不止三个,如果有几十个该怎么处理呢,其实是一样的,首先必须要有一个调度 Agent,它负责寻找并下发任务给其他的 Agent,并在最后根据其他 Agent 的回复总结答案发回给用户,所以呢只要 Agent 的数量不是特别夸张,这个方案就是可用的
OK,以上就是本篇文章的全部内容了
结语
这篇文章我们学习了 A2A 协议的流式返回,并通过抓包分析了三个 Agent 协作时的流程,其中调度 Agent 负责拆分并下发任务,其它两个 Agent 各自完成自己的查天气、查机票的任务
大家感兴趣的可以自己动手试试,也可以多看看 UP 的视频,非常的不错🤗