a2a-protocol

学习来源:A2A Protocol 官方文档 Life of a Task

建立总图

客户端向 Agent 发送一条消息后,Agent 通常有两种响应方式:

  1. 返回一个无状态的 Message
  2. 创建一个有状态的 Task

如果是 Message,说明这次交互可以立即完成,不需要后续追踪。比如简单确认、能力协商、范围说明、轻量回答,都可以只用 Message。

如果是 Task,说明 Agent 接下来要处理一段可跟踪的工作。这个任务可能需要一段时间,可能会不断更新状态,可能会请求用户补充输入,也可能会产出文件、结构化数据、图片等 artifact。

可以把它理解成:

1
2
Message = 一次性的对话回复
Task    = 可追踪的工作单元

A2A 交互方式有三种:请求/响应(轮询)、SSE、服务端主动推送

contextId:把相关交互放进同一个上下文

contextId 是理解 Task 生命周期的第一把钥匙。

它不是某一个任务的 ID,而是一个更大的上下文 ID。一个 contextId 可以关联多个 Task,也可以关联多个独立的 Message。当客户端第一次发起交互时,Agent 会返回一个新的 contextId。如果这次交互创建了任务,响应里还会包含 taskId。

后续客户端如果继续围绕同一个目标对话,就应该带上相同的 contextId。这样 Agent 就知道:这不是一个全新的孤立请求,而是在延续之前的上下文。

它的作用可以概括为三点:

  1. 让多轮交互保持连续性
  2. 让多个任务围绕同一个目标协作
  3. 让 Agent 内部可以基于上下文管理对话记忆或 LLM context

举个例子:

1
2
3
4
5
用户:生成一张帆船图片
Agent:创建 task-1,contextId = ctx-a

用户:把船改成红色
Agent:创建 task-2,仍然使用 contextId = ctx-a,并引用 task-1

这里第二次请求不是重启对话,而是在同一个上下文里对前一个结果做 refinement。

taskId:定位某一个具体工作单元

如果说 contextId 表示“这一串交互属于同一个上下文”,那么 taskId 表示“这一项具体工作”。

一个上下文里可以有多个任务。每个任务都有自己的输入、状态、产物和历史。客户端在后续消息里可以附带 taskId,表示这条消息和某个具体任务有关。

更常见的做法是使用 referenceTaskIds 来提示 Agent:当前请求和之前哪些任务有关。比如修改一张图片时,客户端可以引用生成原图的 task。

需要注意的是,taskId 不等于会话 ID。不要把所有后续交互都塞回同一个任务里。A2A 的设计更倾向于把每一次明确的新工作、新请求、新 refinement 建成新的任务,再用 contextId 和引用关系把它们串起来。

什么时候返回 Message,什么时候创建 Task

官方文档把这个选择交给 Agent,因为不同 Agent 的能力边界不同。但可以用下面的判断方式来学习。

适合返回 Message 的情况:

  1. 可以立即完成
  2. 不需要持续追踪状态
  3. 不会产生需要版本管理的 artifact
  4. 更像一次说明、确认、拒绝或澄清

适合创建 Task 的情况:

  1. 工作可能耗时较长
  2. 需要客户端查询或订阅进度
  3. 过程中可能需要用户补充输入
  4. 会产出 artifact,例如文件、图片、报告、结构化结果
  5. 后续可能围绕结果继续 refinement

一个实用判断是:如果客户端未来可能会问“这个工作现在到哪一步了?”或者“上一版结果是什么?”,那它大概率应该是 Task。

Task 的生命周期状态

Task 创建之后,会进入一个生命周期。它不会永远停留在“处理中”,它会持续更新,直到进入中断状态或终态。

官方文档里提到的中断状态包括:

  1. input-required
  2. auth-required

终态包括:

  1. completed
  2. canceled
  3. rejected
  4. failed

中断状态的意思是:任务还没有结束,但 Agent 无法继续,需要外部动作。比如缺少必要参数,就进入 input-required;需要用户授权,就进入 auth-required。

终态的意思是:任务结束了。无论是成功、取消、拒绝还是失败,它都不应该被重新启动。

可以画成一个简化状态图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
message.send
    |
    v
Task created
    |
    v
working / progressing
    |
    +--> input-required  --用户补充信息--> 新消息继续处理
    |
    +--> auth-required   --用户授权------> 新消息继续处理
    |
    +--> completed
    |
    +--> canceled
    |
    +--> rejected
    |
    +--> failed

这个生命周期设计的价值是把 Agent 执行过程显式化。客户端不必猜 Agent 在做什么,也不必把长任务伪装成同步请求。

Task 是不可变的:终态之后不要重启

Task 一旦进入终态,就不能重新启动。

这是文档中非常重要但容易被忽略的一点。比如一个图片生成任务已经 completed,用户又说“把船改成红色”。这不是把原任务重新打开,而是在同一个 contextId 下创建一个新的任务。

这样做有几个好处:

  1. 输入和输出的映射更清楚
  2. 任务历史更容易审计
  3. orchestration 更容易做
  4. 客户端可以稳定引用某个任务的状态和产物
  5. 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。发现流程:

  1. client agent 发现潜在 A2A 服务器的域名(例如smart-thermostat.example.com)

  2. client agent 对 https://smart-thermostat.example.com/.well-known/agent-card.json 执行 HTTP GET 请求

  3. 如果 Agent Card 存在且可访问,服务器会以 JSON 响应的形式返回。

基于Catalog的发现

适用于企业环境或公共市场,Agent Card通常由中央注册局管理。策划的注册库作为一个中央仓库,允许客户端根据“技能”或“标签”等标准查询和发现Agent。

  1. A2A 服务器会将其Agent Card发布到注册表。

  2. client agent 查询注册库的 API,并根据“特定技能”等条件进行搜索。

  3. 注册处返回匹配的Agent Card或参考。

直接配置 / 私有部署

适用于紧耦合系统、私有代理或开发用途,客户端直接配置代理卡信息或 URL。

Artifact:任务产物不是任务本身

Task 可以产生 artifacts。artifact 是任务的输出结果,可以是文件、图片、结构化数据等。

在官方例子里,Agent 先生成一张帆船图片,任务完成后返回一个 artifact,例如:

1
2
task-boat-gen-123
  artifact: sailboat_image.png

之后客户端要求“把帆船改成红色”。Agent 不会修改原 task,而是创建一个新 task,并产生新的 artifact:

1
2
task-boat-color-456
  artifact: sailboat_image.png

注意这里 artifact 的名字可以保持一致,但 artifactId 应该不同。这意味着它是同一个逻辑产物的不同版本。

文档建议 serving agent 在生成 refinement 结果时使用一致的 artifact name。这样客户端更容易把不同版本识别为同一类产物。

Artifact mutation 应该由客户端追踪

官方文档明确指出:artifact 的变更链路不应该由 serving agent 负责维护,也不是 A2A 协议规范的一部分。

这点很有意思。因为直觉上我们可能会想:既然 Agent 生成了新 artifact,那 Agent 应该知道哪个 artifact 是哪个 artifact 的新版。但 A2A 的设计选择是把这个责任交给客户端。

原因是客户端更适合判断:

  1. 哪个结果被用户接受
  2. 哪个版本是当前有效版本
  3. 哪个 artifact 应该被继续 refinement
  4. 哪些版本应该展示给用户或隐藏起来

所以客户端需要维护自己的版本关系。例如:

1
2
3
sailboat_image.png
  v1: artifact-boat-v1-xyz, from task-boat-gen-123
  v2: artifact-boat-v2-red-pqr, from task-boat-color-456

如果客户端再次请求 refinement,最好显式引用自己认为“最新且可接受”的 artifact。不要假设 Agent 一定能猜对。

refinement 的正确建模方式

refinement 指的是基于已有任务结果继续提出修改、补充、扩展或重新生成。

在 A2A 里,refinement 的推荐模式是:

  1. 使用相同的 contextId
  2. 创建新的 Task
  3. 使用 referenceTaskIds 引用相关旧任务
  4. 如果要修改特定 artifact,尽量在 part metadata 中带上 artifactId 和 taskId
  5. 新任务生成新的 artifact
  6. 客户端维护 artifact 版本链

如果客户端没有明确指出要修改哪个 artifact,Agent 可以尝试根据 contextId 推断。但如果上下文里有多个候选 artifact,Agent 不应该强行猜测,而应该返回 input-required,要求客户端澄清。

这是一个很好的协议设计细节:不确定时,把不确定性显式暴露出来,而不是默默做一个可能错误的决定。

客户端实现时要承担的责任

从客户端视角看,Life of a Task 不只是理解字段,还要设计好本地状态管理。

客户端至少应该记录:

  1. 当前 contextId
  2. 当前上下文下的所有 taskId
  3. 每个任务的状态
  4. 每个任务的 artifacts
  5. artifact 的版本链
  6. 用户接受的是哪个 artifact 版本
  7. 哪些任务处于 input-required 或 auth-required

客户端在发起后续请求时,应该尽量带上:

  1. contextId
  2. 相关的 referenceTaskIds
  3. 如果 refinement 针对某个产物,带上 artifact 引用
  4. 用户补充输入或授权结果

一个客户端的数据模型可以粗略设计为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Context
  id
  messages[]
  tasks[]

Task
  id
  contextId
  status
  artifacts[]
  history[]

ArtifactVersion
  artifactId
  artifactName
  taskId
  previousArtifactId
  acceptedByUser
  createdAt

A2A 不强制你这么建模,但如果要做一个可靠的 Agent 客户端,这些结构迟早会出现。

Agent 实现时要承担的责任

从 Agent 服务端视角看,关键不是把所有请求都变成任务,而是正确判断交互复杂度。

Agent 应该做到:

  1. 对简单交互返回 Message
  2. 对可追踪工作创建 Task
  3. 为首次交互生成 contextId
  4. 为新任务生成 taskId
  5. 在任务推进时更新状态
  6. 需要输入时进入 input-required
  7. 需要授权时进入 auth-required
  8. 任务结束后进入明确终态
  9. 不重启已经终态的任务
  10. 对 refinement 创建新任务
  11. 生成 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 生成一份产品海报。

第一轮:

1
2
3
4
用户:帮我生成一张咖啡新品海报
Agent:创建 task-001,contextId = ctx-001
结果:poster.png, artifactId = art-001
状态:completed

第二轮:

1
2
3
4
5
用户:把标题改得更高级一点
客户端:发送相同 contextId,并引用 task-001 / art-001
Agent:创建 task-002
结果:poster.png, artifactId = art-002
状态:completed

第三轮:

1
2
3
4
5
用户:把刚才那张图做成小红书封面
客户端:引用当前接受版本 art-002
Agent:创建 task-003
结果:poster-xhs.png, artifactId = art-003
状态:completed

在这个过程中:

  1. ctx-001 把所有交互放在同一个上下文里
  2. task-001、task-002、task-003 是三个独立工作单元
  3. art-001、art-002、art-003 是不同产物或版本
  4. 客户端负责知道用户当前接受的是 art-002 还是 art-003
comments powered by Disqus
使用 Hugo 构建
主题 Stack 由 Jimmy 设计