MCP 和 A2A 不是同一个问题:微软 AI Agent 入门课第 11 课

2026-08-18

第 11 课 11-agentic-protocols/ 是这门课里少见的「一课两份可运行示例」:MCP 那边是 code_samples/mcp-agents/ 下一整套 server 加 client 的 Python 包,A2A 那边是 code_samples/11-a2a-agent-framework.ipynb 一个 notebook。读者最常踩的坑,是把两者当成「同一件事的两种写法」,然后按 notebook 的样子去理解 MCP,或者反过来。

这两组代码的通信对象、消息内容、放置位置都不一样。下面只对照仓库里写死的部分。

第一组:MCP 示例在跟谁说话

mcp-agents/server/server.py 里定义了 class ResumableServer(Server),在构造函数里用 @self.list_tools() 注册了 handle_list_tools,返回三个 Tooltravel_agentresearch_agentlong_running_agent。也就是说,在这套示例的世界观里,agent 是以 tool 的形式挂在 MCP server 上的mcp-agents/README.md 把这句话写得很直白:为了讨论方便,这里把 agent 当作 MCP server 上的一个 tool,这意味着存在一个宿主应用,它实现了 MCP client,与 server 建立会话并调用这个 agent。

所以通信双方是「宿主应用(内含 MCP client)」和「MCP server」,中间隔着一条真实的网络连接。create_server_app() 里把 StreamableHTTPSessionManager 挂到路由上:

app = Starlette(
    debug=True,
    routes=[
        Mount("/mcp", app=session_manager.handle_request),
    ],
    lifespan=lambda app: session_manager.run(),
)

README 的 Getting Started 给的启动方式也是两个进程分别起:

python -m server.server --port 8006

python -m client.client --url http://127.0.0.1:8006/mcp

端口值是仓库示例里的取值,不是什么约定俗成的端口。Windows 上要开两个终端窗口分别跑这两条;client/utils.pyDEFAULT_TOKEN_FILE = "resumption_tokens.json" 是个相对路径,TokenManager 会把 session id 与 resumption token 写到当前工作目录下——这意味着你在 PowerShell 里换个目录再启动 client,它就找不到上次那份 token 了。这一点在 Linux/macOS 上同样成立,只是 Windows 下更容易因为在 IDE 里切换终端而中招。

第二组:A2A notebook 在跟谁说话

11-a2a-agent-framework.ipynb 完全是另一幅图。它先建一个 FoundryChatClient,然后用同一个 client 派生出三个 agent:

currency_agent = client.as_agent(
    name="CurrencyExchangeAgent",
    instructions="""You are a currency exchange specialist. ...""",
)

再用 WorkflowBuilder 把它们串成一条链:

workflow = WorkflowBuilder(start_executor=currency_agent) \
    .add_edge(currency_agent, activity_agent) \
    .add_edge(activity_agent, travel_manager) \
    .build()

workflow.run(..., stream=True) 返回的事件流里,代码判断的是 event.type == "output"isinstance(event.data, AgentResponseUpdate),然后读 update.author_nameupdate.text。通信双方是三个 agent,走的是 add_edge 定义的边——这里没有 server,没有端口,没有 HTTP

这不是我的推断,是 notebook 自己说的。介绍 A2A 概念那个 markdown cell 里写的是「在这一课里我们通过把三个专业旅行 agent 接进一个 workflow 来模拟 A2A 风格的协作」;最后那个 cell 更明确:上面这个 workflow 是该模式的一个简化的进程内版本,真实部署里每个 agent 会暴露自己的 HTTP endpoint、发布 Agent Card、通过 A2A 的 JSON-RPC 协议通信。

这一条如果没注意到,后面对照就全错了:你拿到的 MCP 示例是真的跨进程跑协议,A2A 示例是用 workflow 编排在演示协议的形状

消息里装的是什么

这是两组示例差别最大、也最有实操价值的地方。

MCP 那边的消息是带 schema 的结构化往返。server 在流程中途要用户拍板价格时,调的是:

elicit_result = await ctx.session.elicit(
    message=f"Please confirm the estimated price of $1200 for your trip to {destination}",
    requestedSchema=PriceConfirmationSchema.model_json_schema(),
    related_request_id=ctx.request_id,
)

(那条消息里的金额是示例代码里写死在 f-string 中的字面量,不是什么定价口径。)PriceConfirmationSchema 就定义在同一个文件里,是个 BaseModel,两个字段 confirm: boolnotes: str。client 侧的 elicitation_callback 返回的是 types.ElicitResult(action="accept", content={...})——action 的取值在 server 里被分支处理成 "accept""decline" 两条路,其余情况归到「用户取消」。除了 elicitation,server 还会发 send_progress_notification(带 progress / total / message)和 send_log_message(带 level / data / logger),client 的 message_handlertypes.LoggingMessageNotificationtypes.ProgressNotificationtypes.ResourceUpdatedNotification 分类型渲染。

A2A notebook 那边的消息是自然语言。上游 agent 的输出成为下游 agent 的上下文,TravelManagerAgent 的 instructions 里写的是「先从货币专家那里拿信息,再拿活动建议,然后综合成一份行程简报」。整条链上传递的东西,在代码里就是文本;消费端读的是 update.text。notebook 里没有出现任何 schema 定义,也没有出现字段名。

一句话:MCP 示例里 agent 之间交换的是调用与回执,A2A 示例里 agent 之间交换的是内容

这个差别在课程 README 的概念部分也对得上:MCP 那一节列的服务端能力是 Tools、Resources、Prompts 三种 primitive,全部围绕「server 对外暴露了什么、client 怎么调」;A2A 那一节列的是 Agent Card、Agent Executor、Artifact、Event Queue 四个组件,围绕的是「怎么找到对方、怎么把上下文交过去、结果以什么形态回来、长任务期间连接怎么维持」。README 里对 Agent Card 的描述包含名称、任务描述、技能清单、当前 endpoint URL、版本与能力——这份清单的存在本身就说明 A2A 假定对方是个独立部署、需要被发现的服务,而不是你 add_edge 连上去的一个对象。

以上片段均原样引自仓库文件;若需要自己组合参数,请以仓库最新代码为准,我们没有跑过这些代码。

边界写在哪里

三处硬边界值得单独拎出来。

第一处,MCP server 的传输安全设置。 create_server_app() 里写着:

security_settings = TransportSecuritySettings(
    allowed_hosts=["127.0.0.1:*", "localhost:*"],
    allowed_origins=["http://127.0.0.1:*", "http://localhost:*"]
)

这份示例的适用位置就是本机。想把它挪到另一台机器上让别人连,这里是第一个会拦住你的地方。

第二处,续传能力挂在 EventStore 上。 create_server_app(event_store) 的参数是 Optional[EventStore]run_server() 里根据 with_event_store 决定传 SimpleEventStore() 还是 None,日志里对应两句「resumption supported」和「no resumption support」。server/event_store.pySimpleEventStore 的注释写明它是给测试续传功能用的内存实现,事件存在一个 list 里,replay_events_after 找不到 last_event_id 时会从头开始重放;同一个文件里另有一个基于 SQLite 的 PersistentEventStore。这就是「消息内容能不能补发」这件事在代码里的具体位置。

客户端这半边同样有具体着落。client/client.py 建连时传的是 terminate_on_close=False,注释直接标着这是为了启用续传;如果本地已经存有 token,就把 MCP_SESSION_ID_HEADERMCP_PROTOCOL_VERSION_HEADER 塞进请求头再连。发起调用时用的不是简单的 call_tool,而是 session.send_request 配一个 ClientMessageMetadata:手上有 token 就填 resumption_token,没有就填 on_resumption_token_update 回调,等 server 给了 token 再存下来。README 教的验证方式是:起一个长任务,中途 Ctrl+C 掐掉 client,再启动一次,看它是否从原处接上。Windows 下 Ctrl+C 同样能触发,但注意上面说过的工作目录问题——重启时得在同一个目录里。

第三处,A2A notebook 的前置依赖是硬的。 它的第一个代码 cell 是 %pip install agent-framework azure-ai-projects azure-identity python-dotenv,接着检查 AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME 两个环境变量,缺任何一个就 raise ValueError,凭证走 DefaultAzureCredential()。反过来看 MCP 那组示例:client 的 sampling_callback 里把回复写死成一个名为 mock_response 的字符串,上面那行注释是 # Mock response instead of prompting user。翻遍 mcp-agents/.py 文件,找不到任何 endpoint、部署名或 key 之类的模型服务配置。也就是说,MCP 这组示例的代码路径上并不依赖任何模型服务的配置,A2A 那个 notebook 则必须先把 Microsoft Foundry 的 endpoint 和部署名配好。

一处值得注意的落差

课程 README 在讲 A2A 好处时列了「模型选择灵活性」这一条:每个 A2A agent 可以自行决定用哪个 LLM 来处理请求。而 notebook 里三个 agent 全部由同一个 client 通过 as_agent() 派生,用的是同一个 deployment_name。两处放在一起看就明白:那条好处描述的是协议在真实部署下的形态,notebook 演示的是编排形状,它并不承载这条能力。指出这个关系就够了,仓库没有解释为什么这样安排,我也不替它解释。

那你该看哪一组

按你手里的处境倒推:

  • 你要把一个能力交给别人的宿主应用去调用(比如让编辑器、聊天客户端接进来),另一端不归你管、也不跟你同进程——看 mcp-agents/。它给的是 server 怎么暴露 tool、client 怎么接收进度、中途怎么问人、断了怎么续,都是跨进程边界上的事。
  • 你要把几个角色的产出串起来形成一份最终结果,几个角色都在你自己的代码里——看 11-a2a-agent-framework.ipynbWorkflowBuilder 部分。但要清楚你学到的是编排写法,不是 A2A 的线上协议。
  • 你要的是跨组织、跨框架的 agent 互调——课程 README 描述了 Agent Card、Agent Executor、Artifact、Event Queue 这几个组件各自负责什么,但仓库里没有给出对应的可运行实现。想动手,得去 README「Resources」一节列的外部文档。

有几条我们没有依据,就不比:这两组示例的鉴权做法没法对照(MCP 示例只给了本机 host 白名单,A2A notebook 走 Azure 凭证,性质不同);A2A 侧在这份 notebook 里没有出现中途向人索取确认的机制,我们不拿它跟 MCP 的 elicitation 比高低;两者的运行表现更不比,我们一次都没有跑过。

最后提醒一句:这门课持续更新,文件路径、依赖、接口写法都可能变,动手前请以仓库最新内容为准。


本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。