外观
从主从编排到 A2A:订单 Agent 安全调用售后 Agent 实录
结论先行:跨团队 Agent 协作,别再让一个大脑指挥所有手
我的核心判断是:只要两个 Agent 由不同团队独立部署、独立演进,就不该用一个中心 Orchestrator 去硬编码它们的接口,而应走 A2A 协议的对等协作。在我们最近的自动退款审批改造里,订单 Agent 通过标准的 Agent Card 发现售后 Agent、用 OAuth2 client_credentials 完成机器身份鉴权、再靠任务状态事件流异步拿到审批结论,跨团队联调从平均两周压缩到两天,且售后侧改内部规则完全不需要订单团队发版。
这不是又一个多智能体 Demo,而是一次带着真实资金风险的企业落地。下面把我们的取舍、关键代码与踩过的坑讲清楚。
为什么主从编排在这里是错的
最初的方案很“顺手”:在订单服务里写一个 Orchestrator,函数调用售后系统的 HTTP 接口,拿到风控结果后决定退款。跑通只花了两天,但三个月后维护成本爆炸——售后团队每改一次审批字段,订单侧就要跟着改 DTO、重新回归;更难受的是权限,订单服务拿着一把“万能令牌”直连售后数据库视图,安全评审直接被打回。
本质问题在于:主从模式假设所有 Agent 共享同一个发布节奏和同一份内部模型,这在单体团队里成立,在跨部门协作里不成立。A2A(Agent2Agent)把“谁在什么位置、会什么、怎么鉴权、任务进行到哪一步”全部标准化,让两个 Agent 像两个独立微服务一样对话,而不是一个大脑指挥一堆手。
1主从多智能体编排
- 集中控制,链路直观
- 初期 Demo 上手快
- Orchestrator 需感知所有子 Agent 的 API 与字段
- 售后团队改接口要排期联调
- 故障与权限集中在中心,爆炸半径大
适合单团队、流程稳定的内部场景
2A2A 对等协作
- Agent Card 自描述能力,发现即对接
- 鉴权走标准 OAuth2,审计统一
- 任务事件流异步回调,团队解耦
- 需要治理卡片版本与令牌生命周期
- 异步链路可观测性要额外建设
适合跨团队、独立部署的企业级场景
Agent Card:把“能力发现”做成可检索的契约
A2A 的入口是一张部署在对方域名下的 Agent Card,固定路径通常是 /.well-known/agent.json。它不是营销页,而是一份机器可读的能力契约:描述了服务端点、支持的技能、输入模式、鉴权方式与版本号。订单 Agent 启动时拉取并缓存这张卡片,就能知道“退款审批该往哪里发、要带什么字段”。
一份裁剪过的售后 Agent Card 大致长这样:
json
{
"name": "after-sales-refund-agent",
"description": "负责退款单的风控复核与审批",
"version": "1.3.0",
"url": "https://after-sales.internal.example.com/a2a",
"capabilities": { "streaming": true, "pushNotifications": true },
"defaultInputModes": ["text", "data"],
"skills": [
{
"id": "refund-approve",
"name": "退款审批",
"description": "根据订单与售后规则返回批准、拒绝或需人工介入",
"tags": ["refund", "approval"],
"inputModes": ["data"]
}
],
"security": {
"schemes": ["oauth2"],
"oauth2": {
"tokenEndpoint": "https://auth.example.com/oauth2/token",
"scopes": ["refund.approve"]
}
}
}我坚持让售后团队把卡片纳入版本管理并加上 version 字段。原因很现实:订单侧会对卡片做 schema 缓存,一旦对方悄悄改字段,没有版本号你连“对方变了”都感知不到,排查时只会看到莫名其妙的反序列化失败。
OAuth2:机器对机器,别再共用一个员工账号
鉴权是这类场景最容易被“先跑通”糊弄过去的一环。我们早期为了省事,用某个运维同事的个人 PAT 调通了链路,结果那人离职账号一冻结,半夜退款全挂。这个坑我排查了整整一晚上,最后在工单系统里才发现令牌归属人。
正规做法是机器对机器的 client_credentials:订单 Agent 在授权服务器注册自己的 client_id,申请最小 scope,每次调用前换取短时令牌。Python 侧的关键代码如下:
python
import httpx
AUTH = "https://auth.example.com/oauth2/token"
async def fetch_token(client_id: str, client_secret: str) -> str:
# 仅申请退款审批所需 scope,遵循最小权限原则
async with httpx.AsyncClient(timeout=5) as c:
r = await c.post(
AUTH,
data={
"grant_type": "client_credentials",
"scope": "refund.approve",
},
auth=(client_id, client_secret),
)
r.raise_for_status()
return r.json()["access_token"]令牌绝不能写进 Agent Card 或代码仓库,我们放在集中密钥管理里,按环境注入。另一个关键动作是在售后侧做 scope 与资源的绑定校验:持有 refund.approve 只能调审批任务,碰不到工单查询接口。这样即使订单 Agent 被攻破,爆炸半径也被限制在单一能力上。
任务状态事件流:异步才是真实业务的常态
退款审批不是毫秒级返回的纯函数——它可能要等风控评分、等人工主管复核。A2A 用任务对象建模这种长生命周期交互,订单侧发起 tasks/send 后,通过 SSE 订阅 working、input-required、completed、failed 等状态,而不是傻等一个 HTTP 响应。
1
拉取 Agent Card
订单 Agent 从售后域名 /.well-known/agent.json 获取端点、能力与鉴权方式
2
获取令牌
用 client_credentials 向授权服务器换取受限 scope 的访问令牌
3
发起任务
POST tasks/send,携带订单号、金额、退款原因等结构化参数
4
订阅事件
通过 SSE 接收 working / input-required / completed 状态
5
落库与通知
审批结果回写订单库并触发用户通知
发起任务时我们带上业务幂等键,避免重试造成重复退款:
python
import json, uuid
async def submit_refund(token: str, endpoint: str, order: dict) -> str:
payload = {
"jsonrpc": "2.0",
"id": str(uuid.uuid4()),
"method": "tasks/send",
"params": {
"id": f"refund-{order['order_no']}", # 业务幂等 ID
"sessionId": str(uuid.uuid4()),
"message": {
"role": "user",
"parts": [
{"kind": "text", "text": "请审批该订单退款"},
{"kind": "data", "data": {
"orderNo": order["order_no"],
"amount": order["amount"],
"reason": order["reason"],
}},
],
},
},
}
headers = {"Authorization": f"Bearer {token}"}
async with httpx.AsyncClient(timeout=10) as c:
r = await c.post(endpoint, json=payload, headers=headers)
r.raise_for_status()
return r.json()["result"]["id"]拿到 task id 后,事件处理循环按状态推进本地状态机:working 只更新进度、input-required 转人工补件、completed 才真正执行资金退款并通知用户。资金动作必须只由终态事件触发一次,这是我们在代码评审里反复强调的红线。
落地分层:让协议之外的东西有人管
A2A 规范只解决“怎么对话”,企业落地还要补齐身份、治理与观测。我们把整体能力划成四层,权责清晰后跨团队扯皮明显少了。
1
身份与授权层
OAuth2 授权服务器client 注册与 scope 最小化令牌审计
2
能力发现层
Agent Card版本与环境标识能力安全清单
3
任务协作层
tasks/send 与 querySSE 状态事件流幂等与任务续接
4
治理观测层
链路追踪配额熔断敏感字段脱敏
一个容易被忽视的点是可观测性:调用链跨了两个独立服务,光看订单侧日志根本不知道卡在哪。我们要求两边在事件里透传同一组 trace 标识,并把每次状态变迁、令牌申请、scope 校验结果都打进审计日志。事后复盘某次“退款审批超时”,正是靠这些日志定位到售后风控下游的征信接口抖动,而不是 A2A 层本身的问题。
避坑总结:三条用故障换来的经验
- 令牌身份必须独立且短命。 现象:员工离职或密钥轮换后批量任务 401。修正:为每个 Agent 注册独立 client,使用 client_credentials 换取短时令牌,密钥进集中管理并定期轮换,禁止共用个人 PAT。
- Agent Card 必须带版本并做缓存失效。 现象:对方更新字段后本地反序列化静默失败,退款参数丢失。修正:卡片加
version,客户端缓存时记录 ETag,启动与定时双刷新,发现不兼容版本直接熔断并告警,而不是带病调用。 - 资金类副作用只认幂等终态。 现象:网络重试导致同一退款执行两次。修正:用业务订单号生成稳定 task id,售后侧做幂等去重,订单侧只在收到首个
completed事件且本地未处理时执行退款,并以数据库唯一约束兜底。 - 权限按 scope 收窄,拒绝万能令牌。 现象:一个 Agent 失陷即可横向读取售后全量数据。修正:一能力一 scope,资源服务器强制校验,敏感字段在进入对方上下文前脱敏。
从紧耦合的主从编排转向 A2A 对等协作,表面是换了一套调用协议,真正换来的是团队边界的清晰和系统演进的自由度。协议会迭代,卡片会增字段,但“能力自描述、身份最小化、任务可观测”这三条原则值得长期坚持。这篇踩坑记录首发于开发者日记,希望能让正在做多智能体企业落地的你,少熬几个我熬过夜。
<!-- 内容间隙推广位:正文之后内联渲染,不遮挡阅读;变现开关关闭时零渲染零占位 -->
<MonetizeSlot position="inline" domain="essays" />
<!-- 免责声明:文字与 data/site.config.yaml 的 disclaimer 保持完全一致,不得删改 -->
::: warning 免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。
:::