最近在给 Foundry 上的托管代理配置对外能力时,碰到了一个绕不开的问题:怎么让别的 agent 框架也能发现我这个代理、跟它对话。翻了一圈 Microsoft Foundry 的文档,发现官方已经把 Agent-to-Agent(A2A)协议接进了托管代理里,跨框架、跨技术栈的代理之间能直接打通。原文在这儿:Enabling A2A endpoint and Agent Card for a Hosted Agent(原文作者 srisatyakrishna5,发布于 2026 年 8 月 24 日)。我把自己跑通的过程和踩的坑整理了一下。

写代理这件事,本来就不该让你把身份验证、入口管理、A2A 传输、发现端点这些平台该干的活儿再重新造一遍轮子。用 Foundry 托管代理的话,这些能力是继承来的,专心写业务逻辑就行。

这篇笔记记录的是从"本地跑着一个容器化代理"到"在 Foundry 上有一个可被发现的 A2A 端点"最短的一条路。核心是怎么打开 A2A 支持,让别的代理通过 agent card 发现你、并用标准的 Agent-to-Agent 协议跟你对话。

重要提示 托管代理这块的接口还在变。把你的 Azure Developer CLI(azd)扩展和 SDK 版本钉死,字段名对着你环境里装的 schema 核对一遍再用。

最终会得到什么

跑完这一套,你手上会有:

  • 一个监听 8088 端口的容器
  • 一个原生的 POST /invocations 端点
  • 一个作为 A2A 桥接层的 POST /responses 端点
  • 一个受 Entra 保护的 Foundry 端点
  • 一张能被其他代理拉取到的 agent card

我这边用的例子是一个请求校验代理:接收结构化 JSON,返回 approverejectneeds_human_review 这样的判定结果。换成别的业务场景,思路是一样的。

核心思路:两个互相独立的配置面

托管代理的部署其实是两个相关但彼此独立的配置面拼起来的:

配置面 定义的内容 配置方式
控制面(Control plane) 代理版本、镜像或源码构建方式、环境、资源、支持的协议版本 azure.yamlazd
数据面(Data plane) 线上端点、身份验证、启用的协议、发布的 agent card agentEndpoint 和端点更新命令

如果协议只在代理版本里声明了,端点那边没配,调用方照样用不了;反过来也一样,两边都得声明。我一开始就漏了端点这一步,调了半天以为代码有问题,后来才发现是配置面没对齐。

请求路径大概长这样:

A2A client
    |  JSON-RPC over HTTPS with an Entra bearer token
    v
Foundry data plane
    |  validates identity, terminates A2A, and bridges to Responses
    v
Your container on port 8088
    |-- GET  /readiness
    |-- POST /invocations
    |-- POST /responses

这张图其实在提醒一件事:不要在 Foundry 托管的容器里自己加一套应用层的 A2A 路由,这层传输 Foundry 已经包了。

先把运行时契约搭好

配置 azure.yaml 之前,容器得先满足托管运行时的几个要求:

  • 用纯 HTTP 在 8088 端口上服务,TLS 在上游终止
  • 提供一个快速、轻量的 GET /readiness 检查
  • 处理 SIGTERM,把正在处理的请求跑完
  • 发出 OpenTelemetry 信号,方便平台侧做关联

Azure 的 agent server 包已经把托管相关的部分封装好了,一组典型依赖大致是:

dependencies = [
    "azure-ai-agentserver-core==2.0.0",
    "azure-ai-agentserver-invocations==1.0.0",
    "azure-ai-agentserver-responses==1.0.0b9",
    "agent-framework-core==1.13.0",
    "agent-framework-foundry==1.10.4",
]

readiness 检查要保持"轻"

不要把下游依赖(模型、数据库、参考数据服务)的探活塞进 /readiness。这些服务临时抖一下不代表容器本身出了问题,平台不该因为这个把一个明明还能正常处理请求的容器重启掉。

深度的运维诊断另开一个端点,比如 GET /health

让 Invocations 和 Responses 共用一个宿主

Invocations 是直接、原生的协议;Responses 是 Foundry 用来接 A2A 流量的"聊天形状"协议,相当于一座桥。

把两个协议宿主组合起来,指向同一个服务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from azure.ai.agentserver.invocations import InvocationAgentServerHost
from azure.ai.agentserver.responses import ResponsesAgentServerHost


class AgentHost(InvocationAgentServerHost, ResponsesAgentServerHost):
    """Expose both hosted-agent protocols."""


def create_app() -> AgentHost:
    host = AgentHost()
    host.invoke_handler(invocations.handle)
    host.response_handler(responses.handle)
    return host

共用一个 service 很关键:同一个请求,不管是从直接调用进来还是从 A2A 进来,最后给出的判定结果、发现项、关联数据和错误结构都应该是一样的。

原生的 invocation handler 可以写得很朴素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
async def handle(self, request: Request) -> Response:
    correlation_id = current_correlation_id()

    try:
        payload = await request.json()
    except ValueError:
        return self._error(ValidationError("Request body is not valid JSON"), correlation_id)

    if not isinstance(payload, dict):
        return self._error(ValidationError("Request body must be a JSON object"), correlation_id)

    try:
        result = await self._service.run(payload, correlation_id=correlation_id)
    except Exception as error:
        return self._error(error, correlation_id)

    return JSONResponse(result.model_dump(by_alias=True))

失败信息统一放进一个稳定的结构里返回,别把堆栈、内部异常文字或者下游依赖的原始报文直接甩给调用方:

1
2
3
4
5
{
  "detail": "The upstream authorization request failed.",
  "correlationId": "00000000-0000-0000-0000-000000000000",
  "errorCode": "AUTHENTICATION_ERROR"
}

配置托管代理

下面是 azure.yaml 的关键结构。环境相关的值放到 azd 环境变量或者基础设施输出里,别写死在应用代码里做分支判断。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
services:
    ai-project:
        host: azure.ai.project
    test-agent:
        project: .
        host: azure.ai.agent
        language: python
        uses:
            - ai-project
        env:
            CONFIDENCE_THRESHOLD: "0.6"
            MODEL_DEPLOYMENT: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
            KEY_VAULT_URL: ${keyVaultUri}
            LOG_FORMAT: json
            LOG_LEVEL: INFO
        codeConfiguration:
            dependencyResolution: remote_build
            entryPoint: main.py
            runtime: python_3_13
        container:
            resources:
                cpu: "1"
                memory: 2Gi
        kind: hosted
        name: test-agent
        startupCommand: python main.py
        protocols:
            - protocol: responses
              version: 2.0.0
            - protocol: invocations
              version: 2.0.0

Remote Build 这个特性能让 Foundry 直接从你发布的源码构建镜像。Dockerfile 还是留着,本地复现,或者以后需要自定义基础镜像、装系统包的时候用得上。

注意 azure.yaml 是要提交到源码仓库的。名称、URL、调优参数写在里面没问题,但密钥这类东西还是放 Key Vault,启动时通过托管身份去取。

通过端点打开 A2A

A2A 是在数据面的端点上启用的。等版本层的协议声明完了,再加上 agent card 和端点配置。

其他代理和那些由模型驱动的规划器,靠这张卡片判断要不要调用你的代理。写它的时候按"路由用"的思路来,别写成营销文案。输入是什么、输出是什么、终态结果、模态和限制,说清楚就行。

一段好用的卡片描述,基本上就是快速回答三个问题:

  • 这个代理接受什么样的信息?
  • 它返回什么?
  • 什么情况下该选它?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
agentCard:
    description: Validates a request and returns a verdict with findings and a confidence score.
    version: "1.0"
    skills:
        - id: validate-request
          name: Validate Request
          description: Accepts a JSON request as text and returns a JSON verdict.
          tags: [validation, decision-support, automation]

agentEndpoint:
    authorizationSchemes:
        - type: Entra
    protocols:
        - responses
        - invocations
        - a2a

这里有个不对称的地方,我第一次看也没留意:

  • responsesinvocations 写在代理版本里,因为容器自己实现了这两个协议。
  • a2a 写在端点里,因为 A2A 传输是 Foundry 提供并对外暴露的,不是容器自己实现的。
  • responses 必须先启用,因为它是 A2A 的桥接目标。

部署完代理版本之后,再应用端点配置:

1
2
azd ai agent endpoint update change-validator --no-prompt
azd ai agent endpoint show --output json

跨平台边界保留关联 ID

Foundry 会把 x-client- 前缀的头转发进容器。统一用 x-client-correlation-id,按下面这个优先级来解析:

  • x-client-correlation-id
  • W3C traceparent 头里的 trace ID
  • 都没有的话,新生成一个 UUID

同时把平台的这些标识也绑到日志和审计记录里:

请求头 审计/日志字段
x-agent-session-id agent_session_id
x-agent-user-id agent_user_id
x-agent-foundry-call-id foundry_call_id

关联 ID 要在响应头和响应体里都回显一遍,往下游调用继续传,也要写进审计存储里。调用方拿到的报告、容器里的 trace、审计记录、平台侧的日志,最好都能靠这一个值串起来,出问题排查的时候能省不少事。

部署成功不代表配置生效了,端点协议有没有真正应用上,得单独验证,我踩过这个坑:部署脚本跑完显示成功,结果调用方还是拿不到 A2A 协议,最后发现是忘了单独跑端点更新命令。

检查线上的协议

1
azd ai agent endpoint show --output json

确认端点报出来的协议里同时有 a2aresponses

拉取发布出去的 agent card

1
2
curl -H "Authorization: Bearer $token" `
  "$projectEndpoint/agents/test-agent/endpoint/protocols/a2a/agentCard/v1.0"

如果拿到 404,八成是端点更新没生效。要是卡片能拉到但调用失败,先去确认端点的协议列表里有没有 responses

用 A2A 协议暴露代理的时候,有几个限制得留意:

  • 只支持文本模态,文件数据和其它非文本模态都不支持。
  • 不支持流式响应(server-sent events)。
  • 走 A2A 进来的请求依赖 responses 协议,没实现 responses 协议的代理没法暴露成 A2A 端点。

流式响应这条我一开始没太在意,本来想给校验结果加个实时进度提示,试了才发现走不通,只能退回成一次性返回结果。这种限制早知道比晚知道省事。

排查清单

托管的 A2A 部署跑不通的时候,我一般按这个顺序挨个查:

  • 容器是不是监听在 8088 端口、用的纯 HTTP。
  • 版本层的两个协议是不是都声明了。
  • 端点是不是启用了 responsesinvocationsa2a 这三个。
  • 端点更新命令是不是在部署完代理版本之后才跑的。
  • agent card 的 URL 能不能正常返回。
  • 调用方的工作负载身份是不是有对应的 Foundry 权限。
  • 去掉可能的 Markdown 代码块围栏之后,JSON 文本能不能正常解析。
  • 关联头是不是都用了 x-client- 前缀。
  • readiness 是不是还保持轻量、快速。

参考资料: