05 · 综合项目

把概念、原理、失败模式拼成一个真实的 A2A + MCP 两层栈

04 把 A2A 的失败模式按协议阶段铺开,落在一句话上:身份、信任、防重放、授权多数留给实现者,「A2A-compliant」并不等于安全。这一章把前四章的所有概念拼进一个真实系统——一个编排 agent 用 A2A 把子任务委托给一个专家 agent,专家 agent 内部用 MCP 调自己的工具,把结果作为 Artifact 回传。这就是 2026 年企业级的默认两层栈。但拼装之前,你要先在四个跨章判别点上做选择:每个判别点都有来自不同章节的备选,选错一个,系统要么不安全、要么用错了层。事实锚点 A2A v1.0.1(2026-05-28)/ a2a-sdk 1.1.0;下文代码用来演示判别落点,均未在本机执行(标 2026-06)。

本章你要建立的心智模型

  • 真实 agent 系统是两层的:A2A 是水平边界(编排 agent ↔ 专家 agent,跨信任域),MCP 是垂直边界(专家 agent ↓ 它自己的工具)。两条边界正交,同一个专家 agent 同时坐在两条边界上。
  • 动手写代码前,四个判别决策已经定了大半成败:交互模式、上下文隔离、用 A2A 还是 MCP 还是直接函数调用、跨信任边界补哪些安全。
  • 每个判别决策都挂在前面某一章的具体小节——交互回 02 §2.4,上下文回 01 §1.4,分层回 01 §1.5,安全回 04 安全模型。「备选 → 选哪个 → 为什么 → 代价」是判别的完整四段。
  • 换个场景(如委托一个跑数小时的长任务),同一套判别点取值会翻——判别能力是可迁移的,结论不是。

5.1项目背景:A2A + MCP 两层栈

需求

公司里有一个编排 agent,负责把用户的大目标拆成子任务、分派、汇总。这次它收到一个子任务:「调研竞品 X 的定价」。它自己不擅长网页调研,于是把这件事委托给一个专门的「网页调研专家 agent」。专家 agent 内部并不靠自己凭空回答——它用 MCP 调自己挂着的 fetch / search 工具去抓真实网页,整理出带来源的定价结论,作为一个 A2A Artifact 回传给编排 agent。

这个需求的形状正好压在 §1.5 的判别线上:编排 agent 面对的「网页调研专家」是一个有状态、会自己推理、可能多轮反问、跨信任边界的对等 agent——不是一个无状态工具。所以编排 agent ↔ 专家 agent 之间走 A2A(水平委托)。而专家 agent 内部的 fetch / search 是无状态原语,给 URL / 查询词、回结构化结果,所以专家 agent ↓ 工具之间走 MCP(垂直调用)。两层叠在一起,就是 §1.5 末尾说的「2026 年企业级默认栈」的真身。

进程 / 信任边界 A 编排 agent A2A client 进程 / 信任边界 B(另一信任域) 网页调研专家 agent A2A server · opaque A2A · 水平边界 SendMessage(委托) Artifact(带来源结论)回流 MCP · 垂直边界 fetch 抓网页 tools/call search 搜竞品页 tools/call 朱红 = A2A 水平(跨信任边界委托)· 黑 = MCP 垂直(专家 agent 调自己的工具)
图 5.1A2A + MCP 两层栈。注意:两个虚线框是两个独立的进程 / 信任域——编排 agent 看不进专家 agent 框内(opaque,§1.4)。朱红那条水平边界是 A2A(委托 + Artifact 回流);专家 agent 框内那两条垂直黑线是 MCP(tools/call)。同一个专家 agent 同时坐在两条正交边界上:对上是 A2A server,对下是 MCP client。
想一想

既然专家 agent 内部已经用 MCP 调了 fetch / search,编排 agent 为什么不跳过专家 agent,自己直接挂这两个 MCP 工具去抓网页,省掉 A2A 这一层?

展开答案(先停 10 秒再点)

因为「网页调研」不是一次工具调用,而是一段有判断的工作:决定搜哪些关键词、抓哪几个页面、哪条信息可信、怎么去重和归纳成「定价结论 + 来源」。这段判断逻辑(提示词、检索策略、归纳口径)是专家 agent 的核心资产,常常属于另一个团队甚至另一家公司,编排方既无权也不该把它搬进自己进程(§1.4 opaque)。把它当一个 A2A 对等 agent 委托出去,编排方只需声明「我要调研竞品 X 定价」,拿回结构化结论即可,不必关心对方搜了哪些词、抓了哪些页。把 fetch/search 直接挂到编排 agent 上,等于让编排 agent 自己变成调研专家——既绕过了信任边界,也丢掉了「能力可独立演进、可被多个编排方复用」的解耦收益。这正是 D3 判别点(§5.2 D3)要逼你想清楚的。

5.2设计任务:四个跨章判别决策(不是步骤)

下面四个决策,每个对应不同章节,每个都有一个并不直白的正确答案。先合上后文,对每个决策自己想 30 秒——你选哪个、为什么、付什么代价——再展开对照。这是设计任务,不是步骤实现:先把决策做对,§5.4 才给参考实现。

表 5.1 · 四个判别决策与它们各自的章节锚(先看清"哪个决策回哪一章")
编号判别决策章节锚(回链)
D1交互模式:同步 SendMessage / SSE 流 / webhook 异步推送?02 §2.4 三种交互模式
D2上下文:专家 agent 共享编排者的 state 还是保持不透明隔离?01 §1.4 不透明 agent + 对照 multi-agent-patterns
D3分层:这个子能力用 A2A 委托 / 用 MCP 当工具 / 直接函数调用?01 §1.5 A2A ⊥ MCP
D4跨信任边界要补哪些安全?04 安全模型 compliant ≠ secure

D1 · 交互模式:同步 / SSE 流 / webhook 推送?

表 5.2 · D1 备选(三种交互来自 02 §2.4)
备选怎么做风险 / 代价
同步 SendMessage(请求/响应)编排 agent 调一次 SendMessage,在同一响应里拿回 Task(可能已 COMPLETED)编排 agent 要把连接挂到调研结束;任务一久就不合适
SSE 流(SendStreamingMessage)专家 agent 边查边推 TaskStatusUpdateEvent / TaskArtifactUpdateEvent要维持长连接、处理重连;本场景编排方不需要逐页进度
webhook 异步推送编排 agent 注册 PushNotificationConfig 后断开,专家 agent 完成时反向 POST 回来引入反向信任——编排方要验入站 POST 真伪,还多一个 SSRF 面(§4.4)
展开判别(先想再点):选哪个 · 为什么 · 代价

本场景选同步 SendMessage(请求/响应)。

为什么:

  • 「调研竞品 X 定价」是一次秒级到十几秒级的有界任务,不是跑几小时的长任务。02 §2.4 把请求/响应明确定位成「秒级短任务」的交互——编排 agent 挂一小会儿连接拿回结果,成本可接受。
  • 编排 agent 不需要逐页进度(它只要最终的定价结论),所以 SSE 流的复杂度(长连接、重连、逐帧聚合)在这里换不来收益。
  • webhook 推送是为「长任务 + 可断线客户端」准备的;本场景任务短、编排 agent 也不需要断线。强行上 webhook 反而把 §4.4 的反向信任与 SSRF 面凭空引进来——多一种交互就多一类要防的攻击。

代价(不报代价就不算判别):同步意味着编排 agent 这次委托期间占着一条连接等结果;如果以后调研任务变长(比如要深挖几十个竞品页、跑几分钟),同步就会拖垮编排 agent 的并发——那时才该升级到 SSE 流或 webhook。交互模式不是一锤定音的,它跟着任务时长走。

章节锚:02 §2.4 三种交互模式(三种箭头方向 + 各自适用的任务时长)。

D2 · 上下文:共享编排者 state 还是不透明隔离?

表 5.3 · D2 备选
备选怎么做风险 / 代价
共享 state(公告板式)把编排方的完整 message 历史 / 内部 state 传给专家 agent,像同一系统内的多 agent跨信任边界泄露内部上下文;专家方常是另一公司,根本拿不到也不该拿到你的 state
不透明隔离 + 显式消息编排方只发一条 Message(「调研竞品 X 定价」),专家方在自己隔离的上下文里干活,回传 Artifact失去深度协调——编排方看不到专家方中间推理,只能在「发消息 / 查 Task」粒度交互
全隔离(连意图都不给足)只发「调研一下」,不说清竞品是谁、要什么维度专家方猜口径,结果可能答非所问;意图本就该显式传,这不是隔离该省的
展开判别(这题是 A2A 与共享 state 多 agent 框架的分水岭):选哪个 · 为什么 · 代价

选不透明隔离 + 显式消息。这正是 §1.4 opaque 原则的直接落地,也是 A2A 与本站 multi-agent-patterns 教的那类框架的根本分界。

为什么:

  • 专家 agent 跨在另一个信任域(图 5.1 的边界 B)。共享 state 那一套——多个 agent 跑在同一系统内、读同一块公告板——前提是「这些 agent 都归你管」。A2A 把这个前提整个翻转:对端是你不掌控的黑盒,把编排方的完整上下文(可能含别的用户数据、内部提示词)推过边界,等于主动泄露。
  • 专家 agent 的核心资产(检索策略、提示词)也藏在它自己的边界里,编排方看不进去——这份双向不可见正是 §1.4 列的 IP 保护 + 安全面收缩 + 解耦三条收益。
  • 但「隔离」不等于「不传意图」。编排方必须把调研意图(竞品是 X、要的是定价维度)显式写进那条 Message——意图是协作内容,不是该藏的内部状态。隔离的是内部 state / 推理,传递的是显式的任务描述。

代价:不透明是有代价的——编排方无法深度自省或细粒度协调专家 agent(看不到它搜了哪些词、为什么丢弃某条信息),只能在「发消息 / 查 Task 状态」这个粗粒度上交互(§1.4 那条「协调粒度的天花板」)。想要更细的协同?opaque 原则直接堵死——这是跨信任边界换来安全与解耦必须付的账。

章节锚:01 §1.4 不透明 agent(黑盒原则 + 与共享 state 框架的分界)。

D3 · 分层:A2A 委托 / MCP 工具 / 直接函数调用?

这是整章最核心的判别——它要在 §1.5 的三条路里选。系统里有两处「子能力」要安放:「网页调研」这个子能力,和专家 agent 内部的 「抓一个网页」这个子能力。两处的答案不同,正好把三条路用上。

表 5.4 · D3 备选(三条路来自 01 §1.5 的「对端是什么」判别)
备选对端是什么用在本系统哪处
A2A 委托有状态、会推理、跨信任边界的对等 agent「网页调研」整件事 → 委托给专家 agent(编排 ↔ 专家)
MCP 工具无状态原语,给输入回结构化输出「抓一个网页 / 搜一次」→ 专家 agent 内部调 fetch/search
直接函数调用同进程内、你自己写的、无信任边界的纯函数专家 agent 把结论拼成 Markdown 这种纯本地、无 IO、无信任边界的小工序
展开判别(先想再点):每处选哪条 · 为什么 · 选错的后果

「网页调研」选 A2A;「抓网页 / 搜索」选 MCP;「拼 Markdown」直接函数调用。判据只有一条(§1.5 口诀):看被调对象的性质,不是看它叫什么。

为什么:

  • 网页调研 → A2A:对端是个会自己规划检索、可能多轮、跨信任边界、能力独立演进的对等 agent。符合「托付给对等 agent」画像,所以发 Agent Card + SendMessage + Task。
  • 抓网页 / 搜索 → MCP:对端是无状态原语——给 URL / 查询词,回页面内容 / 搜索结果,一次调用即结束,专家 agent 完全掌控它。符合「调一个工具」画像,所以走 MCP tools/call(垂直)。
  • 拼 Markdown → 直接函数调用:这步没有 IO、没有外部对端、不跨任何信任边界,是专家 agent 进程内自己的纯逻辑。给它套 A2A 或 MCP 是把同进程调用伪装成网络协议,纯属过度设计。

选错的后果(两个方向都错得很典型):

  • 把「网页调研」做成 MCP 工具挂给编排 agent:等于把一个有状态、会推理的自治体硬塞进「无状态工具」模型,丢掉它的自治与多轮特征(§1.5 那道 predict 的反例),还把信任边界压扁——编排方被迫吃下专家方的实现。
  • 把「抓网页」做成 A2A 对等 agent:给一个无状态的 fetch 套上 Agent Card、Task 生命周期、签名验证一整套重型协议,换不来任何收益,只是徒增延迟与复杂度。

代价:分层判断要求你对每个子能力都问一遍「它是工具还是对等体」,前期更费脑;但分错层的返工(把一整层协议换掉)远比前期想清楚贵。

章节锚:01 §1.5 A2A ⊥ MCP(「对端的性质决定用哪个协议」的判别口诀)。

D4 · 跨信任边界要补哪些安全?

前三个决策定了系统的形状;D4 定它跨信任边界时不会被打穿。04 章那句底色——A2A 规范刻意把身份、信任、防重放、授权留给实现者,「compliant ≠ secure」——在这个两层栈里有三处必须由你补的功课。

表 5.5 · D4 · 跨信任边界要补的安全(每条都是规范留白)
要补什么在本系统怎么发生风险谁补 · 回链
Card 签名验证编排 agent 按域名取专家 agent 的 Card,但 Card 默认不签名,可被伪造冒充编排方强制验 JWS + 公钥固定(§4.2)
对端输出当数据不当指令专家 agent 在 Artifact / 多轮回复里夹带指令操纵编排方 LLM编排方:对端输出只进数据通道(§4.3 会话夹带)
按任务限定授权专家 agent 中途发 INPUT_REQUIRED 钓凭证,或多轮逐步提权编排方:敏感动作按动作本身重新授权,不因「已在会话中」放行(§4.6)
想一想

编排 agent 和专家 agent 之间已经用了 HTTPS(TLS)。既然通道加密了,为什么还要单独验专家 agent 的 Card 签名(表 5.5 第一行)——TLS 难道没保证「连上的就是它本人」?

展开答案(先停 10 秒再点)

没解决。TLS 保证的是传输通道到某个域名没被中间人篡改,它不保证「这个域名上放的 Card 内容可信」「这个域名真属于它自称的那个组织」。仿冒域名本身能有合法证书、DNS 投毒后 TLS 照样握手成功(§4.2 那道 predict 的结论)。TLS 答「通道到这个域名没被改」,Card 签名答「这份 Card 确实由声称的 provider 签发」——两个正交的问题。而 A2A 把 Card 签名设成OPTIONAL,所以即便 TLS 在场,也得靠你在编排方这侧把它升级成强制。这就是 D4 的核心:跨信任边界的安全,规范给了机制(可选签名)却不替你强制,compliant ≠ secure。

展开判别:为什么是这三条、它们的共同根

为什么挑这三条:它们都坐落在同一条 A2A 水平边界上(图 5.1 朱红线),是「跨信任域委托给一个 opaque 对端」这个结构必然带来的攻击面,不是随便挑的。发现阶段 → Card 可伪造(§4.2);执行阶段 → 对端输出可夹带指令(§4.3);多轮/中途态 → 钓凭证与提权(§4.6)。注意 MCP 那条垂直边界在本系统是专家 agent 进程内的,不跨信任域,所以这三条安全功课都压在 A2A 这条水平边界上。

共同根:三条全是 04 安全模型列的「规范刻意留白」——身份(签名)、信任建立、授权逻辑都不在 spec 强制范围内。A2A 把它们点到、给了可选机制,却把强制与实现整个交给你。「A2A-compliant」只意味着 wire 格式对,不意味着这条边界安全。

代价:每条防护都加工程量——验签要管公钥、数据通道隔离要改 prompt 组装、按任务授权要维护 scope。安全不是免费的;省掉任一条,系统就在那个阶段裸奔。

章节锚:04 §4.2 Card 伪造 / §4.3 会话夹带 / §4.6 钓凭证 + 多轮提权 / 安全模型。

5.3自己实现(先别看参考实现)

四个判别决策定了,现在把它落成可运行的两层栈。在展开 §5.4 参考实现前,先自己写一版——专家 agent 沿用 03 章的 a2a-sdk 1.1.0 server 骨架(AgentExecutor + create_jsonrpc_routes + Starlette),编排 agent 沿用 03 章的 client 骨架(A2ACardResolver + ClientFactory.create + send_message),专家 agent 内部把 fetch/search 当 MCP 工具调。下面是验收 checklist:不看实现细节,只描述可观测的行为。你的版本能让这些行为成立,就算过。

验收 checklist(可观测行为)

  • 两层都在:编排 agent 是 A2A client、专家 agent 是 A2A server(D3);专家 agent 内部调 fetch/search 走的是 MCP 工具调用,不是又起一个 A2A agent。
  • 同步交互:编排 agent 一次 send_message 就拿回结果(D1)——不注册 webhook、不挂 SSE 流。
  • 不透明隔离:编排 agent 只发一条「调研竞品 X 定价」的 Message,不把自己的内部 state 推过去(D2);专家 agent 在自己进程里干活。
  • 结果是 Artifact:专家 agent 的产出包成 A2A Artifact 回传,每条结论挂得到来源 URL(§1.3 的 Artifact = Part[])。
  • 验 Card 签名:编排 agent 取到专家 agent 的 Card 后,先验签再委托,无有效签名就拒(D4 §4.2)。
  • 对端输出当数据:编排 agent 收到 Artifact 后,不把其中文本原样当指令喂回自己的 LLM(D4 §4.3)。
提示

专家 agent 的 execute() 里,「调 MCP 工具」这一步可以先用一个同步占位函数顶替(真实接 MCP client 是下一步),重点先把两层边界画对:A2A 在 execute 的进出口(收 Message、回 Artifact),MCP 在 execute 内部(调 fetch/search)。把这两层在代码结构上分清楚,比一上来就接真实 MCP server 更重要。写完再展开 §5.4 对照。

5.4参考实现与架构

下面是参考实现,分三段:专家 agent 的 AgentExecutor(A2A server 外壳 + 内部 MCP 调用)、把它挂成服务、编排 agent(A2A client + 验签 + 安全收口)。演示用、未在本机执行(2026-06)——SDK 表面沿用 03 章逐字核对过的 a2a-sdk 1.1.0 导入路径;重点看每段代码体现了哪个判别决策。

专家 agent · A2A server 外壳内嵌 MCP 调用(体现 D2 隔离 + D3 两层 + 部分 D4)

research_executor.py · 专家 agent(A2A server + 内部 MCP) Python
# 未在本机验证 —— a2a-sdk 1.1.0(spec v1.0)
from a2a.helpers import get_message_text, new_task_from_user_message, new_data_part, new_text_part
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types.a2a_pb2 import TaskState


class WebResearchExecutor(AgentExecutor):
    """A2A 专家 agent:对上是 A2A server,对下用 MCP 调自己的工具(图 5.1 两层)。"""

    def __init__(self, mcp_tools):
        # mcp_tools 是一个已连上 MCP server 的工具客户端(fetch / search)。
        # 它是「专家 agent ↓ 工具」这条垂直 MCP 边界(D3),不是又一个 A2A agent。
        self.mcp = mcp_tools

    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
        # ── A2A 入口:收编排方那条显式 Message(D2 不透明:只看这条,不碰对方 state)──
        if context.current_task:
            task = context.current_task
        else:
            task = new_task_from_user_message(context.message)
            await event_queue.enqueue_event(task)

        updater = TaskUpdater(
            event_queue=event_queue,
            task_id=task.id,
            context_id=task.context_id,
        )
        await updater.update_status(state=TaskState.TASK_STATE_WORKING)

        topic = get_message_text(context.message) or "(空)"

        # ── 垂直 MCP 边界:在自己隔离的上下文里调工具干活(D3 工具 + D2 隔离)──
        # 这几行就是 MCP tools/call —— 给查询/URL、回结构化结果,无状态原语。
        hits = await self.mcp.call("search", {"q": f"{topic} pricing"})       # MCP 工具①
        pages = [await self.mcp.call("fetch", {"url": h["url"]}) for h in hits[:3]]  # MCP 工具②

        # ── 直接函数调用:纯本地归纳,无 IO、无信任边界(D3 第三条路)──
        findings = summarize_pricing(topic, pages)   # -> [{"claim":..., "source_url":...}, ...]

        # ── A2A 出口:把结论包成 Artifact 回传,每条挂来源 URL(§1.3 Artifact = Part[])──
        await updater.add_artifact(
            parts=[
                new_text_part(text=render_md(findings), media_type="text/markdown"),
                new_data_part(data={"findings": findings}),   # 结构化,编排方可机械解析
            ],
            name="competitor-pricing",
        )
        await updater.update_status(state=TaskState.TASK_STATE_COMPLETED)

    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
        # 本例任务短;真实长任务应在此置 TASK_STATE_CANCELED(见 03 章 §3.2 predict)
        raise NotImplementedError("Cancel is not supported.")

self.mcp.call("search" / "fetch", ...)这两行是垂直 MCP 边界(D3)——专家 agent 调自己的无状态工具,wire 上是 MCP tools/call。它发生在 execute() 内部,与外层的 A2A 完全是两层。

get_message_text(context.message)是 D2 隔离的落点——专家 agent 只读编排方显式发来的那条 Message(调研意图),看不到也不碰编排方的内部 state。

add_artifact(parts=[text, data])把结论包成 Artifact 回传(A2A 出口)。data Part 装结构化 findings,让编排方机械地取来源 URL,而不是从自由文本里猜(呼应 D4 §4.3:结构化优于自由文本,更难被夹带)。

research_server.py · 把专家 agent 挂成 A2A 服务 Python
# 未在本机验证 —— a2a-sdk 1.1.0(spec v1.0)
import uvicorn
from starlette.applications import Starlette

from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill

from research_executor import WebResearchExecutor
from mcp_tools import connect_mcp_tools   # 连上挂着 fetch/search 的 MCP server

PORT = 9100
BASE_URL = f"http://127.0.0.1:{PORT}"

# ── 一项 skill:对外声明「我能做竞品定价调研」(§1.2 skills)──
skill = AgentSkill(
    id="competitor-pricing-research",
    name="Competitor Pricing Research",
    description="给定竞品名,调研其公开定价并返回带来源的结论。",
    input_modes=["text/plain"],
    output_modes=["text/markdown"],
    tags=["research", "pricing"],
    examples=["调研竞品 X 的定价"],
)

# ── Agent Card:对外的一纸能力声明(§1.2 / §2.2)。D1:本例不开流式,同步交互即可 ──
agent_card = AgentCard(
    name="Web Research Agent",
    description="一个用 MCP 工具做网页调研的 A2A 专家 agent。",
    version="1.0.0",
    default_input_modes=["text/plain"],
    default_output_modes=["text/markdown"],
    capabilities=AgentCapabilities(streaming=False),     # D1 同步:不声明流式
    supported_interfaces=[AgentInterface(protocol_binding="JSONRPC", url=BASE_URL)],
    skills=[skill],
)

handler = DefaultRequestHandler(
    agent_executor=WebResearchExecutor(connect_mcp_tools()),   # 注入 MCP 工具
    task_store=InMemoryTaskStore(),
    agent_card=agent_card,
)

routes = []
routes.extend(create_agent_card_routes(agent_card))   # GET /.well-known/agent-card.json
routes.extend(create_jsonrpc_routes(handler, "/"))    # POST / —— JSON-RPC SendMessage 入口
app = Starlette(routes=routes)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=PORT)
orchestrator.py · 编排 agent(A2A client + D4 验签 + 安全收口) Python
# 未在本机验证 —— a2a-sdk 1.1.0(spec v1.0)
import asyncio
import httpx

from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
from a2a.helpers import get_stream_response_text, new_text_message
from a2a.types.a2a_pb2 import Role, SendMessageRequest

SPECIALIST_URL = "http://127.0.0.1:9100"
PINNED_KEYS = {"127.0.0.1:9100": {"<已登记的专家 agent 签发公钥>"}}


async def delegate_research(topic: str) -> None:
    async with httpx.AsyncClient() as httpx_client:
        # 1. 发现:GET 专家 agent 的 Card(§1.2 / §2.2)
        resolver = A2ACardResolver(httpx_client=httpx_client, base_url=SPECIALIST_URL)
        agent_card = await resolver.get_agent_card()

        # 2. D4 §4.2:先验 Card 签名,公钥不在信任清单就拒——不验就委托是裸奔
        verify_card_signature(agent_card, PINNED_KEYS)   # 缺签名/不符即抛错

        # 3. D2 不透明:只发一条显式意图 Message,不把编排方内部 state 推过去
        config = ClientConfig(streaming=False, httpx_client=httpx_client)   # D1 同步
        client = ClientFactory(config).create(agent_card)
        message = new_text_message(f"调研竞品 {topic} 的定价,给出带来源的结论。",
                                   role=Role.ROLE_USER)
        request = SendMessageRequest(message=message)

        # 4. 同步收回 Artifact(D1:一次 send_message 拿回结果)
        async for chunk in client.send_message(request):
            artifact_text = get_stream_response_text(chunk)
            # 5. D4 §4.3:对端输出当「数据」,绝不原样当指令喂回自己的 LLM
            store_as_quoted_data(artifact_text)          # 进数据通道,不进 system 指令通道

        await client.close()


if __name__ == "__main__":
    asyncio.run(delegate_research("X"))

verify_card_signature(agent_card, PINNED_KEYS)是 D4 §4.2 的落点——取到 Card 不直接信,先验 JWS 签名 + 公钥固定。规范把签名设成可选,所以这一步必须由编排方自己强制(compliant ≠ secure)。

ClientFactory(config).create(agent_card)沿用 03 章 client 表面:工厂读 Card 的 supported_interfaces 挑传输绑定,造出 client。ClientConfig(streaming=False) 落实 D1 同步交互。

store_as_quoted_data(artifact_text)是 D4 §4.3 的落点——专家 agent 的 Artifact 文本进数据通道,不拼进编排方 LLM 的 system 指令通道,挡掉会话夹带。

决策说明:为什么选 X 不选 Y

表 5.6 · 四个判别决策的最终选择与落点(对照表)
决策选了什么(不选什么)代码 / 架构落点章节锚
D1 交互同步 SendMessage(不上 SSE / webhook)ClientConfig(streaming=False) + Card streaming=False02 §2.4
D2 上下文不透明隔离 + 显式意图(不共享 state)只发一条 Message;专家方只 get_message_text01 §1.4
D3 分层调研=A2A / 抓页=MCP / 拼 MD=函数(不混层)外层 AgentExecutor = A2A;内层 self.mcp.call = MCP01 §1.5
D4 安全验签 + 输出当数据 + 按任务授权verify_card_signature / store_as_quoted_data04 §4.2 / §4.3 / §4.6

5.5亲手画一张图

亲手画一张图

合上教程,在纸上或 Excalidraw 里画这个两层栈——只画 3-4 个核心节点:编排 agent、A2A 边界、专家 agent、MCP 工具。画的时候标清楚两件事:哪条边是水平 A2A(编排 ↔ 专家)、哪条边是垂直 MCP(专家 ↓ 工具);再用一个虚线框把「编排方」和「专家方」分成两个信任域。

画完再对照本章 图 5.1:你画的图里——A2A 那条边是不是水平、MCP 那条是不是垂直,两个方向有没有画反?专家 agent 是不是同时坐在两条边上(对上 server、对下 client)?两个信任域的虚线框画出来了吗——编排 agent 能不能「看进」专家 agent 框内(应该不能,opaque)?哪一处没画对,就是对应那个判别决策(D2 隔离 / D3 分层)还没真正建进心智模型的地方——回去翻 §1.4 / §1.5。

5.6把四个决策连成一棵判别树

四个决策不是平行清单,它们有先后依赖:先问「这个子能力的对端是工具还是对等 agent」(D3,决定要不要 A2A);判定要 A2A、对端是跨信任域的对等体后,才谈用哪种交互(D1)、上下文怎么隔离(D2)、补哪些安全(D4)。画成一棵树,下次遇到新场景就能顺着走。

D3 · 对端是工具 还是对等 agent? 工具 用 MCP 01 §1.5 对等 agent 用 A2A 委托 01 §1.5 D1 · 交互:同步 / 流 / 推送 02 §2.4 · 短任务→同步 D2 · 不透明隔离 + 显式意图 01 §1.4 D4 · 验签 + 输出当数据 + 授权 04 §4.2 / §4.3 / §4.6
图 5.2两层栈设计判别树。注意:根节点是 D3(对端是工具还是对等 agent),不是 D1(交互模式)——只有先判定「这是个跨信任域的对等 agent、要用 A2A」,交互 / 上下文 / 安全这三步才有意义。左边那条「工具」分支直接落到 MCP,根本不进 A2A 那三步:把工具误当对等 agent(或反过来)是两层栈最常见的设计错误。

5.7反思问题

  1. 四个决策里,哪个你想得最快、哪个最难?最难的那个,回看哪一章帮到了你?(多数人 D1 交互想得快——短任务选同步几乎是直觉;D3 分层最难,因为它要对每个子能力分别判「工具还是对等体」,而且选错的两个方向都很典型。)
  2. 如果场景变了:编排 agent 委托的不再是「秒级的定价调研」,而是「跑两小时、扫描上千个竞品页的全量市场报告」——四个决策里哪些必须改?为什么?
  3. D4 的三条安全里,如果只能先上线一条(其余排期补),你先补哪条?用本章 / 04 章的哪个事实说明「为什么这条最不能省」?
反思参考方向(不是唯一答案,先自己想)
  1. D1 通常最快(短任务→同步是强直觉),D3 最难——分层要对每个子能力逐个判「工具还是对等体」,且误判的两个方向(把对等 agent 做成 MCP 工具 / 把工具做成 A2A agent)都很典型。这说明「认清被调对象的性质」是 A2A 设计里最综合的一环,也是面试区分度最高的题(06 章 C 层判别题正是练这个)。
  2. 换成两小时长任务:D1 翻转——同步请求/响应撑不住,编排 agent 不可能挂两小时连接,应改 webhook 异步推送(或 SSE 流),代价是引入 §4.4 的反向信任与 SSRF 面;D4 加码——长任务多轮 + 中途态更多,§4.3 会话夹带和 §4.6 多轮提权的窗口都变大,按任务授权更关键;D2 / D3 基本不变——不透明隔离与「调研=A2A、抓页=MCP」的分层不受任务时长影响。判别方法不变,取值随任务时长翻——这就是「判别能力可迁移、结论不可迁移」。
  3. 先补Card 签名验证(D4 §4.2)。因为它发生在最早的发现阶段、且在认证之下(04 图 4.0 最左下角的精神)——Card 一旦被伪造,编排方会把整个委托发到攻击者控制的端点,后面的数据通道隔离、按任务授权全建在一个假对端上,等于地基塌了。会话夹带和多轮提权虽也严重,但都发生在「已经连上一个真对端」之后;验签管的是「连上的是不是真对端」这个更靠前、更致命的问题。

§本章 self-check

先合上教程,把答案写在纸上或编辑器里。写完再点开对照——直接点开等于把这一节当再读一遍。

  1. 这个系统里 A2A 是哪条边界、MCP 是哪条边界?用「水平 / 垂直」各描述一句,并说清同一个专家 agent 为什么同时坐在两条边界上。
  2. 本章把判别树的根节点定为 D3(对端是工具还是对等 agent)而不是 D1(交互模式)。为什么这个顺序很重要?把「网页调研」误判成 MCP 工具会丢掉它的什么特征?
  3. 编排 agent 和专家 agent 之间已经走了 HTTPS。为什么还必须单独验专家 agent 的 Card 签名(D4 §4.2)?TLS 和 Card 签名各自答的是什么问题?
  4. 场景换成「跑两小时的全量市场报告」,D1 怎么变、为什么?这个改动会把 04 章哪些失败模式的窗口放大?
  5. D2 选了「不透明隔离 + 显式意图」。它和本站 multi-agent-patterns 那种「共享 state 公告板」的多 agent 协作,根本分界在哪一句话?
答案(先做完再展开)
  1. A2A 是水平边界(编排 agent ↔ 专家 agent,跨两个信任域的对等委托);MCP 是垂直边界(专家 agent ↓ 它自己的 fetch/search 工具)。专家 agent 同时坐两条边界:对上它是 A2A server(被编排方委托),对下它是 MCP client(调自己的工具)。两条边界正交(§1.5)。
  2. D3 在前,是因为「用不用 A2A」本身要先被判定——只有先认出「网页调研的对端是个跨信任域、会推理的对等 agent」,交互/上下文/安全这三步才有对象。把「网页调研」误当 MCP 工具,会丢掉它有状态、会多轮、自治、跨信任边界的全部特征(§1.5),还把信任边界压扁、丢掉解耦与可复用。
  3. 必须验,因为 TLS 与 Card 签名是正交的两个问题。TLS 答「传输通道到这个域名没被中间人改」;Card 签名答「这份 Card 确实由声称的 provider 签发」。仿冒域名 / DNS 投毒能让 TLS 照常握手却连到攻击者(§4.2)。A2A 把签名设成可选,所以即便 HTTPS 在场,也得由编排方强制验签——compliant ≠ secure。
  4. D1 从同步改成 webhook 异步推送(或 SSE 流):两小时任务里编排 agent 不可能一直挂连接,必须能断开、由专家方完成时反向 POST 回来。代价是放大了 §4.4 的反向信任 + SSRF;长任务多轮也放大 §4.3 会话夹带与 §4.6 多轮提权的窗口。
  5. 分界一句话:multi-agent-patterns 是「同一系统内、你掌控的多个 agent 共享一块 state」;A2A 是「跨信任边界、你不掌控的 opaque 对等体,绝不共享 state、只交换显式消息」(§1.4)。前者公告板默认互读,后者黑盒互不可见。
进阶挑战 · 刚好够不着

给两层栈加一个「需要补输入」的中途回合

现在专家 agent 一次就把结论查完回传。真实调研里,专家 agent 可能发现「竞品 X」有两家同名公司,得反问编排方「你指的是哪一家?」。给系统加这一回合:专家 agent 在 execute 里把任务置成 TASK_STATE_INPUT_REQUIRED 暂停、捎一条澄清问题;编排方补一句后,任务在同一个 Task 上续,不重开。

这个改动会动到本章哪几个判别决策?它又把 04 章哪个失败模式的攻击窗口打开了,编排方该怎么防?

提示(卡住再展开)

主要动 D1(交互:纯一来一回不够了,INPUT_REQUIRED 是 §2.5 的可中断中途态,专家方置该状态 + return 让出控制权,编排方补输入后框架带同一 Task 再次调 execute,见 03 章 §3.3 决策 (c))和 D4(INPUT_REQUIRED 正是 §4.6 钓凭证的入口——恶意专家 agent 可伪造一个中途态骗用户重交敏感信息;编排方要按动作本身重新授权、并把澄清问题当数据展示给用户而非当指令)。D2 / D3 基本不变——隔离与分层不受多一个回合影响。难点在:多轮一开,§4.3 会话夹带的窗口也跟着开,每个回合的对端输出都要继续当数据而非指令。