
做多 Agent 系统最怕的其实不是模型答得不对而是你压根不知道某个 Agent 还活着没有。我前几天排查一个故障调度中心明明把任务派给了一个编码 Agent结果那边进程早就假死只留了一个还在呼吸的 HTTP 端口任务堆积了半小时才被队列超时兜住。这种问题靠人工盯不过来的。我最近把这个阶段踩过的坑收敛成了一个叫 Agent-Reach 的轻量级框架目前跑了三周效果比我预想中稳得多。它的核心就一件事把“Agent 是否真的可达”变成一套可度量、可路由、可自愈的机制而不是靠运气。这篇文章我会把它的设计思路、核心代码、部署配置和排障经验完整讲一遍适合正在做多 Agent 编排、智能体工作流、或者微服务调度系统的朋友参考。无论你是想直接用还是只想抄思路都能找到实处。1. Agent-Reach 要解决的核心问题1.1 为什么说注册中心救不了 Agent以前做微服务的时候我们习惯用 Nacos 或者 Consul 做服务注册发现。Agent 启动后往注册中心登记一个实例消费者按名字拉取列表然后负载均衡。这套玩法在普通后端服务上没问题但拿到 Agent 场景里我遇到了三个明显不适配的地方。首先是探活维度太粗。Consul 默认的健康检查是 TCP 连接或者简单的 HTTP 状态码端口还活着不代表 Agent 内部状态健康。我曾经遇到一个数学计算 Agent进程活着、端口通着、/health返回 200但里面的模型推理线程全卡住了。原因是什么是它依赖的 GPU 显存被别的进程占满推理接口每次都要等超时。那种情况下注册中心仍然觉得一切正常路由照发、任务照塞最后全堆在那个 Agent 上。其次是缺少业务语义。服务注册中心只知道“有一个实例叫 xxx”不知道这个 Agent 擅长什么、当前负载多少、是否允许并发、能接受的最大超时是多少。可我们在实际调度里非常需要这些信息。比如翻译 Agent 和摘要 Agent 都实现了同样的文本处理接口但一个擅长长文本、一个擅长短文本注册中心并不知道怎么区分。最后是恢复动作太笨。传统服务发现把实例摘掉就完事Agent 场景里我们更希望有“降级”“灰度隔离”“自动拉起”这些操作。Agent 不是无状态的多副本部署很多 Agent 内部带会话上下文或者专用资源摘掉以后不能随便重建。Agent-Reach 处理的是这整条链路持续探测、状态评分、智能路由、异常处置。1.2 Agent-Reach 的基本定位与设计目标Agent-Reach 定位为一个“Agent 可达性治理与流量编排框架”部署在 Agent 集群和任务调度层之间。它不需要侵入业务代码只需要 Agent 暴露一个最基本的健康检查端点剩下的探测、评分、路由、降级逻辑都由框架接管。设计目标我提炼成了四点。第一真实可用性优先不仅要探测进程和端口还要模拟一次最小业务调用拿返回时间和结果质量反推可达性。第二路由策略可编程通过 Agent 上的标签、优先级、负载系数、历史成功率来综合打分而不是简单轮询。第三故障动作可配置超时、重试、降级、熔断、通知都要有明确的触发阈值。第四轻量可落地单机部署也能跑不依赖额外数据库所有状态可以持久化到本地文件或者 Redis。这三周跑下来我的整体感受是Agent-Reach 解决的是“调度盲区”问题。任务派发出去之前框架先帮你回答三个问题这个 Agent 现在能用吗它被调度的代价有多大如果不派给它有没有更合适的备选这三个问题回答顺了多 Agent 系统的稳定性立刻上了一个台阶。2. 整体架构与核心设计2.1 四大组件的职责划分Agent-Reach 的架构没做得很复杂核心由四个组件构成。Coordinator 负责协调全局状态维护 Agent 列表、路由策略、任务记录Prober 负责周期性地探测所有 Agent生成健康评分Router 负责在任务到达时基于评分和策略选择最佳 AgentRegistry Store 负责保存 Agent 元数据和历史指标。我特意把这四个组件拆成独立的逻辑单元原因有两个。一是职责清楚排障的时候不用翻一整坨代码二是可以独立扩展比如探测频率需要提高只扩容 Prober 就行Router 不用动。实际运行中Coordinator 是唯一的写入口其他组件通过内部事件总线通信这样不会出现多个组件同时修改状态导致的数据竞争。组件之间通过异步消息交互。Prober 每次探测完会把结果抛给 CoordinatorCoordinator 把评分落库同时更新内存中的路由表。Router 接到任务时只查内存缓存不走网络所以任务转发延迟基本可以忽略不计。这样做虽然增加了一点一致性负担但换来了调度路径上的确定性。2.2 数据结构与探活协议设计Agent-Reach 里最核心的数据结构是 AgentNode。它不只是记录一个地址和端口还包括角色、标签、权重、历史成功率、最近一次评分、冷却期等字段。我把字段分成三大类一是基础元数据比如 Agent ID、地址、版本、启动时间二是调度相关数据比如标签、级别、权重、最大并发三是动态指标比如成功率、平均响应时间、连续失败次数。这里有一个常用配置表我在实际部署里就是按这个思路整理的配置项样例值说明agent.idcode-agent-01Agent 唯一标识agent.addr10.0.8.21:8080对外访问地址agent.tagscode,longtext,lowload调度匹配标签agent.priority5优先级数值越大越优先agent.weight80负载权重影响得分agent.concurrency4最大并发任务数agent.health.endpoint/v1/health健康检查路径agent.health.timeout2s单次探测超时时间探活协议我设计成三层。第一层是 Liveness Probe它就是最基础的 TCP 和 HTTP 探测确认端口通着、进程活着。第二层是 Readiness Probe会向 Agent 的健康端点发一个带最小参数的请求比如GET /v1/health?probereadiness确认核心依赖都初始化完成。第三层是 Deep Probe这一步最重它会模拟调用 Agent 的一个核心能力接口传入一个固定的小任务看返回结果是否正确。出于性能考虑Deep Probe 不是每次都跑而是每隔一段时间抽样一次。正是通过这种三层探测Agent-Reach 才能把“进程活着”和“真正可用”区分开。Liveness 挂了会直接摘节点Readiness 挂了会降权Deep Probe 失败会触发告警并自动重复验证避免误杀。3. Agent-Reach 核心实现与关键代码3.1 探活评分机制的实现每个 Agent 的健康状态最终会落成一个分数范围是 0 到 100。分数不是简单的一票否决而是按权重叠加。基础分占 50 分只要 Liveness 和 Readiness 通过就能拿到成功率占 30 分根据最近 N 次 Deep Probe 的成功率线性换算响应时间占 20 分越快得分越高。我写这段代码的时候没有用什么复杂算法但是把“惩罚”处理得比较激进。连续两次失败会触发冷却冷却期内分数直接减半。这样做的原因是Agent 的故障往往不是瞬间恢复的刚恢复的节点如果立刻承担大流量很容易再次被打垮。下面是探活器的一个简化核心代码我用的是 Python httpx实际跑在生产环境的是异步版本# agent_reach/prober.py import asyncio import time import httpx class ReachabilityProber: def __init__(self, base_score50, success_weight30, latency_weight20): self.base_score base_score self.success_weight success_weight self.latency_weight latency_weight async def probe(self, node, client): score 0.0 try: # 第一层基础探测 resp await client.get(fhttp://{node.addr}/v1/health, timeout2.0) if resp.status_code ! 200: return 0.0 score self.base_score # 第二层模拟业务调用 deep_start time.time() deep_resp await client.post( fhttp://{node.addr}/v1/health/deep, json{probe: ping, payload: hello}, timeout3.0, ) latency_ms (time.time() - deep_start) * 1000 if deep_resp.status_code ! 200: return score * 0.3 # 业务探测失败保底分数 score self.success_weight * 0.9 # 第三层响应时间评分 latency_score max(0, self.latency_weight - latency_ms / 100) score latency_score return min(100.0, score) except Exception: return 0.0这段代码里值得注意的一个细节是Deep Probe 失败时我返回的不是 0而是基础分的 30%也就是 15 分。因为这时候进程和端口都活着Agent 可能只是暂时抖动没有必要马上摘除但分数已经低到不会被 Router 优先选中。这种“温水煮青蛙”的降级策略比一刀切更符合生产环境直觉。3.2 路由决策与降级逻辑Router 在收到任务时会从内存缓存里捞出一批候选 Agent先按可用性过滤再按得分排序最后还要做一个“代价核算”。代价核算不只是看健康分还要看目标 Agent 当前并发数、队列长度和权重。简单来说健康分决定一个 Agent 能不能被选上并发和队列决定它适不适合现在被选上。路由决策的代码我写得比较克制没有引入规则引擎。核心逻辑就是一个带权重的选择函数# agent_reach/router.py class AgentRouter: def __init__(self, registry): self.registry registry def route(self, task): candidates self.registry.get_available(task.tags) if not candidates: return self._degrade(task) candidates.sort(keylambda n: self._score_for_task(n, task), reverseTrue) chosen candidates[0] if chosen.health_score 40: return self._fallback_with_retry(task, chosen) return chosen def _score_for_task(self, node, task): affinity 10 if task.session_id node.session_hint else 0 concurrency_penalty node.current_concurrency / max(node.max_concurrency, 1) return node.health_score * 0.7 node.priority * 5 affinity - concurrency_penalty * 30这里我踩过一个坑一开始我把优先级加得非常高结果高优先级 Agent 哪怕状态很差、并发很高也会被选中低优先级 Agent 永远吃不到流量成了摆设。后来改成“健康分乘以 0.7 优先级固定加分 并发惩罚”才让高优先级 Agent 只有在状态足够好时才能胜出。这个权重配比不需要照抄但思路是对的调度不能只看静态优先级动态负载必须参与决策。降级逻辑我实现了三层。第一层是备胎节点如果任务标签有对应的备用 Agent就直接转过去。第二层是同类替代找健康分最高的非精确匹配 Agent。第三层是本地默认如果所有 Agent 都不可用就把任务放到一个死信队列等恢复后重放。死信队列这个设计帮我保住了不少重要任务否则故障期间直接丢任务后面补数据非常痛苦。4. 实操部署 Agent-Reach 并接入第一批 Agent4.1 Docker Compose 一键拉起全套服务Agent-Reach 的部署我尽量做得简单。整个框架可以拆成多个容器但单机测试时我会用一个 Compose 文件把 Coordinator、Prober、Router 都拉起来。下面是我实际在用的一个配置示例# docker-compose.yml services: reach-coordinator: image: agentreach/coordinator:0.4.2 container_name: reach-coordinator ports: - 8080:8080 environment: AGENT_REACH_STORE_DRIVER: file AGENT_REACH_STORE_PATH: /data/reach.db volumes: - ./data:/data networks: - reach-net reach-prober: image: agentreach/prober:0.4.2 container_name: reach-prober depends_on: - reach-coordinator environment: AGENT_REACH_PROBE_INTERVAL: 5 AGENT_REACH_PROBE_TIMEOUT: 3 AGENT_REACH_DEEP_PROBE_INTERVAL: 30 networks: - reach-net reach-router: image: agentreach/router:0.4.2 container_name: reach-router depends_on: - reach-coordinator ports: - 8081:8081 environment: AGENT_REACH_ROUTE_CACHE_TTL: 10 networks: - reach-net三个服务都挂在同一个桥接网络里Router 对外暴露 8081 端口给任务调度层调用。Prober 本身不暴露端口它就是个纯消费组件通过内部消息从 Coordinator 拉 Agent 列表再把探测结果写回去。这种拓扑的好处是接入方只需要跟 Router 打交道不需要知道探活器在哪、长什么样。启动之后我习惯先用一个简单请求验证链路通不通curl http://127.0.0.1:8081/v1/route \ -H Content-Type: application/json \ -d {task_type:summarize,tags:[longtext],session_id:abc123}如果返回了一个 AgentNode 对象说明 Router 已经从 Coordinator 拿到列表并把路由决策走通了。如果返回的是空列表那就先去 Coordinator 的注册接口查 Agent 是否真的登记成功。4.2 接入自定义 Agent 的配置模板每个 Agent 要接入 Agent-Reach不需要装 Agent 端 SDK只要满足两个条件提供一个健康检查端点以及按约定上报自己的元数据。元数据上报我支持两种方式一种是在 Agent 启动时主动调用POST /v1/agents/register注册另一种是 Coordinator 维护一个静态配置文件适合不方便改启动流程的 Agent。我一般建议用主动注册因为这样 Agent 可以顺带把自己的标签、最大并发数这些动态信息一起上报。注册接口的请求体大概长这样{ agent_id: summary-agent-02, addr: 10.0.8.25:9001, tags: [summarize, longtext, batch], priority: 4, weight: 60, max_concurrency: 2, health: { endpoint: /v1/health, timeout: 2 } }注册成功后Coordinator 会返回一个 tokenAgent 可以拿着 token 去主动更新自己的状态比如任务执行完回调POST /v1/agents/{id}/heartbeat来上报剩余并发数。如果不做主动更新也没关系Prober 会隔几秒扫一次只是主动更新的数据会更及时。我建议新接入的 Agent 先以“观察模式”跑一天也就是注册之后不实际接收路由流量只有探活和评分在跑。这样能看到这个 Agent 的基线健康分和响应时间分布再决定要不要给它加权重。我第一次接接入一个新写的 Agent 时太急直接给了最高优先级结果它因为内部一个缓存组件没用上每次调用都要重新算被打到超时整个链路都被拖慢。5. 常见问题与排障实录5.1 线上遇到的典型问题速查表跑 Agent-Reach 这三周我整理了一份高频问题的对照表。这些问题单独看都很基础但组合出现时非常迷惑值得存一份现象可能原因处置方案Agent 已注册但路由总落不到它标签匹配过严或健康分过低检查注册标签是否完整查看健康分趋势探活显示正常但调用仍然超时Deep Probe 没真正触发核心接口调整 Deep Probe 的请求体确保覆盖真实链路所有 Agent 都没了Coordinator 存储损坏或数据目录满检查本地文件权限启用备份恢复频繁在 Agent A/B 之间切换健康分处于临界值抖动触发调低探活频率增加连续成功/失败次数阈值高优先级 Agent 被选中后任务堆积并发惩罚权重不够提高并发惩罚系数限制最大并发数关于第一类问题我特别想强调一点路由落不到某个 Agent很多时候不是它不健康而是标签匹配太严格。我遇到过把tags写成精确字符串匹配实际传入的 tag 是数组结果永远匹配不上。后来我把匹配逻辑改成“只要候选 Agent 的标签集合包含任务所需标签即可”一下子就好了。5.2 一次典型的故障定位过程挑一个最有代表性的故障记录一下。某天下午调度层突然报了一堆agent unreachable错误但我登录 Prober 看日志发现所有 Agent 的 Liveness 探测都是成功的HTTP 状态码全是 200。第一直觉是 Router 的缓存出问题了于是去查路由表发现已经有好几个 Agent 被降级标记。继续追才发现Deep Probe 的成功率在五分钟内骤降。原因不是 Agent 服务挂了而是它们都依赖同一个外部模型网关网关那头被限流导致所有 Deep Probe 请求都超时。这解释了为什么端口通、状态码 200但可用性崩了。如果 Agent-Reach 只做基础探活这个问题根本发现不了任务会全部派过去然后集体超时。这次故障之后我调整了两个配置一是把 Deep Probe 的请求体改小降低它自身对依赖服务的压力二是给 Deep Probe 增加了一个“连续 N 次失败才降级”的窗口避免瞬时抖动触发全局降级。这个窗口很重要我建议至少设 3 次否则高峰期一个慢请求就会引发雪崩。6. 写给要复刻 Agent-Reach 的读者我个人在实际操作中的体会是Agent-Reach 这类可达性框架最难的不是代码而是指标口径的设计。你究竟拿什么指标判断一个 Agent 可用这个指标跟你真实业务场景到底匹不匹配如果只是照搬通用健康检查的套路那跟不用框架没有本质区别。有一个小技巧是我试过最有用的在接入一个新 Agent 类型时故意制造一次故障。比如把代理的某个核心依赖停掉然后看 Prober 的分数变化和 Router 的决策行为。如果 Agent-Reach 没有及时降分说明 Deep Probe 的请求没有真正覆盖核心链路如果分数降了但 Router 还是选中它说明路由权重配置有问题。这种“故障演练”我建议每周做一次比写再多文档都直观。另外Agent-Reach 的扩展方向其实很多。我目前只做了任务分发前的可达性判断下一步准备把 Agent 执行完之后的反馈也纳入评分体系。比如任务返回的结果质量很差或者执行时长异常这些信息如果能回流到健康分里Agent 的可用性画像会更立体。这个内容后续还可以加上“结果质量评分”这个维度让路由决策不再只看探活结果而是结合真实任务的完成质量来动态调整。如果你也在折腾多 Agent 系统不妨先拿我的这套思路和代码去改一版跑一周再回来说说遇到什么新坑。