1. 先说清楚Agent-Reach 在解决什么实际问题1.1 多智能体系统为什么会“相互够不着”做智能体开发这几年我越来越觉得我们正在被一个不起眼的问题卡住Agent 越来越多但彼此之间根本够不着对方。团队最早只有两三个 Agent客服机器人管客服订单机器人管订单偶尔互相调用一下写死几个接口地址就行完全没觉得有什么问题。等到 Agent 数量涨到二三十个情况就完全失控了。每个 Agent 都暴露自己的 HTTP 接口有的用 JSON 格式有的用表单格式有的压根没有文档。订单 Agent 想查一下用户的风控状态得先去翻风控 Agent 的接口文档然后再处理字段对不上的问题。更头疼的是有些 Agent 部署在内网的不同环境里有的在测试环境有的在预发环境调用方根本不清楚该往哪里发请求。整个系统就像一个办公室突然来了一百个新人没有通讯录没有分机号大家都靠贴便签沟通效率可想而知。后来我们统计了一下六十多个 Agent 之间的调用关系已经接近两百条大部分都是硬编码。每次某个 Agent 升级接口都会带出一串连锁故障。那种感觉就像你住的小区里每一栋楼都自己修了一条路但路和路之间全是断头路车根本开不出去。触达这个词听起来简单等真正落地的时候你会发现它背后藏着的是一整套能力发现、路由选择和结果回收的工程问题。1.2 Agent-Reach 把“触达”拆成了哪三层我当时立项的时候就把 Agent-Reach 定位成一个轻量级的智能体触达与编排层不做业务逻辑只解决一件事让任何 Agent 都能稳定、可控地调用其他 Agent 的能力。怎么做到呢核心是把“触达”这件事拆成三个层次每一层单独解决一个问题。第一层是能力发现也就是让系统知道每个 Agent 是谁、能做什么、现在是否健康。没有这一层你连“该调用谁”都判断不了后面的路由更是空谈。第二层是请求路由也就是根据业务意图匹配到具体的 Agent 实例并把这个请求可靠地送过去。这里要处理优先级、负载均衡、超时重试这些老生常谈但必须做好的问题。第三层是执行保障包括链路追踪、结果回调、失败兜底和质量评估。这三层总结下来其实就一句话把“调用”从点对点的硬编码变成平台化的动态触达。Agent-Reach 这个名字也是这么来的——不仅要把请求送到对方家门口还要确保它敲得开门、办得成事、回得了话。如果你也在做多智能体系统或者正打算把单体 Agent 拆成多个协作服务那这篇内容应该对你有用。下面我把整个设计和落地过程中的关键细节都翻出来讲一遍。2. 架构设计与关键选型为什么需要一层“触达网关”2.1 直接点对点调用为什么走不远设计 Agent-Reach 之前我们内部吵过一轮到底要不要引入中心化网关还是干脆给每个 Agent 都装上 SDK做成点对点的网格调用当时的诱惑在于装 SDK 看起来简单业务方在自己代码里加一行依赖就能调别人了不需要改装任何基础设施。但后来我们算了笔账就打消了这个念头。先说维护成本。点对点调用下每新增一个 Agent都要和 N 个存量 Agent 建立调用关系潜在链路数就是 N 乘以 N-1本质上是平方级增长。五十个 Agent 的时候就是将近两千五百条链路你自己维护一下试试。再说治理能力。点对点模式下限流怎么做统一的超时策略怎么下全链路追踪的 trace_id 怎么串起来每个调用方自己实现一遍几乎不可能标准统一。所以我的结论很明确必须有一个触达网关把所有调用关系收口到一个点上。为了降低接入成本不强迫业务方改造原有 Agent而是通过适配器把不同协议转换成统一格式。简单说我们做的是交通枢纽不是让每栋楼都自己修一条通往外界的路。你可能会问中心化网关会不会成为性能瓶颈。这个担心也合理但实际取决于流量规模。Agent 之间的调用量和互联网 C 端流量相比往往小一到两个数量级。我们用 Go 写的网关单机在普通配置下就能扛住每秒几千次请求后面加一层负载均衡和水平扩容就够了。2.2 四层模块接入、路由、执行、观测Agent-Reach 的整体结构我习惯分成四层每层职责非常单一调试和扩展都方便。第一层是接入层负责所有 Agent 的注册接入。每个 Agent 启动时向网关注册自己的能力列表、调用地址、权重信息之后按固定间隔发送心跳。老系统如果不想改代码我们就提供一个适配器容器把原有的 HTTP 接口包一层再注册进来。这一层解决“怎么连进来”的问题。第二层是路由层维护一张实时的能力注册表并根据请求里的意图标签做匹配。这一层解决“请求该给谁”的问题。路由层有几个关键点能力匹配要有优先级高权重的 Agent 优先匹配结果要过滤掉不健康的实例同时要支持灰度比如新版本 Agent 先只接收百分之五的流量。第三层是执行层处理请求下发之后的事情超时控制、重试策略、熔断降级、异步回调。你可以把它理解为网关和各个 Agent 之间的“运输合同”什么时候催单、什么时候放弃、什么时候找备胎都在这一层约定好。第四层是观测层负责链路追踪、日志聚合、指标统计和报警。每一条触达请求从进来开始就生成一个 trace_id贯穿所有 Agent 的调用链出问题的时候直接按 trace_id 捞日志。观测层是很多人容易忽略的部分但实际生产里它比前面几层都重要没有它你就像闭着眼睛开车。四层下来从注册、路由、执行到观测正好覆盖了一次触达请求的完整生命周期。架构上我并不建议一上来就整微服务拆分Agent-Reach 本身就是一个模块边界清晰的单体服务足够支撑现阶段的能力。真到拆分的那一天四层边界已经画好了照着切就行。2.3 协议选型先兼容再收敛协议这块是实际推进时碰到的第一个硬骨头。当时团队里有人提出趁这个机会让所有 Agent 统一换一套标准协议一步到位。我坚决反对原因很现实二十多个存量 Agent 不可能为了接入 Agent-Reach 都停下来改协议代价太大业务方也不答应。我们的做法是“先兼容再收敛”。Agent-Reach 内部定义了一个统一消息体所有请求进入网关后都转换成这个格式带 job_id、trace_id、intent、payload、timeout 这些字段。对外保留一层协议适配层老 Agent 继续用它们原来的格式由适配器完成转换。这就像你到一个新城市一开始不强制所有人都说普通话先配个翻译等到新系统逐步上线之后再慢慢统一。这个决策帮我们避开了最开始的推动阻力。第一个接进来的 Agent 只花了半天就是因为它不需要动任何原有代码。等团队看到效果后面接入新 Agent 时反而会主动按统一协议来写。高层的协同问题往往不是技术问题而是让人们看到收益之后自然发生的。3. 触达引擎的落地实现注册、路由与质量评估3.1 Agent 注册与心跳机制触达要可靠第一步就是注册表里的数据必须是活的。Agent 注册接口长这样入参包含 agent_id、调用地址、能力列表和权重{ agent_id: order-agent-v1, endpoint: http://order-svc:8081/exec, capabilities: [query_order_status, modify_order, cancel_order], weight: 10, version: v1.2.0 }注册不是一次性动作我们要求每个 Agent 每五秒发一次心跳。网关侧维护每个 Agent 的上次心跳时间连续三次没有心跳就标记为 UNHEALTHY不再把新请求路由过去。这个标记一定要用最终一致性的思路去处理不能立即从注册表删除。因为 Agent 可能只是网络抖动或者发生 GC 停顿等服务恢复之后它还想接收新流量。从“不健康”恢复为“健康”也有一套递进检查机制先给一个探测请求成功了再恢复全量。你可能会觉得每五秒一次心跳有点频繁但实际上每个心跳包就几百字节六十个 Agent 对网关的压力可以忽略不计。反而是心跳间隔给长了的代价更大——Agent 已经宕机了服务端还要傻等三十秒才发现期间所有路由到这个 Agent 的请求全部失败。这种级别的失败率在业务上基本不可接受。关于能力列表这里必须提一个教训不要只登记名词要登记动词加对象。比如 query_order_status 和 get_order 看上去类似但一个是查询状态、一个是获取完整数据语义完全不同。能力标签系统如果设计得不清晰路由层匹配出来的结果就会特别不靠谱后面校准会非常痛苦。花在设计标签上的时间比之后排查路由问题的成本低得多。3.2 从意图到目标实例路由计算做对了什么路由引擎是整个 Agent-Reach 的大脑。请求进来之后路由层拿到的是业务意图比如“我买的东西送到哪了”对应的 intent 是 query_order_status。路由计算分三步查找具备这个能力的候选 Agent过滤掉不健康节点最后按权重做负载分配。伪代码大概是这样的def route(intent, payload): candidates registry.match(capabilityintent) healthy [c for c in candidates if c.status HEALTHY] if not healthy: return NoCapableAgentError(intent) total_weight sum(c.weight for c in healthy) target weighted_select(healthy, total_weight) result invoke(target.endpoint, { job_id: new_id(), intent: intent, payload: payload, timeout_ms: 3000 }) metrics.record(intent, result.status, result.latency_ms) return result权重怎么定我一般结合两个因素Agent 的综合能力评分和当前机器负载。比如两个 Agent 都能查询订单状态一个版本新、算力充足权重给到 10另一个老版本稳定但响应慢权重给 5。具体比例再用压测结果调整说白了就是不停试。这里有个容易踩坑的点过滤健康节点不能只看心跳还要看实例的自我保护状态。我们给每个 Agent 配了错误率监控如果某个 Agent 过去一分钟失败率超过百分之三十路由层会临时把它降权甚至摘除。因为心跳代表进程活着进程活着不代表它能正常处理请求。路由层另一个关键能力是灰度。新接入一个 Agent 版本时我建议不要直接全量流量切过去。Agent-Reach 支持在注册表里给实例打标签比如 rollout5那路由层就只放百分之五的流量给这个实例跑一段时间看指标没有恶化再逐步放量。这个机制不复杂但能让 Agent 升级变得安稳很多。3.3 覆盖率如何评估“触达得好不好”建设 Agent-Reach 的过程中我一直被一个问题困扰怎么证明这套系统真正解决了问题光看调用次数没有意义得有可量化的触达质量指标。后来我们定义了一套指标体系核心是两个维度可达率和成功率。可达率描述的是“该连上的能不能连上”计算公式是成功建立连接的请求数除以总请求数主要在传输层统计。成功率描述的是“连上了之后事情有没有办成”需要等 Agent 业务执行完成后回传业务状态码在业务层统计。这两个指标必须分开记录因为它们反映的问题完全不同。可达率低说明网络或者服务发现出了问题成功率低说明 Agent 自身的业务逻辑有问题。另外我们还维护一个更综合的触达健康分简单加权即可触达健康分 可达率 × 0.4 成功率 × 0.6。每个接入的 Agent 都有这张得分表指标含义目标值告警阈值可达率连接成功数 / 请求总数 99.5% 99%成功率业务成功数 / 请求总数 98% 95%平均时延单次触达往返耗时 500ms 1000ms故障恢复时长从告警到自动恢复 5min 15min这套指标上线之后很多隐藏的问题就浮出水面了。有一个老 Agent 平时看起来根本没人调用结果显示它的可达率长期只有百分之七十点进去一看部署它的机器已经跑了快一年没有重启过句柄泄漏导致经常拒绝新连接。放在平时根本发现不了但触达平台把它变成了一张摆在明面上的体检单。4. 从零搭起最小闭环核心流程与关键配置4.1 一条触达请求的完整旅程很多教程一上来就讲各种高大上理论但真正落地的时候你需要的是一个能跑通的最小闭环。我建议按这条链路去搭Agent 注册 - 心跳上报 - 请求路由 - Agent 执行 - 结果回调。拿我们实际的例子来说用户问了一句“我的订单到哪了”网关收到 requests 后生成一个 job_id 和 trace_id路由层匹配到订单 Agent把消息体发送过去{ job_id: job_20250117_0821, trace_id: trace_92ab48f1cde3, intent: query_order_status, payload: { order_id: SO20250116001, scene: user_chat }, timeout_ms: 3000, callback_url: http://reach.local/cb/job_20250117_0821 }订单 Agent 处理完业务后把结果通过回调接口回传给 Agent-Reach而不是直接在响应里返回。这两者的区别是回调方式更可靠因为 Agent 可能处理时间比较长处理期间客户端已经断线了直接用响应返回结果会丢失。回调接口收到结果后用 job_id 关联上最初的任务再往前走一步把结果转给用户对话 Agent 组织话术。为什么要这么绕一圈核心原因是 Agent 执行时长的不确定性。很多 Agent 任务不是简单读一个数据库就能返回的它可能要调用下游好几个服务耗时几十秒甚至分钟级。同步等待会放倒整个服务异步回调才是正解。我建议新系统直接用异步模式同步模式只保留在一些非常轻量、确认能在几百毫秒内完成的操作上。4.2 触发同步与异步的取舍这里想展开聊聊同步和异步的选择因为我们在这块交过学费。最开始为了省事Agent-Reach 对外只提供同步接口网关发出请求之后一直阻塞等结果。测试环境下跑得挺顺结果一上线有某个 Agent 执行超时达到了十秒网关线程池直接就满了后面所有触达请求全部排队。那次事件让我明白一个道理同步最多只能用于内部链路极短的场景绝不能作为默认手段。异步模式的关键配套是回调表和补偿任务。每个 job 在发送时写入回调表状态标记为 PENDING。收到 Agent 的回调后把状态更新为 SUCCESS 或者 FAILED。同时启动一个定时任务每三十秒扫描一次把超过五分钟还处于 PENDING 的任务挑出来重新投递或者直接标记失败。这套机制保证了即使回调消息在网络中丢失我们也能通过补偿发现并对账。异步还有一个好处可以自然地做优先级处理。比如用户直接对话产生的触达请求优先级是高的后台数据刷新任务的优先级是低的。我们就在消息体里加 priority 字段网关内部有两条通道高优先级通道的消费线程池更大队列总是优先被处理。这种精细调度的能力同步接口不太好做。配置上我用得比较多的一组参数是默认超时 3000 毫秒重试次数 2 次单 Agent 错误率熔断阈值 25%熔断窗口 60 秒熔断冷却 30 秒。这个组合是我们在实际压力测试下试出来的不同系统可以自己调但把这些参数显式地放在配置文件里不要藏在代码里。4.3 安全边界与权限控制Agent 之间互相调用最危险的情况是一个不太重要的 Agent 被攻破然后横向调用其他所有 Agent。Agent-Reach 的安全设计围绕两个原则最小权限和审批流。网关给每个接入 Agent 签发独立的访问令牌令牌与 Agent 的能力范围绑定。比如日志分析 Agent 的令牌只能调用日志查询相关的接口调用订单修改接口会直接返回 403。令牌定期轮换轮换期间允许旧令牌和新令牌同时生效一段时间避免 Agent 重启时有调用失败。审批流针对的是高风险动作。比如 Agent 想调用删数据、改价格、发消息这类意图即使令牌里有权限也需要额外带一个审批凭证。Agent-Reach 会拦截这个请求分发到人工审批流审批通过后才真正放行。一开始团队觉得这个流程太烦人但真的防住过几次误操作之后大家也就接受了。触达平台让所有调用都有迹可循但安全这条线必须从一开始就扎进去。5. 踩过的坑四个高频故障与排查实录5.1 触达成功但业务失败指标口径的坑第一个坑出现在指标统计上。上线早期我们发现一个困惑的现象某条链路的成功率明明是 99.8%但业务方一直反馈说任务经常失败。后来排查才发现我们统计的所谓“成功”只是请求成功投递到了对方 AgentAgent 返回了 HTTP 200但业务真正执行的结果是失败的。投递成功和业务成功被混在了一个指标里面。这个问题的本质是观测口径没有分层。我们花了很大力气把状态语义拆清楚Agent 的响应统一带上 status 字段取值是 DELIVERED、SUCCESS、FAILED、TIMEOUT 四种。网关记录 DELIVERED 用于评估可达率记录 SUCCESS 和 FAILED 用于评估业务成功率TIMEOUT 单独统计。如果你正在做类似平台我建议从一开始就把两层指标分开设计否则后面撕数据口径的时间绝对会让你崩溃。我们还额外加了一个字段 biz_code由 Agent 业务层返回数值本身不参与成功率计算只用于快速定位失败原因。排查问题的时候先看命中哪个 biz_code再往下钻日志要快很多。5.2 Agent 重启后反复路由到失联实例第二个坑和状态感知有关。某个 Agent 因为发布重新部署进程有一个短暂的不可用窗口。问题是它的心跳程序跟着进程一起挂了网关要等连续三次心跳缺失才会把它标记为不健康这个反应速度显然不够快。我们后续做了一个改进Agent 启动时向网关发送显式的 startup 信号网关收到后立刻把它标记为 PROBING 状态。PROBING 状态的实例不会接收真实业务流量网关会主动发一个 probe 探针请求测试接口是否真正可用返回正常才切到 HEALTHY。这个探针不是简单的 ping而是调用一个轻量级的 health 接口确保依赖的数据库连接也初始化完成。后来我们又发现光有这个还不够Agent 优雅停机前要发 shutdown 信号但很多老服务根本没办法改到那一步。于是网关侧加了一个自适应逻辑Agent 最近一小时发布的频率高不高比如发布了三次以上网关对它的健康标记就更保守宁可多标记一段时间不健康也不能把请求发给一个正在重启的实例。宁可错杀不可盲发。5.3 一次扇出风暴差点打垮下游这是印象最深的一次事故。某个业务流程里主控 Agent 会同时给十个下游 Agent 下发子任务每个下游 Agent 处理时又会各自调用一个公共服务。最初流量比较小一切正常。结果有一次活动促销用户请求量翻了五倍主控 Agent 的扇出直接把公共服务打到超时公共服务超时又导致下游 Agent 报错下游 Agent 报错后触发重试进一步加大了对公共服务的压力形成了恶性循环。事故复盘的时候我们发现整个链路里没有任何一个环节限制了扇出规模。后来在 Agent-Reach 里加了三道保险。第一单任务扇出上限同一任务最多级联下发十五个子调用超过上限必须显式声明理由第二公共服务队列水位告警队列长度超过阈值时网关自动把低优先级触达请求延后第三所有重试必须做指数退避和随机抖动不允许固定间隔疯狂重试。那场事故教会我一个道理Agent 之间的协作系统最怕的不是单个组件慢而是单个组件慢触发雪崩。Agent-Reach 的执行层看起来只是做超时、重试、熔断实际上它在扮演整个系统的“安全阀”。这个安全阀平时感觉不到存在但真要没有它一次流量波动就能带走整个系统。5.4 回调丢失与本地补偿表异步模式下回调丢失是最磨人的问题。我们第一次遇到回调丢失是在一个上游 Agent 经历长时间 GC 之后回调请求发出来但网关已经断开了连接这条结果数据就凭空消失了用户那边一直显示处理中。后来加了补偿表机制才把这个问题解决。每次下发任务时先往数据库写一条 PENDING 记录回调成功后再更新状态。每三十秒跑一次补扫把超过五分钟还停在 PENDING 的记录重新下发已经处理过的不再重复执行靠 job_id 做幂等判断。这里有一个实测的小技巧补扫的重试要设置次数上限比如三次超过之后进入人工待处理队列不要无限重试。无限重试会造成两拨请求同时打到 Agent 上老任务占用了新任务的资源反而更难恢复。幂等判断必须由业务方 Agent 配合实现不能只靠网关这一边控制。我们最开始以为网关幂等就够了后来发现业务方在没有幂等保护的情况下重复执行了扣款操作这个问题要在一开始就讲清楚绝不能低估。6. 接入 Agent-Reach 后的实际效果与下一步想做的6.1 接入前后到底改变了什么接入之前系统里是接近两百条硬编码调用关系每次升级某个 Agent 都要拉着所有调用方开会。接入之后调用关系收敛成六十几次注册和六十多次路由新增一个 Agent 只需要注册自己的能力和地址开发工作量直线下降。更重要的是所有触达的质量指标都有据可查了哪里慢、哪里失败率高打开观测面板一目了然。我印象最深的一件事是某天晚上一个核心 Agent 的内存泄漏导致它反复重启放在以前要等用户大量投诉之后才能发现。Agent-Reach 的可达率告警在它第一次异常重启时就报了警值班同学按 trace_id 排查五分钟内定位到了问题所在的那段代码赶在用户感知之前就处理完了。这就是把触达质量变成可观测指标之后的价值。6.2 本地调试技巧用 Mock Agent 模拟坏节点最后分享一个我一直在用的小技巧。测试 Agent-Reach 的调度和熔断逻辑时不要真的去搭建几十个 Agent 环境太重了。我在本地起了一组 Mock Agent每个就是一个小脚本支持注册、心跳和接收触达请求。关键是 Mock Agent 可以模拟各种异常行为拒绝连接、随机超时、返回错误码、处理到一半退出进程。我写了一个简单的随机故障测试让每个 Mock Agent 在百分之三十的概率下随机选一种故障模式然后跑一轮压测。正常情况下触达网关会自动降权、熔断、切换健康节点任务成功率最终会回到一个可接受的区间。如果压测结果表明成功率明显下降那大概率是网关的故障快速恢复能力不够这就是要优化的方向。这比等到生产环境出事故再去复盘省心太多。6.3 后面还想继续做的事Agent-Reach 目前解决了“触达”的稳定性和可观测性但下一步我想把它推向更聪明的方向在路由引擎里引入基于语义的意图识别而不是完全依赖人工定义的能力标签。比如用户说“我这个东西怎么还没到”今天的系统需要先把这句话转成 query_order_status 这个 intent 才能路由以后我希望 Agent-Reach 能直接处理自然语言自动匹配到合适的 Agent甚至组合多个 Agent 完成一个复杂意图。另一个想推进的方向是触达成本的治理。Agent 之间相互调用是有代价的一次复杂任务可能触发几十次触达每次都消耗计算和网络资源。下一步我会在 Agent-Reach 上做一个触达链路优化器定期检查哪些调用是冗余的、哪些链路可以合并主动给业务方提出优化建议。这块本质上是把触达行为当成一种可以度量的资源来做调配我觉得这个方向挺有意思也欢迎有同样困惑的同行一起聊聊。踩过几次坑之后我越来越觉得多智能体系统的核心瓶颈不是单体的智能水平而是智能体之间的协作可信度。Agent-Reach 只是一层基础设施真正让协作变可靠的是对每一个触达细节的较真。先把稳定性和可观测性做扎实再去谈更高级的编排能力这个顺序不能乱。希望这篇实操记录能帮你少走一些我走过的弯路。