Agent-Reach:轻量级多Agent协作框架的服务发现与语义路由设计

📅 发布时间:2026/10/7 11:48:14
Agent-Reach:轻量级多Agent协作框架的服务发现与语义路由设计
最近在折腾 Agent-Reach 这个项目起因很简单——我发现自己手上那批 AI Agent 单兵作战的效率实在太低了。这个项目说白了就是一套让 Agent 之间能互相发现、按能力触达、安全协作的轻量级框架核心解决多 Agent 系统里最常见的那几个坑服务发现、任务路由、结果回传。如果你在做智能运维、自动化编排或者正打算把 AI 能力组件化、让不同模型各自负责一块业务这篇文章就是我踩坑之后整理出来的完整复盘从设计思路到可运行代码都有照着抄能省不少时间。1. 为什么需要 Agent-Reach从一个真实场景说起1.1 单 Agent 的“能力天花板”在哪里先说一个反直觉的事实很多人以为“只要把 GPT 类模型接入系统它就能自动搞定一切”实际操作下来根本不是这么回事。单个 Agent 的能力天花板是真实存在的而且比你想象的低得多。这个天花板来自三个层面。第一层是工具调用的硬限制——一个 Agent 通常只接入了有限的 API、数据库和执行环境它拿不到其他业务系统的能力。比如我做一个负责网络排查的 Agent它能跑 ping、能查路由表但它没法直接调用数据库团队那个负责慢查询分析的 Agent。第二层是上下文窗口和记忆的瓶颈一个 Agent 一旦被塞进太多任务描述和历史记录推理质量会直线下降它不适合同时处理跨领域的一堆事。第三层是职责边界——单 Agent 什么都做意味着它什么都不精尤其在专业场景下让一个 Agent 兼做日志分析和故障自愈效果往往不如两个专业 Agent 协作。这时候大家通常会想那我把任务拆开不就行了对但问题来了——拆开之后谁来调度谁来发现另一个 Agent 是否存在任务结果怎么传回来这就是 Agent-Reach 要解决的事。1.2 多 Agent 协作的三大痛点发现、连接、信任我最早尝试的方案特别朴素把所有 Agent 的调用关系在代码里写死。比如 A 要调 B就直接在 A 的配置里写上 B 的 HTTP 地址。这个方案在只有两三个 Agent 的时候没问题一旦超过五个维护成本就开始失控。在实践中我总结出多 Agent 协作必须跨过三道坎第一道坎是“发现”。A 怎么知道当前系统里有哪些 Agent 在线每个 Agent 提供什么能力能力版本有没有变化如果 B 实例重启了、换端口了、升级了能力协议A 还拿着旧地址去连那就是事故。这道坎的本质是服务注册与发现但比传统微服务的服务发现多了一个维度——除了“谁在线”还要知道“谁能干什么”。第二道坎是“连接”。这里的连接不只是网络层面的连通更关键的是协议和消息结构。A 传给 B 的消息应该长什么样是 HTTP 回调、消息队列还是走共享存储任务上下文要不要带全貌还是只带引用连接方式决定了整个系统的耦合度和容错能力。第三道坎是“信任”。“信任”这个词在工程语境里听起来玄但它非常具体一个是鉴权另一个是任务合法性校验。任何 Agent 都能随意提交任务给其他 Agent这在本地做 demo 没事上了生产大概率会出事。需要至少在任务消息里带上来源标识、权限级别和调用链信息才谈得上可控。1.3 Agent-Reach 解决什么问题适合谁Agent-Reach 就是把上面三个痛点打包处理的一套轻量级框架。它的核心是一个“触达层”——让 Agent 不必关心对方是谁、在哪里、怎么调只需要按能力名提交任务框架负责把任务送到该去的 Agent再把结果原路传回。它可以部署在自己的业务网络里也可以嵌进已有的消息中间件体系。这个项目适配三类人第一类是在做智能运维平台、想把故障检测和自愈拆成多个 Agent 协作的第二类是业务系统里有多个 AI 功能模块比如客服、质检、数据分析希望它们能互相调用、而不是各做各的第三类是技术兴趣驱动、想理解多 Agent 架构里服务发现和路由到底怎么回事的开发者。如果你只是想调一个 API 做个 demo或者只有一两个 Agent 且永远不打算扩展那 Agent-Reach 的收益不明显用不上就别硬上。2. Agent-Reach 的整体设计核心模块与关键协议2.1 架构总览注册中心、触达路由、消息通道先看整体结构。Agent-Reach 由四个核心模块组成注册中心、触达路由、消息通道、Agent 执行器。开发之前我画过一版详细的分层设计图落地时可以按模块理解注册中心负责管理所有 Agent 的在线状态与能力清单。每个 Agent 启动后向注册中心登记自己的身份和能力之后持续发送心跳保活。注册中心相当于电话簿别人想找某个能力时先来这里查。触达路由接收上游提交的“能力调用请求”根据能力名、标签、优先级等条件从注册中心选出一个或一组目标 Agent再转发任务。消息通道负责任务消息的可靠传递、结果回传和异常事件上报。通道要保证消息不丢至少做到 at-least-once。Agent 执行器运行在 Agent 进程内负责接收任务、调用本地模型或工具链、把结果写回消息通道。执行器是每个 Agent 接入框架的“客户端 SDK”。部署形态上注册中心、路由和通道可以集中部署Agent 进程则分散在各自业务主机上。集中式的好处是逻辑简单、状态一致性好问题也明显——单点风险。所以我在设计里给注册中心加了一层本地缓存路由节点即使短暂连不上注册中心也能靠缓存的服务列表继续工作一段时间。2.2 触达协议从“点名调用”到“语义路由”Agent-Reach 的触达协议是整个项目的灵魂消息格式本身不复杂复杂的是消息背后的“路由语义”。协议把一个任务请求抽象成四段capability目标能力名比如 “mysql.slow_query_analyze”这是路由的主要依据。input_schema任务的参数结构包含字段名、类型、必填项。context调用上下文包括任务 ID、发起方身份、trace_id、超时时间。让下游 Agent 知道这是谁发起的、全链路追踪标识是什么。callback回传地址上游声明“你去哪把结果告诉我”可以是队列名也可以是回调接口地址。这里最关键的设计决定是路由不依赖 Agent 名字而是依赖“能力名”。换句话说调用方不用写死“调 B 这个 Agent”而是写“找一个能做慢查询分析的 Agent”。这种语义路由在多 Agent 系统里的价值在于解耦——B 下线了、换成了 C、或者新增了一个更强的 D调用方完全不用改代码。路由的匹配过程类似服务网格里的“基于标签的路由”先精确匹配 capability 名称如果有多个候选再根据标签比如 envprod、regioncn-east、健康状态、当前负载做选优。选不中则直接返回错误绝不把任务硬塞给不匹配的 Agent。2.3 关键选型轻量级实现 vs 重框架为什么这样选在技术选型时我面临一个经典对比自研轻量级框架还是直接用市面上已有的多 Agent 编排框架市面上的重框架确实强大自带复杂的状态机、人机协同面板、会话记忆管理层……但对我来说有三个致命问题第一是学习曲线陡峭想改一个路由逻辑得先读懂它的抽象概念第二是定制成本高公司的技术栈和数据面不一定跟它契合第三是运行资源开销大很多框架为了编排能力牺牲了轻便性不适合低配服务器。Agent-Reach 选择轻量级自研本质上遵循“够用就好”的原则。它只做了三件必要的事服务注册、语义路由、消息传递。没有花哨的可视化没有自带的模型网关也没有强上 WebSocket 实时同步。它可以配合已有的监控体系、消息中间件和权限系统插进现有架构而不是反过来要求架构适配它。我整理了一个对比表方便你根据自己的场景判断维度Agent-Reach轻量级自研重框架式编排总线部署复杂度和资源占用低一个路由节点 注册中心即可高依赖独立存储与多组件路由灵活性能力名 标签匹配规则可配置依赖框架内置 DSL定制成本高与现有技术栈融合能力强消息队列、数据库均可复用弱迁移成本大适合场景已有明确业务系统需要将 AI 能力协作化从零搭建全托管多 Agent 平台我的结论是如果你在已有业务网络里做多 Agent 协作轻量级自研框架的性价比要高得多如果你要做一个独立的大型多 Agent 产品那才值得考虑重框架。3. 从零实现 Agent-Reach一个可跑通的轻量级框架3.1 前置准备与技术栈选择我直接说实际用的技术栈Python 3.10 Redis FastAPI Docker。Redis 承担注册中心存储和消息通道两个角色FastAPI 暴露路由、注册和回调接口Agent 执行器是一个 Python SDK内置心跳线程和任务处理循环。有人可能会问为什么不用 Kafka理由很简单——对于一个几十个 Agent 规模的项目Kafka 的部署和运维成本远超收益Redis Streams 已经足够支撑“生产者-消费者”模式下的任务分发和回传。用 Redis 另一个好处是所有人都熟悉排错和开发门槛都低。部署上我把 Redis 和路由节点用 docker-compose 起在同一内网Agent 则部署在各自业务主机上。下面给出 Redis 部分的核心配置services: redis: image: redis:7-alpine container_name: agent_reach_redis command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} ports: - 6379:6379 volumes: - redis_data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 5s retries: 3注意几个细节一定要开启 appendonly否则 Agent 注册信息和任务记录重启就丢requirepass 必须设因为 Redis 网络暴露后是内部系统被攻击的重灾区healthcheck 不是摆设编排依赖它保证启动顺序。3.2 第一步实现 Agent 注册与心跳注册中心是所有 Agent 的“户口本”。每个 Agent 启动后会向注册中心写入一条记录包含 agent_id、能力名、标签、运行时信息。数据模型我用 Redis Hash 存储key 是agent:{agent_id}field 是各项元数据。注册接口我设计成幂等的Agent 重复注册不会报错只会更新元数据这样重启和滚动更新会很从容。核心代码如下# registry.py import json import time import redis REDIS_POOL redis.ConnectionPool(hostlocalhost, port6379, passwordyourpass) R redis.Redis(connection_poolREDIS_POOL) def register_agent(agent_id: str, capabilities: list[str], tags: dict, ttl: int 30): agent_key fagent:{agent_id} pipe R.pipeline() for cap in capabilities: pipe.sadd(fcapability:{cap}, agent_id) pipe.hset(agent_key, mapping{ agent_id: agent_id, capabilities: json.dumps(capabilities), tags: json.dumps(tags), registered_at: str(int(time.time())), }) pipe.expire(agent_key, ttl) pipe.execute() return Truettl 是心跳机制的关键。Agent 每 10 秒发送一次心跳每次心跳都对 agent_key 做 expire 续期ttl 设为 30 秒。这样只要 Agent 进程崩溃或网络隔离注册中心最多 30 秒就能把它的信息清掉不会出现“僵尸 Agent 占着能力名不放”的情况。心跳接口就更简单了本质就是“续命”def heartbeat(agent_id: str, ttl: int 30): return R.expire(fagent:{agent_id}, ttl)这个设计的核心思路是“软状态 租约”。它不需要注册中心主动去探活也不需要 Agent 下线时精细地推送离线消息——一切靠过期时间自然收敛。省掉大量分布式一致性代码这正是轻量级框架该有的克制的智慧。3.3 第二步实现触达路由与任务分发路由是 Agent-Reach 的大脑。调用方提交一个任务请求路由进程根据能力名去 Redis 里找到所有候选 Agent然后按策略挑一个把任务推送到对应 Agent 的消息队列。我用 Redis 的集合capability:{能力名}存储候选 Agent ID路由选择时先取集合再逐个检查它们的 agent_key 是否还活着这一步过滤掉刚过期但集合还没来得及更新的情况最后从存活列表里随机选一个实现简单的负载均衡。任务分发的核心代码如下# router.py import json import uuid import redis from registry import R def dispatch_request(capability: str, payload: dict, requester: str, timeout: int 30): agent_ids list(R.smembers(fcapability:{capability})) healthy [] for aid in agent_ids: agent_key fagent:{aid} if R.exists(agent_key): healthy.append(aid) if not healthy: return {ok: False, error: fno_available_agent: {capability}} task_id str(uuid.uuid4()) target healthy[0] msg { task_id: task_id, capability: capability, payload: payload, requester: requester, timestamp: int(time.time()), timeout: timeout, } # 推送任务到目标 Agent 专用队列 R.xadd(fqueue:{target}, msg) return {ok: True, task_id: task_id, target: target}这里有个很容易被忽视的问题当capability对应的集合里混入多个版本的 Agent比如老版本只支持输入一个 list新版本支持 dict那么单纯靠能力名匹配会出错。所以更严谨的做法是在注册时额外登记capability_version路由选择时先按版本过滤。我建议你在设计输入协议时从一开始就加入 version 字段否则将来迭代时路由表会变成一团乱麻。消息通道用 Redis Streams 的xadd实现。相比老旧的lpush brpop方式Streams 自带消息 ID 和消费者组语义方便做 ACK 和回溯查询。3.4 第三步实现结果回传与失败重试任务发出去只是开始更麻烦的是接管结果和异常。Agent-Reach 采用“队列回传 回调钩子”的双通道模式每个 Agent 处理完任务后把结果写入以发起方 task_id 命名的回传队列同时如果调用方注册了 callback 地址也会收到一次 HTTP 回调。Agent 执行器内部的任务循环大致是# agent_worker.py import json import time import redis from registry import R def process_on_queue(agent_id: str, handler): stream_key fqueue:{agent_id} last_id 0-0 while True: entries R.xread({stream_key: last_id}, block5000, count1) if not entries: continue for _, messages in entries: for msg_id, fields in messages: task fields try: result handler(task.get(payload)) # 回写结果 R.xadd(ftask:result:{task[task_id]}, { agent_id: agent_id, status: success, payload: json.dumps(result), }) except Exception as exc: R.xadd(ftask:result:{task[task_id]}, { agent_id: agent_id, status: failed, reason: str(exc), }) last_id msg_id失败重试我放在路由侧而不是 Agent 侧。路由发现任务超时或回传队列里出现 failed 状态时会重新发起一次调度但最多重试 3 次且只重试“幂等类任务”——比如查询分析、数据汇总绝不重试扣费、下单之类的非幂等动作。为什么重试次数定为 3这是我实测后选的数字。一次重试在故障场景下成功率提升最明显两次能覆盖大部分瞬时抖动三次以上边际收益趋近于零反而可能触发“Agent 风暴”下面会细说。另外重试间隔采用 5 秒、15 秒、45 秒的指数退避避免多个任务同时重试把目标 Agent 打垮。3.5 多 Agent 之间如何安全传递上下文这里额外补充一个我在生产环境里踩出来的经验多 Agent 协作最危险的不是任务找错人而是上下文不分家。所谓“上下文不分家”是指 A 调 B 时B 只知道 A 丢过来的片段不知道整个事情的来龙去脉。比如 A 在做故障自愈先让 B 分析根因再让 C 执行回滚。如果 B 和 C 拿到的都是孤立数据C 就可能在不恰当的条件下执行危险操作。Agent-Reach 解决这个问题的方式是引入一份轻量的 shared_context 对象它是一个透明传引用的 KV 存储# context.py class SharedContext: def __init__(self, ctx_id: str): self.ctx_id ctx_id self._data {} # 生产中替换为 Redis Hash天然支持跨进程 def add(self, key: str, value): self._data[key] value def get(self, key: str): return self._data.get(key) def fork(self, child_id: str): 子任务拿到父任务的引用但写入只在子集生效 child SharedContext(child_id) child._data.update(self._data) return child路由分发任务时只把ctx_id传给下游 Agent而不是把整个上下文塞进消息体。下游 Agent 需要什么信息按需从共享上下文里拉取。这样有两个好处一是消息体不会无限膨胀二是关键数据只保留一份权威副本不会出现多份各改各的脏数据。在实际场景里我会在故障自愈链路中让根因分析 Agent 把“疑似故障模块”“证据链”写进共享上下文回滚 Agent 读取这些字段后再决策。这样每一步操作都有前置条件整个协作文档了。4. 常见问题与排查技巧实录4.1 服务发现断连心跳超时参数怎么调第一个常见问题是 Agent 频繁掉线明明进程活着路由却报“no available agent”。这里十有八九是心跳参数和网络状况不匹配。我最初的配置是心跳间隔 10 秒、ttl 30 秒在局域网内跑得很稳。后来某个 Agent 部署到了跨机房环境网络延迟偶尔飙到几百毫秒心跳偶尔丢失ttl 30 秒还够用。但如果你的 Agent 网络波动更频繁建议直接把 ttl 调到 60 秒心跳间隔保持 10 秒不变这样即使连续丢 5 次心跳也不会误杀。排查方法也很简单看 Redis 里的TTL agent:{agent_id}如果 ttl 一直在 30 以下反复横跳说明心跳在正常续期如果 TTL 变成 -2说明 key 已过期Agent 的注册信息掉了优先检查 Agent 和 Redis 之间的网络连接。这里有个“过度续期”的隐性坑ttl 设到 120 秒以上会导致一个 Agent 崩溃后它的能力要等 2 分钟才从路由候选里消失。期间新任务会被路由到这个死 Agent 上全部超时。所以 ttl 不是越大越好找到“容忍短暂抖动”和“快速摘除死节点”的平衡点才是调参的核心。4.2 任务重复执行幂等设计的坑讲到失败重试就必须讲幂等。这是我在生产上栽得最惨的一次某个数据清洗 Agent 收到任务后处理到一半 Redis 连接超时任务状态没有及时回传。路由判定超时重试了一次。结果这次连接恢复了Agent 又把同一批数据清洗了一遍直接导致下游报表数据翻倍。要避免这种事故路由层必须在重试时带上明确的幂等键。我的方案是在任务消息里增加idempotent_key字段Agent 接收到任务后先在 Redis 里用SETNX抢锁能抢到才执行抢不到则直接返回“任务已在处理中”def acquire_task_lock(task_id: str, agent_id: str, ttl: int 60): lock_key ftask_lock:{task_id} acquired R.set(lock_key, agent_id, nxTrue, exttl) if acquired: return True current R.get(lock_key) # 如果锁还没过期但执行 Agent 已不在线主动接管 if current and not R.exists(fagent:{current}): if R.getset(lock_key, agent_id) current: return True return False这个方案的好处是即使任务被重复投递最终也只会被一个 Agent 实例真正执行。坏处是引入了一个锁组件需要关注锁的过期时间是否覆盖任务的最长执行时长。否则任务还没跑完锁就过期了另一个重试又进来了。4.3 “Agent 风暴”循环调用与消息风暴多 Agent 系统里最让我害怕的现象是“Agent 风暴”也就是 Agent 之间的调用形成了循环A 调用 BB 调 CC 发现缺数据又调 A……然后整个系统的消息数量指数级增长几秒钟就能把 Redis 内存打爆。Agent-Reach 目前用三重手段防暴第一重是调用深度限制。每个任务消息里带上depth字段初始值为 0每经过一次路由加 1。当 depth 超过 10 时直接拒绝并返回max_depth_exceeded错误。10 这个数字来源是我审查了所有可能的调用链路最长的一条正常路径是 6 层留出 4 层余量。第二重是全链路频率控制。用 Redis 的 INCR 记录同一 trace_id 在 1 秒内发起的调用次数超过 50 次直接熔断。这个 50 次看起来很随意但它是我在压测环境里用“最激进合法场景”模拟出来的30 个 Agent 同时处理任务时1 秒内原本就应该有几十次调用阈值设得太低会误伤正常请求。第三重是队列积压告警。定时扫描 Redis Streams 的长度一旦单个 Agent 的队列里积压超过 5000 条消息就触发人工介入而不是放任路由继续向这个 Agent 塞任务。这个机制配合 Redis 自带的XINFO STREAM命令能很快定位到“哪个 Agent 是风暴的汇聚点”。4.4 任务超时与结果迟到的排查路径最后一个频发问题是“任务超时但 Agent 显示执行成功了”。这个现象背后通常是两个原因一个是任务的实际执行时间超过了路由层的超时阈值另一个是 Agent 处理完任务后回传消息在通道里排队迟迟没能回到路由。排查路径我固定按三步走先看 Agent 日志里任务处理完成的时间点确认是不是执行本身就慢再查 Redis 回传队列的长度和消费者状态确认回传是否被堵最后才怀疑路由层超时设置太激进。很多时候是业务方盲目把超时时间设成 10 秒但下游 Agent 要调外部接口做重计算实际需要 60 秒。建议超时设置前先做一次冷启动压测拿到真实执行时间后加 50% 冗余。如果你遇到的结果异常准确率高、但偶发迟到的场景我还会做个辅助动作在 Agent 处理完任务、准备回传的代码前后各打一行带时间戳的日志。这样能精确区分“执行慢”和“回传慢”不用靠猜。4.5 Agent-Reach 还能怎么演进根据我这几个月的使用体会Agent-Reach 目前最大的短板是缺少能力协商机制。现在调用方按能力名提交任务但如果目标 Agent 上线后能力参数变了调用方是不知道的。下一步我准备在注册信息里增加一份 JSON Schema 格式的能力契约路由层负责在分发前做入参校验不匹配直接拒绝。这本质上是把“编译期类型错误”提前到“路由期拦截”。另外一个很值得做的方向是动态标签路由。当前标签是 Agent 自己上报的但如果在部署时由运维平台统一注入比如regioncn-east、envprod路由就能实现“同一套代码在不同环境自动选不同 Agent”这对于多环境隔离特别有用。最后说说这个项目的意义。我在实际部署 Agent-Reach 之前也犹豫过自研是不是重复造轮子但做完之后我认为这套轻量级架构的价值恰恰在于“把选择权留在自己手里”。多 Agent 协作的难点不在于代码而在于你对业务链路的理解——能力怎么划分、上下文怎么流转、失败怎么兜底这些问题想透了代码反而是最简单的部分。如果只把 Agent-Reach 当一个现成框架来用它帮不了你太多如果你把它当作一个梳理多 Agent 协作逻辑的工具它的设计会逼着你想清楚每一层依赖关系。我个人更推荐后一种心态去用它。