ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Nasiko A2A Registry 设计解析:把“Agent 发现“本身做成一个 A2A Agent

Nasiko A2A Registry 设计解析:把“Agent 发现“本身做成一个 A2A Agent 【免费下载链接】nasikoDeveloper Control Plane for your AI Agents项目地址https://gitcode.com/gh_mirrors/na/nasiko点击查看免费下载在 NasikoDeveloper Control Plane for your AI Agents中Agent 之间的通信、发现与代理全部统一在 A2AAgent-to-Agent协议之下控制平面在部署时只向每个 Agent 注入一个环境变量A2A_DISCOVERY_URLAgent 通过标准的/.well-known/agent-card.json发现注册表 Agent再用SendMessage查询它来找到同类。本指南完整解读 docs/A2A_REGISTRY_DESIGN.md 中定义的注册表架构并对照仓库源码server/src/registry_a2a.rs、react-agent/src/registry.rs、agents/assistant-agent/main.py 等说明其落地细节。读完你将掌握A2A 规范下注册/发现的设计约束、控制平面如何注册与健康巡检 Agent、如何把一个注册表伪装成一个普通 A2A Agent、以及代理转发链路上的 ACL、限流与审计是如何串联的。A2A 规范给注册表留下的设计约束A2A 协议Linux Foundation 维护v1.0本身对注册表几乎没有规定Nasiko 的注册表设计完全建立在这些约束之上Agent Card 是注册单元——承载 name、skills、capabilities、security schemes 与 interfaces即 docs/A2A_PROTOCOL.md 中描述的AgentCard结构Well-Known URL 是唯一的标准发现入口——/.well-known/agent-card.json必须通过未认证的 GET提供这是规范里唯一的固定 URLExtended Agent Card——认证后返回的更丰富卡片Nasiko 目前未启用各 Agent 的capabilities.extendedAgentCard均为false没有标准注册表 API——注册CRUD、查询、发现的 API 完全由实现方自定义协议是 pull 型pull-based——客户端主动去 well-known URL 拉卡片或查询注册表规范没有定义 push/注册 webhook 或统一的发现查询格式。结论很直接既然规范不给答案Nasiko 选择自己实现注册表并且把它做成一个符合 A2A 协议的普通 Agent——这成为整个设计文档的核心决策见下文注册表本身就是一个 A2A Agent。关键设计决策设计文档明确列出了五条顶层决策它们是后续所有机制的前提所有 Agent 间通信都经过控制平面代理不存在 Agent 直连 Agent每个部署的 Agent skills 不可变——更新 skills 就等于新部署新版本按能力/标签发现——Agent 不需要按名字认识其他 AgentAgent 永远不知道其他 Agent 的私网 IP——它只拿到代理 URL注册表本身就是一个 A2A Agent——Agent 用它们做其他一切事情的同一套 A2A 协议来发现彼此没有私有 REST API。第五条直接引出了仓库中的实现server/src/registry_a2a.rs 的文件头注释明确写着the control plane acting as the registry agent from docs/A2A_REGISTRY_DESIGN.md并说明平台 Agent 被注入唯一的 URLA2A_DISCOVERY_URL通过向{base}/a2a/v1POST JSONRPCmessage/send来发现同类然后读取result.artifacts[0].parts[0].data.agents。Agent 契约平台强制什么、不强制什么要在平台上运行一个 Agent 容器只需满足三条在/.well-known/agent-card.json提供合法 AgentCard schema在声明的 interface URL 上实现A2A JSON-RPC至少实现SendMessage响应健康检查A2A 端点返回 HTTP 200。而控制平面不能也不试图强制内部实现、框架或 LLM 选型响应质量声明的 skills 是否真的可用那是开发者自己的责任。这套最小契约与仓库中各 Agent 的 Dockerfile/入口一致例如 agents/assistant-agent/main.py 用 Starlette 挂载create_agent_card_routes与create_jsonrpc_routes(handler, rpc_url/)容器内统一监听 8000 端口见PORT默认值Agent 只负责卡片合法 JSON-RPC 可用 健康检查 200。注册流程注入、健康检查、抓卡、入库控制平面部署一个 Agent 容器时执行如下步骤创建容器并注入发现环境变量A2A_DISCOVERY_URLhttp://cp:8080 ← base URL of the registry agent等待健康检查通过从 Agent 私网 IP 抓取/.well-known/agent-card.json校验卡片 schema将卡片存入注册表数据库卡片从此对其他 Agent 可见。Skills 在这一刻被冻结——想更新 skills 就必须部署新版本。仓库侧的证据非常完整server/src/seed.rs 中种子 Agent 部署时构建环境变量env.insert(A2A_DISCOVERY_URL, discovery_url)默认回退到http://host.docker.internal:8080第 136–138 行同一文件里部署成功后调用crate::agents::utils::fetch_agent_card_with_retry(state.db, state.http_client, agent.id, agent_url)第 200–206 行实现等待健康 → 抓卡 → 校验 → 入库的完整链路config/src/lib.rs 中a2a_discovery_url也由A2A_DISCOVERY_URL环境变量提供第 279 行config/tests/config.rs对其有断言覆盖。发现注册表本身就是一个 A2A AgentAgent 之间发现彼此不需要私有 REST API——它们对注册表说 A2A。注册表对外暴露标准卡片GET {A2A_DISCOVERY_URL}/.well-known/agent-card.json{ name: Nasiko Agent Registry, description: Discovers and lists agents by capability, tags, or natural language query, version: 1.0.0, supportedInterfaces: [ { url: http://cp:8080/a2a/v1, protocolBinding: JSONRPC, protocolVersion: 1.0 } ], capabilities: { streaming: false, pushNotifications: false }, defaultInputModes: [application/json, text/plain], defaultOutputModes: [application/json], skills: [ { id: discover-by-capability, name: Discover Agents by Capability, description: Find agents that match given tags, capabilities, or natural language description, tags: [discovery, registry, a2a, search], examples: [ Find agents that can translate text, Which agents support streaming?, List all agents with tag: summarization ], inputModes: [application/json, text/plain], outputModes: [application/json] }, { id: get-agent-card, name: Get Agent Card, description: Retrieve the full Agent Card for a specific agent by ID or name, tags: [discovery, registry, lookup], inputModes: [application/json, text/plain], outputModes: [application/json] }, { id: list-agents, name: List All Agents, description: List all active agents with their skills and endpoints, tags: [discovery, registry, list], inputModes: [application/json], outputModes: [application/json] } ] }注意capabilities.streaming: false——注册表只做同步应答不需要流式。这与 server/src/registry_a2a.rs 的实现一致它只接受非流式方法并直接返回完整目录 JSON。发现流程Agent → Registry全程 A2AStep 1Agent 抓注册表的卡片标准 A2A 发现GET http://cp:8080/.well-known/agent-card.json → 得到注册表 Agent 的卡片得知它的 A2A 端点Step 2Agent 用 A2A SendMessage 查询注册表结构化查询程序化调用更推荐POST http://cp:8080/a2a/v1 A2A-Version: 1.0 { jsonrpc: 2.0, id: req-1, method: SendMessage, params: { message: { role: ROLE_USER, parts: [ { data: { action: discover, filter: { tags: [translation, multilingual], capabilities: { streaming: true } } } } ] } } }自然语言查询同样可用POST http://cp:8080/a2a/v1 A2A-Version: 1.0 { jsonrpc: 2.0, id: req-2, method: SendMessage, params: { message: { role: ROLE_USER, parts: [ { text: find agents that can translate between languages } ] } } }Step 3注册表返回匹配的 Agent标准 A2A 响应{ jsonrpc: 2.0, id: req-1, result: { task: { id: task-uuid, contextId: ctx-uuid, status: { state: TASK_STATE_COMPLETED }, artifacts: [ { parts: [ { kind: data, data: { agents: [ { agent_id: translation-agent-uuid, name: Translation Agent, description: Translates between 40 languages, skills: [ { id: translate-text, name: Text Translation, tags: [translation, nlp, multilingual] } ], capabilities: { streaming: true }, endpoint: http://cp:8080/api/agents/translation-agent-uuid }, { agent_id: deepl-agent-uuid, name: DeepL Agent, description: High-quality translation via DeepL, skills: [ { id: deepl-translate, name: DeepL Translation, tags: [translation, multilingual] } ], capabilities: { streaming: false }, endpoint: http://cp:8080/api/agents/deepl-agent-uuid } ] } } ] } ]} } }Step 4Agent 调用被发现的 Agent标准 A2A经由代理POST http://cp:8080/api/agents/translation-agent-uuid A2A-Version: 1.0 { jsonrpc: 2.0, id: req-3, method: SendMessage, params: { message: { role: ROLE_USER, parts: [ { text: Translate hello world to Spanish } ] } } }仓库中的真实响应形状设计文档里的响应是一个理想化的 A2A 1.0task包装仓库实现 server/src/registry_a2a.rs 实际返回的是扁平结构——result.artifacts[0].parts[0].data.agentsagents_response函数第 144–155 行。文件头注释明确指出两个树内消费者都按这个形状解析Python 侧agents/assistant-agent/main.py 的_discover_agents()遍历artifacts[].parts[]取kind data的 part读data.agents第 132–138 行Rust 侧react-agent/src/registry.rs 的discover_from_cp()用response.result.pointer(/artifacts/0/parts/0/data/agents)定位数组第 108–111 行再把每个条目反序列化为AgentInfo要求id/name/description/endpoint/skills。registry_a2a.rs的测试用例还专门锁定了这两个消费者的读取路径response_exposes_agents_at_the_react_agent_pointer断言 agents 数组出现在该指针位置entries_satisfy_both_in_tree_consumers断言每个条目同时携带id/agent_id/name/description/url/endpoint且url endpoint第 165–219 行——这正是设计文档示例中endpoint与agent_id字段在实现中合并归一的结果。注册表端点的另一个实现细节它刻意不做认证Agent 不携带平台凭证——代理在转发前会剥掉authorization头但做了全局限流暴露的内容是设计文档本就声明公开的运行中 Agent 的名字、描述、skills以及运行时内部端点VPC 私网 IP / localhost这些在部署网络之外本就不可达registry_a2a.rs头部注释。端到端每条路径都是 A2A┌──────────────────────────────────────────────────────────────────────┐ │ CONTROL PLANE │ │ │ │ ┌──────────────────┐ ┌───────────┐ ┌─────────────────────┐ │ │ │ Registry Agent │ │ Proxy │ │ Agent Card Store │ │ │ │ │ │ │ │ (PostgreSQL) │ │ │ │ /.well-known/ │ │ /api/ │ │ │ │ │ │ agent-card.json│ │ agents/ │ │ │ │ │ │ /a2a/v1 │ │ {id} │ │ │ │ │ └──────────────────┘ └───────────┘ └─────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────┘ Agent Developers mental model: Theres one URL in my env var. I fetch its agent card. I talk to it via A2A to discover other agents. It gives me endpoints. I talk to those endpoints via A2A. Everything is A2A. I dont learn any proprietary API.这个开发者心智模型在仓库中有一个完整的实现样本agents/assistant-agent/main.py 的 Assistant Agent 正是这样工作的——读A2A_DISCOVERY_URL第 49 行→_discover_agents()用message/send查注册表 →_plan()让 LLM 从目录里挑 Agent 并生成[{name,url,sub_query}]计划 →_delegate()向每个 URL 发 A2A 1.0SendMessage带A2A-Version: 1.0头→_synthesize()汇总各 Agent 的响应。它甚至处理了 Docker 网络细节把发现结果里的localhost重写为host.docker.internal第 140–143 行并过滤掉自己name ! assistant-agent避免自委托。Well-Known URL 代理面向外部 A2A 客户端对于不在平台上运行的外部 A2A 客户端文档规划了按 Agent 粒度的 well-known 代理但明确标注尚未实现not implemented yetGET https://platform.example.com/agents/{agent-id}/.well-known/agent-card.json ← not implemented实现后它会返回 Agent Card且supportedInterfaces[].url指向公共代理{ supportedInterfaces: [ { url: https://platform.example.com/api/agents/{agent-id}, protocolBinding: JSONRPC, protocolVersion: 1.0 } ] }外部客户端同样可以通过注册表发现 AgentGET https://platform.example.com/.well-known/agent-card.json ← registrys own card POST https://platform.example.com/a2a/v1 ← query the registry也就是说外部客户端面对的注册表面与平台内 Agent 完全一致——这也正是注册表是一个 A2A Agent这一决策的收益无需为外部客户端单独设计一套发现协议。代理通信流程每一次调用都经过控制平面┌──────────┐ ┌───────────────────┐ ┌──────────┐ │ Agent A │ │ Control Plane │ │ Agent B │ │ │ │ │ │ │ │ 1. Fetch registry card (A2A standard) │ │ │ │ ─────────────────► │ │ │ │ │ GET /.well-known/ │ │ │ │ │ agent-card.json │ │ │ │ │ │ │ │ │ │ │ 2. Query registry (A2A SendMessage) │ │ │ │ ─────────────────► │ │ │ │ │ POST /a2a/v1 │ │ │ │ │ find translation │ │ │ │ │ │ │ │ │ │ │ ◄─────────────────── 3. Returns agents │ │ │ │ [{endpoint: │ with proxy URLs│ │ │ │ /api/agents/B}]│ │ │ │ │ │ │ │ │ │ │ 4. Call Agent B (A2A SendMessage) │ │ │ │ ─────────────────► │ 5. Authenticate │ │ │ │ POST /api/agents/B │ 6. Check ACL │ │ │ │ │ │ 7. Log interaction│ │ │ │ │ │ 8. Forward ──────────────► │ │ │ │ │ POST /a2a/v1 │ │ │ │ │ │ │ │ │ │ │ │ ◄──────────────────── 9. Response │ │ ◄─────────────────── 10. Return to A │ │ │ │ │ │ │ │ │ └──────────┘ └───────────────────┘ └──────────┘ Every arrow is A2A protocol. Steps 1-4 from Agent As perspective are indistinguishable from talking to any other A2A agent.代理在每次调用中做的事步骤 5–8认证调用方——转发前先识别调用 Agent 的身份检查 Agent 到 Agent 的 ACL——调用check_agent_acl(caller_agent_id, target_agent_id)查agent_acl表。设计文档定义的语义是 allowlist调用方无任何行 不受限有任何行 只能调用列出的目标。该检查实现在 server/src/acl.rs 的CpCallGuard::before_call()中。需要注意当前代码实现比文档描述的语义更严check_agent_acl的注释与实现为默认拒绝default-deny——调用方和目标必须在agent_acl中有一条显式记录才放行allowed.unwrap_or(false)并在 ACL 拒绝时返回agent ACL denied: caller cannot invoke ...。这与用户到 Agent 的访问控制agent_grants/is_public是两套独立机制后者只约束 API 层访问记录交互——caller、target、时间戳、延迟、状态形成审计轨迹限流——防止恶意 Agent 对另一个 Agent 发起洪泛转发请求——转发到目标 Agent 真实的私网 IP:port返回响应——剥掉内部头后返回给调用方。CpCallGuard::before_call()server/src/acl.rs 第 184–221 行在 ACL 检查之后还会继续执行FlowGuard检查深度、环检测、扇出、token 预算、超时全部基于 Redis 的FlowGuard即 flow/src/guard.rs 的实现并在after_call()中记录 token 消耗与调用返回。ACL 拒绝、流控拒绝都会在转发发生之前拦截因此每次 Agent 间调用都是先认证、再授权、再计流控、最后转发。Registry 数据模型实时 schema 位于migrations/。核心表如下migrations/0001_schema.sql 中有完整定义-- Agent registry (one row per registered agent) -- agents.id is UUID (original schema); name is the human/A2A identifier. -- name is unique PER OWNER among active rows (partial unique index on -- (owner_id, name) WHERE deleted_at IS NULL) — NOT globally unique. -- search_vector is a GENERATED tsvector — never SELECT *. CREATE TABLE agents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, owner_id UUID NOT NULL REFERENCES users(id), url TEXT, -- agents A2A endpoint (private IP) capabilities JSONB NOT NULL DEFAULT {}, skills JSONB NOT NULL DEFAULT [], -- denormalised for fast card serialisation tags TEXT[] NOT NULL DEFAULT {}, is_public BOOLEAN NOT NULL DEFAULT FALSE, -- replaces Redis agent:{id}:public status TEXT NOT NULL DEFAULT registered, -- registered|running|stopped|failed secrets_env JSONB NOT NULL DEFAULT {}, -- {key: aes_gcm_ciphertext} encrypted with agent-scoped HKDF key deleted_at TIMESTAMPTZ, -- ... other columns omitted for brevity; see migrations/0001_schema.sql search_vector tsvector GENERATED ALWAYS AS (...) STORED -- exclude from SELECT lists ); -- Normalised skills (mirrors agents.skills JSONB for indexed querying) CREATE TABLE agent_skills ( id UUID PRIMARY KEY, agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, skill_key VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, tags TEXT[] NOT NULL DEFAULT {}, examples JSONB NOT NULL DEFAULT [], UNIQUE (agent_id, skill_key) ); -- User grants for agent access -- grant_type: user | public -- grantee_id: UUID string for user grants, * for public -- (The Nasiko enterprise edition extends grants to teams, departments, -- and organization-wide access.) CREATE TABLE agent_grants ( id UUID PRIMARY KEY, agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, grant_type grant_type NOT NULL, grantee_id TEXT NOT NULL, granted_by UUID REFERENCES users(id), UNIQUE (agent_id, grant_type, grantee_id) ); -- Agent-to-agent invocation allowlist -- No rows for caller → unrestricted. Any rows → only listed targets. CREATE TABLE agent_acl ( caller_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, target_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, granted_by UUID REFERENCES users(id), PRIMARY KEY (caller_agent_id, target_agent_id) );与 migrations/0001_schema.sql 对照有几个值得注意的落地点agents.skills是 JSONB 反规范化列加速卡片序列化同时agent_skills表保留了规范化副本带UNIQUE (agent_id, skill_key)、tags 的 GIN 索引和强制小写的触发器供索引查询与发现筛选使用agent_acl与agent_grants都带ON DELETE CASCADE软删除的 Agent 通过deleted_at隔离search_vector是GENERATED ALWAYS AS ... STORED的 tsvector由 name/description/tags 拼接生成仓库注释特别提醒never SELECT *——发现查询应只取需要的列agents表上还有针对发现场景的索引idx_agents_status、tags 的 GIN 索引、idx_agents_ftssearch_vector以及每个 owner 每活跃名字唯一的部分唯一索引agents_owner_name_active_uniq——保证同一 owner 下活跃 Agent 名字不冲突但允许软删除后重名。注册表查询本身在 server/src/registry_a2a.rs 的discoverable_agents()中实现只选status running AND deleted_at IS NULL的 Agent并动态补齐url——当agents.url为空新部署的 Pod 尚未 Ready 时会从state.runtime.endpoint(ContainerId::from_uuid(row.id))实时解析保证发现结果里不会出现已知是死链的 URL仍未就绪的 Agent 保留在列表里名字和描述对规划器仍有用调用方会自行跳过没有 URL 的委托目标。健康与活性控制平面每 30 秒轮询一次所有活跃 AgentGET {private_url}/.well-known/agent-card.json连续 3 次失败→ 标记unhealthy从发现结果中排除VM 被终止 → 标记removed卡片内容不会被重读以捕捉 skills 变化每个部署不可变。最后一条与skills 冻结决策呼应健康轮询只关心Agent 还活着不关心Agent 的卡片有没有变化——因为卡片内容在部署时已固定轮询去重读它毫无意义。管理/UI 访问非 A2A内部Web UI 和 CLI 使用标准 REST 做管理操作不走 A2AGET /api/agents ← list all agents (with status) GET /api/agents/{id} ← full agent details POST /api/agents ← register/deploy new agent PUT /api/agents/{id} ← update agent DELETE /api/agents/{id} ← stop and remove agent POST /api/agents/upload ← upload source for a server-side build /api/containers/* ← container ops (stop, start, restart, scale, logs)设计文档给出的理由是这些是控制平面内部 API服务对象是平台运维人员而非 Agent所以不需要 A2A。这与 A2A 协议的无标准注册表 API约束一致——注册/销毁/容器管理等操作本来就是实现自由区。LLM Router 集成绕开 A2A 的内部快车道LLM Router 是控制平面的内部组件不是独立 Agent它直接读注册表数据DB 查询无 A2A 往返用户向控制平面发送消息Router 查询 PostgreSQL 获取所有活跃 Agent 卡片Router 把 Agent 卡片 用户消息发给 LLMLLM 基于 skills/description 挑出最佳 Agent控制平面把请求代理给选中的 Agent响应流式回传给用户。Router 之所以绕过 A2A 发现协议是因为它在控制平面内部、有 DB 直连权限。只有外部 Agent 和客户端才走 A2A 发现路径。这是一个重要的架构分层同一条注册数据对外是 A2A 协议面对内是 SQL 面——migrations/0001_schema.sql 中的router_request_log表和agent_selection_stats物化视图正是这一DB 直查路径留下的审计与统计痕迹。小结Nasiko 的 A2A 注册表设计可以浓缩成三句话对外统一协议面注册表是一个 A2A AgentAgent 只认识A2A_DISCOVERY_URL一个 URL发现、查询、调用全部走SendMessage没有私有发现 API对内分层实现注册表 registry_a2a端点A2A 面 PostgreSQL 数据模型agents/agent_skills/agent_grants/agent_acl 健康轮询30s管理面走 RESTLLM Router 走 SQL安全收敛在代理Agent 间通信永远经过控制平面代理代理统一完成认证、agent_acl授权、审计日志、限流与 FlowGuard 流控Agent 永远接触不到彼此的私网 IP。对照仓库这套设计不仅是文档而是被 server/src/registry_a2a.rs含针对两个树内消费者的形状测试、react-agent/src/registry.rs、agents/assistant-agent/main.py 与 server/src/acl.rs 完整实现的真实系统——读者可以直接从这些文件继续深入观察 A2A 注册表在真实流量下的行为。赞分享【免费下载链接】nasikoDeveloper Control Plane for your AI Agents项目地址https://gitcode.com/gh_mirrors/na/nasiko点击查看免费下载相关推荐Nasiko × Google ADK构建一个暴露 A2A 协议的可流式 Wikipedia 研究 AgentNasiko × Google ADK构建一个暴露 A2A 协议的可流式 Wikipedia 研究 Agent 本文以 agents/google adk/EMQX A2A Agent Card Registry基于 MQTT 5.0 的 AI Agent 发现与注册机制EMQX A2A Agent Card Registry基于 MQTT 5.0 的 AI Agent 发现与注册机制 EMQX 的 A2AAgent to后端物联网消息队列通信如何把 Langflow 流程发布为 A2A agent 并通过 A2A 端点调用如何把 Langflow 流程发布为 A2A agent 并通过 A2A 端点调用 如果你的 Langflow 里已经搭好了一个能收发消息的流程并且希望让其人工智能大模型AI AgentRAG后端前端MCP 服务工作流自动化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表