2026/10/14 7:29:25

Agent-Reach:多Agent系统中的可达性感知与智能路由实践

Agent-Reach:多Agent系统中的可达性感知与智能路由实践 做多 Agent 系统做久了你会发现最折磨人的不是怎么让单个 Agent 的 prompt 更聪明而是请求发出去了你根本不知道对面那个 Agent 现在是死是活。我有一次在生产环境里遇到过一个代码生成 Agent 进程没挂、日志正常、健康检查接口一路 200但背后的模型服务已经限流五分钟了所有请求都堆在它的队列里排队排队超过 15 秒后才一个个超时然后重试又打到同一个 Agent 上。那是我第一次意识到传统的进程活着跟能干活完全是两码事。之后我花了相当一段时间折腾 Agent-Reach 这个思路——一套专门解决多 Agent 路由前感知问题的轻量框架今天把整个设计逻辑、核心原理和踩坑过程完整分享一下。Agent-Reach 这个名字其实点得很透Reach 是可达性也就是在把一个任务交给某个 Agent 之前先搞清楚它现在到底够不够得着、接不接得了、响应是否靠谱。它解决的核心问题有三个一是实时知道每个 Agent 的健康状态不是那种敷衍的 /healthz 返回 200而是真正能反映业务可用性的状态二是把这些状态量化成一个可计算的分值让调度器能做理性决策三是在 Agent 不可达时自动完成故障转移而不是让调用方傻等超时。这文章适合谁看如果你正在做多 Agent 编排、准备给自己的项目接入 Agent 路由层、或者被某个 Agent 偶尔变慢但查不出原因折磨过那这套思路可以直接抄作业。1. 先说清楚为什么传统的健康检查在多 Agent 场景里不够用1.1 你以为 Agent在线其实它已经掉线了多 Agent 系统里每个 Agent 通常被包装成一个独立服务有自己的 API 端口、健康检查路由和日志文件。常规监控套路是每 30 秒打一次 /healthzTCP 能连上、进程能响应就认为是健康的。但实际生产里故障形态远比这复杂进程活着但线程池已经被慢请求占满。Agent 内部如果用同步阻塞方式调用底层模型接口一个请求的耗时可能长达几十秒。假设线程池大小是 10同时来了 10 个耗时的代码审查任务线程池直接打满第 11 个请求进来只能排队等待。进程是活的健康检查也能响应但实际上已经不具备处理新任务的能力。底层依赖在悄悄变慢。Agent 本身逻辑正常但它依赖的向量数据库、模型推理端点、搜索引擎 API 出现延迟抖动。比如正常情况模型首 token 延迟 300ms现在变成 3 秒。健康检查测的是 Agent 自己测不到下游依赖。Agent 正在执行不可并发的长任务。代码生成 Agent 通常带工作区上下文同一个 Agent 实例同时跑两个任务可能会互相污染文件状态。所以很多实现里会给 Agent 加互斥锁锁被占着的时候新任务进不来但你从外部完全看不见这个状态。这三种情况有个共同特征Agent 的进程状态和业务可达状态严重脱节。Agent-Reach 里我把这种状态称为假在线——进程活着但实际接不了活。1.2 健康检查接口 200 不代表业务可用这个坑必须早踩很多团队习惯把健康检查当成Agent 是否可用的唯一依据这在单机单体时代问题不大但在 Agent 这种依赖链很长的服务上健康检查接口往往只检测了我这个服务进程能响应 HTTP 请求完全没有覆盖下游依赖。更尴尬的是有些 Agent 的健康检查实现里甚至只是返回了一个写死的 JSON。我在设计 Agent-Reach 的时候定了一个原则一切不经过真实业务链路的检测都只能算弱信号。真正能反映 Agent 可用性的是用最小粒度的真实请求去试探那条链路。比如检索类 Agent 的健康探针就真的去检索一个预设的测试索引代码生成 Agent 的探针就真的去跑一段几十毫秒的代码补全请求。只有链路通到底才算真的活。2. 可达性评分机制把活没活变成多适合接这个任务2.1 三层信号采集心跳、探针、生产旁路Agent-Reach 的核心不是某一个检测手段而是三层信号的综合。单看任何一层都有盲区但三层叠在一起可信度会高很多。第一层是基础心跳。每 10 秒一次确认进程存活、端口可达。它只负责排除物理死亡的情况本身信息量很低但它是其他机制的基础。第二层是业务探针。按照各 Agent 声明的能力类型定期发起一个最小化的真实业务请求。频率不需要太高每 60 秒一次就够了。关键点在于探针请求必须走完整的业务链但负载必须可控。探针不是压测目的是验证链路不是测上限。第三层是生产请求旁路统计。这个是我觉得价值最大的部分——所有真实业务请求的结果都会被异步记录包括成功/失败、耗时、超时次数。这些数据不额外产生流量而且最能反映真实情况。毕竟探针 1 分钟才一次生产流量可能 1 秒就有几十次统计量级完全不同。三层信号最终汇总成若干指标availability基于心跳和探针的失败率换算衡量进程链路的存活度latency_p50 / latency_p95通过探针和生产请求共同计算saturation根据线程池使用率、队列深度、并发数估算反映还能不能接新活capability_match当前任务类型和该 Agent 能力画像的匹配度这个后面单独说2.2 EWMA 平滑不被一次抖动带偏刚开始我犯过一个错误直接用最近一次探针的结果作为当前状态。结果就是 Agent 偶尔一次 3 秒延迟我这边立刻把它标成不健康路由策略开始大量把流量切走等下一次探针恢复正常又把流量切回来。这种抖动导致整个系统频繁震荡比不切更糟。后来改成指数加权移动平均EWMA。简单说就是每个指标保留一个平滑后的估计值新的观测结果到来时按权重融入历史。公式很简单s_t alpha * x_t (1 - alpha) * s_{t-1}alpha 取 0.2 到 0.3 之间。取值越小历史影响越大曲线越平滑但对突变反应越迟钝反过来则对抖动敏感。经过验证0.25 左右是个不错的平衡点。一次瞬时失败对整体平滑值的影响会被稀释不会直接导致流量大规模迁移。为了照顾特定场景我还给指标加了衰减逻辑超过 5 分钟没有新信号的 Agent它的分数会随时间递减。这样做是为了防止一个 Agent 已经悄悄断连五分钟但旧数据还显示它很健康的情况。2.3 效用分数匹配度、可用性、成本三者怎么权衡路由决策最终落到一个可比较的数字上。我用的是一个加权效用函数reach_score w1 * availability w2 * capability_match - w3 * cost_factoravailability 来自前文说的三层信号汇总范围 0~1。capability_match 表示这个 Agent 处理当前任务的适配程度比如一个 Python 专项 Agent 处理 Python 任务的匹配度是 0.95处理前端任务可能只有 0.3。cost_factor 则结合了预估延迟和运行成本。权重 w1、w2、w3 需要根据系统偏向调。如果业务需求是宁可慢一点也要准w2 就调高如果需求是快速响应优先w3 和延迟权重就调高。这个不复杂但值得单独说一句capability_match 往往比 availability 更影响最终效果。一个很健康的 Agent 如果根本不擅长当前任务把请求发给它只会得到一堆低质量输出不如发给一个稍慢但专精此道的 Agent。这也是我把 Agent-Reach 和单纯的服务发现框架区分开的核心差异——它做的是业务语义匹配健康状态的联合判断。3. 调度决策全流程从请求进入到任务落到 Agent 手里的完整链路3.1 能力画像匹配先筛出能干的再从里面挑能用的假设我现在有一个需求帮我重构这段 Python 代码并补充类型注解。 这是个典型的代码任务。第一步不是看谁健康而是从 Agent 注册中心拉出所有声明了代码处理能力的候选 Agent。这个环节我称为能力路由。每个 Agent 在注册时需要提交一份能力声明内容大致包括任务类型代码生成、文档检索、数据分析、SQL 执行适用语言/框架Python、Go、React、Vue输入约束最大 token 数、是否支持流式输出运行环境影响是否需要专用工作区、是否允许并发这一步的匹配用规则引擎就够了不需要上模型分类。维护一个简单的标签体系请求进来打上标签Agent 注册时也打标签做个相交运算就行。复杂语义的匹配可以用 embedding 相似度但别在一开始就搞那么重。候选集筛出来之后才是走 reach_score 排序的环节。注意排序的依据是那个加权效用函数不是简单的健康程度。很多任务在代码 Agent 可用性偏低时宁可选择等一等也不能顺手丢给一个全能的通用 Agent否则输出质量会明显下降。3.2 同步调用和异步任务的调度差异很关键Agent 任务分两类调度策略完全不同。同步调用场景比如快速总结一下这篇文档用户等着结果。这类请求对延迟敏感路由时应该优先选 latency 低、saturation 低的 Agent哪怕 capability_match 稍微低一点也能接受。因为用户等不了太久与其丢给一个擅长但排长队的 Agent不如选一个够用且空闲的。这里我给了 latency 和 saturation 额外权重。异步任务场景比如跑一遍这个仓库的测试并生成报告用户不需要立即看结果。这类请求对延迟不敏感但对结果质量敏感应该优先选 capabilitymatch 最高的 Agent哪怕它当前 saturation 偏高也可以接受。排队就排队晚点出结果没关系质量不能打折。这两种模式的权重参数是完全相反的两套配置Agent-Reach 里我分开维护两套权重集调度器根据任务类型选择使用。3.3 超时、重试和熔断状态机把失败控制在小范围内路由选好了不代表任务一定成功。Agent 可能在执行中挂掉、模型接口可能在调用中限流。Agent-Reach 实现了一个三级保护逻辑第一级是请求级超时。给每个任务设置合理的超时时间来源是 Agent 声明的能力报告中预估完成时间 X 系数 1.5。超时后不立刻重试同一个 Agent因为刚超时的 Agent 大概率还处于不可用状态。第二级是路由级重试。同一个请求可以尝试多个候选 Agent但要做一个重要限制最多重试 2 次超过 2 次直接进入降级流程比如返回当前没有可用 Agent而不是继续做无意义的尝试。原因是 Agent 任务普遍耗时长重试 2 次的成本已经很高盲目重试只会把雪崩问题传给你的下游。第三级是熔断状态机。每个 Agent 维护四种状态正常Normal、波动Evicted、熔断Fused、恢复Recovering。状态迁移规则如下正常状态连续 3 次失败或 reach_score 跌破阈值进入波动波动状态是一个灰度观察期持续 30 秒。期间该 Agent 仍接收约 20% 流量如果期间成功率回升则回到正常否则进入熔断熔断状态流量完全不进入该 Agent持续 60 秒后自动进入恢复恢复状态先放 5% 流量试探表现稳定再逐步提高流量比例直到回到正常这套状态机最关键的一点是不搞一刀切熔断、突然全量恢复而是引入一个灰度恢复的过渡期。很多自研轮子都死在熔断后恢复瞬间被流量冲垮上有了恢复期会稳很多。4. 生产环境实测我踩过的五个坑和对应的补救办法4.1 探针请求和真实请求抢资源Agent 反而更慢了这是我接入 Agent-Reach 后遇到的第一个严重问题。为了拿到更多的探针样本我把探针频率调到了每 20 秒一次结果高峰期时探针请求和真实业务请求共用同一个线程池额外增加了负载。原本就紧巴巴的处理能力又分出去一部分Agent 的 p95 延迟反而上升了。后来做了两个修正一是探针频率改成动态调整高峰期自动拉长到 120 秒一次低峰期再回到 60 秒二是探针请求的优先级设为最低在 Agent 内部通过单独的线程池处理保证不挤占真实请求的资源。这个坑对所有做类似健康检查机制的人都适用监控是有成本的别让你的监控变成事故本身的一部分。4.2 冷启动的 Agent 被误判成不健康流量根本进不去新注册的 Agent 没有任何历史数据EWMA 初始化值如果为 0它的 reach_score 会非常低调度器几乎不会把流量分给它。结果就是新 Agent 永远没有机会获得生产数据形成恶性循环因为没数据所以分数低因为分数低所以没有流量因为没有流量所以没数据。解决方式是搞了一个冷启动信任期。新 Agent 注册后默认给一个中等分数0.7并标记为 Parole 状态允许承接少量流量比如 5%。这些流量产生的真实数据会快速参与 EWMA 平滑通常几分钟后分数趋于稳定再放开全量调度。另外冷启动期内如果连续失败则提前终止信任期进入熔断避免行为异常的新 Agent 一直带病接收流量。4.3 全局统一超时配置是最坑的做法早期我给所有 Agent 设置了相同的 30 秒超时。结果阻塞型检索 Agent 常在 20 秒左右完成代码生成 Agent 动辄 40 秒。因为全局超时设成 30 秒代码 Agent 的 40 秒任务频繁被判定失败导致该 Agent 的失败率异常升高被熔断。但实际它是健康的只是任务本身耗时长。这个洞察很直接超时时间必须跟任务特性绑定不能跟 Agent 绑定。Agent-Reach 里我改成从能力声明里读取该 Agent 支持的最大耗时每个 Agent 独立配置超时系数。代码生成 Agent 的超时设成 90 秒检索类设成 20 秒文档总结设成 30 秒。这样失败率和超时之间才真正对得上。4.4 可观测性没做好之前别急着调权重权重 w1、w2、w3 到底该设多少没有任何官方答案能告诉你必须靠自己的业务数据来验证。我一开始凭感觉把 availability 的权重设得特别高结果系统频繁在几个 Agent 之间做无意义的流量搬移整体延迟不降反增。后面我把每一条调度决策的原因、当时的各因子分数、最终结果都记录到结构化日志里专门拿这些数据去反推权重的合理性。比如观察发现capability_match 0.9 的 Agent 虽然 latency 高一点但产出质量明显优于 0.5 匹配度的 Agent用户满意度也更高。于是逐步调低 availability 占比提高 capability_match。这个调优过程完全是数据驱动的不是拍脑袋定的。4.5 Agent 忙碌度信息必须由 Agent 自己主动上报外部探针再聪明也观察不到 Agent 内部的锁状态和上下文占用情况。后来我在 Agent 侧加了一个可选的 SDK 上报能力Agent 在开始处理长任务时调用 reportBusy(true)结束后调用 reportBusy(false)同时上报当前并发任务数。调度器收到这些主动上报的信号后saturation 指标会更精准。这个信息甚至比生产请求旁路统计更超前因为它反映的是现在正在做什么而统计反映的是过去做了什么。如果暂时不想改 Agent 代码也可以靠外部推断——通过响应延迟的上升斜率大概判断拥堵程度但准确性远远不如主动上报。我的建议是条件允许的话还是侵入 Agent 侧把上报做进去这是收益最明显的一笔投入。5. Agent-Reach 落地后的收益和下一阶段的扩展方向5.1 上线三个月后的实测数据我们内部系统接入 Agent-Reach 之后跟踪了大约三个月核心指标变化比较明显Agent 不可用导致的任务失败率从 7.6% 降到 1.2%终端任务平均响应时间从 6.4 秒降到 4.1 秒高峰期系统雪崩次数从每月 3 次左右降到接近 0新增 Agent 从注册到稳定承接流量从人工观察配置的半天缩短到 10 分钟以内的自动信任期最让我意外的是整体资源利用率也提高了。原因在于原来流量集中在少数几个看起来健康的 Agent 上现在被摊薄到整个集群里所有实际健康的 Agent 上单点压力明显减小。5.2 与 LangChain、Semantic Kernel 这类编排框架的配合很多人会问用了 LangChain 还需要自己搞 Agent-Reach 吗这两个定位完全不同。LangChain 解决的是一个任务的执行链路怎么设计Agent-Reach 回答的是同一层级有多个执行者时把任务交给谁。实际情况是在 LangChain 的 router 组件里接入 Agent-Reach 的调度接口作为它路由的一个外部判断来源互补效果很好。如果你想在一个轻量的 FastAPI 服务里用 Agent-Reach核心就两步Agent 启动时向 Reach 的注册中心提交能力声明和探针地址调度时从注册中心拉候选集、按 reach_score 排序、选择最优目标、发起调用并回写结果。完全不需要替换你已有的 Agent 实现只需要在调用链路上加一层透明代理。这种非侵入式的接入方式是项目一开始就确定的所以落地阻力小很多。5.3 下一阶段从实时感知到预测感知Agent-Reach 目前的机制基本属于事后感知 快速反应数据反映的是过去几十秒的状态。下一版本的方向是在这些历史数据上做时序预测提前预判一个 Agent 是否即将进入饱和状态。比如根据过去一小时流量趋势和 Agent 的并发曲线预测未来 5 分钟内该 Agent 的负载是否会超过阈值。如果可以提前预判调度器就能在真正拥堵之前开始分流这比拥堵发生后靠熔断机制恢复要平滑得多。我准备在下一阶段把这类预测模块接进去目前也在验证具体模型选型不过那就是另一个话题了。还有一条我觉得价值很大的路如果是多区域部署不同机房的 Agent 能力相同但网络延迟完全不同。Agent-Reach 可以把就近性也纳入 cost_factor让调度器在能力相当的情况下优先选择同机房或低延迟机房的 Agent减少跨区调用的损耗。这个改动对多区域团队来说收益会非常直接。最后分享一点个人体会。这类调度框架最难的不是算法也不是代码实现而是你到底敢不敢把生产流量交给一个自动决策系统。我的经验是从小流量灰度开始先让 Agent-Reach 只做观测和日志记录不实际干预路由跑几天看看它的判断是否和真实情况一致确认可靠后再从 5% 流量逐步放开。这个过程慢是慢一点但它能让你在出问题的时候一眼看清楚是数据不准还是决策模型不对。系统越复杂越是这种朴素的信任建设过程最有价值。