做Agent应用,最开始我以为最难的是模型本身。后来真上了生产环境,才发现最磨人的是Agent之间怎么相互触达——一个对话Agent要调用数据Agent取数,一个任务编排Agent要把活儿派给三个不同技术栈的执行Agent,大家各自部署、各自维护接口、协议还不统一,全靠人肉拉群对接口。为了把这个痛点彻底解决掉,我搭了Agent-Reach,一套专门给智能体用的“连接、发现、调度”中间层。这篇文章就聊聊这个项目的完整设计思路、核心模块的实现细节、踩过的坑,以及上线之后的真实效果和建议。想给多Agent系统做架构选型的朋友,可以直接参考这套方案去改。
1. 项目定位与设计思路拆解
1.1 为什么要做一套Agent的触达层
先说背景。我们团队当时有三十多个Agent在并行开发,有做自然语言理解的、有做SQL生成的、有做流程审批的、有做数据可视化的。前后端调用关系一开始是网格状的,每个Agent都存了一堆别的Agent的HTTP地址。改动一个Agent的接口,牵连出来七八个调用方要跟着改。更麻烦的是,有些Agent内部还做了异步任务队列,调用方拿不到完成信号,全靠轮询,触达路径长得离谱。
Agent-Reach的定位就是把这些混乱的调用关系收敛起来。它解决三个具体问题:第一,Agent能力的可见性,让每个Agent知道当前环境里有哪些兄弟节点可以提供什么服务;第二,Agent之间的通信安全,不能让随便一个服务都来调核心Agent;第三,触达路径的稳定性,当目标Agent负载高了、临时下线了、协议变了,调用方不能直接崩掉。
1.2 触达不止是打通网络
很多人一听说Agent互联,第一反应是“这不就是个API网关吗”。实际上,网关解决的是南北向流量,而Agent之间多数是东西向流量,业务语义更强。Agent触达要理解的是“找谁办事、什么条件、怎么办、办了之后怎么确认”。比如一个Agent的响应说“稍后再试”,调度方需要决定是重试还是换一个供应商Agent,这已经超出了转发层能做的范围。
所以Agent-Reach本质上是一层智能调度总线。它对外暴露的是两件事:一幅实时的Agent能力地图,以及一套可编程的触达策略。调用方只需要告诉Reach“我要一个能做某类事情的服务”,Reach负责匹配、调度、鉴权、超时管理、结果返回,调用方不应该关心目标到底是谁。
1.3 为什么没有直接选开源的注册中心方案
调研阶段我们对比过几类方案。纯粹的服务注册中心(比如Consul、Nacos)解决“谁活着”的问题,但它不理解Agent的能力语义;工作流引擎(比如流程编排平台)解决“怎么编排”的问题,但它太重,Agent之间的轻量调用套进去非常啰嗦;干脆自己写个配置文件维护Agent列表,方案更不靠谱,变更频繁、没有限流、没有容错。
最终选择了自研中间层的方式,核心原因是触达语义这件事必须按Agent业务来定。注册中心管“容器”,Agent-Reach管“能力”。注册中心告诉我某个服务实例存不存在,Agent-Reach告诉我某个能力当下能不能用、谁来提供、什么代价、什么优先级。这套语义是通用中间件给不了的。
2. 系统架构与核心模块解析
2.1 三横两纵的整体结构
Agent-Reach的整体架构可以分成三横两纵。三横是接入层、调度层、执行层;接入层负责把Agent以标准方式接入Reach,调度层负责能力匹配和路由选择,执行层负责最终触达和结果回收。两纵贯穿全链路,一条是观测与日志,一条是权限与安全。
接入层对Agent的唯一要求是提供一个HTTP健康检查端口和一个能力描述文件。Agent不需要额外开发SDK,也不用换框架,接入成本控制在半小时以内。调度层是无状态设计,多实例水平扩展,所有调度决策依赖一份在内存中实时刷新的能力路由表,路由表由Reach自己从注册信息中构建。执行层处理具体协议转发、超时控制、熔断降级。
权限与安全单独拉了一条线,因为Agent之间的信任边界必须收紧。每个接入的Agent会拿到租户Token和角色标识,Reach在调度前先做一次RBAC校验,只有调用方具备目标Agent的访问权限才放行。
2.2 触达模型里的三个核心对象
Agent-Reach有三个核心数据对象:Agent节点、能力项、触达策略。
Agent节点是物理维度,记录Agent的实例ID、机房、版本、健康状态、负载指标。能力项是业务维度,描述一个Agent能对外提供的具体功能,包括能力名称、输入参数Schema、输出参数Schema、语义标签、调用协议。一个Agent可以注册多个能力项,一个能力项也可以由多个Agent同时提供。触达策略是决策维度,描述调用者在某个业务场景下如何选择目标Agent,包括优先级、权重、超时、重试、熔断阈值。
这三层分开设计,好处是解耦。Agent扩容了,不需要改能力项;能力升级了,不需要改节点列表;业务方的触达偏好变了,只调整策略配置。
2.3 关键设计取舍:控制面与数据面分离
设计时的一个关键取舍是把控制面和数据面分离。控制面负责Agent注册、心跳维持、策略下发、健康监测;数据面负责实际触达流量转发。两者分离后,策略配置的变更不会影响数据面的吞吐,数据面的抖动也不会拖垮控制面的稳定性。
控制面用了独立的存储和一个定时同步任务,每30秒把Agent注册表和策略配置同步给所有调度实例。调度实例启动时先加载全量快照,之后订阅增量更新。这样一个数据面实例挂了,其他实例依然能根据最近一次快照继续处理流量,不会因为控制面短时不可用而导致全链路瘫痪。
从部署结果看,这套设计让单实例调度能力轻松跑满千级QPS,而控制面CPU占用几乎可以忽略不计。这也是我会推荐在类似场景沿用“控制面做慢决策、数据面做快转发”的根本原因。
3. 实操过程与核心环节实现
3.1 Agent接入注册流程
接入注册是整个项目里决定成败的第一步。Agent上线之前必须完成三个动作:声明身份、声明能力、证明自己还活着。
声明身份时,Agent提交一份注册信息到控制面接口,核心字段包括agentId、namespace、version、endpoint。namespace用于隔离不同的业务域,比如“客服域”“供应链域”是绝对不能互调的。endpoint就是Agent对外提供触达的HTTP地址。
能力声明使用一份JSON描述文件,我直接贴一个当时用的示例结构:
{ "agentId": "data-service-agent", "namespace": "bi", "capabilities": [ { "name": "query_dataset", "inputSchema": { "dataset": "string", "dimensions": "array", "metrics": "array" }, "outputSchema": { "columns": "array", "rows": "array" }, "tags": ["data", "query", "bi"] } ] }能力声明的关键在于inputSchema和outputSchema。Reach在调度时不直接理解Agent内部实现,它靠Schema来校验调用方的参数是否匹配。这样做的好处是,调用方传错参数时,Reach能在触达之前就把错误拦住,避免脏调用污染下游Agent。
注册完成后,Agent进入“待验证”状态。Reach会发起一次主动连线测试,请求Agent的健康检查端口并获得响应,验证通过后才会标记为“可用”。这一步非常重要,因为很多Agent网关配置之后自己测没问题,但Reach所在网络环境根本访问不到,提前验证可以避免路由表里出现大量假活节点。
3.2 健康检查与实际可用性评估
健康检查是Agent-Reach里最容易被低估的模块。早期我们只做了简单的TCP连通性检查,结果线上出现过TCP能通但业务请求全部超时的情况。后来改成两层健康评估。
第一层是探活探测,每10秒一次,请求一个固定的探活路径,要求Agent在2秒内响应一个标准JSON。第二层是质量评分,Reach会基于实际触达结果给每个Agent节点打一个滚动加权分数,指标包括过去5分钟的成功率、平均响应时间、排队超时次数。
质量评分的作用是给调度器做参考。比如有两个Agent提供同样的“翻译文本”能力,A成功率高但慢,B速度快但偶尔失败,评分模型可以通过权重配置来引导流量偏向更符合业务目标的节点。我用的计算逻辑是这样的:综合分 = 成功率权重项 + 延迟权重项,具体公式为 base_score + success_weight * success_rate - latency_weight * normalized_latency,其中base_score默认100,success_weight取60,latency_weight取40。延迟归一化采用5秒作为阈值,超过5秒该项记为满分负向。这个公式落地后,调度选路比之前纯随机和最少连接都要稳定,尤其在目标Agent负载波动时,能明显减少错误率。
3.3 路由调度与触达策略配置
路由调度的核心是策略优先、权重兜底。每条触达策略包含四个要素:能力名称、标签匹配条件、候选Agent列表、调度参数。
我把一个实际用过的策略配置简化一下:
{ "capability": "translate_text", "matchTags": ["production", "low_cost"], "candidateAgents": ["agent-a", "agent-b", "agent-c"], "routing": { "mode": "priority_ratio", "priorities": ["agent-a", "agent-b"], "ratio": [80, 20], "fallback": "agent-c", "timeoutMs": 3000, "retryTimes": 1 } }这段配置的意思是:当调用方请求“translate_text”能力时,优先把80%流量分给agent-a,20%给agent-b,如果两者都失败就直接兜底到agent-c。timeoutMs设为3秒,只重试1次。实际运行中有一个需要特别注意的点:重试只能用于幂等操作,翻译这种接口还好,但如果是创建订单的Agent,重试会造成重复单据。所以Application里应该对不同能力项做幂等标记,没有幂等标记的能力项强制关闭自动重试。
触达执行的过程是:调度器拿到请求后先做一次规则匹配,再根据当前健康评分过滤掉已经熔断的节点,然后按配置的比例随机选路,最后通过数据面按目标Agent的协议格式把请求发出去。这里看起来简单,但真实落地时对协议适配的灵活性要求很高——有的Agent吃Restful JSON,有的Agent要传表单参数,有的Agent内部是消息队列消费,数据面必须支持多种适配器,不能把协议差异暴露给调用方。
3.4 超时、熔断与降级的联动
超时、熔断、降级这三个动作在链路里是联动的,不是各自独立。Agent-Reach的熔断逻辑不是简单地看到一个错误就断开,而是基于滑动窗口计数。在一分钟的时间窗口内,如果某个Agent节点的错误率连续超过40%,或者平均延迟超过阈值,调度器会自动把该节点从候选列表里摘除,并触发降级策略。
降级策略要提前配置好。我常用的方式是把触达目标从“实时Agent”降级为“本地缓存结果”或“后备Agent”。例如报表Agent挂了,降级就返回上一次的缓存报表,而不是返回一个错误给前端。降级配置必须在Reach控制面上列清楚,否则调度器不知道该从哪个维度兜底。
这里有个经验要分享:熔断恢复不能太积极。我最初设置的恢复探测是每5秒扫描一次,后来发现目标Agent重启时往往需要一分多钟加载模型,导致短暂恢复后又有大量请求涌入,再次触发熔断。最终把冷却时间改为“连续成功探活30次且成功率回升到80%以上”才允许重新加入候选列表。效果很稳,没有再出现振荡。
3.5 观测系统与告警配置
没有观测的触达层等于盲飞。Agent-Reach里为每次触达都做了一份独立的链路追踪记录,字段包括请求ID、调用方Agent、目标Agent、选路原因、耗时拆解、结果码。这个字段设计的价值在于:当出现慢请求时,能立刻判断是调度器决策慢,还是目标Agent执行慢,还是网络传输慢。
告警规则我设置了三条:一是单个Agent错误率超阈值立即告警;二是某个能力项全局触达成功率低于99%告警;三是调度器选路时出现“无候选Agent”的次数超过告警阈值。第三条很多团队会忽略,但它在灰度下线Agent时非常好用,能在业务方感知异常之前就提醒管理员去调整策略。
日志输出统一用JSON格式,单独抽了一个日志主题,不跟业务日志混在一起。查询时按requestId检索,能在十秒内拉出整条触达链路的完整日志。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
我把上线过程中遇到的典型问题整理成了一张速查表,适合直接贴在监控台上看。
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 部分流量报“无可用Agent” | 触达策略的标签条件过严,候选全部被过滤 | 查看策略匹配日志,放宽matchTags |
| Agent看起来是活的,但请求超时 | 健康检查探活路径未覆盖核心业务依赖 | 检查探活路径是否只验证了框架心跳,未验证数据库连接 |
| 调度器频繁切换目标Agent | 评分权重设置过敏感,小波动就翻盘 | 调低延迟权重,增大滑动窗口时长 |
| 某类请求重复执行 | 非幂等能力被自动重试 | 检查能力声明的幂等标记,强制关闭retry |
| 注册表里出现大量离线节点 | 下线Agent未调用注销接口 | 接入流程强制要求下线时先注销,且控制面定期清理超过2小时没心跳的节点 |
| 跨namespace强调成功,但响应体为空 | 目标Agent拒绝了非本域请求 | 检查RBAC权限配置和namespace隔离规则 |
速查表的好处是处理日常告警不用每次从头看日志。我们的值班同学按这张表能定位八成的异常。
4.2 三个典型实战案例
第一个案例是Agent-A在某个版本里升级了接口返回格式,从纯JSON包了一层code字段。由于没有改能力声明的outputSchema,Agent-Reach这边还按旧Schema做校验,调用方拿到的数据解析直接走了老的逻辑,现场表现为页面数据字段全部错位。后来定位到问题后,我把规则改成“outputSchema必须跟随Agent版本一起变更,否则版本号不允许发布”。最终是结构校验和版本联动一起生效,这类问题几乎绝迹。
第二个案例是调度器把大量流量导到了刚重启完的Agent-C节点。当时重试冷却时间没设充足,Agent-C重启后模型还需要加载几十秒,探活是通的,但业务请求全部排队。我把探活路径升级为“完整执行一次轻量级推理并返回耗时”,效果立竿见影,冷却期之后的流量才真正有意义。
第三个案例是权限隔离问题。一个内部测试Agent拿到了管理员Token,在调试时不小心调用了支付域的Agent接口,直接把测试环境的支付流程带崩了。问题在于权限模型中默认放行了同环境资源,而环境隔离和租户隔离是两套维度。修正后,Agent-Reach把每个Agent的Token绑定角色最小权限集,任何跨域调用都必须显式审批。
4.3 避坑清单和验收标准
基于这些案例,我总结了一份避坑清单。新Agent接入时必须逐条核对:探活路径必须是业务真实依赖链路,不能只测框架存活;能力声明的Schema要和接口文档强绑定,变更必须走上线流程;所有非幂等接口必须显式标记幂等字段;所有自定义Header必须在能力声明里登记,防止参数穿透失败;Tag不能乱打,Too many tags导致路由匹配变慢,少而精更好。
对Agent-Reach这类中间层,建议建立一套上线验收标准,否则业务方会让你无限加规则。我的做法是验收只看三件事:触达成功率能达到99.9%以上;P99延迟相对原有人肉调用链路损耗小于30ms;任何Agent节点异常下线不影响总体成功率超过5分钟。三个指标不过就不得上线。
5. 落地效果与个人体会
5.1 上线后的观测数据
Agent-Reach上线运行三个月后,几个关键数字可以参考一下。全平台五十多个Agent接入,能力项超过一百二十个,触达平均P99延迟已经稳定在80ms以内,相比原来网格状直连的平均600ms降了一大截。原因主要是去掉了调用方反复重试的连锁效应,以及减少了大量无效轮询。
成功率方面,在最稳定的一个月里,全局触达成功率在99.5%以上。发生故障时,由于有兜底和熔断机制,绝大部分异常都限制在一个局部域内,没有出现过跨业务域的连锁故障。这个稳定性指标是这套触达层存在的最大价值。
5.2 做这个项目最深的三个体会
第一个体会是,Agent触达层本质上是对“契约”的管理。Agent之间只要接口约定清楚了,技术选型反而不那么重要。比起整天纠结grpc还是http,不如先把每个Agent的能力Schema管好。
第二个体会是,不要把调度规则写死在代码里。触达决策必须是动态可配、可灰度、可回滚的。我们在生产环境调整过很多次路由比例,都是改配置发布,代理层完全不需要重启。
第三个体会是,中间层的价值一直被低估。很多团队给Agent做了很强的模型能力,却让Agent之间的互调停留在“写一个API网关”的层面。如果多Agent不是几十个、而是几百个的时候,没有触达层是走不了多远的。
5.3 后续扩展的三个方向
Agent-Reach后续的扩展空间,我个人比较看好三个方向。
第一个方向是语义触达,让调用方不再显式指定能力项名称,而是用一句话自然语言描述需求,由触达层通过语义索引自动匹配Agent能力。这意味着Agent接入门槛进一步降低,因为调用方连API文档都不用看,问一句“帮我查一下上周销量”就能被路由到对应的查询Agent。
第二个方向是对Agent执行结果的质量评估。现在触达层只负责把请求送到并把响应带回来,但响应本身是不是高质量的,基本没有判断。后续应该接一个质量评估模块,对于低质量执行结果进行重试或者切换目标Agent,形成闭环。
第三个方向是跨站点、跨组织的Agent触达。当Agent所有权属于不同团队甚至不同公司时,需要一套联邦式的注册和信任机制,触达层只传递必要信息,不落地业务数据,而Agent身份由各自控制面背书。这是规模化协作的前提。
这个项目做到现在这个状态,我最满意的一点是它并没有变成一套复杂的重系统,而是成为Agent之间相互协作的“轻契约”。后续如果你们的Agent数量也涨上来了,我建议从最小的注册表开始做,先把“找一个Agent”这件事标准化,再逐步叠加调度策略,不用一上来就把所有模块做齐。