学习来源:A2A Protocol 官方文档 Life of a Task
建立总图
客户端向 Agent 发送一条消息后,Agent 通常有两种响应方式:
- 返回一个无状态的
Message - 创建一个有状态的
Task
如果是 Message,说明这次交互可以立即完成,不需要后续追踪。比如简单确认、能力协商、范围说明、轻量回答,都可以只用 Message。
如果是 Task,说明 Agent 接下来要处理一段可跟踪的工作。这个任务可能需要一段时间,可能会不断更新状态,可能会请求用户补充输入,也可能会产出文件、结构化数据、图片等 artifact。
可以把它理解成:
|
|
A2A 交互方式有三种:请求/响应(轮询)、SSE、服务端主动推送
contextId:把相关交互放进同一个上下文
contextId 是理解 Task 生命周期的第一把钥匙。
它不是某一个任务的 ID,而是一个更大的上下文 ID。一个 contextId 可以关联多个 Task,也可以关联多个独立的 Message。当客户端第一次发起交互时,Agent 会返回一个新的 contextId。如果这次交互创建了任务,响应里还会包含 taskId。
后续客户端如果继续围绕同一个目标对话,就应该带上相同的 contextId。这样 Agent 就知道:这不是一个全新的孤立请求,而是在延续之前的上下文。
它的作用可以概括为三点:
- 让多轮交互保持连续性
- 让多个任务围绕同一个目标协作
- 让 Agent 内部可以基于上下文管理对话记忆或 LLM context
举个例子:
|
|
这里第二次请求不是重启对话,而是在同一个上下文里对前一个结果做 refinement。
taskId:定位某一个具体工作单元
如果说 contextId 表示“这一串交互属于同一个上下文”,那么 taskId 表示“这一项具体工作”。
一个上下文里可以有多个任务。每个任务都有自己的输入、状态、产物和历史。客户端在后续消息里可以附带 taskId,表示这条消息和某个具体任务有关。
更常见的做法是使用 referenceTaskIds 来提示 Agent:当前请求和之前哪些任务有关。比如修改一张图片时,客户端可以引用生成原图的 task。
需要注意的是,taskId 不等于会话 ID。不要把所有后续交互都塞回同一个任务里。A2A 的设计更倾向于把每一次明确的新工作、新请求、新 refinement 建成新的任务,再用 contextId 和引用关系把它们串起来。
什么时候返回 Message,什么时候创建 Task
官方文档把这个选择交给 Agent,因为不同 Agent 的能力边界不同。但可以用下面的判断方式来学习。
适合返回 Message 的情况:
- 可以立即完成
- 不需要持续追踪状态
- 不会产生需要版本管理的 artifact
- 更像一次说明、确认、拒绝或澄清
适合创建 Task 的情况:
- 工作可能耗时较长
- 需要客户端查询或订阅进度
- 过程中可能需要用户补充输入
- 会产出 artifact,例如文件、图片、报告、结构化结果
- 后续可能围绕结果继续 refinement
一个实用判断是:如果客户端未来可能会问“这个工作现在到哪一步了?”或者“上一版结果是什么?”,那它大概率应该是 Task。
Task 的生命周期状态
Task 创建之后,会进入一个生命周期。它不会永远停留在“处理中”,它会持续更新,直到进入中断状态或终态。
官方文档里提到的中断状态包括:
input-requiredauth-required
终态包括:
completedcanceledrejectedfailed
中断状态的意思是:任务还没有结束,但 Agent 无法继续,需要外部动作。比如缺少必要参数,就进入 input-required;需要用户授权,就进入 auth-required。
终态的意思是:任务结束了。无论是成功、取消、拒绝还是失败,它都不应该被重新启动。
可以画成一个简化状态图:
|
|
这个生命周期设计的价值是把 Agent 执行过程显式化。客户端不必猜 Agent 在做什么,也不必把长任务伪装成同步请求。
Task 是不可变的:终态之后不要重启
Task 一旦进入终态,就不能重新启动。
这是文档中非常重要但容易被忽略的一点。比如一个图片生成任务已经 completed,用户又说“把船改成红色”。这不是把原任务重新打开,而是在同一个 contextId 下创建一个新的任务。
这样做有几个好处:
- 输入和输出的映射更清楚
- 任务历史更容易审计
- orchestration 更容易做
- 客户端可以稳定引用某个任务的状态和产物
- refinement 不会破坏原始任务结果
这也是 A2A 和很多临时拼接式 Agent API 的不同之处。A2A 更像是在定义一个可追踪的工作账本:每一个明确的工作单元都应该留下自己的记录。
Agent Card
Agent Card 用于描述 agent 身份、能力(SSE、推送、轮询)、请求端点、技能及认证要求的 JSON 元数据文档。 帮助客户端发现agent,并了解如何安全有效地使用。
Agent 注册发现策略
通过 Well-Known URI
适用于公共代理或在特定领域内。
A2A 服务器通过在其域名中托管一个标准化且知名的 URI 来使其代理卡可被发现。标准路径是 https://{agent-server-domain}/.well-known/agent-card.json。发现流程:
-
client agent 发现潜在 A2A 服务器的域名(例如smart-thermostat.example.com)
-
client agent 对 https://smart-thermostat.example.com/.well-known/agent-card.json 执行 HTTP GET 请求
-
如果 Agent Card 存在且可访问,服务器会以 JSON 响应的形式返回。
基于Catalog的发现
适用于企业环境或公共市场,Agent Card通常由中央注册局管理。策划的注册库作为一个中央仓库,允许客户端根据“技能”或“标签”等标准查询和发现Agent。
-
A2A 服务器会将其Agent Card发布到注册表。
-
client agent 查询注册库的 API,并根据“特定技能”等条件进行搜索。
-
注册处返回匹配的Agent Card或参考。
直接配置 / 私有部署
适用于紧耦合系统、私有代理或开发用途,客户端直接配置代理卡信息或 URL。
Artifact:任务产物不是任务本身
Task 可以产生 artifacts。artifact 是任务的输出结果,可以是文件、图片、结构化数据等。
在官方例子里,Agent 先生成一张帆船图片,任务完成后返回一个 artifact,例如:
|
|
之后客户端要求“把帆船改成红色”。Agent 不会修改原 task,而是创建一个新 task,并产生新的 artifact:
|
|
注意这里 artifact 的名字可以保持一致,但 artifactId 应该不同。这意味着它是同一个逻辑产物的不同版本。
文档建议 serving agent 在生成 refinement 结果时使用一致的 artifact name。这样客户端更容易把不同版本识别为同一类产物。
Artifact mutation 应该由客户端追踪
官方文档明确指出:artifact 的变更链路不应该由 serving agent 负责维护,也不是 A2A 协议规范的一部分。
这点很有意思。因为直觉上我们可能会想:既然 Agent 生成了新 artifact,那 Agent 应该知道哪个 artifact 是哪个 artifact 的新版。但 A2A 的设计选择是把这个责任交给客户端。
原因是客户端更适合判断:
- 哪个结果被用户接受
- 哪个版本是当前有效版本
- 哪个 artifact 应该被继续 refinement
- 哪些版本应该展示给用户或隐藏起来
所以客户端需要维护自己的版本关系。例如:
|
|
如果客户端再次请求 refinement,最好显式引用自己认为“最新且可接受”的 artifact。不要假设 Agent 一定能猜对。
refinement 的正确建模方式
refinement 指的是基于已有任务结果继续提出修改、补充、扩展或重新生成。
在 A2A 里,refinement 的推荐模式是:
- 使用相同的
contextId - 创建新的
Task - 使用
referenceTaskIds引用相关旧任务 - 如果要修改特定 artifact,尽量在 part metadata 中带上
artifactId和taskId - 新任务生成新的 artifact
- 客户端维护 artifact 版本链
如果客户端没有明确指出要修改哪个 artifact,Agent 可以尝试根据 contextId 推断。但如果上下文里有多个候选 artifact,Agent 不应该强行猜测,而应该返回 input-required,要求客户端澄清。
这是一个很好的协议设计细节:不确定时,把不确定性显式暴露出来,而不是默默做一个可能错误的决定。
客户端实现时要承担的责任
从客户端视角看,Life of a Task 不只是理解字段,还要设计好本地状态管理。
客户端至少应该记录:
- 当前
contextId - 当前上下文下的所有
taskId - 每个任务的状态
- 每个任务的 artifacts
- artifact 的版本链
- 用户接受的是哪个 artifact 版本
- 哪些任务处于
input-required或auth-required
客户端在发起后续请求时,应该尽量带上:
contextId- 相关的
referenceTaskIds - 如果 refinement 针对某个产物,带上 artifact 引用
- 用户补充输入或授权结果
一个客户端的数据模型可以粗略设计为:
|
|
A2A 不强制你这么建模,但如果要做一个可靠的 Agent 客户端,这些结构迟早会出现。
Agent 实现时要承担的责任
从 Agent 服务端视角看,关键不是把所有请求都变成任务,而是正确判断交互复杂度。
Agent 应该做到:
- 对简单交互返回
Message - 对可追踪工作创建
Task - 为首次交互生成
contextId - 为新任务生成
taskId - 在任务推进时更新状态
- 需要输入时进入
input-required - 需要授权时进入
auth-required - 任务结束后进入明确终态
- 不重启已经终态的任务
- 对 refinement 创建新任务
- 生成 refinement artifact 时尽量保持 artifact name 一致
如果 Agent 使用 LLM,那么 contextId 可以帮助它管理内部上下文。但这不意味着 LLM 的完整上下文一定要暴露给客户端。contextId 是协议层的连续性标识,LLM memory 是服务端内部实现细节。
常见误区
误区一:把 contextId 当成 taskId
contextId 是上下文,taskId 是任务。一个上下文可以有多个任务。
误区二:把 refinement 写回旧任务
终态任务不可重启,refinement 应该创建新任务。
误区三:让 Agent 负责 artifact 版本树
A2A 建议客户端维护 artifact mutation。Agent 可以帮助保持命名一致,但不负责判断哪个版本是用户认可的最新版本。
误区四:上下文不清时让 Agent 猜
如果多个 artifact 都可能是目标,正确做法是进入 input-required,请求客户端明确。
误区五:所有请求都建成 Task
简单、立即、自包含的交互用 Message 更自然。过度任务化会让系统变复杂。
用一个例子串起来
假设用户要用 Agent 生成一份产品海报。
第一轮:
|
|
第二轮:
|
|
第三轮:
|
|
在这个过程中:
ctx-001把所有交互放在同一个上下文里task-001、task-002、task-003是三个独立工作单元art-001、art-002、art-003是不同产物或版本- 客户端负责知道用户当前接受的是
art-002还是art-003