首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >银河it-A2A + Skills:智能体协作的“协议层”与“能力层”如何真正咬合

银河it-A2A + Skills:智能体协作的“协议层”与“能力层”如何真正咬合

原创
作者头像
用户12502707
修改于 2026-09-29 10:44:00
修改于 2026-09-29 10:44:00
780
举报

引子:两个被分开解决的问题

2025 年,AI Agent 领域出现了两条几乎同时推进的技术线索。一条是 Google 主导的 Agent2Agent(A2A)协议,试图定义智能体之间如何发现、委派和协作;另一条是 Anthropic 提出的 Agent Skills 标准,试图定义智能体的专业能力如何被封装、分发和按需加载。

这两条线索经常被放在一起讨论,却很少被真正拆开来看。一个容易被忽略的事实是:A2A 和 Skills 解决的是不同层次的问题。 A2A 处理的是“协作层”——智能体之间如何谈判、委派和追踪任务状态;Skills 处理的是“能力层”——智能体内部如何组织专业知识、脚本和资源。二者不是替代关系,而是上下咬合的关系。

理解这层咬合,是理解下一代智能体系统架构的关键。

一、协作层:A2A 协议真正标准化了什么

A2A 在 2025 年 4 月由 Google 发布,同年 6 月捐赠给 Linux 基金会,2026 年 3 月 v1.0 冻结,随后迁入 Agentic AI Foundation,与 MCP、AGENTS.md 等标准并列。一个值得注意的判断来自 A2A v1.0 Builder's Guide:剥去营销语言,A2A 实际上只标准化了四个对象——Agent Card、Task、Message 和 Artifact。

这个“四对象”视角很重要,因为它暴露了 A2A 的核心设计约束:对端智能体是不可见的。 你拿不到它的工具列表、记忆、模型或内部计划。你能得到的是一张 Agent Card、一个 Task ID,以及 Task 经过的状态序列。

Agent Card 是一份 JSON 清单,发布在 /.well-known/agent-card.json 路径下,声明智能体的身份、能力、传输绑定和认证方式。其中最关键的部分是 skills 数组——这是智能体向外宣告“我能做什么”的标准化方式。一个 skill 通常包含 id、name、description、tags、examples 以及支持的输入输出模态。

值得仔细品味的是,A2A 的 skill 定义和 MCP 的 tool 定义在抽象层级上并不相同。MCP 的 tool 是具体的 API 操作,而 A2A 的 skill 是更高层的任务类别,通过自然语言来描述,留给对端智能体判断是否委派的空间。这意味着 A2A 的 skill 本质上是“意图声明”,而非“函数签名”。

Task 生命周期是 A2A 协作语义的核心载体。v1.0 定义了一个八状态生命周期,从 submitted 到最终状态,支持阻塞和流式两种模式。流式模式下,客户端通过 SSE 订阅 Task 更新,接收增量生成的 Artifact。这套机制的设计意图很明确:让智能体之间的协作在时间维度上可观察、可恢复、可中断。

二、能力层:Skill 作为可发现、可按需加载的专业知识包

如果 A2A 定义了智能体“如何对外说话”,那么 Skill 定义的是智能体“内部如何组织做事的知识”。

Anthropic 在 2025 年 10 月正式提出 Agent Skills,其核心洞察非常朴素:与其为每个用例构建定制智能体,不如让通用智能体通过加载可组合的技能来获得专业能力。Skill 的物理形态极其简单——一个目录,里面有一个 SKILL.md 文件,可选地附带脚本、参考文档和资源。

但简单的外表下藏着一个精心设计的信息架构。SKILL.md 的 YAML frontmatter 包含 name 和 description,这两个字段在智能体启动时被预加载到系统提示中。这是第一层渐进式披露:只给够判断“什么时候该用这个 skill”的信息,而不加载全部内容。

当模型判断 skill 相关时,它读取完整的 SKILL.md。这是第二层。如果 skill 内部引用了额外文件(比如 reference.md 或 forms.md),模型只在需要时读取那些文件。这是第三层及更深层。

渐进式披露之所以关键,是因为它把“技能包能有多大”这个问题从上下文窗口的约束中解放了出来。一个 skill 可以捆绑数百页的参考文档,只要模型不会在每次使用时都全部读取。在 Claude Code 的实践中,skill 被放置在 .claude/skills/<name>/SKILL.md 路径下,目录名成为用户输入的命令。

这个设计有一个容易被忽略但极其重要的后果:Skill 是文件系统原生的。 它不需要任何特殊的运行时或注册机制。一个 skill 就是一个你可以 cat、git clone、zip 上传的目录。这种“无聊”的设计选择,恰恰是它能够跨平台、跨框架传播的前提。

三、咬合点:A2A 如何发现 Skill,Skill 如何支撑 A2A

现在来到核心问题:这两个层次如何连接?

第一咬合点:Agent Card 的 skills 数组 = A2A 对 Skill 的发现接口。

当外部智能体通过 A2A 发现一个对端时,它读取 Agent Card 中的 skills 列表。这个列表告诉它:对端能处理哪些任务类别,以及每个任务类别的自然语言描述和示例 prompt。这是 A2A 层面的“能力广告”。

而 Skill 层面提供的是“能力实现”。一个 A2A 智能体对外宣称的 skill,其内部可能由一个或多个 SKILL.md 目录来支撑。但关键在于:A2A 规范并不规定对端如何实现这些 skill。 对端可以用一个 SKILL.md,可以用一组 SKILL.md,也可以用完全不同于 Skill 标准的内部机制。A2A 只关心“你能否处理这个任务”,不关心“你用什么文件结构来处理”。

这种设计的分层是刻意的。它让 A2A 保持了对实现细节的完全不可见——这正是“黑箱交接”的架构含义。

第二咬合点:工具委派(Delegation)让 Skill 可以“借用”对端的工具。

A2A 生态中一个值得关注的开源模式是 delegation 包所实现的“透明工具委派”。问题场景是这样的:一个 A2A 服务端智能体需要访问数据库或搜索 API,但这些能力只有客户端智能体拥有。与其让服务端直接访问这些工具,不如让客户端把工具“借”给服务端。

实现机制是:客户端通过 Agent Card 或消息元数据向服务端广告一组 skills(这些 skills 对应客户端本地的工具)。服务端将这些 skills 转化为代理工具,加入自己的智能体循环中。当服务端的模型调用“search”这个代理工具时,代理工具向客户端发送一个 input required 的 A2A Task,阻塞等待客户端执行本地工具并返回结果。服务端的模型完全不知道这个工具是远程的。

这个模式的意义在于,它让 Skill 的边界和 A2A 的边界可以对齐,也可以不对齐。客户端可以在 Agent Card 中只暴露一部分 skills,服务端也可以只借用其中一部分。二者之间的映射是灵活的。

第三咬合点:ADK 中的 SkillToolset 把 SKILL.md 直接变成 A2A 可发现的能力。

Google 的 Agent Development Kit(ADK)从 1.25.0 版本开始引入 SkillToolset,直接支持加载 SKILL.md 格式的技能包。加载后的技能通过三个层次暴露:L1 是 frontmatter 中的 name 和 description,被注入系统提示;L2 是完整的 SKILL.md body;L3 是捆绑的额外文件。

当一个 ADK 智能体被转换为 A2A 服务端时,AgentSkill 对象被定义在 __main__.py 中,与 AgentCard 一起对外发布。这意味着 Skill 的元数据(name、description)天然成为 A2A 技能发现的信息源,二者之间的信息流是单向且自然的:Skill 是能力的本地表述,Agent Card 是能力的网络表述。

四、代码视角:一个最小可运行的 A2A + Skill 集成

以下代码展示一个最小模式:一个 A2A 服务端智能体,其 Agent Card 中声明的 skill 对应本地加载的 SKILL.md,并通过代理工具机制将对端工具纳入自身能力边界。

代码语言:javascript
复制
# a2a_skill_server.py — A2A 服务端,暴露一个 skill,同时借用客户端工具

import asyncio
import json
from pathlib import Path
from typing import Any

# ============================================================
# 1. Skill 加载层 — 从文件系统加载 SKILL.md
# ============================================================

class SkillLoader:
    """极简 SKILL.md 加载器,模拟渐进式披露的三个层次。"""

    def __init__(self, skills_root: str):
        self.skills_root = Path(skills_root)

    def load_frontmatter(self, skill_name: str) -> dict:
        """L1: 只加载 frontmatter,用于构建 Agent Card 和系统提示。"""
        skill_file = self.skills_root / skill_name / "SKILL.md"
        text = skill_file.read_text()
        # 解析 --- 之间的 YAML frontmatter
        _, frontmatter, _ = text.split("---", 2)
        meta = {}
        for line in frontmatter.strip().split("\n"):
            key, _, value = line.partition(":")
            meta[key.strip()] = value.strip()
        return meta

    def load_body(self, skill_name: str) -> str:
        """L2: 加载完整 SKILL.md body(不含 frontmatter)。"""
        skill_file = self.skills_root / skill_name / "SKILL.md"
        text = skill_file.read_text()
        parts = text.split("---", 2)
        return parts[2].strip()


# ============================================================
# 2. 代理工具层 — 将客户端 Skills 转化为本地可调用工具
# ============================================================

class DelegatedSkill:
    """一个从客户端借来的 skill,在服务端表现为代理工具。"""

    def __init__(self, skill_id: str, description: str,
                 requester: Any):
        self.skill_id = skill_id
        self.description = description
        self.requester = requester  # A2A 客户端句柄

    async def invoke(self, params: dict) -> Any:
        """调用代理工具时,向客户端发送 input-required Task 并等待结果。"""
        result = await self.requester.request_input(
            skill_id=self.skill_id,
            params=params,
        )
        return result


# ============================================================
# 3. A2A 服务端 — 发布 Agent Card,处理传入 Task
# ============================================================

class A2ASkillServer:
    def __init__(self, agent_name: str, base_url: str,
                 skill_loader: SkillLoader,
                 local_skills: list[str],
                 delegated_skills: list[DelegatedSkill]):
        self.agent_name = agent_name
        self.base_url = base_url
        self.skill_loader = skill_loader
        self.local_skills = local_skills
        self.delegated_skills = delegated_skills

    def build_agent_card(self) -> dict:
        """从本地 skills 和委派 skills 构建 Agent Card。"""
        skills = []

        # 本地 skill:从 SKILL.md frontmatter 导出
        for name in self.local_skills:
            meta = self.skill_loader.load_frontmatter(name)
            skills.append({
                "id": name,
                "name": meta.get("name", name),
                "description": meta.get("description", ""),
                "tags": meta.get("tags", "").split(",") if
                        meta.get("tags") else [],
            })

        # 委派 skill:从客户端广告的 skills 导出
        for ds in self.delegated_skills:
            skills.append({
                "id": ds.skill_id,
                "name": ds.skill_id,
                "description": ds.description,
            })

        return {
            "name": self.agent_name,
            "url": self.base_url,
            "version": "1.0.0",
            "capabilities": {"streaming": True},
            "skills": skills,
        }

    async def handle_task(self, task: dict) -> dict:
        """处理传入的 A2A Task,路由到本地 skill 或代理 skill。"""
        skill_id = task["params"].get("skill_id")
        payload = task["params"].get("input", {})

        # 1. 检查本地 skill
        if skill_id in self.local_skills:
            body = self.skill_loader.load_body(skill_id)
            # 在实际实现中,这里会将 body 注入模型上下文并执行
            return {
                "task_id": task["id"],
                "state": "completed",
                "artifacts": [{
                    "type": "text",
                    "content": f"[local skill {skill_id} executed]"
                }],
            }

        # 2. 检查委派 skill
        for ds in self.delegated_skills:
            if ds.skill_id == skill_id:
                result = await ds.invoke(payload)
                return {
                    "task_id": task["id"],
                    "state": "completed",
                    "artifacts": [{"type": "text", "content": result}],
                }

        return {
            "task_id": task["id"],
            "state": "failed",
            "error": f"Unknown skill: {skill_id}",
        }

这段代码的要点在于 build_agent_card 方法:它将本地 SKILL.md 的 frontmatter 和委派 skills 的声明合并到同一个 skills 数组中。对 A2A 的发现者来说,二者看起来是一样的——都是可委派的能力。但内部路由逻辑清楚地区分了两条路径:本地 skill 直接执行,委派 skill 通过 A2A 反向调用客户端。

五、当前边界与真正的设计张力

A2A + Skills 的组合虽然方向正确,但仍有几处未被充分解决的张力,值得开发者警惕。

张力一:Agent Card 中 skill 的粒度控制。

A2A 的 Agent Card 是一个公开的 JSON 文档。你声明的 skills 越详细,发现者越容易正确路由;但你也暴露了越多的能力拓扑。在安全敏感场景中,Agent Card 的 skills 数组本身就是一个攻击面。目前的协议允许 skill 包含 examples 字段——示例 prompt 如果写得太具体,可能泄露内部工作流的信息。

张力二:渐进式披露在 A2A 委派中的不可见性。

Skills 的渐进式披露是智能体内部的加载策略。当一个 A2A Task 被委派给对端时,委派方完全看不到对端内部用了哪个 SKILL.md、加载了哪些参考文件。这既是优势(黑箱封装),也是风险:如果对端的 skill 实现质量不佳,委派方无法在 Task 执行前进行任何质量评估。A2A 的 Task 生命周期提供了状态可见性,但没有提供能力质量的可见性。

张力三:delegation 模式的安全边界。

代理工具机制让服务端可以“借用”客户端的工具,但这也意味着客户端的工具在服务端的模型上下文中被间接调用。服务端的模型决定何时调用代理工具、传入什么参数。如果服务端被恶意 prompt 注入攻击,代理工具可能被诱导执行超出预期的操作。当前的 delegation 包没有内置的权限细化机制——借用就是全量借用。

张力四:多智能体协调经验的丢失。

Swarm Skills 的研究指出了一个更深层的问题:当前的多智能体协调协议(包括 A2A 的 Task 路由逻辑)存在于框架内部代码或静态配置中,无法作为可分发的资产被复用。每次多智能体会话结束后,协调决策——任务如何分解、角色如何动态实例化、冲突如何解决——随着执行轨迹一起被丢弃。Skills 标准解决了单智能体能力的沉淀问题,但多智能体协调经验的沉淀仍然是一个开放问题。

结语:协议层与能力层的分工是健康的

A2A 和 Skills 之间的关系,最好用一句话概括:A2A 是智能体之间的“HTTP”,Skills 是智能体内部的“文件系统”。 HTTP 不关心你的文件系统怎么组织,文件系统也不需要知道 HTTP 怎么传输。

这种分层之所以健康,是因为它允许每一层独立演进。A2A 可以继续完善 Task 生命周期和流式语义,而不需要触及 skill 的实现细节;Skills 可以继续探索更复杂的渐进式披露结构,而不需要关心对端用什么传输协议来发现它们。

真正的工作,在于理解二者之间的映射层——如何决定哪些内部 skill 应该对外声明,如何设计 Agent Card 中 skill 的粒度和描述,如何在 delegation 场景中划定权限边界。这些不是协议层面的问题,而是工程层面的判断。而恰恰是这些判断,决定了一个多智能体系统是“能跑”还是“可信”。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 引子:两个被分开解决的问题
  • 一、协作层:A2A 协议真正标准化了什么
  • 二、能力层:Skill 作为可发现、可按需加载的专业知识包
  • 三、咬合点:A2A 如何发现 Skill,Skill 如何支撑 A2A
  • 四、代码视角:一个最小可运行的 A2A + Skill 集成
  • 五、当前边界与真正的设计张力
  • 结语:协议层与能力层的分工是健康的
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档