在第 8 章中,我们探讨了 AI 驱动型应用的架构模式,并在概念层面介绍了 agentic workflow。现在,我们将从“架构”转向“这些系统在生产环境中运行时的实际挑战”。由于 2026 年的 AI 生态仍在高速演进,很多技术细节可能几个月后就会过时。与其罗列那些也许很快消失的框架,不如把重点放在跨工具、跨标准依然成立的运维模式上。我们的目标,是为你提供一种无论最终选择哪种框架都能适用的指导。
本章将聚焦在 Kubernetes 上运行 agentic application 的三个核心挑战:
安全性(Security)
Agent 会代表用户与外部工具和数据源交互。你需要健壮的身份管理、认证模式和授权控制,既要保留用户上下文,又要允许 agent 自主运行。
Agent 协同(Agent coordination)
多智能体系统需要标准化的通信协议。Agent 必须能够发现彼此的能力、委派任务,并跨服务边界跟踪进度。
状态管理(State management)
与无状态的 REST API 不同,agent 会在多轮交互中持续维护会话上下文。生产部署需要能够跨 Pod 重启存活、并支持水平扩展的持久化存储模式。
本章会介绍两个在 2024 年底逐渐成为事实标准的协议,它们分别用于 agent 通信中的两个关键层面。Model Context Protocol(MCP) 标准化了 agent 到工具(agent-to-tool) 的通信,而 Agent-to-Agent(A2A) 则标准化了 agent 之间(inter-agent) 的协作。这些并不是某个官方标准组织制定的理论规范;尽管如此,OpenAI、Google、Microsoft、AWS 以及开源社区等行业领导者已经逐渐在它们之上形成共识。2025 年成立的 Agentic AI Foundation,则为这些标准化努力提供了一个中立的归属地(见下面的侧栏)。
AGENTIC AI FOUNDATION
Agentic AI Foundation(AAIF) 是 Linux Foundation 于 2025 年启动的一个项目,目标是为 agentic AI 系统制定开放标准。其 8 家白金级创始成员包括:AWS、Anthropic、Block、Bloomberg、Cloudflare、Google、Microsoft 和 OpenAI。
基金会官方提出的愿景是:提供“一个中立、开放的基础,使这一关键能力能够以透明、协作的方式演进,并推动领先开源 AI 项目的采用”。
基金会启动时包含三个初始项目:
Model Context Protocol(MCP)
一种开放协议,用于定义 LLM 应用如何连接到外部数据源和工具。Agent 借助 MCP 通过 JSON Schema 定义发现可用函数,并用标准的 JSON-RPC 消息格式发起调用。
goose
一个开源 AI agent,能够安装软件包、运行 shell 命令、修改文件并执行测试。与仅建议修改的代码补全工具不同,goose 会直接执行这些操作,并且可与任意 LLM 后端协同工作。
AGENTS.md
一种文件格式规范,用于描述 AI 编码 agent 应当如何与代码库交互。项目通过 AGENTS.md 文件来说明目录结构、构建流程、测试约定以及推荐工作流。
该基金会遵循 Linux Foundation 的治理模式,这意味着技术决策由指导委员会推进,而不是由某一家企业单独控制。新的项目和成员组织都可以通过 Linux Foundation 的标准贡献流程加入。
AAIF 仍然非常年轻(在本文写作时仅成立数月)。不过,8 家大型科技公司共同参与,意味着它很可能会在未来几年 agentic AI 标准的发展中扮演重要角色。
下面,我们先从 Model Context Protocol 开始,它为 agent 提供了一种标准化方式,使其能够连接到完成工作所需的工具和数据源。
Model Context Protocol
Model Context Protocol(MCP) 是一种开放协议,它允许 AI 驱动的 agent 以一致、结构化的方式连接外部工具、数据源和服务。Anthropic 在 2024 年底将 MCP 提出为“AI 应用的 USB-C 接口”,它之所以迅速成为 agent 与工具互操作的事实标准,是因为它解决了早期工具调用方案中的集成痛点。在 MCP 出现之前,各种框架依赖临时拼凑的 API 调用、私有插件,以及无法扩展的 M×N 集成方式;工具之间传递上下文既脆弱又容易出错。MCP 借鉴了 Language Server Protocol(LSP) 的思路,用一种清晰的 M+N 架构 取代了那张由定制集成织成的复杂网络:任何兼容 MCP 的 agent 都可以调用任何通过 MCP 暴露出来的工具。工具通过元数据来描述,包括名称、说明和输入 Schema,从而让 LLM 可以判断何时使用它。你可以把一个 MCP server 理解成一组函数的集合,类似于操作系统提供的系统调用,或者编程语言提供的标准库。图 9-1 展示了这种简化方式。
归根结底,MCP 为 AI agent 和工具提供了一种通用语言,使双方能够在保持互操作性的同时独立演进。
图 9-1 统一协议简化后端系统访问
一个典型交互可能如下进行(见图 9-2):一个 AI 助手收到用户请求,识别出自己需要外部信息,然后向某个 MCP server 查询它的工具列表。接着,它选择并调用合适的工具。MCP server 执行动作并返回结果,agent 再利用这个结果组织最终答案返回给用户。
图 9-2 MCP 在 agentic 循环中的使用
在这个流程中,agent 内部的 LLM 会通过选择合适工具并为它们填入参数,构造出一连串工具调用;这个过程由工具描述和元数据引导。例如,如果用户问:“巴黎今天的天气怎么样?另外你能把天气预报发邮件给我吗? ” agent 可能会先在一个天气类 MCP server 上调用 weather_lookup 工具,并把位置参数设为 “Paris”;然后再在一个邮件类 MCP server 上调用 email_send 工具,并把天气数据作为参数传进去。MCP 确保这些调用是以结构化、可追踪的方式完成的,而不是通过脆弱的 prompt 文本硬拼出来。
一个 MCP server 本质上就是一个微服务,它通过 MCP 协议向 AI agent 暴露一个或多个工具。在 Kubernetes 中,你通常会把每个 MCP server 都作为一个 Deployment 来运行,并把必要运行时封装进容器里。比如,如果你想给 AI agent 提供一个 PostgreSQL 查询工具,你就可以部署官方的 Postgres MCP server 容器,并通过环境变量或 Secret 为它配置数据库连接字符串。
如果某个 MCP server 需要同时处理来自大量 agent 的并发请求,它完全可以在 Kubernetes Service 后面做水平扩展。虽然 MCP 协议在持续对话中会维护 session 状态,但大多数 MCP server 实现会把这些状态外置到数据库或缓存中,因此单个 server 实例在处理请求时通常仍可被视为无状态。这使得你依然可以使用标准 Kubernetes 的扩缩容与调度策略。为每个 server 设置资源 request/limit,并在负载波动明显时用 Horizontal Pod Autoscaler(HPA) 进行弹性伸缩。
在某些情况下,也值得考虑共置(co-location) 。如果某个 MCP server 与 agent 数据紧密耦合——例如,一个文件系统工具需要操作和 agent 本身看到的是同一批文件——那么你就可以把它作为 sidecar 容器,和 agent 放在同一个 Pod 里。这样做能确保低延迟的本地调用,以及共享存储卷。代价则是资源重复和生命周期耦合:是每个 agent Pod 携带一个 sidecar,还是用一个共享服务,最终需要根据你的使用模式来评估。
如果 MCP server 的数量很多,那么管理和发现它们的 endpoint URL 会逐渐变得麻烦。一种做法是使用服务注册表或命名约定。由于 MCP server 本身就会自描述其工具,agent 理论上可以查询一个中心目录,以找到自己需要的工具。实践中,很多团队会把一组相关工具合并进同一个 MCP server,以减少服务数量。不过,这种模式只能在一定程度上奏效,因为 agent 能够同时考虑的函数数量本身是有限的。如今还出现了更高级的工具选择技术,例如基于 RAG 的工具相似度搜索,或者程序化的工具发现——由 agent 自己编写代码,遍历保存工具定义的文件系统,并只加载完成当前任务真正需要的工具。
MCP 安全性(MCP Security)
当一个 AI agent 调用某个 MCP 工具去读取客户记录、向 Slack 发消息,或者查询数据库时,一个根本性问题会立刻浮现:上游 API 到底应该看到谁的身份?
它应该看到触发 agent 的终端用户身份?还是应该看到 agent 自己的服务账号?还是别的什么?
在传统微服务架构中,服务到服务(service-to-service)的授权问题已经相对成熟。你可能会使用带有服务网格的双向 TLS(mTLS)、OAuth2 的 client credentials flow,或者作用域受限的 API key。类似 token relay 或 ambassador pattern 这样的身份透传模式,也能帮助你把用户上下文贯穿多个调用链路。
但 agentic 架构又带来了额外挑战。
首先,它引入了非确定性(nondeterminism) 。与一个行为确定的微服务不同,agent 的行为会受到 LLM 推理过程的影响,这意味着你无法准确预测它究竟会调用哪些工具、按什么顺序调用。像“服务 A 可以调用 endpoint B”这种传统授权策略,并不能直接迁移到 agent 场景——因为这里的“服务 A”其实是一个 agent,而它也许会根据用户 prompt 调用 10 个不同工具。
其次,它带来了身份歧义(identity ambiguity) 。当 agent 代表某个用户调用工具时,上游 API 到底应该看到用户身份,以便执行按用户维度的权限控制和配额限制?还是应该看到 agent 的身份,以便追踪 agent 的动作并施加 agent 级别的速率限制?答案取决于你的合规要求,但这个问题本身,显然比传统流程复杂得多。
这些挑战迫使你必须对身份传播做出明确选择,而这些选择在更简单的架构里往往是隐式的、或者自动处理掉的。下面要介绍的四种方式¹,代表了在安全性、运维简洁性和与现有基础设施集成之间的不同权衡点:
Agent Impersonation(Token Passthrough)
Agent 将用户的访问令牌一路转发给 MCP server 和上游 API,从而保留用户身份,以便进行 RBAC 与审计。
Service Account Delegation
利用 Kubernetes ServiceAccount token,在集群内的 agent、MCP server 与上游 API 之间建立认证。
Delegated Identity via OAuth2 Token Exchange
通过 Token Exchange(RFC 8693)创建同时携带用户身份和agent 身份的凭据,从而兼顾责任归属与服务级可见性。
Mutual TLS with SPIFFE/SPIRE
通过与工作负载进行加密绑定的身份,以及短生命周期证书,实现零信任认证,而不依赖可被窃取的 token。
我们先从最直观的一种方式开始:把用户 token 透传到整条调用链中。
Agent Impersonation(Token Passthrough)
为了传播用户身份,agent 会在与 MCP 交互时代表(或模拟)用户。这种 impersonation 的好处是:它能够在不改动现有 RBAC 基础设施的情况下,直接复用它。你的审计日志也会自然地记录“哪个终端用户访问了哪些数据”,从而一步满足合规要求。你还可以基于用户维度实施配额和速率限制,防止单个用户耗尽共享资源。
在 agent impersonation 模式下,MCP server 会从 agent runtime 接收到终端用户的凭据,并在调用上游 API 时直接使用这些凭据。这样,上游服务看到的请求发起者就是用户本人,而不是 agent。这在概念上类似于 OAuth2 的 token passthrough:agent runtime 把用户 access token 传给 MCP server,后者再把它放到 Authorization header 中调用上游 API。
以医疗场景为例:护士 Alice 通过一个医疗助手 agent 查询病人记录。护士先通过 OpenID Connect 登录到 agent runtime,并拿到一个 access token。当护士发出请求:“把 4711 号病人的化验结果给我看看”时,agent runtime 会把 Alice 的 token 连同工具调用请求一起转发给 MCP server。随后,MCP server 再使用 Alice 的 token 作为 Authorization header 去调用医院的病历 API。病历 API 会执行它原有的用户级权限检查——例如,验证这位护士是否有权查看 4711 号病人的记录——而审计日志里记录的也是“护士 Alice 查看了 4711 号病人的化验结果”,而不是模糊地记成“某个 agent 访问了这些数据”。
不过,这种模式也会带来一些运维复杂度,尤其是在 token 生命周期 上。用户 access token 往往只在几分钟到几小时内有效,如果 agent 执行的任务持续时间超过 token 生命周期,那么调用就会失败,除非你额外实现 token refresh 逻辑。你还会遇到 scope explosion 问题:用户 token 必须对 agent 可能调用到的每一个上游 API 都有效。这往往意味着你需要赋予用户过于宽泛的 OAuth scope,从而违背最小权限原则。比如,一个病人助手 agent 可能会访问 lab API、pharmacy API 和 scheduling API,那么护士的 token 就得对这三个系统都有 scope,即便这次查询只实际用到了其中一个。
此外,还存在凭据失窃风险。如果 MCP server 被攻破,攻击者就可能窃取并重放用户 token,以访问用户本有权限访问的任意资源。针对这一点的防御手段包括:使用更短的 token 生命周期、服务间使用强 mTLS,以及加强 Pod 内运行时安全。
在 Kubernetes 上,这种模式通常会配合一个 ingress controller 使用:由它来完成用户认证,并把 access token 注入到某个 HTTP header 中。你可以使用 Traefik、NGINX + oauth2-proxy,或者 Istio + RequestAuthentication,在边界层完成这种 access token 的透传。图 9-3 展示了这条完整链路,并高亮了其中的验证步骤。
图 9-3 Agent impersonation 流程:展示用户 token 的传播与校验
即便你是通过透传用户 token 来做授权,服务之间仍然应该启用 mutual TLS——无论是直接启用,还是通过 Istio、Linkerd 等服务网格启用——从而加密 MCP server 与上游 API 之间的流量,并验证这些流量确实来自受信任的工作负载。
Service Account Delegation
当用户级权限非常重要,而且你的身份基础设施又支持它时,impersonation 会是一个不错的选择;但如果 agent 与上游服务都运行在同一个 Kubernetes 集群中,而且只需要做到 agent 级别的责任归属,那么其实还有一个更简单的替代方案。
这种方案不再依赖外部 token server,而是直接利用 Kubernetes 原生的 workload identity,从而减少系统组件数量和运维负担。Kubernetes 里的每个 Pod 天生就拥有一个 ServiceAccount,并且它可以通过标准 RBAC 承载权限。这种模式利用这些内置原语,在 agent runtime、MCP server 和上游 API 之间建立信任,而无需额外的身份提供方。
ServiceAccount 作为工作负载身份
在 Kubernetes 中,ServiceAccount 是一种命名空间级别的身份。当你创建一个 Pod 时,Kubernetes 会给它分配一个 ServiceAccount——要么是你显式指定的那个,要么是该命名空间下的默认 ServiceAccount。这个身份不对应某个具体人类用户,而是对应一个工作负载,因此非常适合 service-to-service 认证。
每个 ServiceAccount 都会关联一个 token,Kubernetes 会自动把它挂载进 Pod 内部,路径通常是:
/var/run/secrets/kubernetes.io/serviceaccount/token
这个 token 是一个已签名的 JWT(JSON Web Token) ,其中包含了识别该 ServiceAccount 的 claims,例如它的名称、命名空间和唯一标识。Kubernetes API server 会用自己的私钥为这些 token 签名,而任何信任 API server 的组件都可以验证它们。
ServiceAccount 创建好之后,你只需要在 Pod spec 中设置 serviceAccountName 字段,就能把它分配给该 Pod。Pod 启动后,Kubernetes 会把对应 token 作为文件注入进去,并自动为你刷新。
这里的自动刷新非常关键。ServiceAccount token 不是静态凭据;为了安全,Kubernetes 会定期轮换它们。因此,任何读取该 token 的代码,都必须在每次使用时重新从文件系统读取,而不是把它缓存到内存里。每次从文件读取,才能确保你拿到的是当前最新、仍然有效的 token。
ServiceAccount token 有两种使用场景:集群内使用,以及(额外配置后)集群外使用。
在集群内部,ServiceAccount token 是 Kubernetes 原生一等公民,Kubernetes API server 本身就能直接理解它。当某个 Pod 调用 Kubernetes API,并在 Authorization header 中带上自己的 ServiceAccount token 时,API server 会验证签名、提取身份,然后依据 RBAC 策略判断该请求是否被允许。
ServiceAccount token 也可以在集群外被验证,前提是 API server 暴露了一个 OIDC discovery endpoint。大多数托管 Kubernetes 服务——例如 GKE、EKS、AKS——默认都会开启这一能力。在这种模式下,ServiceAccount token 是一个有效的 JWT,任何能够访问集群 OIDC 公钥的外部服务都能验证它。代价则是更高的复杂度:你需要配置外部服务信任你的集群 OIDC issuer,获取它的签名公钥,并处理 token 验证逻辑。关于集群外验证,我们会在“External validation via OIDC/JWT”中进一步展开。
Server identity 与 agent identity
ServiceAccount delegation 实际上可以分成两种流向,区别在于:上游 API 最终看到的是谁的身份。两种方式都使用 ServiceAccount token,但到达上游 API 的究竟是哪一个 token,不同。
在 server identity 模式下,MCP server 在调用上游 API 时使用的是它自己的 ServiceAccount token。agent runtime 的身份不会继续向上传播;上游 API 只会根据 MCP server 的身份来实施权限控制。这是更简单的一种方式,很适合所有使用这个 MCP server 的 agent runtime,在访问上游资源时权限完全一致的场景。
在 agent identity 模式下,agent runtime 会把自己的 ServiceAccount token 发给 MCP server,而 MCP server 再把这个 token 原样转发给上游 API。于是,上游 API 基于 agent runtime 的身份 进行授权。这样一来,即使多个 agent runtime 调用的是同一个 MCP server,它们也可以拥有不同的访问级别。
图 9-4 将这两种流向并排展示。
图 9-4 Server identity 与 agent identity 对比
这里最关键的决策点在于:你想要多细粒度的控制。
如果某个 MCP server 的所有工具都应该对所有 agent runtime 一视同仁,那么就用 server identity。
如果不同 agent runtime 需要不同权限等级,那么就用 agent identity。
接下来我们会依次详细讲解这两种方式,从创建 ServiceAccount 与读取 token 的机制开始。
ServiceAccount 的使用方式
要为 ServiceAccount 授予合适权限,你必须定义 RBAC 规则。对于 agent 安全来说,最好使用面向具体用例的自定义 API group 和资源名称,而不是直接使用 Kubernetes 的标准资源。
这里有一个关键区别:
保护 Kubernetes 的 Service 资源访问权限,并不能保护这个服务真正暴露出来的 endpoint。
一个对某个 Service 拥有 get 权限的 ServiceAccount,最多只能读取这个 Service 的元数据,并不能直接调用它背后的真实服务。
因此,更推荐的做法是:定义能表达应用级权限的资源。示例 9-1 展示了一个基本的 RBAC 配置,其中包括 Role、ServiceAccount 和 RoleBinding。如果你想系统了解 Kubernetes 中 RBAC 的配置方式,可以参考 Kubernetes Patterns 里的 Access Control 模式。
示例 9-1 基于自定义资源 RBAC 的 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: customer-support-mcp
namespace: agents
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: data-platform
name: customer-data-reader
rules:
- apiGroups: ["agents.example.com"]
resources: ["customer-queries"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: customer-support-mcp-binding
namespace: data-platform
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: customer-data-reader
subjects:
- kind: ServiceAccount
name: customer-support-mcp
namespace: agents
这里:
- 使用了一个面向 agent 域的自定义 API group:
agents.example.com - 使用了代表应用级权限的自定义资源名:
customer-queries - 把
agents命名空间里的 ServiceAccount 绑定到data-platform命名空间中的这个 Role
像示例 9-1 里的 customer-queries(或者类似的 medical-records、support-tickets 等)这种应用特定资源,并不一定需要在 Kubernetes API server 中注册成 CRD。它们只出现在 RBAC 规则中,并且仅用于通过 SubjectAccessReview 做授权判断。这样你就能获得细粒度、面向业务语义的授权能力,而不必真正维护一套 CRD。
Kubernetes 会把 ServiceAccount token 挂载到每个 Pod 内部的固定路径中。读取它本身很简单,但必须用对方式,否则你可能会误用已经过期的 token。
示例 9-2 里的函数在每次被调用时都重新读取 token,从而保证你永远使用的是最新版本。
示例 9-2 正确读取 ServiceAccount token
from pathlib import Path
def get_serviceaccount_token() -> str:
"""Read the current ServiceAccount token from the filesystem."""
token_path = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
return token_path.read_text().strip()
这里的关键点是:
每次调用都重新从文件系统读取 token,不要把它缓存到内存里。
因为 Kubernetes 会自动在文件系统中刷新这个 token。
发起带认证的请求
在 server identity 模式中,MCP server 调用上游 API 时使用的是自己的 ServiceAccount token。
而在 agent identity 模式中,agent 会把它自己的 ServiceAccount token 发给 MCP;MCP 在验证通过后,再把这个 agent token 拷贝到向上游 API 发出的请求的 Authorization header 里。
示例 9-3 展示了在 service-level identity 模式下,如何把 token 带到 HTTP 请求中。
示例 9-3 MCP server 使用自己的 token 调用上游服务
import httpx
from pathlib import Path
async def call_upstream_with_service_token(
endpoint: str,
payload: dict,
user_id: str | None = None
) -> dict:
"""Call upstream API with the MCP server's ServiceAccount token."""
sa_token = get_serviceaccount_token()
headers = {
"Authorization": f"Bearer {sa_token}",
"Content-Type": "application/json"
}
if user_id:
payload["_audit_user_id"] = user_id
async with httpx.AsyncClient() as client:
response = await client.post(endpoint, json=payload, headers=headers)
response.raise_for_status()
return response.json()
这里:
- 通过示例 9-2 中定义的函数,重新从文件系统读取当前 ServiceAccount token
- 把它作为
Bearer token放入Authorizationheader - 如有需要,还可以把终端用户 ID 作为
_audit_user_id放进 payload,用于审计
在这种模式下,上游 API 看到的是 MCP server 的身份,并据此做权限控制。
如果你还需要知道究竟是哪个终端用户触发了这次请求,可以把用户身份信息放在请求体里,或者放进类似 X-User-ID 这样的自定义 header 中。
这种方式简单、开销也很低,但它的限制也很明显:所有通过该 MCP server 的 agent runtime,都拥有相同级别的访问权限。
通过 TokenReview 做认证校验
当 MCP server 在 agent identity 模式下,从某个 agent runtime 那里收到一个 ServiceAccount token 时,它必须先验证这个 token,才能信任它。
同理,任何接收来自 MCP server 请求的上游 API,也必须在信任它之前对 token 做验证。
Kubernetes 正好提供了一个专门的 API 来做这件事:TokenReview API。
TokenReview API 以一个 token 为输入,返回它是否有效,以及它代表的是谁。示例 9-4 展示了 MCP server 如何通过 Kubernetes API 来验证这个 token。
示例 9-4 使用 TokenReview 校验 agent token
import httpx
from kubernetes import client, config
config.load_incluster_config()
auth_v1 = client.AuthenticationV1Api()
async def validate_agent_runtime_token(token: str) -> dict:
"""Validate agent runtime token using Kubernetes TokenReview API."""
token_review = client.V1TokenReview(
spec=client.V1TokenReviewSpec(token=token)
)
result = auth_v1.create_token_review(token_review)
if not result.status.authenticated:
raise ValueError("Token validation failed: not authenticated")
username = result.status.user.username
if not username.startswith("system:serviceaccount:agents:"):
raise ValueError(f"Token from unauthorized namespace: {username}")
return {
"username": username,
"uid": result.status.user.uid,
"groups": result.status.user.groups
}
这里:
load_incluster_config()会从集群内的 ServiceAccount 自动加载 Kubernetes 配置- 创建一个
TokenReview对象,把待验证 token 放进去 - 调用 Kubernetes API server 提交这个 TokenReview,这是一次同步调用,结果会填充到 TokenReview 的
status字段中 - 检查
authenticated,确认 token 既没有过期,也确实由受信任的 API server 签发 - 从
status.user.username中提取 ServiceAccount 的用户名,格式通常是:
system:serviceaccount:namespace:name - 最后做一个 allowlist 校验:这里只允许来自
agents命名空间的 token
这个验证步骤对安全性至关重要。通过调用 TokenReview,MCP server 实际上是在确认:这个 token 确实由 Kubernetes API server 签发并签名。
上面这种 allowlist 检查,本质上只是一个基于 namespace 的初步访问控制。它限制了哪些 ServiceAccount 能使用这个 MCP server,避免无关命名空间中的 Pod 误调用你的工具。
如果你想做更细粒度的、基于 RBAC 的授权控制,则应进一步使用 SubjectAccessReview,也就是下面要介绍的机制。
Token 验证本身会引入一点额外延迟。为了减少开销,你可以根据 token 的哈希值做短 TTL 缓存。但要注意:这个缓存必须遵守 token 自身的过期时间,不能让缓存活得比 token 更久。
使用 SubjectAccessReview 做授权判断
验证 token 只能证明“你是谁”,却不能告诉你“你是否有权做这件事”。
Kubernetes 提供了 SubjectAccessReview API 来解决授权问题。
SubjectAccessReview 会向 Kubernetes API server 发起这样一个问题:
“这个 ServiceAccount,能否对这个资源执行这个动作?”
它会严格按照当前集群中的所有 RBAC 规则来给出答案。示例 9-5 展示了如何调用它。
示例 9-5 使用 SubjectAccessReview 检查权限
from kubernetes import client
authz_v1 = client.AuthorizationV1Api()
async def check_agent_permission(
username: str,
namespace: str,
api_group: str,
resource: str,
verb: str
) -> bool:
"""Check if a ServiceAccount has permission to perform an action."""
sar = client.V1SubjectAccessReview(
spec=client.V1SubjectAccessReviewSpec(
user=username,
resource_attributes=client.V1ResourceAttributes(
namespace=namespace,
group=api_group,
resource=resource,
verb=verb,
)
)
)
result = authz_v1.create_subject_access_review(sar)
return result.status.allowed
这里:
username是从 TokenReview 返回的 ServiceAccount 用户名,例如:
system:serviceaccount:agents:agent-runtimeapi_group是你的自定义资源所在的 API group,例如:agents.example.comresource是资源类型,例如:customer-queriesverb是动作,例如:get、list、create、update、delete- 最终把 SubjectAccessReview 提交给 API server
这种方式最大的价值在于:它让你能够直接复用示例 9-1 中定义的 Kubernetes RBAC,来实现面向应用语义的权限控制,而不必另起一套授权系统。
这里检查的那些“自定义资源”(如 customer-queries、medical-records 等)并不真的需要作为 CRD 存在;它们只是用于授权判断的“虚拟资源”。
集群外通过 OIDC/JWT 验证(External validation via OIDC/JWT)
虽然 ServiceAccount delegation 最常见的用法是集群内认证,但在有些场景下,你也会需要在集群外部验证 ServiceAccount token。
例如:
- 你要调用一个支持 OIDC federation 的云厂商 API
- 或者你的架构是混合式的,部分服务跑在 Kubernetes 外面,但仍然需要信任来自集群内的身份
Kubernetes 可以把 ServiceAccount token 暴露成符合 OIDC 标准的 JWT,从而让任何支持 OIDC 的服务都能验证它。
这要求集群的 API server 被配置成带有一个 OIDC issuer URL,而大多数托管 Kubernetes 服务默认都支持这一点。
示例 9-6 展示了如何通过程序方式完成对 ServiceAccount token 的外部验证。
集群 API server 会在如下路径暴露一个 OIDC discovery endpoint:
<cluster-url>/.well-known/openid-configuration
这个 endpoint 会公布:
- 集群的 OIDC issuer URL
- 用于签发 token 的公钥集合(JWKS)所在位置
外部服务会先取回 JWKS,再用其中公钥验证 token 的签名,同时校验过期时间、audience 等 JWT 标准 claim。
示例 9-6 通过 OIDC 在集群外验证 ServiceAccount token
import jwt
import httpx
async def validate_sa_token_externally(
token: str,
cluster_issuer: str,
expected_audience: str
) -> dict:
"""Validate a Kubernetes ServiceAccount token using OIDC discovery."""
discovery_url = f"{cluster_issuer}/.well-known/openid-configuration"
async with httpx.AsyncClient() as client:
discovery_resp = await client.get(discovery_url)
discovery_resp.raise_for_status()
discovery = discovery_resp.json()
jwks_uri = discovery["jwks_uri"]
jwks_resp = await client.get(jwks_uri)
jwks_resp.raise_for_status()
jwks = jwks_resp.json()
signing_key = jwt.PyJWKClient(jwks_uri).get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=expected_audience,
issuer=cluster_issuer
)
return claims
这里:
- 先从 issuer URL 推导出 OIDC discovery endpoint
- 获取包含签名公钥的 JWKS
- 根据 token header 中的 key ID 找到正确的签名公钥
- 校验 token 的
audience,确保它确实是签发给你这个服务的 - 最终返回通过验证的 claims,其中就包含了 ServiceAccount 身份
要让这件事成立,集群在生成 ServiceAccount token 时,必须在其中带上正确的 audience claim。
默认情况下,Kubernetes 集群为自己签发的 ServiceAccount token 使用 API server 启动参数 --service-account-issuer 指定的 issuer URL 作为 audience。
你也可以通过 --api-audiences 配置一个以逗号分隔的 audience 列表,覆盖默认行为。
示例 9-7 展示了如何为某个 Pod 单独指定 ServiceAccount token 的 audience。
示例 9-7 为 Pod 声明 ServiceAccount 的 audience
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
serviceAccountName: my-sa
containers:
- name: app
image: ghcr.io/example/app:latest
volumeMounts:
- name: oidc
mountPath: /var/run/my-audience
readOnly: true
volumes:
- name: oidc
projected:
sources:
- serviceAccountToken:
path: token
audience: "https://my.service.example"
expirationSeconds: 3600
这里:
serviceAccountName指定了挂载给 Pod 的 ServiceAccountmountPath是这些 token 文件挂载进去的目录projected.sources允许你挂载多个不同 audience 的 tokenpath是保存 token 的文件名audience会被写入 token JWT 的audclaim 中
如果你需要面向多个上游服务配置不同 audience,可以在示例 9-7 中声明多个 serviceAccountToken 条目,并分别挂载成不同文件;或者使用 TokenRequest API 来为多个 audience 动态签发 token。
ServiceAccount delegation 非常适合集群边界内的 workload-to-workload 认证,但它本质上是一种工作负载身份模型,而不是用户身份模型。
一旦你需要在跨系统边界的场景中,把动作精确归因到某个具体用户,OAuth2 就会成为最主流的委托访问标准。OAuth2 允许用户在不泄露自己凭据的情况下,授权某个应用代表自己执行操作——而这正是 agent 代表用户去调用上游 API 时最需要的能力。
虽然本书不会系统展开 OAuth2 全貌,但理解 token exchange(一种关键 OAuth2 扩展)对 agent 安全模式来说非常重要。Token exchange 允许你把一个凭据换成另一个面向不同 scope 和 audience 的凭据,从而实现细粒度委托,并同时保留用户身份与 agent 身份。
OAUTH2 与 MODEL CONTEXT PROTOCOL
当 MCP server 需要受保护资源访问时,MCP 规范采用 OAuth 2.1 作为授权基础。
在这种模型中,MCP server 充当的是 OAuth 的 Resource Server,通过标准 OAuth2 机制来保护其工具和资源。
MCP 的实现依赖于成熟的 OAuth2 相关规范:
- MCP client 必须支持 OAuth 2.0 Authorization Server Metadata(RFC 8414) ,以发现授权端点
- 实现应支持 Dynamic Client Registration(RFC 7591) ,以简化接入
- 所有 client 都必须在 authorization code flow 中使用 PKCE
而对于多用户 agentic system 中需要“委托语义”的场景,RFC 8693(Token Exchange) 提供了保留用户与 agent 双重身份的机制;下一节我们会详细介绍。
如果你想系统理解 OAuth2 安全模式与 delegation flow,可以参考 Gary Archer 等人所著的 Cloud Native Data Security with OAuth(O’Reilly)。
通过 OAuth2 Token Exchange 实现委托身份(Delegated Identity via OAuth2 Token Exchange)
Impersonation 模式保留了用户上下文,但引入了 token 生命周期复杂度和 scope 扩散问题;ServiceAccount 模式则简化了运维,却失去了用户级别的归因能力。
OAuth2 Token Exchange(RFC 8693) 则试图同时提供两者的优点:它是一种基于标准的方式,既保留用户身份,又让上游系统看到当前发起调用的服务身份。
Token exchange 实现的是一种“委托语义(delegation semantics) ”:原始用户依然可以被区分出来,同时代表其执行操作的服务也同样可见。
交换后得到的 token 会同时携带两种身份:
sub:表示“此操作是为谁做的”,也就是终端用户,例如alice@example.comact:表示“当前是谁在执行这项工作”,也就是 actor,例如customer-support-agent或customer-data-mcp-server
这个“双身份 token”允许上游服务实施复合策略,例如:
“只有当用户本身有权限,并且当前这个服务也被授权执行该操作时,才允许访问。”
在一个 MCP 工作流里,token exchange 可能发生在两个位置:
- agent runtime 可以把用户 token 交换成一个面向 MCP server 的 token,其中同时标识用户与 agent
- MCP server 也可以把自己的 token 再交换成一个面向上游 API 的 token,其中依然保留用户身份,同时把当前 actor 换成 MCP server 自己
这两步使用的是同一种 exchange 机制,唯一变化的是 act claim 中反映的 actor 身份。
本节我们重点讨论第一个场景:agent runtime 把用户 token 交换成一个面向 MCP server 的 token。
同样的模式也完全适用于 MCP server 进一步向上游 API 交换 token。
Token exchange 流程通常需要一个 token service,一般就是你的 identity provider,或者一个专门的 security token service,如图 9-5 所示。
用户先向 identity provider 完成认证,并获得一个 access token。接着,agent runtime 调用 token exchange endpoint,把用户 token 作为 subject_token,把 MCP server 作为目标 audience。Token service 会验证用户 token,然后返回一个 delegated token。Agent runtime 把这个 delegated token 发给 MCP server,后者可以直接使用它,或者再次交换它后再去访问上游 API。
图 9-5 OAuth2 Token Exchange
仍然沿用我们在“Agent Impersonation(Token Passthrough)”里提到的医疗场景:
当护士 Alice 查询病人记录时,agent runtime 会用她的 token 换取一个 delegated token,这个 token 同时标识:
- 医疗助手 agent
- 护士 Alice
于是,上游的病历 API 就可以实施一种复合策略:
“只有当当前 agent 是 medical-assistant,并且 Alice 对这个病人有访问权限时,才允许读取。”
两个身份都被保留在同一个经过加密签名的 token 里。
这种模式特别适合那些既要满足合规要求、必须知道**谁(用户)和什么(agent)**访问了数据,又已经拥有支持 token exchange 的身份平台的场景。像 Keycloak、Auth0、Azure AD 这样比较现代的身份平台,都已经支持 RFC 8693。
如果你有多个 agent,而它们需要不同 scope,那么 token exchange 还能帮助你做细粒度 scope 控制,而不需要为每个 agent 单独创建一批用户账号。
在 Kubernetes 中,要实现 token exchange,首先要让你的 identity provider 支持 RFC 8693——例如 Keycloak 原生就支持。然后由 agent runtime 在调用 MCP server 前先完成 exchange,示例如 9-8 所示。
示例 9-8 agent runtime 通过 RFC 8693 做 token exchange
import httpx
async def exchange_token(
user_token: str,
agent_token: str,
upstream_audience: str,
token_endpoint: str
) -> str:
"""Exchange user token for delegated token via RFC 8693."""
payload = {
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": user_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"audience": upstream_audience,
"actor_token": agent_token,
"actor_token_type": "urn:ietf:params:oauth:token-type:access_token",
}
async with httpx.AsyncClient() as client:
response = await client.post(token_endpoint, data=payload)
response.raise_for_status()
token_data = response.json()
return token_data["access_token"]
这里:
grant_type使用 RFC 8693 规定的 token exchange grant typesubject_token是用户的 access token,也就是“代表谁做事”的主体audience指定目标 MCP server 或上游 API,用于把 token 限定到特定服务actor_token是 agent 自己的 access token,用来表示当前执行操作的 actor- 按 RFC 8693,既然传了
actor_token,就必须同时传actor_token_type - 返回的 delegated token 会同时包含两种身份:用户写在
sub中,agent 写在act中
MCP server 从 agent runtime 收到这个 exchanged token 之后,有两种做法:
- 直接把这个“双身份 token”向上游 API 透传
- 或者再执行一次 token exchange,把
actclaim 中的 actor 从 agent runtime 换成 MCP server 自己,同时继续保留用户身份在sub中
这种模式的代价,是额外的系统复杂度。你必须运营一个 token exchange endpoint,处理 exchange 过程中的错误,并且缓存这些 exchanged token,否则每次 agent 调用都去交换一遍,会显著增加延迟。
在缓存设计上,一个合理方式是根据如下三元组构造 cache key:
(user_subject, agent_identity, audience)
当你拿到一个 exchanged token 时,可以先解码 JWT,取出其中的 exp(Unix 秒级过期时间戳),然后把缓存 TTL 设为:
exp - current_time - safety_margin
其中 safety_margin 用来抵消时钟漂移和网络延迟,通常设置为 30–60 秒。
永远不要让缓存 TTL 超过 token 的真实生命周期。
如果缓存中残留的是一个已经过期的 token,那么它最终还是会被上游 API 拒绝,你还是不得不重新 exchange,这只会浪费缓存空间和一次失败的 API 调用。
如果你的缓存实现不支持按条目设置单独 TTL(例如 Redis 支持每个 key 单独过期,而某些简单实现不支持),那就至少要让每个缓存项都带着自己的过期时间。对于内存缓存,可以存成如下结构:
{token, expires_at}
然后在读取时检查是否过期。
在高并发下,针对同一个 (user, agent, audience) 元组,可能会有多个请求同时触发 exchange。此时可以使用 cache-aside 模式加一个短期锁;当然,很多时候也可以接受少量重复 exchange,而不是为了完全避免它而引入复杂的分布式锁。
使用 SPIFFE/SPIRE 的双向 TLS(Zero-Trust)
无论是 OAuth2 access token、Kubernetes ServiceAccount token,还是 API key,它们作为 bearer token 都有一个根本性的弱点:可以被偷走。
只要攻击者截获或窃取到 token,他就能在 token 过期前伪装成合法调用方。
SPIFFE(Secure Production Identity Framework for everyone) 与 SPIRE(SPIFFE Runtime Environment) 提供了解法:把身份以密码学方式绑定到工作负载本身,从而使凭据无法在不攻破整个 Pod 的前提下被单独窃取。
SPIFFE 通过自动签发、轮换和验证的证书,为工作负载提供身份。在 MCP 场景中,这意味着你的 agent runtime、MCP server 以及上游 API 都可以通过 mutual TLS(mTLS) 相互认证,而无需管理任何共享 secret。每一跳连接都经过密码学验证,而且证书默认 TTL 只有 1 小时(通常在 TTL 过半时轮换,也就是大约每 30 分钟轮换一次)。
SPIFFE 如何在 MCP 中工作
SPIFFE 会给每个工作负载分配一个唯一身份,称为 SPIFFE ID,它是一个 URI,类似:
spiffe://example.com/ns/agents/sa/customer-support
工作负载通过一个叫做 SVID(SPIFFE Verifiable Identity Document) 的东西来证明自己的身份。SVID 本质上是一张 X.509 证书,其中在 SAN(Subject Alternative Name) 里写入了 SPIFFE ID。
你可以把 SPIFFE ID 理解为“工作负载的名字”,而 SVID 就是它那张带有加密签名的“工牌”。
SPIRE Server 充当证书颁发机构(CA),负责在验证工作负载身份后签发 SVID,这个过程称为 attestation。
在 Kubernetes 环境里,attestation 通常意味着:通过 Kubernetes API 验证调用方的 ServiceAccount token。
SPIRE Agent 则会以 DaemonSet 形式跑在每个节点上,通过 Unix socket 在 /run/spire/sockets/agent.sock 暴露一个 Workload API。工作负载只需把这个 socket 所在路径挂载到自己容器里,就能通过本地调用的方式拿到自己的 SVID——无需网络请求、无需挂载 secret,仅通过本地 API 就能完成,SPIRE Agent 会负责识别调用进程并验证它。
现在,我们沿着完整认证链路,从 agent runtime 一直到 MCP server,再到上游 API 走一遍。
agent runtime 与 MCP server 都会先从本地 SPIRE Agent 取回自己的 SVID。之后,就可以用 mTLS 保护每一跳:
- agent runtime 连接到 MCP server,并把自己的 SVID 作为客户端证书出示出来
- MCP server 验证 agent 的 SPIFFE ID(例如
spiffe://example.com/ns/agents/sa/agent-runtime),然后返回它自己的 SVID 作为服务端证书 - agent runtime 反过来验证 MCP server 的 SPIFFE ID,从而完成双向认证
- 当 MCP server 调用上游 API 时,同样的握手流程会再来一遍:双方先出示并验证各自的 SVID,之后才开始传输任何数据
图 9-6 展示了这一架构。
图 9-6 Agent 到 MCP 通信中的 SPIFFE/SPIRE 认证架构
在 Kubernetes 上部署 SPIRE
SPIRE 需要两个核心组件:
一个 SPIRE Server(通常部署成 StatefulSet),以及若干 SPIRE Agent。
示例 9-9 给出了一个以 DaemonSet 部署的 SPIRE Agent。Server 维护根信任并签发 SVID,Agent 则跑在每个节点上,向本机工作负载提供 Workload API。
示例 9-9 SPIRE Agent DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: spire-agent
namespace: spire
spec:
selector:
matchLabels:
app: spire-agent
template:
spec:
hostPID: true
serviceAccountName: spire-agent
containers:
- name: spire-agent
image: ghcr.io/spiffe/spire-agent
volumeMounts:
- name: spire-socket
mountPath: /run/spire/sockets
- name: spire-token
mountPath: /var/run/secrets/tokens
volumes:
- name: spire-socket
hostPath:
path: /run/spire/sockets
type: DirectoryOrCreate
- name: spire-token
projected:
sources:
- serviceAccountToken:
path: spire-agent
audience: spire-server
这里:
hostPID: true是为了支持基于 cgroup 的工作负载 attestation(agent 需要读取/proc来识别调用进程)/run/spire/sockets是工作负载获取 SVID 的 Unix socket 路径spire-token是 agent 与 server 之间认证所需的 ServiceAccount token
部署完 SPIRE 后,你需要通过把 Kubernetes selector(例如 namespace、ServiceAccount 名)映射到某个 SPIFFE ID 的方式,去注册每个工作负载,如示例 9-10 所示。这个 registration entry 告诉 SPIRE:哪些 Pod 应该拿到什么身份。
示例 9-10 注册 MCP server 工作负载
kubectl exec -n spire spire-server-0 -- \
/opt/spire/bin/spire-server entry create \
-spiffeID spiffe://example.com/ns/mcp/sa/customer-support \
-parentID spiffe://example.com/spire-agent \
-selector k8s:ns:mcp \
-selector k8s:sa:customer-support
这里:
-spiffeID指定要赋予工作负载的 SPIFFE ID-selector表示:凡是位于mcp命名空间、且使用customer-supportServiceAccount 的 Pod,都匹配这条注册规则
对于有成百上千个工作负载的生产集群来说,手工注册显然不现实。
这时可以使用 SPIRE Controller Manager:它会监听 Kubernetes 资源,并根据注解或 ClusterSPIFFEID 之类的自定义资源,自动创建 registration entry。通常它可以通过 Helm 和 SPIRE Server 一起安装。
使用 SPIFFE
你的 MCP server 会从本地 SPIRE Agent 取回自己的 SVID,并用它来建立 mTLS 连接。
如果你用 Python 开发,示例 9-11 中的 py-spiffe 库能显著简化这部分工作。
示例 9-11 MCP server 使用 SPIFFE 建立 mTLS
import ssl
import tempfile
import httpx
from spiffe import WorkloadApiClient
from cryptography.hazmat.primitives import serialization
def svid_to_pem_files(svid):
"""Write SVID certificate chain and private key to temporary PEM files."""
...
return cert_path, key_path
with WorkloadApiClient() as client:
x509_ctx = client.fetch_x509_context()
svid = x509_ctx.default_svid
cert_path, key_path = svid_to_pem_files(svid)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
for bundle in x509_ctx.x509_bundle_set.bundles:
for ca_cert in bundle.x509_authorities:
ssl_context.load_verify_locations(
cadata=ca_cert.public_bytes(serialization.Encoding.PEM).decode()
)
async with httpx.AsyncClient(verify=ssl_context) as http_client:
response = await http_client.post(
"https://api.example.com/endpoint",
json={"query": "list tickets"}
)
这里:
svid_to_pem_files()是一个辅助函数,用来把 SVID 证书链和私钥写成临时 PEM 文件,方便 Pythonssl模块使用fetch_x509_context()会从本地 SPIRE Agent 拉取 X.509 上下文(包含 SVID 和 trust bundle)- 再把 SVID 的证书链与私钥加载进
ssl_context - 遍历 trust bundle,把 CA 证书加载进验证配置
- 最终,
httpx使用 mTLS 调用上游 API,使其能够验证 MCP server 的 SPIFFE ID
SPIRE Agent 会自动轮换 SVID,通常是一小时一次。WorkloadApiClient 库会在后台自动处理轮换:当证书临近过期时,它会透明地重新获取新证书,因此你的应用不需要显式重载或重启。
当你需要验证入站连接时,可以从对方的客户端证书中提取 SPIFFE ID,并与 allowlist 对比,示例 9-12 展示了这一点。
示例 9-12 在 MCP server 中校验客户端 SPIFFE ID
from cryptography import x509
from cryptography.x509.oid import ExtensionOID
ssl_info = request.transport.get_extra_info('ssl_object')
if not ssl_info:
raise PermissionDenied("TLS connection required")
client_cert_der = ssl_info.getpeercert(binary_form=True)
if not client_cert_der:
raise PermissionDenied("Client certificate required")
cert = x509.load_der_x509_certificate(client_cert_der)
san_ext = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
spiffe_ids = [
name.value for name in san_ext.value
if isinstance(name, x509.UniformResourceIdentifier)
and name.value.startswith("spiffe://")
]
if not spiffe_ids:
raise PermissionDenied("No SPIFFE ID in client certificate")
client_spiffe_id = spiffe_ids[0]
allowed_ids = ["spiffe://example.com/ns/agents/sa/agent-runtime"]
if client_spiffe_id not in allowed_ids:
raise PermissionDenied(f"Unknown SPIFFE ID: {client_spiffe_id}")
这里:
- 从 TLS 连接中提取 DER 编码的客户端证书
- 解析证书,并从 SAN 扩展中提取 SPIFFE ID
- 再把完整 SPIFFE ID URI 与 allowlist 对比
需要注意的是,SPIFFE 认证的是工作负载(Pod / 容器) ,而不是终端用户。
如果你还需要做用户级别的归因——例如,究竟是哪个 customer support agent 帮用户提交了工单——那么常见做法是:SPIFFE 负责 workload authentication,用户身份则通过请求头或 JWT claim 传递。
例如,agent runtime 通过它的 SPIFFE ID(如 spiffe://example.com/ns/agents/sa/agent-runtime)建立 mTLS 连接,同时在请求头里带上一个 X-User-ID。MCP server 验证该 SPIFFE ID,从而信任调用方工作负载;随后再利用用户 ID 进行授权决策与审计记录。
SPIFFE 的一个巨大优势,是它几乎彻底消除了 secret 扩散问题——不再需要管理 API key、token 或 client secret。SPIRE 会自动完成凭据签发与按小时轮换,而且无需重启应用。
如果你已经在使用 service mesh,还可以把 SPIRE 配成 mesh 的 CA,从而在整个平台上统一 workload identity,而不必维护多套 CA。
当然,这套体系的运维要求也不低。
SPIRE Server 是你的根信任锚点,必须作为关键基础设施严肃对待。
在生产环境里,通常会把它部署在专门的 namespace 中,并通过严格的 NetworkPolicy 进行保护;RBAC 权限应只授予极少数管理员;它的持久卷也要定期备份。
SPIRE 升级必须谨慎进行,还应监控 SVID 签发模式是否出现异常,并针对意外 SPIFFE ID 或失败 attestation 配置告警。
相较 bearer token,SPIFFE/SPIRE 的学习曲线确实更陡。但它提供的是一种无法被单独窃取的工作负载身份,并且凭据会自动轮换,从而从根本上消除了凭据窃取类攻击。
MCP Gateway
另一种思路,是不把安全逻辑散落在每个 MCP server 中,而是通过一个 MCP gateway 作为集中式策略执行点。
MCP gateway 本质上是一种反向代理,它位于 agent runtime 与 MCP server 之间,负责在统一位置执行:
- 认证
- 授权
- 限流
- 审计日志
2025 年出现了多个 MCP gateway 实现,但这还只是开始。
例如:
- Microsoft MCP Gateway 提供了带会话感知的有状态路由、Kubernetes 生命周期管理,以及 OAuth 2.0 / RBAC 支持
- IBM ContextForge 支持多个 gateway 部署之间的联邦、虚拟 server 组合(把多个 MCP server 组合成一个逻辑 endpoint),以及 stdio、SSE 和 HTTP 之间的协议转换
- Envoy AI Gateway 在 Envoy 的架构上扩展了 MCP proxy,处理 JSON-RPC 多路复用,并集成 Envoy 的安全扩展
- Solo.io agentgateway 支持自动发现 MCP server;把多个工具 server 聚合成单个 endpoint;并通过内建指标、日志、trace 提供集中式可观测性
这一领域目前还非常年轻,因此你真正阅读到这里的时候,很可能已经出现更多新选择。
MCP gateway 的价值,在于把原本散落到各个 server 里的能力收拢起来。
在认证与授权方面,gateway 可以统一接入像 Keycloak 这样的身份提供方做 SSO,并通过 OPA、Cedar 等策略引擎执行细粒度访问控制。
代价则是更高的运维复杂度和额外延迟。
Gateway 会变成一个单点关键组件,因此必须以高可用方式部署,并配合多副本和负载均衡。每一条请求也都会因策略检查而增加额外延迟,具体取决于策略复杂度。
对于拥有大量 MCP server、多租户环境、或者复杂授权要求的系统而言,这些运维收益通常值得付出这些成本。
但对于更小规模的部署而言,把安全逻辑直接嵌入 MCP server,或者依赖 service mesh,也许会更合适。
如何选择合适的安全模式
没有任何一种模式适用于所有场景。
正确的选择取决于:
- 组织的安全成熟度
- 合规要求
- 现有基础设施
- 运维能力
我们前面提到的几种模式——agent impersonation、service account delegation、token exchange、SPIFFE/SPIRE——分别在简洁性、安全深度、运维开销之间做了不同取舍。要做出判断,就必须认真理解各自的收益与代价。
在实践中,生产系统很少只孤立使用某一种模式。更常见的是把多种模式叠加起来,形成纵深防御(defense in depth) 。
一个很常见的组合是:
- 用 SPIFFE/SPIRE 负责 workload-to-workload 的 mTLS
- 再用请求元数据携带用户身份
也就是说,agent runtime 会通过自己的 SPIFFE ID 建立一条加密、双向认证的通道,同时在请求中带上用户身份(例如通过 X-User-ID header,或者通过 exchanged token 的 claim)。
MCP server 先验证 SPIFFE ID,从而信任发起调用的 workload;然后再提取用户身份,做授权决策和审计记录。
这样,你就能同时获得 SPIFFE 的优势——密码学级 workload identity、自动轮换、防凭据窃取——以及每用户级别的可追踪性。
你还可以进一步把workload 身份和用户身份一起纳入授权策略。
例如,使用 Open Policy Agent(OPA) 来实施类似这样的规则:
“只有当调用方 workload 是 customer-support-mcp,并且该用户也有权访问请求的 customer record 时,才允许访问。”
这种组合式授权可以避免被攻破的 workload 任意访问数据,同时确保:服务本身和用户本人都必须被授权。
当你的系统需要和外部第三方 agent 协同时,还会出现额外复杂度。
如果用 token exchange,你需要做 Identity Provider federation,让你的身份提供方能够验证外部组织签发的 token。
如果用 SPIFFE/SPIRE,则需要做 trust bundle federation,让你的 SPIRE 部署和对方的 SPIRE 基础设施彼此信任。
两种方式都可行,但联邦配置显然会带来比纯内部部署更高的运维复杂度。
MCP 解决了 agent 与外部工具和数据源之间的连接问题,但一个生产级 agentic system 还会面临另一个集成挑战:
如果你有多个专门化 agent 需要协作,它们该如何通信?
目前大多数框架都发明了自己的一套协调机制,这就重复制造了 MCP 曾经为工具集成解决过的那类“碎片化问题”。
A2A 协议,正是在这种背景下出现,用来标准化跨 agent 协调。
Agent-to-Agent Protocol
在“Multiagent Systems”中,我们已经介绍过多智能体系统:由多个专门化 agent 协作来完成复杂任务。
例如,一个 planner agent 可能会把某个功能需求拆成几个更小的步骤,再把代码生成任务委托给 coder agent,然后把生成出来的实现交给 tester agent 去验证。
每个 agent 都带有自己的领域专长,但如果没有统一的通信方式,这类系统会变得脆弱,而且会被某种具体框架锁死。
截至 2026 年初,大多数多智能体框架仍然使用自己内部的协调机制。
例如:
- LangGraph 使用进程内 Python 函数调用,把控制权在 agent 之间传递
- CrewAI 则通过自定义 REST endpoint 在 agent 之间通信
这种碎片化会在生产环境中带来明显运维问题:
- 你无法轻易地让分布式 agent 跨集群协作
- 如果不同团队使用不同框架,它们很难彼此配合
- 当合规要求你必须审计 agent 之间的通信时,那些藏在框架内部抽象里的交互也几乎无法被统一观测和治理
你很难把一个 LangGraph planner 直接接到 CrewAI coder 上;同样,你也无法对那些发生在框架内部的 agent 通信实施统一监控或安全策略。
Agent-to-Agent(A2A) 协议,就是为解决这种协调问题而出现的。
A2A 标准化了 agent 如何:
- 发现彼此的能力
- 委派任务
- 跟踪进度
- 以及流式传递结果
它为多智能体系统提供了一种通用语言,正如 HTTP 之于 Web 服务。
2025 年 8 月,IBM 的 Agent Communication Protocol(ACP) 并入了 Linux Foundation 旗下的 A2A,完成了当时两条主要 agent-to-agent 通信路线的整合。
ACP 带来的是一种更偏 RESTful 的 agent 通信方式(与 MCP 的 JSON-RPC 风格形成互补),它并入 A2A 之后,也进一步强化了 A2A 作为 agent 互操作统一标准的地位。
A2A 与 MCP 的关系:互补,而非替代
在真正进入 A2A 本身之前,先要厘清它与上一节 MCP 的关系。
两者都是连接 AI 组件的标准,但它们服务于不同目的,也工作在不同抽象层次。
MCP 连接的是 agent 与工具 / 数据源。
它基本遵循的是一种同步 request-response 模式:²
- agent 发出一个带类型的请求
- 工具执行操作
- 工具返回结果
因此,MCP 特别适合让 agent 接入周围的操作环境。
A2A 连接的则是 agent 与 agent。
当一个 agent 需要另一个 agent 去执行一项包含推理、规划或迭代过程的任务时,它就使用 A2A 来进行委派。
接收方并不是一个被动工具,而是一个自治系统:它会自己拆解问题、调用自己的工具,并决定如何推进。
A2A 因此采用的是一种异步任务委派模型,并带有完整的生命周期跟踪:
- 发起方提交任务
- 得到一个 task identifier
- 然后通过轮询或订阅状态更新,跟踪接收方 agent 的处理过程
这种异步性非常符合现实:agent 往往需要时间去推理、迭代,才能产出结果。
当然,你也完全可以把另一个 agent 包装成一个“MCP tool”,然后用 MCP 实现 agent 间通信。
在一些非常简单的 delegation 场景下,这种做法是成立的:你只想把一个请求交出去,然后等结果回来。
但如果把 agent 仅仅看作工具,就会失去 A2A 所带来的 richer semantics。
因为 MCP 目前还不具备:
- agent capability discovery:你无法程序化地找到拥有特定技能的 agent
- task lifecycle tracking:你无法监控长时间运行的 agent 工作,也无法取消不再需要的任务
- 中间过程流式暴露:MCP 作为面向经典确定性 API 的前端,无法很好表达 agent 在多步计划中的中间 reasoning/progress
因此,最佳实践通常是:
- 用 A2A 做跨 agent 协调
- 用 MCP 做每个 agent 内部的工具集成
这种职责分离会让系统更具可组合性:
每个 agent 通过 MCP 接自己的工具和数据源,而当任务需要委派给另一个推理系统时,则通过 A2A 协调。
这样一来,你可以独立升级、替换或扩展某个 agent,而不会破坏其工具接入;也可以独立更换某个工具实现,而不影响 agent 间协作。
A2A 速览(A2A in a Nutshell)
A2A 建立在标准 Web 协议之上:
- HTTP 用作传输层
- JSON 用作数据表示
- JSON-RPC 用于结构化方法调用
这使得它对已经熟悉 REST API 和 service-to-service 通信的团队来说上手并不困难。
这个协议引入了三个核心概念,使得多智能体协调真正具备工程可用性。
1. Agent Card
第一类核心概念,是 agent card。
它本质上是一个 JSON 文档,用来描述某个 agent 会做什么。
其中会列出:
- 该 agent 提供哪些 skill
- 它接受哪些输入格式
- 它输出哪些格式
- 它支持哪些协议版本
它在 A2A 中扮演的角色,很像 OpenAPI 规范在 REST API 中的角色:它告诉其他 agent,与这个 agent 交互时应该期待什么。
示例 9-13 展示了一个简化版“代码审查 agent”的 agent card。
示例 9-13 A2A 协议中的代码审查 agent card
{
"agent_id": "code-reviewer",
"skills": ["code_review", "security_scan"],
"input_modes": ["text/plain", "application/json"],
"output_modes": ["application/json"],
"protocols": ["a2a/v1"]
}
当一个 planner agent 需要委派一次 code review 时,它就可以先向某个 discovery service 查询哪些 agent 具备 code_review skill,再拉取 agent card,确认这个 reviewer 是否能接受自己打算发送的输入格式。
2. 任务生命周期(Task lifecycle)
第二类核心概念,是 task lifecycle,也就是一个状态机,用来跟踪被委派工作从创建到结束的整个过程。
当一个 agent 把任务委托给另一个 agent 时,它会带着输入 payload 提交任务,并收到一个 task ID。
随后,这个任务会经历一系列明确定义的状态:
created:任务刚提交in_progress:接收方 agent 已开始工作- 最终进入
completed、failed或cancelled
发起方 agent 可以轮询 /task endpoint 查询状态,也可以在接收方支持的情况下订阅状态推送。
这种生命周期模型非常适合长时间运行的任务,并且让你能清楚知道任意时刻每个 agent 在做什么。
3. Artifact Streaming
第三类核心概念,是 artifact streaming,也就是一种在 agent 之间传递大结果或中间结果的机制,而不必等整个任务完成后再一次性返回。
随着 agent 处理任务,它可以把部分结果持续流回给请求方。
例如,一个文档生成 agent 在编写一个多章节报告时,可以在每写完一节后立刻把这一节流出来。这样,下游 agent 就能在后续章节还未完成之前,先对已完成部分开始处理。
Artifact streaming 能显著降低多智能体流水线的端到端延迟,也使你在任务尚未彻底结束前,就能看到 agent 当前进度。
这里最关键的理解是:
任务的生命周期独立于单个 HTTP 请求本身。
Planner 可以先提交任务,然后断开;稍后再重新连接回来查看进度。
这种解耦让 A2A 在分布式环境中更健壮——即便发生网络分区、agent 重启,长任务也仍有办法继续推进。
在 Kubernetes 上运行 A2A(Running A2A on Kubernetes)
从 Kubernetes 的角度来看,A2A 使一种部署模型成为可能:
每个 agent 都以独立 Deployment + 独立 Service endpoint 的方式运行。
这其实正是贯穿本书的一条主线:把 agent 也当作微服务,只不过现在它们不再是普通应用逻辑,而是自治推理系统。
在这种模型下,agent 可以根据自己的工作负载特征独立扩缩。
例如,一个代码审查 agent 请求量很大,那它就可能需要比一个 planner agent 更多副本。
这种独立性同样适用于发布节奏:你可以只升级 reviewer,而完全不动 planner 和 tester。
安全方面,你可以利用 NetworkPolicy 限制哪些 agent 能和哪些 agent 通信,从而为整个 multiagent system 增加一层防御。
而在可靠性方面,service mesh 提供的 circuit breaker 策略可以在某个 agent 不健康时实现优雅降级,避免故障层层传播。
在这里,agent card 就成了一种可以在部署阶段校验的契约。
在发布某个 agent 的新版本之前,你可以检查它的新 agent card 是否仍然与现有调用方兼容。
例如,如果一个新的 reviewer agent 不再支持 text/plain 输入,而你的 planner 仍然发送纯文本 diff,那么这种不兼容就可以在上线前被发现,而不是等到生产里爆出来。
任务生命周期跟踪也让 multiagent 运维变得可观测。
你可以把监控栈接起来,专门观测:
- 哪些任务在
in_progress状态停留过久 - 哪些 agent 的失败率较高
- 多步工作流中的瓶颈到底出在哪一段
这种可见性,对于在生产环境中调试和优化 agent 系统至关重要。
然而,即便 agent 都部署成了独立服务,MCP 与 A2A 也只解决了“集成”问题——前者让 agent 能接工具,后者让 agent 能互相协调。
它们都隐含了一个前提:agent 本身是可以在负载均衡后面水平扩展的无状态服务。
但只要你真正部署了一个对话型 agent,它就必须在多轮交互中记住上下文,这个假设立刻就失效了。
协议解决的是通信问题;而持久化,你还得自己解决。
Agent 状态管理(Agent State Management)
当你把 agent 部署到 Kubernetes 上时,最先撞上的问题之一,往往就是有状态性(statefulness) 。
与传统 REST API 中每个请求彼此独立不同,agent 天生就是会话型的。
它们需要记住:
- 用户三轮之前问过什么
- 已经检索过哪些文档
- 已经得出了哪些中间结论
例如,一个客服 agent 正在帮助用户排查数据库连接问题:
- 第一轮先识别数据库类型
- 第二轮要求用户提供错误日志
- 第三轮再基于前两轮上下文,提出具体配置修改建议
这种不断累积的“记忆”,正是 agent 能真正有用的根本。
这种 statefulness 会立即引出一系列运维问题:
- 这些状态到底存在哪里?
- Pod 重启后如何保留它们?
- 当你水平扩展时,每个 agent 实例如何访问同一份会话历史?
这一节中,我们会系统梳理解决这些问题的常见模式:从适合开发环境的简单内存方案,到生产可用的基于 KV store 和 数据库 的方案。
状态存储模式(State Storage Patterns)
Agent 的状态大体可以分成两类:
- 短期记忆(short-term memory) :保存当前会话中的活跃上下文
- 长期记忆(long-term memory) :保存用户偏好、历史交互、学习得到的模式,跨会话存在
短期记忆需要在活跃对话中以很低延迟被访问;长期记忆则更偏向支持:
- 分析
- 个性化
- 审计
最简单的短期记忆方案,就是全部放在 Pod 内存里。
你的 agent 维护一个字典,用 session ID 映射到对应会话历史,把整段上下文都保存在进程内存里。
在开发和测试阶段,这种方式非常舒服,因为完全没有外部依赖,迭代速度极快。
但它在生产环境中的限制也很明显:
- 一旦 Pod 重启——无论是发布、节点故障还是资源压力导致——所有会话状态全部丢失
- 正在交互中的用户会突然“失忆”,系统不再记得他们之前说过什么
- 也无法真正水平扩展,因为每个 Pod 都只持有自己那份隔离状态
- 如果负载均衡把同一个用户的连续请求打到不同 Pod,会话上下文就跟不过去
因此,到了生产环境里,最常见的短期记忆模式,是使用一个分布式 键值存储(KV store) ,例如 Redis。
KV store 提供近乎内存级别的访问速度,同时具备持久化保证,因此即使 Pod 重启,会话状态仍能保留下来。
模式本身也很简单:当用户开启一次会话时,你生成一个 session ID,并把它当作 KV store 的 key。每一轮对话结束后,把当前 conversation state 序列化后写回去,并设置一个与会话过期策略匹配的 TTL(time to live) 。
图 9-7 展示了 agent 在请求生命周期中如何管理状态:短期 session state 存放在 KV store 中,而长期记忆则可以选择性地落库到数据库中。
在 Kubernetes 上,你通常会把状态存储部署为一个 StatefulSet + PersistentVolume,确保即便 Pod 重启,数据依然存在。
这种方式带来几个重要好处:
- 状态会随着持久卷保留下来,不会因 Pod 重启丢失
- 你的 agent Pod 可以自由水平扩展,因为它们都连接到同一个状态存储
- TTL 机制会自动清理长时间不活跃、被放弃的 session,无需人工干预
图 9-7 Agent 状态管理流程:KV store 用于 session,数据库可用于长期记忆
在 KV Store 与数据库之间如何选择(Choosing Between Key-Value Stores and Databases)
在生产系统里,往往会同时使用 KV store 与 数据库,分别对应 agent 的两类记忆:
- KV store:保存短期记忆(活跃 session state)
- 数据库:保存长期记忆(历史模式、审计轨迹)
理解什么时候该用哪一层,能避免两种常见错误:
- 过度设计:其实只需要 KV store,却过早引入数据库
- 设计不足:一开始只用 KV store,结果业务一上来就撞上它的边界
前一小节已经用非常接近的表述解释过这一点。
如果你构建的是一种“每个 session 都是独立的、也不需要跨 session 查询”的纯对话型 agent,那么只使用一个 KV store 作为短期记忆层就足够了。
只有当你开始需要长期记忆能力,而这些能力超出了单个 session 的读写范畴时,数据库才真正变得必要。
例如,SQL 数据库允许你做跨 session 查询:
- “过去一周里,所有提到价格担忧的对话有哪些?”
- “有多少比例的会话最终升级到了人工支持?”
这种跨会话分析,是 KV store 的简单 key-value 模型无法做到的。
此外,数据库还提供长期保存、不可变审计轨迹所需的耐久性和保留能力。
如果某些监管要求你必须证明:六个月前 agent 给过用户什么建议,那么这些数据就必须存进一个有备份、可查询、并且具备明确 retention policy 的数据库中。
在生产里最好用的模式,往往是分层组合:
- 每一次请求都先从 KV store 读 / 写短期记忆(session state)
- 当你需要把某些信息沉淀成长期记忆——如审计日志、用户偏好、分析洞察——则异步写入数据库,避免阻塞用户响应
- 当 agent 在新 session 中需要拿到历史偏好或长期模式时,再从数据库读取,并把这些结果缓存进短期记忆层
这种分层设计同时兼顾了:
- 速度:用户请求的关键路径仍然主要在内存型 KV store 中
- 查询能力:长期数据可以被 SQL 或分析系统查询
- 耐久性:长期记忆不会像 KV 缓存那样轻易失效
因此:
- 你的分析团队可以基于长期记忆数据库做报表,理解 agent 的行为模式
- 合规团队可以基于带有 retention policy 的日志进行审计
- 工程团队可以用 SQL 直接查询生产数据来排查问题
- 用户仍然能得到足够快的响应,因为热路径依然留在内存中
对于简单 agent——例如内部工具、开发环境,或完全临时性的对话——你可以先从 只有 KV store 的短期记忆开始。
当你出现以下任一需求时,再引入数据库作为长期记忆层:
- 监管要求不可变的审计轨迹
- 分析需求必须跨 session 查询
- 数据保留周期远超你愿意在 KV store 中保存的时长
此时,KV store 仍然是你的短期记忆性能层;数据库则成为你的长期记忆耐久层与查询层。
面向长时间运行 agent 的 Checkpointing(Checkpointing for Long-Running Agents)
有些 agent 的工作流会持续数小时,甚至数天。
例如:
- 一个 research agent 可能要阅读上百篇文档、提取关键发现、最后综合成一份报告
- 一个 testing agent 可能要运行整套实验、分析结果,再给出建议
这类长任务需要另一种模式:checkpointing,也就是把中间结果保存下来。
它的核心思想很简单:
在完成工作流中的每个重要步骤之后,agent 都会把当前状态和进度存成一个 checkpoint。
这样,如果 Pod 被驱逐或崩溃,agent 就可以从最近一次 checkpoint 继续,而不是从头再跑。
这在 Kubernetes 上尤其有价值,因为 Pod 本来就是易失性的,随时可能被重新调度。
示例 9-14 展示了一个最简单的 checkpoint 实现,这里先暂时忽略任何用户上下文。
示例 9-14 简单 checkpoint 算法
import json
from pathlib import Path
def save_checkpoint(step: int, state: dict):
# Store checkpoints on a PersistentVolume mounted at /data
checkpoint_dir = Path("/data/checkpoints")
checkpoint_dir.mkdir(exist_ok=True)
# Save state as JSON with a sequential step number
(checkpoint_dir / f"step_{step:03d}.json").write_text(json.dumps(state))
def load_latest_checkpoint() -> tuple[int, dict]:
checkpoint_dir = Path("/data/checkpoints")
# Find all existing checkpoints and sort by filename
checkpoints = sorted(checkpoint_dir.glob("step_*.json"))
if not checkpoints:
# Start from scratch if no checkpoints exist
return 0, {}
latest = checkpoints[-1]
step = int(latest.stem.split("_")[1])
return step, json.loads(latest.read_text())
在 Kubernetes 的 Job manifest 中,你通常会这样配合:
- 设置
restartPolicy: OnFailure - 把一个
PersistentVolumeClaim挂载到/data
这样,当 Pod 启动时,它会先调用 load_latest_checkpoint(),判断自己应当从哪里恢复。
每完成一个重要阶段——例如处理完一批文档、或完成一个分析阶段——就调用 save_checkpoint() 保存进度。
如果 Pod 因故障或驱逐而退出,Kubernetes 会把它重新拉起来,而 agent 可以准确地从中断点继续。
Checkpoint 目录本身还会成为一个非常有价值的调试工具。
你可以直接查看中间状态,理解 agent 在每个阶段到底“想了什么、做了什么”。
如果最终输出结果不符合预期,就能沿着 checkpoint 反向追踪,看看到底是哪一步的推理开始跑偏。
对于那些决策链可能长达几十步的复杂 agent 来说,这种可见性极其重要。
到这里,MCP 为工具集成提供了基础,A2A 为 agent 协调提供了协议,而状态管理模式则为持久化提供了解法。
这三块加起来,基本构成了你在 Kubernetes 上部署 agentic application 的核心基础设施:它们分别覆盖了最关键的生产问题——安全通信、标准化协作,以及跨分布式 Pod 的有状态会话管理。
经验总结(Lessons Learned)
在生产环境中运行 agentic application,意味着你必须把它们当作一种自治系统来看待:它们会根据自然语言输入进行推理、迭代,并自主做出决策。
与那些故障模式可预测、行为确定的微服务不同,agent 具有一些独特特征:
- 非确定性
- 多跳推理流程
- 涌现式行为
因此,它们要求与传统微服务不同的运维方法。
深入理解 MCP 和 A2A 在“线上的真实协议层行为”——而不只是把它们看作框架提供的抽象——会给你很强的能力:
你可以真正调试生产问题,制定自定义安全策略,并构建平台服务,把组织范围内的控制措施统一落实下去。
有几条运维原则,能在生产中帮你少走很多弯路:
安全模式的选择,应基于合规要求,而不是便利性
如果你需要用户级归因,那么无论复杂度多高,都应选择 agent impersonation 或 token exchange。
如果你在做的是零信任环境,那么即便学习曲线很陡,也应投入到 SPIFFE/SPIRE 上。
只有在你同时掌控 agent 和 API,并且你的合规要求允许时,才应选择 service account delegation。
从一开始就把状态外置
内存内状态一旦遇到水平扩展或 Pod 重启,就会立刻失效。
对 session state 使用 KV store;对长时间运行工作流实现 checkpointing。
如果等扩到生产规模后再补状态管理,那会非常痛苦。
充分利用 Kubernetes 原语
Deployments、Services、NetworkPolicies、RBAC、StatefulSets、Jobs——这些在传统微服务场景中已经被反复验证的模式,在 agentic workload 中同样适用。
从简单开始
先把 agent 作为普通 Kubernetes deployment 部署起来;在真正遇到具体瓶颈之前,不要急着上 service mesh 或自定义 operator。
只有在你碰到明确限制时,再逐层增加复杂性,而不是因为白板上的架构看起来优雅就先做复杂设计。
你现在打下的这套运维纪律,无论未来技术如何演化,都会一直重要。
协议本身也许会变化,但以下需求不会消失:
- 安全通信
- 标准化协调
- 持久化状态
¹ 这些模式受到了 Christian Posta 的启发,并可视作对他文章 “MCP Authorization Patterns for Upstream API Calls” 中类似模式的补充。
² 截至 2026 年初,MCP 已经加入了异步任务执行能力,进一步缩小了它与 A2A 在功能上的差距。随着两个协议都已进入 Linux Foundation 治理框架之下,它们目前被定位为互补层;但考虑到 MCP 的采用优势,并不排除它最终会逐渐吸收 A2A 的一部分能力。