跳到主要内容

A2A 端点

A2A 端点把夜莺暴露成可被其他 Agent 平台发现和调用的 Agent,Agent Card 声明地址与支持的方法。

这页办完你会得到:一个第三方 Agent 平台能自动发现并调用的夜莺 Agent, 以及一条验证过的 message:send 请求。

和 MCP 是两种东西​

/mcp/a2a
对面拿到的74 个细粒度工具,自己编排一个会说话的 Agent
交互形态JSON-RPC 工具调用自然语言进,自然语言出
需要配大模型吗不需要——工具直接打到夜莺 API需要,它驱动的就是内置助手
适合谁Claude Code、Cursor 这类自带模型的客户端别人家的 Agent 编排平台

两个端点默认都开着,用同一套鉴权。A2A 在页面上答不上来的问题, 在 A2A 上也答不上来——先在 Nightingale AI 里聊通了再说,见 Nightingale AI 概览。

Agent Card:第三方唯一需要的地址​

curl -s https://n9e.example.com/.well-known/agent-card.json

这个地址不需要认证(规范要求可发现)。/.well-known/agent.json 是给老客户端的别名。

卡片里声明的:

字段值
nameNightingale Agent
supportedInterfaces一个 HTTP+JSON 接口,URL 是 <BaseURL>/a2a
capabilities.streamingtrue
defaultInputModes / defaultOutputModes都是 text
securitySchemesx-user-token(apiKey,放在请求头里);开了 RSAuth 还会多一个 oidc
skills内置 Skill 的名字、描述、示例问句,逐条列出

skills 是从内置 Skill 的 frontmatter 生成的,所以对面能看到夜莺具体擅长什么 (排查告警没触发、分析仪表盘、生成 PromQL……),而不是一句笼统的介绍。

BaseURL 留空时是从请求头猜的。放在反向代理后面务必显式配, 否则第三方拿到的可能是内网地址:

[HTTP.A2A]
BaseURL = "https://n9e.example.com"

发一条消息​

REST 绑定的路径是 /a2a/message:send(没有 /v1 前缀), messageId、role、parts 三个字段都必填,role 的枚举值是 ROLE_USER:

curl -s -X POST https://n9e.example.com/a2a/message:send \
-H 'Content-Type: application/json' \
-H 'X-User-Token: YOUR_TOKEN' \
-d '{
"message": {
"messageId": "req-0001",
"role": "ROLE_USER",
"parts": [{ "text": "现在有哪些活跃告警?" }]
},
"metadata": { "lang": "zh_CN" }
}'

预期结果:一个 task 对象,里面有 id、contextId、status.state, 以及 metadata 里的 n9e.chat_id / n9e.seq_id。 status.state 是 TASK_STATE_COMPLETED 说明这一轮跑完了;答案在 artifacts 里。 这一步可能要几十秒——模型在思考并调工具,属正常。

多轮对话把 "contextId": "<上一次返回的 contextId>" 放进 message 里。 contextId 一一对应夜莺的 chat_id,也就是说 A2A 的会话和页面上的会话是同一条, 在 Nightingale AI 的会话列表里能看到。

支持哪些方法​

方法路径
发消息(同步)POST /a2a/message:send
发消息(流式 SSE)POST /a2a/message:stream
查任务GET /a2a/tasks/{id}
取消任务POST /a2a/tasks/{id}:cancel
重新订阅POST /a2a/tasks/{id}:subscribe

tasks/list 故意没实现,按规范返回 UnsupportedOperation—— 跨用户枚举任务对一个内置助手没意义,还会泄漏别人的活动。

直接 GET /a2a 根路径会返回一段 JSON 提示,把上面这些路径列出来。 这是为了让误用旧 JSON-RPC 写法(tasks/send)的客户端拿到一句人话, 而不是被重定向到前端页面。

任务状态存在 Redis 里,TTL 24 小时,多实例共享;过期后 tasks/get 返回 task not found, 但对话本身还在数据库里。

反向代理和超时​

A2A 在根路径,且是长连接:

location /.well-known/ {
proxy_pass http://127.0.0.1:17000;
}

location /a2a/ {
proxy_pass http://127.0.0.1:17000;
proxy_http_version 1.1;
proxy_set_header Host $host;

proxy_buffering off; # 不关掉,流式回答永远吐不出来
proxy_read_timeout 3600s; # 默认 60 秒会把回答拦腰截断
proxy_send_timeout 3600s;
}

服务端每 30 秒会发一个空的 working 状态心跳,就是为了绕过网关的空闲超时—— 但网关自己的读超时还是得放宽。

命令行客户端​

仓库里带了一个基于官方 SDK 的测试客户端:

go run ./cmd/a2a-cli --server http://127.0.0.1:17000 --token YOUR_TOKEN \
--message "查看当前正在告警的事件"

# 接着上一轮聊
go run ./cmd/a2a-cli --server http://127.0.0.1:17000 --token YOUR_TOKEN \
--context-id CHAT_ID --message "进一步分析其中第一条"

默认走流式;加 --get 会在结束后再调一次 tasks/get 验证任务确实存下来了。

关掉它​

[HTTP.A2A]
Disable = true # /a2a、/mcp、Agent Card 一起关

只想关 MCP 保留 A2A 的话用 DisableMCP = true。

下一步​