
今年年初我们团队做了一轮“全员Agent化”的冲刺。客服、数据分析、库存预警、报表生成每个小组都开始用智能体承接业务。结果一个月后问题来了这些Agent相互不认识也不会互相调用。客服Agent查库存得给库存组再提一个工单数据分析Agent想拉业务侧的数据又得走一遍接口申请流程。说白了Agent本身很聪明但它们之间没有任何“握手”的机制。我后来花了两周时间做了个内部工具叫Agent-Reach专门解决Agent之间“找得到、调得通、控得住”这三个问题。它不是一个大而全的平台就是一个轻量的Agent触达与调用中间层让任何Agent都能注册自己的服务能力让任何Agent都能通过统一协议找到并调用另一个Agent。这篇文章我会把整个搭建思路、协议设计、代码实现和排查经验完整写一遍给正在做多Agent系统、或者想打通异构Agent协作的团队做参考。1. 为什么需要一个叫Agent-Reach的触达层1.1 Agent孤岛三个团队三种“方言”先说说我们当时的混乱局面。团队A用LangChain写意图识别Agent输出的是JSON团队B用CrewAI做任务编排内部通信走自定义的Python对象团队C自己从零写了一个RAG问答Agent对外暴露的是REST接口还有一组外部系统只能通过MQ消息异步通信。这五个系统如果想互相协作理论上每对之间都要开发一套对接逻辑。6个Agent两两对接就是15条链路50个Agent就是1225条链路。这还没算版本升级带来的连锁改动。我和同事们当时开玩笑说这像是在给每个城市修一条“方言翻译专线”而不是直接修一条标准化公路。Agent-Reach的核心出发点就是把这密密麻麻的点对点调用收敛成一个“中心辐射”结构。每个Agent只需要和Agent-Reach一个节点对接剩下的发现、路由、协议转换全部由中间层负责。这不是什么新概念微服务时代我们就这么干过现在只不过是把服务实例换成了Agent。1.2 直连方案的三个致命问题有人可能会问Agent数量不多的时候直接互相调用不是更简单吗确实两三个Agent之间直连完全没问题但一旦超过五六个三个问题就藏不住了。第一是耦合。A调BB调CC调D一旦D的接口签名改了所有上层链路都要跟着动。Agent的能力是会迭代的尤其是我刚才说的那类用提示词驱动的Agent它的入参含义可能今天和明天都不一样直连模式下的接口变更基本就是灾难。第二是稳定性。每个Agent部署的位置、使用的框架、超时策略都不同。如果每条链路上都要自己写超时、重试、熔断代码量翻倍还不说逻辑还很难统一。同一个超时值A觉得够用B觉得不够最后只能互相猜。第三是不可观测。出问题时请求到底走到哪一步了是A Agent没返回还是B Agent崩溃了还是中间的网络抖动没有统一Trace的情况下排查一次调用问题至少要拉三个群问五个人翻六份日志。Agent-Reach把这三个问题统一收口协议管理、状态管理、链路追踪全部通过中间层做掉。调用方不需要知道目标Agent是Python写的还是Java写的也不需要自己实现重试逻辑这些能力由网关统一提供。1.3 Agent-Reach的定位目录、网关、策略引擎Agent-Reach在架构上由三个核心组件组成。第一个是Agent目录服务负责存储所有Agent注册的服务能力。每个Agent启动时向目录中心注册自己能干什么、输入是什么、输出是什么、收费还是免费、有没有调用限制。这就像是一个“家政服务黄页”让其他Agent知道你家里有个会修水管的师傅。第二个是统一网关负责接收所有调用请求完成身份认证、参数校验、协议转换然后把请求转发给对应的Agent。用一句话说网关是所有Agent流量的进出门。第三个是策略引擎负责在多个Agent都能胜任同一个任务时决定到底选谁。比如同时有两个Agent都能做库存查询一个精确率高但贵一个速度快但偶尔不准策略引擎可以根据当前场景和预算做路由决策。打个比方Agent-Reach之于多Agent系统就像服务网格之于微服务。它不一定每个业务都非用不可但当Agent协作规模大起来之后这个中间层是保证不乱套的基础设施。2. 核心细节拆解注册、协议、配额2.1 能力SchemaAgent协作的“契约”我在设计Agent-Reach时第一个想清楚的问题不是代码怎么写而是Agent的能力如何描述。一个Agent对外提供服务光有一个名字是不够的调用方需要知道传什么参数、返回什么结构、什么时候会报错。我们采用了一种基于JSON Schema的能力描述格式每个Agent注册时都必须提供一个capability定义。大概长这样{ capability_id: order-stock-query, name: 订单库存查询, description: 根据订单号或SKU查询当前实时可用库存量, version: 3.2.0, tags: [库存, 订单, 实时查询], input_schema: { type: object, required: [query_type, query_value], properties: { query_type: {type: string, enum: [order_no, sku]}, query_value: {type: string}, region: {type: string, default: cn-east} } }, output_schema: { type: object, properties: { stock_items: { type: array, items: { type: object, properties: { sku: {type: string}, available_qty: {type: integer}, eta_days: {type: integer} } } } } }, cost_per_call: 0.01, rate_limit: {rpm: 300} }这个Schema本身就是Agent之间协作的契约。调用方不需要阅读任何文档只需要看这个JSON就知道该传什么、能收到什么。我强烈建议在这个环节不要偷懒把description写得足够的“人话”一点因为很多Agent的调用策略引擎会做语义匹配描述越清晰被匹配到的概率越准确。这里有一个容易踩坑的地方接口版本。能力一定会迭代如果老版本调用方还在线上运行你直接升级Agent能力定义分分钟把老调用方打挂。我们的做法是不允许同capability_id覆盖注册只能新增版本旧版本继续保留直到调用方迁移完成。这个约定写在Agent-Reach的接入规范第一页后面排查问题时救了我好几次。2.2 统一Envelope协议把“方言”翻译成“普通话”有了能力定义之后下一步是统一请求和响应的封装格式。Agent-Reach规定所有Agent交互都必须走同一个Envelope结构不管里面装的是什么业务数据。请求Envelope长这样{ protocol_version: 1.0, request_id: req_8f3b2c1a, trace_id: trace_9d7e6c5a, source_agent: customer-service-v2, target_capability: order-stock-query, deadline: 2025-06-15T10:30:00.000Z, payload: {query_type: order_no, query_value: SO123456}, auth: { caller_token: token_value, subject: service-account-cs, scope: [order:stock:read] } }响应Envelope也一样统一{ protocol_version: 1.0, request_id: req_8f3b2c1a, trace_id: trace_9d7e6c5a, status: success, code: 0, message: ok, data: {stock_items: [{sku: SKU-001, available_qty: 25, eta_days: 2}]}, usage: {agent_id: inventory-agent, tokens: 152, latency_ms: 380} }这里面的细节都很关键。request_id用于调用方把响应和请求对上号trace_id用于全链路日志串联deadline是绝对时间戳不是相对超时时间。为什么要用绝对时间而不是客户端设置的timeout因为请求可能经过多层转发B Agent调用C Agent时不能重新计算一个超时而是把最初的deadline继续传下去C才算得准自己还有多少时间预算。如果链路上还有队列等待、重试逻辑、人工审批流程相对超时早就乱套了。用deadline这种绝对时间戳每个节点只需要做一个减法就能判断“这单还接不接”。这个设计是从分布式系统里抄来的经典做法但很多自建Agent系统的团队都会忽略。错误码也要统一。我们约定0表示成功1xxx表示参数校验类错误2xxx表示目标Agent不存在或不可用3xxx表示配额不足4xxx表示内部依赖错误。所有Agent返回的异常都必须包在统一Envelope里禁止直接抛裸异常。这样网关才能根据错误码做重试、降级、告警分类。2.3 鉴权、限流与配额管理Agent之间调用不能像内网裸奔一样随意。我们最初上线的第一版Agent-Reach根本不带鉴权结果有一周某个Agent的调用量莫名其妙飙到平时的十倍查了半天发现是另一个团队在测试环境里写了个死循环脚本反复调用生产目录里的Agent。后来我们在Agent-Reach里补了双层鉴权。第一层是调用方身份校验每个Agent接入时发一个独立的API Token网关在转发请求前先验证Token有效性第二层是权限范围校验Token关联scope比如客服Agent的Token只能调用库存查询和订单查询不能去调用支付退款类的能力。配额管理分两级。第一级是调用方配额比如某个Agent每分钟最多500次外部调用超过就直接返回429第二级是目标Agent的保护配额比如库存Agent每个小时最多被调用5万次防止某个上游业务出问题后把下游打爆。配额用完了可以选择排队或者降级降级逻辑比较灵活比如回退到缓存数据或者返回一个“稍后重试”的状态。这个环节我能给的建议是配额值宁可先紧后松。上线初期你根本不知道真实的容量水位先定一个大家都觉得“紧”的配额然后用监控数据去校准。一上来就设大数值等真被流量打爆的时候想收都收不住。3. 实操过程从零搭建Agent-Reach3.1 注册中心选型为什么我们选了etcdAgent-Reach最基础的部分是Agent目录服务。这个目录可以被所有Agent读写必须有很强的实时性一个Agent下线后路由不能继续往它那边发流量一个新Agent上线后最好几秒钟内就能被发现。我们对比过几个常见方案。Redis的简单KV结构很方便但缺少真正的watch机制Agent列表变化时网关只能靠轮询感知轮询间隔短了浪费CPU长了感知有延迟。Nacos功能很齐全但对于我们这种内部小规模部署引入全套Nacos平台的成本有点高。最后我们选了etcd核心原因是它天然支持租约和watch机制非常契合服务注册的场景。etcd的存储结构我们设计得很简单/agent-reach/capabilities/{capability_id}/{version} - Capability定义JSON /agent-reach/instances/{instance_id} - Agent实例信息 /agent-reach/leases/{instance_id} - 租约Agent启动时做三件事把自己的Capability定义写进etcd、写入实例信息、创建租约并启动keepalive。实例信息里包含Agent的地址、当前负载、状态、带额外路由偏好的元数据。租约TTL我们设的是30秒keepalive每5秒续一次这样可以做到正常情况下服务不掉线Agent宕机后最多30秒被目录剔除。网关不是每次请求都去etcd读一次目录那样性能扛不住。我们让网关在启动时和watch到变更事件时把全量目录缓存到内存里。具体做法是在etcd的watch回调里触发重建缓存或者更新对应capability的缓存条目。实测下来从Agent注册成功到网关可以路由到这个Agent平均耗时不超过2秒。3.2 路由网关的核心实现网关本体我们用Python FastAPI实现主要是因为团队现有的AI Agent代码全是Python生态维护一个语言统一。如果你团队是Java为主用Spring Cloud Gateway去接etcd也完全没毛病逻辑是通用的。核心的路由选择逻辑是一个函数给定一个目标capability描述和请求上下文返回最合适的Agent实例。我写了一个简化版本的逻辑async def route_request(target_capability: str, context: dict): candidates catalog.get_capable_agents(target_capability) if not candidates: raise NoAgentAvailable(target_capability) scored [] for agent in candidates: score 0.0 # 1. 负载因子低负载优先 load_factor max(0.5, 1.0 - agent.current_load / agent.max_load) score load_factor * 0.3 # 2. 匹配度语义描述和上下文关键词的重合度 overlap len( set(agent.capability[tags]) set(context.get(hint_tags, [])) ) match_hit 1.0 if overlap 0 else 0.3 score match_hit * 0.4 # 3. 成本因子低单价优先 cost_factor min(1.0, 1.0 / (1.0 agent.capability.get(cost_per_call, 0.0))) score cost_factor * 0.2 # 4. 偏好因子: 使用方指定preferred_agent则加固定分 if agent.agent_id context.get(preferred_agent): score 1.0 scored.append((score, agent)) scored.sort(keylambda x: x[0], reverseTrue) best_agent scored[0][1] return await forward_request(best_agent, context)实际的线上版本比这个复杂加入了更多的指标比如说Agent的历史成功率、平均响应时延、当前队列积压数策略引擎会综合这些维度算出最终得分。但核心思想就是这个多个候选Agent之间做权衡而不是永远选第一个。网关转发请求时用了标准的httpx.AsyncClient每个出站请求都带上统一的headers超时用的是从请求Envelope里剥离出来的deadline减去当前时间计算得出的剩余时间。这里有一个血泪教训出站请求不能再设置一个独立的静态超时否则如果前面的链路已经消耗了大量时间出站等待时间就会被截断可能在Agent真正处理完成前就断开连接。路由代码之外网关还负责把所有Agent的响应统一包成Envelope返回调用方永远不会直接看到某个Agent内部返回的原始错误只看到标准化的status、code、message。这保证了所有调用方写解析逻辑时只需要面对一套接口。3.3 端到端跑通客服Agent调用库存Agent光有代码实现还不够下面我把我们第一个完整用例的流程摆出来你会发现Agent-Reach真正跑起来时大家的职责边界会非常清晰。场景是这样的用户对客服Agent说“帮我查一下订单号SO123456有几个SKU有货”。客服Agent本身不具备库存查询能力它需要找库存Agent帮忙。第一步库存Agent启动时向etcd注册了capabilitycapability_id是order-stock-query描述的tags是“库存、订单、实时查询”实例地址指向库存Agent的服务端口。第二步客服Agent接到用户请求后没有自己去拼数据库查询而是构造了一个Envelope请求target_capability填order-stock-querydeadline设置当前时间加10秒payload里带上订单号发给Agent-Reach网关。第三步网关从本地缓存中找到两个候选的库存Agent实例一个空闲、一个负载较高。策略引擎给空闲实例打了更高分于是请求转发给了空闲实例。第四步库存Agent处理完请求把结果按Envelope格式返回给网关网关把结果再包一层原样返回给客服Agent。第五步客服Agent拿到stock_items之后再把结果组织成自然语言回复给用户。整个过程对客服Agent来说它只知道“我调用了一个叫order-stock-query的能力传了订单号拿到了库存结果”至于这个能力背后的Agent是什么框架、部署在哪里、有几个实例它完全不需要关心。这也就意味着如果明天库存Agent从REST接口换成了MQ异步接收客服Agent的代码一行都不用改只需要Agent-Reach内部的适配层改一下。4. 常见问题与排查技巧实录4.1 注册了却路由不到这个问题在接入初期几乎天天遇到。Agent明明已经注册成功在管理端也能看到但调用方就是报“NoAgentAvailable”。我盘点了几种常见原因。第一种是租约过期Agent实例可能因为GC停顿或者线程阻塞没有及时续约被etcd摘掉了网关的缓存也跟着删除了。第二种是能力名不一致注册时能力ID叫stock-query调用时报的是order-stock-query看起来差不多但字符串完全匹配不上。第三种是多环境干扰有些人把测试环境的Agent注册到了生产目录里导致调用测试环境时匹配到了生产实例。我的排查套路很固定先看etcd里的实际key和数据用etcdctl直接读etcdctl get /agent-reach/instances/ --prefix --keys-only etcdctl get /agent-reach/capabilities/ --prefix如果发现实例key存在但网关路由说找不到那基本就是网关缓存问题手动触发一次目录刷新或者看看watch连接有没有断开。如果实例key根本不存在就去查Agent的keepalive日志看看续约线程是否正常。4.2 超时、重试与调用风暴多Agent系统最常见的问题不是单次超时而是超时触发了重试重试又引发下游压力飙升最后变成调用风暴。我们的场景是客服Agent调库存Agent库存Agent偶尔会慢到8秒但客服Agent设置的Deadline是5秒5秒一到客服Agent就报错重试结果就是库存Agent已经被打满但客服Agent还在不断重试。解决这个问题靠三个手段配合。第一所有重试必须做指数退避和抖动第一次1秒第二次2秒第三次4秒然后随机加一个0到500毫秒的抖动防止多个调用方同时重试导致拍振。第二网关侧做熔断如果某Agent的最近10次调用失败率超过50%网关直接打开断路器返回一个快速失败的错误码给调用方让它等30秒后再试。第三Deadline必须有传递性像我在2.2里强调的绝对时间戳一直传到链路末端每个中间环节都判断“剩余时间还够不够”不够就直接终止不再往下游转发。我还专门在Agent-Reach里加了一个“重试预算”字段放在Envelope请求头里。网关默认允许重试3次每重试一次这个数字减1减到0就直接返回错误防止有人配置了一个无限重试脚本。4.3 协议转换中的类型丢失Agent-Reach的Envelope用了JSON传输这对大多数场景没问题。但我们的库存Agent用的是Python返回的available_qty是一个很大的整数放在JSON里传输没问题但到了Java或者Go的调用方那边没有用BigInteger处理的话超出2的53次方的整数精度就丢了。这个问题在常规的业务数字上不会暴露但一旦出现就非常隐蔽因为最终结果只差了一位数字存量校验根本看不出来。我们的解决方案是在Capability的Schema定义里明确标注哪些字段是int64类型Agent-Reach在协议转换层对这类字段做字符串序列化处理调用方拿到后再转成对应的native类型。同时我们在内部约定所有涉及金额、数量、ID类的字段除非明确是32位以内的小整数否则一律按字符串传给Agent。这看起来很不“程序员”但它在实际协作中少掉了很多脑细胞。另一个协议问题是时间格式。有的Agent返回ISO8601带时区的字符串有的返回时间戳有的返回无时区的本地时间。我们统一了规则所有Envelope内的时间字段必须是RFC3339格式且必须带UTC时区偏移不允许带裸本地时间。网关层会做校验不符合的直接拒绝并返回参数错误。这个规则执行起来非常简单但能让下游少踩无数个“差八小时”的坑。4.4 安全边界与审计Agent-Reach在安全方面的情况有点像家里的路由器虽然放在角落里但所有流量都从它这过天然适合做安全控制点。我们遇到过一个比较严重的问题某个Agent的Token被内部员工复制到自己的本地脚本里直接批量调用下游Agent结果造成配额被这两个调用方疯狂抢占。事后排查审计日志时发现这个Token的调用时间跨度和Agent本身的运行时间完全不匹配才定位到问题。从那以后Agent-Reach的鉴权模块做了一次强化。所有Token绑定Agent实例ID而不是绑定Agent类型一旦实例重装Token立即失效。每次调用都要过一遍scope白名单比如库存查询的scope只有order:stock:read凡是超出scope的申请直接在权限校验层打回。最后所有调用记录全部写审计日志包括调用方身份、目标Agent、请求摘要、响应状态、耗时、Token的指纹不定期做权限使用审计。我给其他团队的建议是哪怕你的Agent系统现在只在内部运行没有对外暴露任何公网入口安全也要做全套。因为Agent领域里大量真正的风险不是来自外部黑客而是来自内部的“权限放大”——一个本来只该读订单的Agent因为不小心被赋予了完全管理权限又因为某个逻辑漏洞被AI大模型诱导执行了恶意指令最后造成的破坏远超预期。我把Agent-Reach上线之后遇到的最典型的几个问题整理成了一张速查表方便现场排障症状可能原因排查动作调用报NoAgentAvailable租约过期、能力名不一致、环境目录串了查etcd key、检查能力ID拼写、隔离多环境前缀调用偶尔成功偶尔失败目标Agent实例慢、负载不均看网关监控面板的实例延迟和负载指标超时后请求堆积重试逻辑无退避、重试预算过大配置指数退避抖动、减少重试预算返回数据精度不对大整数JSON精度丢失、时区处理不一致Schema标注int64转字符串、统一RFC3339时间配额消耗异常快Token泄漏、死循环脚本调用轮换Token、按实例绑定身份、审计日志回溯目录刷新慢etcd watch连接断开、网关缓存异常检查watch状态、手动触发缓存重建最后说说我自己在实际使用Agent-Reach过程中的体会。这个系统上线前我一直以为最难的环节是路由算法的设计——到底是加权随机好还是写死优先级好。实际上真正跑起来之后最难的、也是最出价值的部分是让每个Agent团队清晰地说出“我能干什么、怎么调用、有什么限制”。Agent-Reach本质上是在逼着所有Agent提供方把它们的接口标准化这个收益远远大于那一点点路由调度的技术优化。再分享一个小技巧给每个Agent的Capability里加一个自定义的metadata字段比如business_unit、visibility、preferred_region。这些元数据可以在路由策略里做偏好过滤比如数据合规要求只能让中国区的Agent处理特定区域的请求。一开始设计Agent-Reach时我并没有考虑这个字段后来是在一次合规审查中被要求做区域隔离才补上的。如果你想从零做一个Agent触达中间层建议在Schema里一开始就预留metadata别到时候再改。这个内容后续还可以扩展的方向很多比如支持流式输出、把调用记录做成自动反馈数据回喂给路由策略做动态调优、支持更复杂的Agent编排DAG。但我的建议是先把最基础的“能注册、能发现、能调用、能排查”这四件事做瓷实了再谈这些高阶玩法。