
如果你管过二十个以上同时在协作的Agent实例一定见过这种画面Agent A处理完订单要把结果交给Agent B继续做下一步代码里写满了await和手动retryAgent C需要查一下库存先得从配置中心翻出D的地址再拼一遍URL某个实例凌晨滚动更新整条链路就跟着抖一阵。我这次想聊的Agent-Reach不是一个花哨的Agent编排框架而是解决那个最朴素的问题A要触达B路径得短过程得稳结果得能确认。它本质上是一个Agent之间的通信触达层把谁在哪儿、谁能干什么、消息怎么送、送没送到这件事集中管起来。适合正在做多Agent系统、被Agent互相调用搞得焦头烂额的团队参考也适合想给现有机器人服务加一层可靠通信的开发者拿来类比。1. 为什么触达成了Agent项目里最容易被低估的环节1.1 我从点对点直连改到中间层的那段经历最早我负责的项目里Agent之间全是直连。A调用BB调用C每个调用方都自己维护目标服务的地址、接口协议、超时时间和重试策略。一开始只有五六个Agent能跑大家都挺满意。等到Agent数量上到三十几个问题就藏不住了。第一是拓扑混乱谁依赖谁只能打开代码一个个翻。第二是重试逻辑各写各的有的Agent重试三次还带退避有的失败就抛异常有的压根不重试。第三也是最难受的每次有人改接口字段所有下游都要跟着动部署顺序稍微错一步线上就飘红。后来我下决心做一个统一触达层。出发点不是我们要上中间件而是再这样写下去所有人都得耗在联调里。Agent-Reach的第一版其实非常简陋只有两张表、一个转发服务和一个重试任务但上线之后效果立竿见影——新Agent接入不再关心目标是谁只关心发什么topic经过什么路由找到目标实例由触达层完成投递。这不只是少写代码的问题而是把Agent之间怎么通信从业务代码里抽离出来了。1.2 触达到底在解决什么可靠性、拓扑解耦与说人话的接口把触达单独做成一层本质上是在回答三个问题。第一是可靠性。直连时一次调用失败调用方往往不知道是该重试、该降级还是该放弃。有了触达层消息先落库再投递失败就进重试队列发送方只需要知道我发出去了一条消息它的状态在触达层里可以查。这等于把尽力而为升级成了有据可循。第二是拓扑解耦。触达层维护一份Agent注册信息每个Agent上线时上报自己的身份和能力。调用方不需要知道B在哪不需要知道B有几个副本甚至不需要知道B是不是换了语言重写。只要topic没变路由就能把消息送到。第三是统一接口。我认为好的触达层应该让Agent之间的交互说人话。Agent A不需要给Agent B单独定义一个RPC接口A只需要对外说我发了order.created这个topic的消息然后在注册中心声明自己能消费order.created。事件语义替代方法调用语义好处是扩展性变强坏处是事件格式需要约束这个我后面细讲。2. Agent-Reach的四层管线消息从投递到确认的完整路径2.1 接入层HTTP、gRPC和MQ不该变成选择题Agent-Reach的接入层把三种方式都收了进来HTTP/JSON、gRPC、以及从消息队列里消费的事件。我见过不少项目在该用同步还是异步上吵个不停其实没必要。同步接口用于急等结果的场景比如Agent A要立刻知道Agent B有没有接下这个任务异步topic用于可以慢慢处理的场景比如通知类消息B收到后自己入队做后续处理。接入层真正要解决的是协议转换和身份认证。所有进来的消息不管原始协议是什么统一转成内部结构体然后带着发送方的Agent ID一起往下走。这一步不做业务逻辑纯粹是收件。接入层的配置里我建议把每个Agent的QPS配额放在这层做限制。ingress: port: 8080 protocol: [http, grpc, mq] auth: mode: token token_ttl: 3600 quota: order-svc: 5000 # 每秒最大接入条数 payment-svc: 2000配额不是摆设。某个Agent一旦发起消息风暴触达层如果照单全收后面的路由和分发全都会被拖垮。在接入层拦住等于给整个系统上了第一道保险。2.2 路由层与分发层谁负责找到人谁负责送到手路由层拿到一条消息后先解析topic然后查注册表看哪些Agent声明了消费这个topic。注意这一步查出来的不是一个具体实例而是一个候选列表。真正的送信动作由分发层来做。两层的职责一定要分开。路由层是大脑只管做决策分发层是手脚只管把消息发给选定的实例。如果混在一起会出现一种很尴尬的情况路由决策和实际投递互相耦合一方改成另一方跟着抖。分开之后路由层可以独立测试分发层也可以单独做连接池优化。分发层的核心是连接管理。每个目标Agent保持一条长连接连接池需要复用不能每条消息都新建连接。我用的是gRPC长连接加连接池连接空闲超过60秒就回收避免大量空闲连接占用文件描述符。2.3 确认层让发送方拿到一个可信的回执触达层收到路由分发的返回结果后要把状态更新到确认层。我把它做成一张状态表每条消息都有完整的生命周期状态含义触发时机PENDING已接入未路由接入层落库后ROUTED路由完成待分发路由层选出候选后DELIVERING正在投递分发层发起连接前DELIVERED目标已确认收到目标Agent返回ackREJECTED目标明确拒绝目标Agent返回错误码DEAD重试耗尽或过期重试队列判定发送方可以随时查消息状态。我在Agent-Reach里暴露了一个查询接口GET /messages/{message_id}返回上面的状态和更新时间。这个接口的引用频率远比我预期的高因为排查问题的时候大家终于不用再逐台机器翻日志了。3. 路由不是关键词匹配能力声明、优先级与降级策略3.1 注册中心里存的不只是地址而是能力描述路由如果只看topic做精确匹配系统会非常脆。比如order.created这条消息可能有两个Agent都能消费一个负责履约一个负责风控。如果路由只按topic分发两条都发会造成重复处理如果只发给第一个风控Agent就收不到。所以Agent-Reach在注册表里存的不是简单地址列表而是能力描述。每个Agent注册时要上报自己的capabilities结构里包含三个部分{ agent_id: risk-control, instance: [10.0.1.3:9090, 10.0.1.4:9090], capabilities: [ { topic: order.created, mode: subscribe, tags: [risk, high-priority] } ] }mode是subscribe还是handle决定了消息是投递给它后由它自己决定是否处理还是要求它返回处理结果。tags给路由提供了更细的分类能力。风控Agent想收order消息但只想收高优先级的这可以在路由条件里用tag过滤。3.2 候选集打分排序与降级路径有多个Agent都能消费同一topic时路由需要决策。Agent-Reach的做法不是随机选或者轮询而是给每个候选打分。分数由四个因素加权计算优先级用户配置默认权重0.4健康度最近5分钟成功触达率权重0.3负载当前连接数/目标实例数权重0.2就近性同机房优先权重0.1按分数从高到低排序取最高分作为首选。这种策略的好处是灵活集群A性能好但因为流量太高分数下降消息就会自动流向集群B某实例健康度从99%掉到85%它的排序也会自然靠后。降级路径是必须做的。首选投递失败时Agent-Reach不会直接重试同一实例而是把候选列表里第二个、第三个实例依次排上来。这条逻辑我看了很多线上数据才意识到有多重要——同一批实例往往是一起挂的重试首选实例大概率还是失败还不如早一点切换到其他候选。3.3 匹配失败时去死信队列还是踢回给发送方路由层查不到任何匹配的Agent时我在第一版的选择是直接丢弃后来被线上教训改了。某次Agent C发了一条inventory.updated的消息但消费方还没注册完消息就被丢了等消费方上线数据已经对不上。后来我定了两个策略先判断消息是否设置了过期时间。如果消息设置了expire_at说明可以容忍延迟丢进死信队列等消费方注册后由管理员手动回放。如果没设置过期时间判定为关键消息直接返回错误码给发送方让发送方知道这条消息没有送出去你要自己决定是否补偿。死信队列不能只堆不清理。我安排了一个每日巡检任务如果死信队列里某条消息超过24小时且消费方仍未注册就把它的状态置为DEAD并给负责Agent的开发人员发告警。这样至少保证每条消息都有下落不会悄无声息地消失。4. 超时、重试与幂等把尽力而为变成有据可循4.1 超时预算的设定逻辑从一次真实的超时风暴说起超时参数的设定最忌讳拍脑袋。我第一版里随便设了个3秒结果遇到目标Agent GC停顿3秒挂在那里干等触达层的连接池被占满后续所有消息都开始排队。那次线上事故让我学到一个原则超时要有总预算每一跳要有独立时限总时限不能超过预算。Agent-Reach里对每条消息的端到端超时预算设为45秒内部切分成四段环节时限说明接入落库1s本地磁盘写入超过说明存储有问题路由决策2s查注册表打分开销极小分发投递10s包含网络往返和目标处理时间重试间隔32s多次重试的总间隔上限超过总预算后消息状态直接置为DEAD发送方会第一时间收到失败回执。宁可让它失败得干脆也不要让一条消息在半死不活的状态里占用整个系统的资源。4.2 退避公式里的数学指数退避与抖动重试必须带退避这已经是常识了。但很多人不知道退避要加抖动jitter。没有抖动的退避有个经典问题一批消息同时失败同时等待2秒又同时重试重试流量又形成一波尖峰可能再次击垮目标。这不是重试这是自我攻击。Agent-Reach里用的退避公式delay min(base * 2^attempt, max_delay) * (0.8 random(0, 0.4))base取200msmax_delay取4s每次重试attempt加1。抖动范围控制在±20%既能错开流量尖峰又不会因为抖动太大导致等待时间不可控。重试次数上限我建议设为3次。算上首次尝试总共4次机会。再多也没意义——连续4次都投递失败说明目标Agent大概率处于不可恢复的状态继续重试只会加剧问题。func nextBackoff(attempt int, base, max time.Duration) time.Duration { delay : base * time.Duration(1uint(attempt)) if delay max { delay max } jitter : time.Duration(float64(delay) * (0.8 rand.Float64()*0.4)) return jitter }4.3 幂等不是去个重那么简单触达层重试了业务层就要做好重复处理同一消息的准备。幂等设计有个常见误区只在前端判断这条message_id我见过没有用Redis set一下就算完。我一开始也这么干后来发现两个问题Redis里的key过期了怎么办消息处理到一半进程崩了第二次进来时Redis里已有标记但业务其实没做完怎么办Agent-Reach的幂等策略是双保险。第一道message_id在Redis里以SETNX方式写入TTL设为300秒300秒内重复投递直接忽略。第二道数据库里对message_id建唯一索引确保即使Redis丢失也不会出现两条相同消息同时被处理。两道的触发场景不同。Redis挡住的是正常情况下的重复数据库唯一索引挡住的是极端情况下的并发。只靠任何一道都有盲区双保险也谈不上多大成本但能省掉后面排查重复数据的精力。5. 消息格式与兼容性演进一个字段引发的连环事故5.1 消息体里最值得较真的几个字段消息格式设计得不好触达层再稳也白搭。Agent-Reach对消息体有明确的字段约束不是每个字段都要填但下面这几个必须有{ message_id: rs-01HZXK8Q2R9Y3TQ7A1B2C3D4E5, topic: order.created, trace_id: trace-7f4a2c1e9b8d4f3a, producer: order-svc, schema_version: 3, created_at: 2025-01-15T08:30:00Z, expire_at: 2025-01-15T09:30:00Z, payload: {} }message_id必须是全局唯一我用的是前缀时间随机数的组合保证在分布式环境下不碰撞。trace_id贯穿整条链路Producer发消息时生成所有后续处理都会带着它排查问题省事很多。schema_version是我用一次事故换来的深刻教训。之前的Agent之间用一个共享的protobuf消息里加字段用optional标记觉得向后兼容没问题。结果有个Agent用新版本解析旧数据因为某个枚举值变了整个消息解析失败。5.2 schema_version与明文payload的组合后来我把payload改成不透明设计。消息体里只保留上下文信息业务数据全部放到payload里且Agent-Reach不解析payload的内容。它只是个搬运工把整个payload原样送到目标Agent。这样最稳妥触达层不需要理解业务字段自然不会被业务字段的变更影响。但随之而来的问题是如果payload不解析怎么保证消费者能正确解码靠schema_version。消费者从消息里读到版本号后按自己的兼容策略处理。我可以接受消费者有自己的兼容规则但不能让触达层去维护这种兼容规则。接收端我建议的JSON解析原则只取自己认识的字段不认识的字段一律忽略字段缺失时使用默认值不直接报错枚举值解析失败时降级为未知而不是终止处理这样即使生产端先升级、消费端后升级也不会出现阻塞。你要知道Agent系统的发布节奏不可能完全一致协议层的容错能力决定了系统能承受多乱的发布顺序。5.3 元数据与业务数据分离的存储策略消息存储如果全量存数据库增长会非常快。Agent-Reach把元数据和业务数据分开存元数据message_id、topic、状态、时间戳、trace_id放PostgreSQLpayload按需存对象存储或者直接放在消息队列里不落库。出现故障需要回放时我们大多只需要看元数据就能定位问题。真正需要完整payload的情况很少。这个设计把存储成本降了一大截查询速度也快不少。我还给每张消息表加了分区按月分区。查询历史消息时先按时间裁剪分区避免全表扫描。等消息表积累到一个月以上的数据量效果差距非常明显。6. 可观测性不是事后诸葛一次凌晨故障的排查复盘6.1 凌晨三点报警为什么我先看的是重试率而不是成功率某天凌晨三点值班告警突然响了。我爬起来第一件事不是看成功率而是看重试率。因为成功率这个指标有个天然的骗局重试成功也会被记成成功只要重试次数够多成功率看起来依旧漂亮但系统实际已经处于抖动状态。那次告警对应的重试率从平时的1%飙到了37%。这是非常典型的前兆表面上看消息都投递成功了但背后有大量消息在反复重试整个链路在临界点附近挣扎。而成功率指标在那种情况下依然显示99.9%因为触达层把重试也算作最终成功。这说明一个观测上的关键点你要区分第一次尝试就成功的比例和最终成功的比例这两个数据缺一不可。6.2 从超时日志反推故障链路的完整过程我顺着重试率异常的告警开始追查。先看了路由层的时间分布发现P99延迟从前一天的4ms涨到了220ms这明显不正常。然后我去翻了目标Agent的实例列表发现半夜它们在做滚动更新更新窗口内注册中心里的实例数没有变化但实例其实处于接收新连接但不处理的状态。具体链路是这样的某个Agent在更新前需要先停止接收新消息。但更新脚本是先停服务等实例把内存里的存量消息处理完再注销。按正常流程这个实例应该先从注册中心下线再停服务。结果脚本里的注销时机晚了一步。Agent-Reach的路由层仍然把这个实例当健康节点消息发过去连接刚建立就被拒绝于是进入重试重试又选到同一批正在更新的实例恶性循环。日志里最讽刺的一句是connection refused和connection reset by peer交替出现那条消息被重试了6次最终也没成功。而按照Agent-Reach的设计每轮重试应该选不同候选实例但当时候选列表里全是正在更新的机器没有其他可用实例所以换谁都一样。6.3 事后补上的三个告警项这次故障给我的教训不是滚动更新要写对顺序而是系统的自我保护要能识别这类场景。事后我加了三个告警第一重试率超过10%就告警这是系统正在挣扎的第一信号。第二路由层按实例维度的健康度低于某阈值时自动将对应实例摘除出候选列表不再投递任何新消息。第三所有实例的连接池建立成功率低于80%时触发一条P1告警因为它说明目标Agent可能在更新或者网络分区。这三个告警在不长的时间里陆续兜住了几次类似场景。团队后来开玩笑说凌晨的告警从会被吵醒变成了可以安心睡觉因为大部分情况系统自愈了只有真正需要人介入的才会响起。7. 落地部署与容量预估从压测数据谈阈值7.1 最小可用部署长什么样Agent-Reach的最小可用部署包含三个部分三个触达层节点作为无状态服务一个PostgreSQL存元数据和状态一个Redis做幂等和缓存。三者都可以独立扩展初期不需要上复杂的高可用方案。触达层节点本身没有状态挂了就摘掉由负载均衡器负责切换。真正的状态在数据库和Redis里所以核心是保证这两处的高可用。PostgreSQL用主从加自动故障转移就够Redis用哨兵模式也能满足大多数场景。注册表不搞独立服务。第一版里我曾经想做一个独立的注册中心服务后来发现根本没有必要——注册表的数据量很小存在PostgreSQL里加上Redis做缓存加速性能完全够用。少一个组件就少一个故障点。7.2 容量估算公式和一组实测数据容量规划我总结了一个粗略公式并发连接数 QPS × P99延迟秒如果目标QPS是2000P99延迟期望控制在100ms以内那并发连接数约等于200。按照每个线程处理50个并发连接的水平四个线程足够。这个公式不求精确但能给你一个量级上的感知不会被高并发三个字吓住。我还做了一组压测触达层单节点3副本集群目标是模拟100个Agent实例互相发消息的场景压测QPSP99延迟CPU使用率内存占用成功率5002.8ms12%210MB100%10004.5ms24%245MB100%20009.1ms45%290MB100%400022.4ms78%365MB99.99%600048.7ms95%430MB99.9%从数据上看单节点撑2000 QPS没有问题瓶颈在8000 QPS附近开始出现。如果你的目标QPS超过5000我建议直接横向扩到5个节点而不是硬压单机性能。成本不高效果立竿见影。7.3 最后几条早该知道的工程经验第一不要做全链路阻塞。触达层如果目标Agent不可用不能让链路卡死。凡是超过预算的消息要么进死信要么快速失败绝不允许无限期挂起。我见过其他团队把触达层做成了ESB那种重中间件引入复杂的事务管理和编排引擎最后维护成本远高于收益。轻量、快、只解决触达问题就够了。第二payload大小一定要限制。我设的是256KB上限超过直接拒绝。大数据量的场景应该走对象存储消息体里放引用地址而不是把大块数据塞进消息里。第三先跑通一条topic再铺开。上线初期我建议你只接入一条低频topic跑一两个星期把路由、重试、幂等、告警全验证一遍再逐步接入其他topic。这会让你少踩很多坑也方便你积累一套针对自己业务场景的默认参数。实际运行这几个月最让我感慨的是Agent之间的触达问题虽然在架构图里只是一个不起眼的框但它直接影响所有上游下游的稳定性和开发效率。把这块做扎实了后续再做Agent编排、任务分发地基不会晃。如果你也正在设计自己的Agent通信层我建议把重心放在我反复强调的那几件事上路由决策清晰、超时预算可计算、消息状态可查询、重试幂等双保险。这四个点踩实了剩下的其实都是细节。