
最近在折腾一件事:让桌面上的 AI 助手 workbuddy 把活派给 Elasticsearch 集群里的智能体去干。比如在聊天窗口里说一句"分析最近三十天的高危操作",本地助手并不亲自去查数据,而是把任务转给 Elastic Agent Builder 里的审计 agent,等它跑完,再把结构化报告接回对话里。两个不同厂商、不同框架的 agent 协同工作,靠的是 A2A 协议。
这套东西跑通之后确实好用,但过程里踩的坑比预想的多。这篇把原理、接口设计和几个关键的工程问题记下来。
A2A(Agent-to-Agent)是一个智能体互操作协议,解决的问题很朴素:每家的 agent 都活在自己的框架里,彼此之间没有通用的对话方式。A2A 的做法是给每个 agent 发一张"名片"(Agent Card),描述它是谁、擅长什么、支持哪些能力(比如是否支持流式输出);通信层用 JSON-RPC 2.0,核心方法就一个 message/send——把一条带角色和文本的消息发过去,拿回一个 Message 或 Task。
Elastic 在 Agent Builder 里内置了 A2A 服务端,三个 HTTP 端点就够用了:
GET /api/agent_builder/agents # 列出所有 agent
GET /api/agent_builder/a2a/{id}.json # 某个 agent 的 card
POST /api/agent_builder/a2a/{id} # JSON-RPC 消息入口换句话说,你在 Kibana 里创建的每一个 agent,天然就是一个 A2A 服务。任何实现了客户端的框架——不管是桌面助手、另一个 agent 平台还是一段脚本——都能直接调它。
客户端第一版是 Node.js 写的,依赖 axios。装进桌面助手才发现,很多 AI 框架的运行沙箱里根本没有 Node,或者不允许装第三方包。教训很直接:给 AI 框架写的工具,依赖越少越好。
重写的版本只用 Python 标准库:urllib 发请求,json 解析,subprocess 管后台进程,单文件实现,丢进任何有 python3 的环境都能跑。配置只有两个环境变量——A2A 端点地址和 API key。密钥这种东西写进代码等于公开发行,环境变量是底线;skill 在配置缺失时会一步步引导用户去 Kibana 创建 key、设置变量,而不是报个错就完事。
对外的命令保持一层很薄的封装:discover 发现 agent,get-card 看能力,send 同步发送,send-async 异步提交,result 和 fetch 取结果,history 和 show 提取历史会话。调用方(也就是 workbuddy 背后的大模型)根据这几个命令自由组合,JSON-RPC 的细节全部藏在里面。
配好环境变量之后,第一步是发现。workbuddy 执行 discover,拿到实例里全部 agent 的清单,再翻每个 agent 的 card——审计的、安全的、管告警的,各管一摊。之后用户说一句"看看当前有哪些告警",它就知道该把消息发给谁。这是 A2A 里最舒服的部分:调用方不需要预先知道对面有什么,名片机制让能力发现变成了运行时的事。
真正的考验从下发任务开始。
本地助手问 agent 一句"你好",两秒就回。但"分析三十天审计日志"这种任务,远端要加载技能、反复查询 ES、多轮推理,跑上两三分钟很正常。而 HTTP 这条路上有个隐形杀手:云端入口代理会掐断耗时过长的连接。实测大约三十到六十秒就会收到 502,报文里写着 backend closed connection——请求死了,但远端任务还活着。
第一反应是用 A2A 标准里的 tasks/get,凭任务 ID 轮询状态。试了,服务端不持久化 task,返回 Task not found,此路不通。
转机在会话存储。Elastic 会把每一次 A2A 对话持久化下来,文档 ID 就是 a2a- 前缀加上 contextId。更关键的一个发现是:contextId 可以由客户端预先生成,服务端照单全收。这两个事实拼在一起,方案就清晰了:
发送之前,先在本地生成 contextId 并写进任务记录,然后才发请求。连接中途被掐断?无所谓,远端任务照跑,跑完把结果写进会话存储。本地的后台进程每二十秒去 GET 一次对应的会话文档,看到轮次状态变成 completed,把最终回复读回来就收工。整个恢复过程是纯读取,不会让 agent 把任务重新跑一遍——这点很重要,重跑一次重型分析,时间和 token 都是双倍的。
这个设计还附赠了跨会话恢复:contextId 存在本地,哪怕明天换一台机器,凭这个 ID 一样能把结果捞回来,重新注入到新的对话里。
顺带记一个只有在框架环境里才会遇到的坑:后台 worker 进程启动即崩,报 Bad file descriptor。排查下来是子进程继承了父进程的 stdin,而无终端的沙箱环境里那个描述符是无效的,Python 解释器初始化标准流时直接挂掉。spawn 的时候把 stdin 显式接到 DEVNULL 才解决。这种问题在终端里永远复现不出来。
有了异步机制,什么时候还走同步?最初的判断标准是"简单问题走同步"。实测很快被打脸:给审计 agent 发一句"用一句话回答,审计日志里最新一条记录是什么时间",它照样跑了一百秒。加载技能、查询数据、组织推理,这些是每次调用的固定开销,跟问题写多短没有关系。
所以真正的标准是看 agent 而不是看问题:重型工具型 agent,一律异步;轻量对话型 agent,才适合同步等待。skill 的文档里给调用它的大模型写了一张决策表,还留了一条兜底原则——拿不准就异步。判断错了也不丢任务:就算同步等待超时断开,远端任务依然会执行完并落库,凭预生成的 contextId 随时能取。
多 agent 协同还有个容易被忽略的问题:历史会话的提取。一次审计分析的完整会话里,推理步骤和工具调用的原始返回,体积能比最终报告大一个数量级。如果助手把会话全文拉回自己的上下文,几轮就撑爆了。
处理方式是两级:history 只返回标题和会话 ID 的清单,零内容;用户选中哪条,再用 show 提取那一条的最终回复,中间的推理和工具调用全部丢弃,多轮会话默认也只给最后一轮。听起来简单,但这类"防上下文过载"的取舍在 agent 互相调用的场景里会越来越重要——上游 agent 的每一句废话,都是下游 agent 要付的 token。
最后一个和审计有关的细节。会话存储里有个 user_name 字段,记录的是 API key 的属主身份,而不是消息 metadata 里自报的名字。想在 Elastic 端区分"这个任务是哪个助手派来的",给每个调用方单独签发一把 API key 是唯一可靠的办法——自报的字段可以伪造,认证身份不行。消息 metadata 里带的调用方标识可以作为辅助,但别把审计建立在它上面。
整套跑下来,分工是干净的:workbuddy 负责理解意图、挑选 agent、管理任务的生命周期;Elastic 侧的 agent 贴着数据干重活。A2A 这层协议让两边解耦得很彻底——今天接的是审计 agent,明天换成任何一个实现了 A2A 的服务,客户端一行代码都不用改。
做完的感受是:多 agent 协同这件事,协议本身已经不是瓶颈,工程细节才是。连接会断、任务会长、上下文会爆、身份要可审计——把这几个问题一个个解决掉,剩下的就是让每个 agent 安心干好自己那摊事。
完整实现已开源,单文件、零依赖,仓库链接见评论区。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。