AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

A2A Agent Card 与任务模型

我的学习 →

正在读取学习记录…

A2A是Agent之间交换任务与结果的协议。调用方不用知道远端Agent内部如何规划、调用什么模型,只要按共同的接口发送请求、跟踪任务并接收产物。

先分清三个对象。AgentCard是服务名片,说明服务能处理哪些内容、有哪些技能、支持什么接口。Message是一条交互消息,可以发起请求或补充资料。Task是一项有状态的工作,让调用方在连接结束后仍能用编号跟踪进度。

名片像店门口的招牌,Message像顾客提出的问题,Task像已经受理的业务单。看到招牌不表示业务已经办完,收到一条“处理中”的话也不等于拿到结果。

用A2A 1.0字段读一组对象

服务名片的supportedInterfaces列出接口。例子选JSONRPC绑定、protocolVersion为1.0、HTTPS地址https://refund.example/a2a。version为1.0.0表示这位Agent自身的版本,它与协议版本不是同一个字段。

JSONRPC是JSON-RPC远程调用的绑定名称;这里的1.0表示A2A协议版本。实际JSON-RPC报文使用"jsonrpc": "2.0",并按A2A要求选择请求的协议版本。名片的协议版本与承载报文的JSON-RPC版本需要分别读取,可对照规范中的AgentInterface和JSON-RPC请求结构。

用户Message包含messageId、role和parts。1.0的JSON角色值使用ROLE_USER或ROLE_AGENT;文本Part直接写text字段。任务状态写在status.state中,使用TASK_STATE_WORKING等枚举名称。

程序在本地创建这些字典,检查示例字段与状态变化;https://refund.example只是固定名片数据,没有发网络请求。对象结构按1.0阅读,传输与认证要在接入服务时另外实现。

保存完整程序并运行

在自己的学习目录打开PowerShell,创建一个新的子目录:

Bash
mkdir l6-a2a
cd l6-a2a

把下面完整代码保存为 a2a_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。

Python
"""按A2A 1.0字段编写的本地对象与业务状态检查。"""
from copy import deepcopy
from urllib.parse import urlparse

CARD = {
    "name": "退款资料助手", "description": "读取退款规则并整理依据",
    "version": "1.0.0",
    "supportedInterfaces": [{
        "url": "https://refund.example/a2a", "protocolBinding": "JSONRPC",
        "protocolVersion": "1.0",
    }],
    "capabilities": {"streaming": False},
    "defaultInputModes": ["text/plain"], "defaultOutputModes": ["text/plain"],
    "skills": [{"id": "refund.read", "name": "退款规则读取",
                "description": "按订单查询可用规则", "tags": ["refund"]}],
}
MESSAGE = {"messageId": "M-17", "role": "ROLE_USER",
           "parts": [{"text": "查询O-17的退款规则"}]}
STATES = {"TASK_STATE_" + value for value in (
    "UNSPECIFIED", "SUBMITTED", "WORKING", "COMPLETED", "FAILED",
    "CANCELED", "INPUT_REQUIRED", "REJECTED", "AUTH_REQUIRED")}
TERMINAL = {"TASK_STATE_" + value for value in ("COMPLETED", "FAILED", "CANCELED", "REJECTED")}
NEXT = {
    "TASK_STATE_SUBMITTED": {"TASK_STATE_WORKING", "TASK_STATE_REJECTED"},
    "TASK_STATE_WORKING": {"TASK_STATE_INPUT_REQUIRED", "TASK_STATE_AUTH_REQUIRED"} | TERMINAL,
    "TASK_STATE_INPUT_REQUIRED": {"TASK_STATE_WORKING", "TASK_STATE_CANCELED"},
    "TASK_STATE_AUTH_REQUIRED": {"TASK_STATE_WORKING", "TASK_STATE_CANCELED"},
}

def text(value):
    return isinstance(value, str) and bool(value.strip())

def strings(value):
    return isinstance(value, list) and bool(value) and all(text(item) for item in value)

def validate_card(card):
    # 检查当前例子所用的字段和JSONRPC 1.0接口配置。
    for key in ("name", "description", "version"):
        if not text(card.get(key)):
            raise ValueError("AgentCard缺少" + key)
    if not isinstance(card.get("capabilities"), dict):
        raise ValueError("capabilities必须是对象")
    for key in ("defaultInputModes", "defaultOutputModes"):
        if not strings(card.get(key)):
            raise ValueError(key + "必须是非空媒体类型列表")
    interfaces = card.get("supportedInterfaces")
    if not isinstance(interfaces, list) or not interfaces:
        raise ValueError("缺少supportedInterfaces")
    for item in interfaces:
        address = urlparse(item.get("url", ""))
        if address.scheme != "https" or not address.netloc:
            raise ValueError("接口地址必须是HTTPS绝对地址")
        if (item.get("protocolBinding"), item.get("protocolVersion")) != ("JSONRPC", "1.0"):
            raise ValueError("当前客户端需要JSONRPC 1.0接口")
    skills = card.get("skills")
    if not isinstance(skills, list) or not skills:
        raise ValueError("缺少skills")
    for skill in skills:
        if not all(text(skill.get(key)) for key in ("id", "name", "description")) or not strings(skill.get("tags")):
            raise ValueError("技能字段不完整")

def validate_parts(parts):
    if not isinstance(parts, list) or not parts:
        raise ValueError("parts不能为空")
    for part in parts:
        # v1.0的内容联合字段,不再使用旧kind判别字段。
        if not isinstance(part, dict) or len(set(part) & {"text", "raw", "url", "data"}) != 1:
            raise ValueError("每个Part只能有一种内容")
        if "text" in part and not isinstance(part["text"], str):
            raise ValueError("text必须是字符串")

def validate_message(message, task=None):
    if not text(message.get("messageId")) or message.get("role") not in {"ROLE_USER", "ROLE_AGENT"}:
        raise ValueError("消息编号或角色不合法")
    validate_parts(message.get("parts"))
    if message["role"] == "ROLE_AGENT" and not text(message.get("contextId")):
        raise ValueError("服务端消息缺少contextId")
    if task is not None:
        if message.get("taskId") not in (None, task["id"]):
            raise ValueError("taskId不匹配")
        if message.get("contextId") not in (None, task.get("contextId")):
            raise ValueError("contextId不匹配")

def transition(task, state):
    current = task["status"]["state"]
    if state not in STATES:
        raise ValueError("未知任务状态")
    if current in TERMINAL:
        raise ValueError("终态任务不能重新运行")
    # 允许边由这份退款应用定义;协议定义状态含义。
    if state not in NEXT.get(current, set()):
        raise ValueError("应用不允许此状态变化")
    result = deepcopy(task)
    result["status"]["state"] = state
    return result

def main():
    validate_card(CARD)
    validate_message(MESSAGE)
    interface = CARD["supportedInterfaces"][0]
    print("名片接口:" + interface["protocolBinding"] + " " + interface["protocolVersion"])
    print("用户消息:" + MESSAGE["parts"][0]["text"])
    task = {"id": "T-17", "contextId": "C-17", "status": {"state": "TASK_STATE_SUBMITTED"}}
    print("任务:" + task["id"] + " / " + task["contextId"])
    for state in ["TASK_STATE_WORKING", "TASK_STATE_INPUT_REQUIRED",
                  "TASK_STATE_WORKING", "TASK_STATE_COMPLETED"]:
        task = transition(task, state)
        print("状态:" + state)
    try:
        transition(task, "TASK_STATE_WORKING")
    except ValueError as error:
        print("再次运行:" + str(error))

if __name__ == "__main__":
    main()

保存后执行 dir,确认能看到 a2a_demo.py,再运行:

Bash
ls
python -X utf8 a2a_demo.py

程序输出:

TXT
名片接口:JSONRPC 1.0
用户消息:查询O-17的退款规则
任务:T-17 / C-17
状态:TASK_STATE_WORKING
状态:TASK_STATE_INPUT_REQUIRED
状态:TASK_STATE_WORKING
状态:TASK_STATE_COMPLETED
再次运行:终态任务不能重新运行

名片告诉调用方怎样交互

supportedInterfaces不是一个裸URL,而是接口对象列表。每项包含url、protocolBinding与protocolVersion;列表第一项表达优先接口。规范的核心绑定包括JSONRPC、GRPC与HTTP+JSON。validate_card把当前客户端配置限定为JSONRPC 1.0,因此其他合法绑定也会被这位客户端拒绝,这是客户端适配范围而不是协议不支持。

defaultInputModes和defaultOutputModes使用媒体类型,例子都是text/plain。skills表示具体业务能力,包含id、name、description和tags。capabilities中的streaming表示是否支持流式更新,它不等于某项退款业务技能。

这份检查器核对所用字段、非空列表和HTTPS接口配置。部署时还需要完整规范或对应版本SDK检查更多对象、可选字段与传输错误。能力声明帮助选择候选服务,服务认证与订单权限仍要由独立机制核对。

服务名片描述接口,消息发起交互,任务保存状态;工作可等输入后恢复,完成后不可重开
A2A 1.0对象各有职责。图中状态缩写在源码中都使用完整TASK_STATE_枚举。

消息内容与三个编号怎样关联

messageId标识一条消息;Task.id标识一项工作;contextId用于归组相关交互。一个上下文可以关联多个任务,一项任务也可以经历多条消息。不要用messageId替换taskId去查询任务。

Part是内容块,1.0中text、raw、url和data四种内容字段每块只能选一种。两块内容可以放在两个Part里;一块同时写text和data会被例子拒绝。旧示例中的kind="text"不再是1.0的内容判别方式。

客户端补充消息可以带taskId。若同时携带contextId,两者应属于同一任务关系;服务端Message需要contextId。validate_message可以接收task参数来核对这种关系,避免把另一个会话的补充内容塞进当前退款单。

等输入、成功结束与失败结束

输出依次进入WORKING、INPUT_REQUIRED、WORKING、COMPLETED。INPUT_REQUIRED表示需要额外输入,例如缺订单号;这是可恢复的中断。AUTH_REQUIRED则表示等待所需认证。它们和成功或失败的终态不同。

COMPLETED、FAILED、CANCELED和REJECTED都是终态。已经结束的任务不能重新变成WORKING;后续相关新工作应有新任务。程序最后一次尝试重开完成任务,被拒绝。

NEXT给出这个退款应用采用的允许边,例如待处理任务先进入工作,再等待输入。协议定义各状态含义,应用还要根据业务决定何时更新、允许哪些路径;不要把例子的NEXT当成全协议唯一状态机。

具体结果通常以任务的artifacts返回。Message可说明进度、澄清问题或补充输入,Task负责跟踪,Artifact承载实际产物。任务成功状态也需要与真正保存的产物对应,不能只更新一个枚举就宣布资料已经交付。

改一个内容块,触发本地校验

在MESSAGE的现有Part中增加data字段,使它成为{"text": "查询O-17的退款规则", "data": {"order": "O-17"}},运行python -X utf8 a2a_demo.py。程序报“每个Part只能有一种内容”。

若需要同时传文本和结构数据,把parts改为两个对象:[{"text": "查询退款规则"}, {"data": {"order": "O-17"}}],再运行同一命令。联合字段检查通过;输出中用户消息显示第一块文本。

把接口protocolVersion的1.0改为0.3再运行,会报当前客户端需要JSONRPC 1.0接口。不要只改版本字符串而继续发送旧字段;协议版本改变时,应同时核对对象结构、传输绑定和状态枚举。

先将MESSAGE.parts恢复为原来的一个text对象,再将protocolVersion恢复为1.0,保存并重跑。应重新显示四次状态变化及终态拒绝,随后再运行配套检查。

参考阅读:A2A规范重点对照AgentCard、AgentInterface、Message、Part与TaskState字段;Life of a Task关注可恢复中断和不可重开的终态。

运行配套检查

把下面测试完整保存为 test_a2a_demo.py,与主程序放在同一目录。6项检查覆盖名片与消息、接口配置、Part联合字段、服务端上下文、任务关联和终态约束。

查看测试代码:test_a2a_demo.py
Python
"""检查v1.0示例中的字段、内容联合、关联编号和终态。"""
import copy
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import a2a_demo as demo

class A2ATests(unittest.TestCase):
    def test_valid_card_and_message(self):
        demo.validate_card(demo.CARD)
        demo.validate_message(demo.MESSAGE)

    def test_missing_interface_and_wrong_version(self):
        for key, value in (("supportedInterfaces", []), ("skills", [])):
            broken = copy.deepcopy(demo.CARD)
            broken[key] = value
            with self.subTest(key=key), self.assertRaises(ValueError):
                demo.validate_card(broken)
        broken = copy.deepcopy(demo.CARD)
        broken["supportedInterfaces"][0]["protocolVersion"] = "0.3"
        with self.assertRaises(ValueError):
            demo.validate_card(broken)

    def test_part_requires_exactly_one_content(self):
        for parts in ([], [{}], [{"text": "规则", "data": {}}], [{"text": 12}]):
            with self.subTest(parts=parts), self.assertRaises(ValueError):
                demo.validate_parts(parts)

    def test_server_message_requires_context(self):
        message = dict(demo.MESSAGE, role="ROLE_AGENT")
        with self.assertRaises(ValueError):
            demo.validate_message(message)
        demo.validate_message(dict(message, contextId="C-17"))

    def test_task_context_mismatch(self):
        task = {"id": "T-17", "contextId": "C-17"}
        with self.assertRaises(ValueError):
            demo.validate_message(dict(demo.MESSAGE, taskId="T-other"), task)

    def test_interruption_can_resume_but_terminal_cannot(self):
        task = {"id": "T-17", "status": {"state": "TASK_STATE_WORKING"}}
        task = demo.transition(task, "TASK_STATE_INPUT_REQUIRED")
        task = demo.transition(task, "TASK_STATE_WORKING")
        task = demo.transition(task, "TASK_STATE_COMPLETED")
        with self.assertRaises(ValueError):
            demo.transition(task, "TASK_STATE_WORKING")

if __name__ == "__main__":
    unittest.main()
Bash
python -X utf8 -m unittest -v test_a2a_demo.py

测试名称后应显示 ok,末尾显示 Ran 6 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · Agent Card和Task的用途怎样区分

Agent Card和Task的用途怎样区分?

对应知识点:L6-a2a

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

A2A服务能力和任务状态怎样接起来?

对应知识点:L6-a2a

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

写一个断言检查任务状态从工作中变成已完成后存在产物。

对应知识点:L6-a2a

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

收到请求已受理后,还需要观察什么才能取得退款资料?

对应知识点:L6-a2a

图片查看器
100%

文章配图 · 按 Esc 关闭