A2A 调用的链路关联与按调用方计费
外部 A2A 智能体把平台签发的调用凭证带到模型调用里,平台据此把智能体内部的模型调用关联到具体某一次 A2A 调用,并把费用记到调用方名下
A2A 调用的链路关联与按调用方计费
单位管理员 A 接入的外部 A2A 智能体,会被 B、C 等多位成员调用。智能体处理请求时自己也要调用平台的模型网关。默认情况下,这些模型调用都记在智能体使用的模型密钥名下,也就是 A 名下;平台也无法分辨某一次模型调用是为哪一次 A2A 调用服务的。
接入「A2A 调用凭证」后:
- 每一次 A2A 调用都能在请求详情里看到智能体内部发起的全部模型调用,以及这些调用的次数、令牌数与花费合计;模型调用详情也能跳回对应的 A2A 调用。
- 智能体内部的模型调用记在调用方 B、C 名下,额度按调用方的人员额度判定,同时仍受该密钥自身额度的约束。
- 智能体详情的监控页签按调用方列出模型花费,没有携带凭证的调用单独列为「智能体自身调用」。
关联与计费都不依赖链路追踪编号,也不要求调用方改动任何请求。
1. 工作方式
- 调用方通过平台调用智能体时,平台每转发一次 A2A 请求就签发一枚调用凭证(以
zac_开头),同时放在两处:请求头X-Zenova-Call-Context,以及params.message.metadata中的zenova_call_context字段。 - 智能体从入站请求中取出凭证,放进本次请求的作用域(见第 4 节)。
- 智能体使用自己的专属模型密钥调用平台模型网关,在请求体
metadata.zenova_call_context中原样带上凭证。 - 模型网关在本地验证凭证的签名与有效期,确认凭证签发给的智能体就是该密钥绑定的智能体,然后把本次模型调用记在调用方名下,并记下它属于哪一次 A2A 调用。
凭证由网关节点在本地签发和验证,不需要访问管理控制台的任何接口。
2. 计费口径
- 只有调用方本人申领的凭据才按人计费。 调用方通过应用市场申领的连接、个人 MCP 令牌,或在平台页面上以本人身份发起调用时,平台才签发凭证。管理员在智能体详情页签发、交给业务系统使用的令牌不签发凭证,这类调用记在密钥属主名下。需要按人计费时,请让调用方自己申领连接。
- 不带凭证的模型调用记在密钥属主名下。 平台对这类调用一律放行,不报错;监控中归入「智能体自身调用」。智能体自己发起的定时任务等场景就属于这一类。
- 凭证有效期 1 小时。 以模型请求的开始时间判定:在有效期内发起、超过有效期才结束的流式调用照常完成。超过 1 小时的长任务,之后发起的模型调用会收到
A2A_CALL_TOKEN_EXPIRED。 - 额度同时判定两处。 模型调用既要满足该专属密钥的额度,也要满足调用方的人员额度,任一处不足都会被拒绝。调用方额度不足时返回现有的额度错误码,对外消息中不包含调用方的额度明细。
3. 前置条件
- 接入智能体:在管理控制台「A2A 目录」中接入该 A2A 智能体并启用。
- 创建智能体专属模型密钥:打开该智能体的「A2A 智能体详情」,在「详情」页签的「模型密钥」区点「新建」。智能体已预填并锁定,选择工作区和模型分组后提交。密钥明文只显示一次,请立即交给智能体的运维人员保存。普通 API 密钥不能用于按调用方计费:普通密钥携带凭证时,凭证会被忽略,调用记在密钥属主名下。
- 配置模型网关地址:「模型密钥」区下方的「接入说明」给出本部署的模型网关地址和可直接复制的代码片段。
- 调用方申领:调用方在应用市场申领该智能体的连接(也可以使用个人 MCP 令牌,或在平台页面上直接调用),然后照常调用,不需要任何额外配置。
4. 智能体侧接入
4.1 取出凭证
凭证同时位于请求头 X-Zenova-Call-Context 和 params.message.metadata.zenova_call_context,取哪一处都可以。两处都存在但值不同时,说明请求在途中被改动,应直接拒绝该请求。两处都没有时,本次请求不带凭证,模型调用记在密钥属主名下。
4.2 按请求作用域保存凭证
同一个智能体进程会同时处理多位调用方的请求。凭证必须保存在请求作用域内,例如 Python 的 contextvars.ContextVar。
请勿把凭证存进全局变量、模块变量,或模型客户端的默认参数(例如默认请求头、默认请求体)。否则 C 的请求到达时会覆盖 B 的凭证,B 后续的模型调用会带上 C 的凭证,费用记到 C 名下。asyncio.create_task 与 asyncio.gather 会复制当前上下文,工具调用与子任务因此会自动继承本次请求的凭证。
import contextvars
METADATA_KEY = "zenova_call_context"
_current_token = contextvars.ContextVar("zenova_call_context", default=None)
def model_call_metadata():
token = _current_token.get()
return {METADATA_KEY: token} if token else None4.3 在每一次模型调用中携带凭证
OpenAI Python SDK:每次调用时现取凭证,写入 metadata。
metadata = model_call_metadata()
kwargs = {"metadata": metadata} if metadata else {}
response = await client.chat.completions.create(
model=model,
messages=messages,
**kwargs,
)AgentScope:智能体内部调用模型时只传消息与工具,因此需要子类化 OpenAIChatModel,在 _call_api 中逐次写入 metadata。
from agentscope.model import OpenAIChatModel
class ZenovaChatModel(OpenAIChatModel):
async def _call_api(self, model_name, messages, tools=None, tool_choice=None, **kwargs):
metadata = model_call_metadata()
if metadata is not None:
kwargs["metadata"] = metadata
return await super()._call_api(
model_name, messages, tools=tools, tool_choice=tool_choice, **kwargs
)模型网关也接受请求头 X-Zenova-Call-Context,请求头与请求体 metadata.zenova_call_context 任选其一;两处都携带时,值必须相同。多数智能体框架不便按请求修改请求头,因此推荐只使用请求体。
完整的可运行示例(OpenAI Python SDK 版、AgentScope 版及并发自测脚本)位于产品仓库的 lobsterpool/examples/a2a-call-attribution/ 目录。
5. 查看关联与花费
| 位置 | 内容 |
|---|---|
| A2A 请求详情 →「智能体内部模型调用」 | 本次 A2A 调用期间智能体携带凭证发起的全部模型调用,以及次数、令牌数与花费合计;点击一行进入该模型请求的详情 |
| 模型请求详情 | 调用方显示为「智能体 X(代 B 调用)」,并提供「查看 A2A 调用」跳回对应的 A2A 请求 |
| A2A 智能体详情 →「监控」页签 →「按调用方的模型花费」 | 所选时间范围内该智能体专属密钥的模型花费,按调用方汇总;未携带凭证的调用汇总为「智能体自身调用」一行 |
A2A 调用没有任何内部模型调用时,「智能体内部模型调用」区显示「该次调用没有经平台的模型调用,或智能体未携带调用凭证」。
6. 错误码
模型网关校验凭证失败时会直接拒绝请求,不会改为按密钥属主计费。智能体应把失败原样返回给调用方。
| 错误码 | HTTP | 含义 | 处理方法 |
|---|---|---|---|
A2A_CALL_TOKEN_CONFLICT | 400 | 请求头与请求体 metadata 中都带了凭证,但值不同 | 只保留一处,或保证两处值相同 |
A2A_CALL_TOKEN_MALFORMED | 400 | 凭证不是单个字符串,例如请求体中有重复字段,或请求头出现多个值 | 原样传入站时取得的字符串,不要改写 |
A2A_CALL_TOKEN_EXPIRED | 401 | 凭证已超过 1 小时有效期 | 见第 2 节第 3 条 |
A2A_CALL_TOKEN_INVALID | 401 | 签名校验失败(凭证被改动、伪造或已轮换),或凭证所属单位与密钥所属单位不同 | 只传平台转发时注入的凭证,并使用本单位创建的专属密钥 |
A2A_CALL_TOKEN_AGENT_MISMATCH | 403 | 凭证签发给的智能体与密钥绑定的智能体不同 | 每个智能体使用自己的专属密钥 |
全部错误码见「错误参考」。