拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent 架构分层实战:MCP、A2A 与 Agent Skills 协作指南

AI Agent 架构分层实战:MCP、A2A 与 Agent Skills 协作指南

1. 从一堆协议名词说起:Agent 生态到底在分层什么

过去一年里,只要你在做 AI Agent 相关的东西,几乎不可能绕开三个词:MCP、A2A、Agent Skills。我第一次同时看到这三个概念摆在一起的时候,脑子里第一反应是——这不就是当年微服务刚火时的那套分层逻辑吗?工具调用、服务间通信、能力封装,各管一段,谁也别越界。

但真正动手搭过几个 Agent 项目之后,我发现事情没那么简单。很多人(包括早期的我)会把这三个东西混为一谈,觉得"不都是让 Agent 干活用的吗"。结果就是架构设计的时候一团乱麻:工具注册写在哪、Agent 之间怎么传消息、可复用的能力怎么沉淀,全糊在一起。项目小的时候还能跑,一旦 Agent 数量上去、工具变多,维护成本直接爆炸。

所以这篇我想干的事很明确:把 MCP、A2A、Agent Skills 这三层拆开讲清楚,它们各自解决什么问题、边界在哪、怎么协作。这不是一篇概念科普,而是我在实际项目里踩过坑之后总结出来的分层思路。如果你正在做 Agent 开发,或者准备把单 Agent 扩展成多 Agent 系统,这篇应该能帮你少走点弯路。

先给个一句话的定位,方便你建立整体印象:

  • MCP管的是 Agent 和"外部世界"(工具、数据源、服务)之间的连接,是纵向的、向下的。
  • A2A管的是 Agent 和 Agent 之间的协作,是横向的、平级的。
  • Agent Skills管的是 Agent 自身能力的封装和复用,是内向的、可沉淀的。

这三层不是竞争关系,而是正交的。理解这一点,后面所有的设计决策都会顺很多。

2. MCP:Agent 连接外部工具的标准化接口层

2.1 MCP 到底解决的是什么问题

MCP 全称 Model Context Protocol,直译过来是"模型上下文协议"。名字听着玄乎,但它的本质非常朴素:给 Agent 提供一套标准化的方式去调用外部工具和数据源。

在 MCP 出现之前,我们是怎么让 Agent 用工具的?基本就是硬编码。比如你要让 Agent 查数据库,就在代码里写一个函数,然后把这个函数的描述塞进 prompt 里,模型输出一个 JSON,你再解析 JSON 去调函数。每个工具都要这么来一遍,工具一多,prompt 里塞满了函数描述,token 哗哗地烧,而且换个模型、换个框架就得重写。

MCP 的思路是把这件事标准化:工具提供方实现一个 MCP Server,Agent 侧实现一个 MCP Client,两边通过统一的协议通信。工具的描述、参数 schema、调用方式、返回格式,全部走协议约定。Agent 不需要知道工具内部怎么实现的,只需要知道"有这么个工具,参数是这样,返回是这样"。

我打个生活化的比方。以前你家里要装电器,每个电器的插头形状都不一样,你得准备一堆转接头。MCP 就像是统一了插座标准——不管你是冰箱、电视还是电脑,插头都是标准的三孔,插上就能用。工具就是电器,MCP Server 就是那个标准插座。

2.2 MCP 的核心机制拆解

MCP 协议本身基于 JSON-RPC 2.0,通信方式支持 stdio(标准输入输出)和 HTTP/SSE 等传输层。它的核心能力大概有这么几类:

能力类型作用典型场景
Tools暴露可调用的函数查数据库、调 API、执行命令
Resources暴露可读取的数据文件内容、配置、日志
Prompts暴露预设的提示模板常用任务模板、角色设定
Sampling反向请求模型生成Server 主动让 Client 侧模型补全

这里我要重点说一下Tools和Resources的区别,因为很多人会搞混。Tools 是"动作",是有副作用的,比如"发一封邮件";Resources 是"数据",是只读的,比如"读取某个文件的内容"。设计 MCP Server 的时候,把只读的数据访问做成 Resource,把有副作用的操作做成 Tool,这个边界划清楚,Agent 的行为会可控很多。

还有一个容易被忽略的点:MCP 的传输层选择。stdio 适合本地进程,启动快、无网络开销,但只能本机用;HTTP/SSE 适合远程服务,可以跨机器,但引入了网络延迟和鉴权问题。我在实际项目里的经验是,本地工具优先用 stdio,远程共享的服务用 HTTP,不要为了"统一"强行全用远程,那样调试起来会很痛苦。

2.3 实操:写一个最小可用的 MCP Server

光说概念没意思,直接上代码。下面是一个用 Python 写的最小 MCP Server,暴露一个查询天气的 Tool:

from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app = Server("weather-server") @app.list_tools() async def list_tools(): return [ Tool( name="get_weather", description="查询指定城市的当前天气", inputSchema={ "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "get_weather": city = arguments["city"] # 这里替换成真实的天气 API 调用 result = f"{city} 当前晴,气温 22 摄氏度" return [TextContent(type="text", text=result)] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())

这段代码的关键点在于inputSchema——它用的是 JSON Schema 标准,模型就是靠这个 schema 来理解"这个工具需要什么参数"的。schema 写得越清晰,模型调用越准。我见过太多人 schema 写得含糊,然后抱怨模型老是传错参数,其实问题出在自己身上。

提示:description 字段不要偷懒。模型判断"该不该调这个工具"完全依赖 description。写清楚"什么时候用、什么时候不用",比写一堆参数说明更有价值。

2.4 MCP 使用中的几个坑

第一个坑是工具数量爆炸。MCP 让加工具变得太容易了,于是很多人一口气挂几十个 MCP Server,每个 Server 又暴露十几个 Tool。结果就是每次对话都要把所有工具的 schema 塞进上下文,token 消耗巨大,而且模型在几十个工具里选,准确率直线下降。我的做法是按场景分组,动态加载——当前任务只挂相关的 MCP Server,不相关的先摘掉。

第二个坑是错误处理。MCP 调用失败时,返回的错误信息如果太技术化(比如一堆堆栈),模型根本看不懂,就会瞎猜。错误信息要写成模型能理解的自然语言,比如"数据库连接超时,请稍后重试",而不是"ConnectionError: timeout after 30s at line 42"。

第三个坑是鉴权。远程 MCP Server 一定要做鉴权,token 不要硬编码在客户端,走环境变量或者密钥管理服务。我见过有人把 token 直接写在前端代码里的,那基本等于裸奔。

3. A2A:Agent 之间的横向协作协议

3.1 为什么单靠 MCP 不够

MCP 解决的是"Agent 怎么用工具",但它没解决"Agent 怎么和另一个 Agent 协作"。

你可能会想:那我让 Agent A 把 Agent B 当成一个工具不就行了?理论上可以,但实践中有几个问题。第一,Agent B 本身可能也是个复杂的系统,有自己的状态、自己的工具集,把它降级成一个 Tool 会丢失很多信息。第二,Agent 之间的通信往往是双向的、多轮的,不是简单的"调用-返回"。第三,Agent 之间可能需要协商、委派、汇报,这些语义用 Tool 调用表达很别扭。

A2A(Agent-to-Agent)协议就是为这个场景设计的。它的核心是定义一套 Agent 之间通信的标准:怎么发现对方、怎么描述自己的能力、怎么发起任务、怎么汇报进度、怎么处理失败。

3.2 A2A 的核心概念

A2A 里几个关键概念,我用大白话解释一下:

Agent Card:每个 Agent 对外的一张"名片",描述自己是谁、能干什么、怎么联系。这有点像 MCP 里的 Tool schema,但描述的是整个 Agent 而不是单个工具。

Task:A2A 里的任务单元。一个 Agent 可以把一个 Task 委派给另一个 Agent,然后跟踪它的状态(submitted、working、completed、failed)。

Message 和 Artifact:Message 是通信内容,Artifact 是任务产出的结果。这个区分很重要——Message 是过程,Artifact 是结果。

Streaming 和 Push Notification:长任务不可能一直等着,所以 A2A 支持流式返回进度,也支持任务完成时主动推送通知。

我个人的理解是,A2A 之于 Agent,就像 HTTP 之于 Web 服务。它不规定你内部怎么实现,只规定你怎么和别人说话。

3.3 MCP 和 A2A 的边界在哪

这是最容易混淆的地方,我专门画个表对比一下:

维度MCPA2A
通信方向Agent → 工具(纵向)Agent ↔ Agent(横向)
对端性质无状态的能力提供方有状态的智能体
交互模式调用-返回为主多轮协商、委派、汇报
能力描述Tool schemaAgent Card
典型场景查库、调 API、读文件任务分解、专家协作、流水线

判断标准很简单:如果对端是个"死"的工具,用 MCP;如果对端是个"活"的 Agent,用 A2A。当然现实中会有模糊地带,比如一个包装得很像 Agent 的服务,这时候看它需不需要维护会话状态、会不会主动发起交互,就能判断出来。

3.4 实操:搭一个最小的 A2A 协作

假设我们有两个 Agent:一个"研究员"负责查资料,一个"写手"负责写报告。研究员通过 A2A 把写报告的任务委派给写手。

# 研究员 Agent 侧:发起任务委派 import httpx async def delegate_writing_task(topic: str, research_data: str): task_payload = { "task": { "type": "write_report", "input": { "topic": topic, "data": research_data } } } async with httpx.AsyncClient() as client: resp = await client.post( "http://writer-agent/a2a/tasks", json=task_payload, headers={"Authorization": "Bearer <token>"} ) return resp.json()["task_id"] # 写手 Agent 侧:接收并处理任务 from fastapi import FastAPI app = FastAPI() @app.post("/a2a/tasks") async def receive_task(payload: dict): task = payload["task"] if task["type"] == "write_report": # 实际处理逻辑 report = generate_report(task["input"]) return {"task_id": "task-001", "status": "completed", "artifact": report}

这个例子很简化,但核心逻辑清楚了:任务描述标准化、状态可追踪、结果以 Artifact 形式返回。真实项目里还要加上任务队列、重试机制、超时处理、进度回调,但骨架就是这样。

注意:A2A 协作最容易出问题的地方是任务边界不清。委派方以为对方会做 A+B,接收方只做了 A,结果就扯皮。所以任务描述一定要写清楚输入、输出、验收标准,宁可啰嗦也别含糊。

4. Agent Skills:可复用能力的封装与沉淀

4.1 Skills 和 Tool 的区别

很多人第一次听到 Agent Skills 会问:这不就是 Tool 吗?还真不是。

Tool 是原子能力,比如"读文件"、"发请求"、"查数据库"。Skill 是复合能力,是多个 Tool 加上决策逻辑、加上领域知识打包成的一个"技能包"。比如"写一份竞品分析报告"就是一个 Skill,它内部可能调用了搜索 Tool、读文件 Tool、写文件 Tool,还包含了一套分析框架和写作模板。

打个比方:Tool 是厨房里的刀、锅、铲,Skill 是"做一道宫保鸡丁"这道菜。你可以用同样的刀锅铲做出无数道菜,但每道菜有自己的配方和流程。

Skills 的价值在于沉淀。一个团队做 Agent 项目,如果每次都从 Tool 开始拼,效率极低。把常用的复合能力封装成 Skill,下次直接复用,这才是工程化的做法。

4.2 Skill 的典型结构

一个设计良好的 Skill 通常包含这几部分:

  • 元信息:名称、描述、适用场景、触发条件
  • 输入输出定义:需要什么参数,产出什么结果
  • 执行流程:分几步,每步做什么,用什么 Tool
  • 领域知识:prompt 模板、few-shot 示例、规则约束
  • 异常处理:失败了怎么办,什么情况该放弃

我特别想强调触发条件这一项。Skill 不是越多越好,关键是"什么时候该用这个 Skill"。如果触发条件写得模糊,Agent 会在不该用的时候乱用,反而添乱。

4.3 实操:封装一个"代码审查"Skill

class CodeReviewSkill: name = "code_review" description = "对给定的代码片段进行审查,输出问题列表和改进建议" trigger = "当用户提供代码并询问质量、bug、改进建议时使用" input_schema = { "type": "object", "properties": { "code": {"type": "string"}, "language": {"type": "string"}, "focus": {"type": "string", "enum": ["bug", "style", "security", "all"]} }, "required": ["code", "language"] } async def execute(self, code: str, language: str, focus: str = "all"): # Step 1: 静态检查(调用 lint Tool) lint_result = await self.call_tool("run_linter", { "code": code, "language": language }) # Step 2: 模型审查(用专门的 prompt) review_prompt = self.build_review_prompt(code, language, focus, lint_result) review_result = await self.call_llm(review_prompt) # Step 3: 汇总输出 return { "issues": lint_result["issues"] + review_result["issues"], "suggestions": review_result["suggestions"] } def build_review_prompt(self, code, language, focus, lint_result): # 这里放领域知识:审查框架、关注点、输出格式要求 return f"""你是一位资深 {language} 工程师。 请从 {focus} 角度审查以下代码。 已知静态检查结果:{lint_result} 代码: {code} 请按以下格式输出:..."""

这个 Skill 把"静态检查 + 模型审查 + 结果汇总"三步封装在一起,对外只暴露一个execute接口。下次任何 Agent 需要代码审查,直接调这个 Skill 就行,不用重新拼流程。

4.4 Skills 的版本管理

这是很多人忽略的一点。Skill 是会演进的——prompt 要调优、流程要改、依赖的 Tool 要升级。如果没有版本管理,今天跑得好好的 Skill,明天可能就崩了。

我的做法是给每个 Skill 打版本号,重大变更升大版本,小调整升小版本。Agent 调用时明确指定版本,避免"悄悄变了行为"。同时保留旧版本一段时间,方便回滚。

5. 三层如何协作:一个完整的实战架构

5.1 分层协作的整体图景

把三层拼起来,一个完整的 Agent 系统大概是这样运转的:

用户提出一个复杂需求,比如"帮我分析一下最近三个月的销售数据,写一份报告"。主 Agent 接到需求后,先判断这需要多个能力协作,于是:

  1. 通过A2A把任务拆解,委派给"数据分析 Agent"和"报告撰写 Agent"
  2. 数据分析 Agent 通过MCP调用数据库 Tool 拉数据,调用统计 Tool 做分析
  3. 报告撰写 Agent 调用Skill("商业报告写作"),把分析结果组织成报告
  4. 两个 Agent 通过A2A汇报进度和结果,主 Agent 汇总后返回给用户

这里的关键是每一层各司其职:MCP 负责"够得着"外部资源,A2A 负责"叫得动"其他 Agent,Skills 负责"干得好"具体任务。

5.2 分层带来的实际收益

我在项目里切到这套分层之后,最明显的三个变化:

第一,可维护性大幅提升。以前改一个工具,可能牵动整个 Agent 的逻辑;现在工具层(MCP)独立,改工具不影响上层。同理,换一个 Agent 实现,只要 Agent Card 不变,协作方无感知。

第二,可复用性变强。Skill 沉淀下来之后,新项目直接拿来用。我们团队现在有一个 Skill 库,新 Agent 项目起步就能复用 60% 以上的能力。

第三,调试变简单。出问题的时候,先定位是哪一层:工具调不通看 MCP,Agent 之间扯皮看 A2A,任务做不好看 Skill。分层清晰,排查路径就清晰。

5.3 一个容易踩的架构坑

最常见的错误是层级穿透。比如让 A2A 的 Agent 直接去调 MCP 的 Tool,跳过了 Skill 层。短期看省事,长期看就是灾难——能力没法沉淀,每个 Agent 都在重复造轮子。

我的建议是定一条规矩:跨层调用必须经过中间层。Agent 要用工具,通过 Skill 封装;Skill 要用工具,通过 MCP 调用。这样每一层都有明确的职责,不会乱。

6. 常见问题与排查技巧实录

6.1 问题速查表

现象可能原因排查方向
Agent 不调用工具Tool description 不清检查 MCP Server 的 schema 和描述
工具调用参数错误inputSchema 定义不严补全 required 和类型约束
Agent 之间任务丢失A2A 任务无状态追踪加任务 ID 和状态查询接口
Skill 触发不准trigger 描述模糊明确"何时用/何时不用"
上下文爆炸工具/Skill 加载过多按场景动态加载
长任务超时无流式或异步机制用 A2A 的 streaming 或 push

6.2 几个独家避坑经验

经验一:MCP Server 要"薄"。我见过有人把复杂业务逻辑塞进 MCP Server,结果 Server 变得又大又难维护。MCP Server 应该只做"协议适配",把业务逻辑放在 Skill 层。Server 越薄,越稳定。

经验二:A2A 的任务要幂等。网络抖动、重试是常态,如果任务不幂等,重试就会产生重复副作用。给每个任务一个唯一 ID,接收方根据 ID 去重,这是基本操作。

经验三:Skill 要有"逃生舱"。再好的 Skill 也会遇到处理不了的情况。设计 Skill 时一定要留一个"我搞不定,转人工/转其他 Skill"的出口,不要让 Agent 在死胡同里打转。

经验四:日志要分层打。MCP 层打工具调用日志,A2A 层打任务流转日志,Skill 层打执行步骤日志。三层日志分开存,出问题的时候按层过滤,效率高很多。

6.3 关于协议选型的建议

最后说个实际的问题:这三个协议是不是必须都用?不是。

  • 如果你只做单 Agent + 几个工具,MCP 就够了,别上 A2A 和 Skills,过度设计。
  • 如果你有多个 Agent 协作,但能力比较固定,MCP + A2A就够。
  • 如果你需要大量复用能力、团队协作开发,三层都上才有价值。

技术选型永远看场景,不要为了"架构完整"而堆砌。我见过太多项目,明明一个 Agent 能搞定,非要拆成五个 Agent 加一堆协议,最后维护成本高到自己都受不了。

我个人在实际操作中的体会是,先把 MCP 这层做扎实,因为它是基础,工具接不好,上面全是空中楼阁。等 MCP 稳定了,再根据协作需求引入 A2A,最后把重复的能力沉淀成 Skills。这个顺序,比一上来就搭全套架构要稳得多。

返回列表