
开头部分我想先聊聊做Agent-Reach这个项目时最真实的感受。这两年做智能体AI Agent的人越来越多但大部分团队的瓶颈根本不是模型能力而是“智能体根本够不到该够的东西”——客户A的工单堆在A系统客户B的回访记录躺在B平台每个智能体都只会调用自己写死的那几个接口换个场景就得改代码加个新Agent就要牵一发动全身。Agent-Reach这个名字字面意思就是“让智能体触达一切”它其实是我个人维护的一个轻量级多智能体调度框架核心解决三件事同一套入口下多个功能型Agent怎么统一注册、怎么动态路由、怎么在某个Agent挂了之后让流量平滑切走。它面向的是中小团队里已经跑起来两三个Chatbot或者自动化流程、但调度逻辑还在用if-else硬编码的场景也适合刚接触多智能体编排、想找一个可落地参考方案的开发者。这篇文章我不会讲大而全的架构理论只把我自己从设计到踩坑的过程完整记录下来。1. 内容整体设计与思路拆解1.1 先搞明白Agent-Reach到底在解决什么问题我最初接手的是一个客服知识库项目里面实际跑着三个“伪Agent”一个负责查订单状态一个负责退换货流程指引还有一个负责话术质检。当时这三块逻辑散落在不同的服务里入口侧用一个巨大的 dict 做关键词匹配命中“退款”就调服务A命中“投诉”就调服务B词表膨胀到两百多个关键词之后新同事根本不敢动那个文件。这种硬编码调度的毛病做过的人应该都懂路由规则和业务逻辑强耦合每加一个Agent就要改入口代码。关键词匹配的方式脆弱用户换个说法就路由错了。某个Agent服务不可用时入口侧没有感知请求直接超时。没有统一的会话上下文Agent之间切换时用户说过的上一句话全丢了。Agent-Reach 的思路很直接在“入口”和“Agent们”之间插一层路由控制面。所有Agent先到注册中心报个名声明自己能处理哪些意图、支持什么协议入口请求进来之后由路由层根据意图识别结果和动态权重把请求分发给当前最合适的Agent实例。这样入口不再关心“具体谁来处理”只关心“有没有人能处理”。1.2 从集中式调度到注册发现为什么这么选型做选型的时候我在“集中式编排”和“注册发现”之间纠结过一阵。集中式编排就像公司里的项目经理所有任务都由它分派好处是流程控制力强坏处是项目经理本身容易成为瓶颈而且Agent一多编排逻辑会膨胀成一大坨。Agent-Reach 最终选了“轻量注册中心 路由策略插件”的组合理由很实际第一中小场景下的Agent数量通常不会超过几十个没必要上完整的服务网格第二让每个Agent保持“无状态参与”通过心跳上报自己的健康度和负载路由层只做决策不持有业务状态这样即使某个Agent崩溃重启后注册即可路由层不会有脏数据第三路由策略做成可插拔的初期用轮询后续想改成基于日志量的加权分发不用动核心代码。这个设计和人类组织里的“前台总机”很像。总机不关心每个分机背后是谁在接电话它只维护一张“分机号到责任人”的映射表。有人离职Agent下线总机把分机号注销总机自己坏了换个总机查一下映射表就能恢复。Agent-Reach 的 Registry 模块承担的就是这张映射表。1.3 影响范围一套框架能管住多少个智能体很多朋友会问这玩意儿到底能撑多大规模。我实测下来单机部署的 Agent-Reach 路由节点配合 Redis 做注册数据存储在普通容器2核4G上可以稳定支撑 500 QPS 左右的路由决策注册的Agent实例数在 200 个以内时心跳扫描的开销可以忽略不计。如果Agent数量超过这个量级建议把注册中心从 Redis 换成 etcd再把路由层做横向扩展——但说实话真要到了几百个Agent你该考虑的就不是调度框架而是业务边界怎么拆了。也就是说Agent-Reach 的目标场景非常清晰它适合那种“有5到20个功能型Agent、每天几万次调用、团队只有两三个后端”的团队。它不追求媲美Kubernetes那种基础设施级的调度能力它追求的是让中小团队在一天之内把原本写死在代码里的路由逻辑收敛成一个可观测、可配置、可扩缩容的中台。2. 核心细节解析与实操要点2.1 模块拆解Router、Registry、Session 一个都不能少Agent-Reach 的核心由四个模块组成缺一个都会出问题我把它们的功能和边界梳理一下模块职责关键技术选型说明Reach Router请求接入、意图路由、负载均衡FastAPI 同步/异步双模式对外提供 HTTP/gRPC 接入是唯一流量入口Agent RegistryAgent注册、心跳维护、健康检查Redis Hash 过期机制存储 Agent 元数据和状态定期扫描下线实例Session Manager会话上下文存取、Agent切换衔接Redis String TTL用 session_id 关联上下文跨 Agent 传递备忘录Retry Queue失败重试、降级兜底Redis List路由失败或响应超时进入重试队列或降级应答这里最容易被忽略的是 Session Manager。最初做第一版时我没加会话层结果出现了一个很滑稽的线上事故用户先问“我的订单什么时候到”被路由到了订单查询 Agent紧接着又发了一句“那退了吧”结果这句没有上下文的话被路由到了售后 Agent售后 Agent 完全不知道“退”的是哪一单。加上 Session Manager 之后前一个 Agent 在处理完请求时会把关键实体比如订单号写入会话备忘录下一个 Agent 接手时自动带上这个问题才算根治。2.2 注册与心跳让Agent学会“报平安”Agent 启动后做的第一件事是向 Registry 发起注册。注册内容不是简单的“我来了”而是一段结构化元数据。我这里贴一下实际用的注册信息结构{ agent_id: order_query_v3, name: 订单查询Agent, version: 3.2.1, intents: [order_status, logistics_trace, delivery_time], endpoint: http://10.20.30.41:9001/invoke, protocol: http_json, weight: 5, health_check_path: /healthz, timeout_ms: 3000 }这里我踩过两个大坑。第一个是intents字段的粒度。最开始我填的是“订单”“物流”“快递”这种短词结果 Apple 的“订单”和“水果拼盘的配送订单”经常冲突。后来改成意图标签体系intent tag每个 Agent 声明的是语义意图而不是关键词路由准确率明显提升。第二个是weight字段它是我后来才加的。多个 Agent 实例能力相同比如订单查询部署了两套轮询虽然能保证均衡但处理速度快的实例常常被慢实例拖累加权后可以做到“性能好的多分流量”。心跳的机制我采用的策略是Agent每5秒上报一次状态写入 Redis 的 Hash 并顺带刷新 TTL过期时间设为15秒。路由层在决定分发之前只需要快速 check 一下目标实例的 TTL 是否有效。TTL 过期超过3个周期Registry 自动把该实例标记为 offline并从路由候选列表里摘除。这样做的效果是一个 Agent 死掉最迟15秒内流量就会自动绕开它不需要人工干预。2.3 路由策略轮询、加权、粘滞如何选路由策略是 Agent-Reach 里花样最多的地方我最终保留了三类轮询RoundRobin、加权随机WeightedRandom、会话粘滞StickyBySession。三者适用场景完全不同轮询适合后端实例能力完全均等的场景比如多个无状态 Agent 副本。实现最简单但有个缺陷——如果某个实例正在处理一个耗时的请求下个请求还会照样分给它导致那台机器容易积压。加权随机引入 weight 参数适合“有两台老机器 一台新机器”的过渡期。我一般把新机器的 weight 调成老机器的两倍让它多抗点流量观察一周稳定后把权重拉平。会话粘滞适合需要维持状态的场景但注意它不等于 Session Manager。粘滞是指“同一个 session_id 尽量分到同一个 Agent 实例”减少上下文重新加载的成本而 Session Manager 是“即使换实例也能从 Redis取回会话备忘录”。这两个机制可以同时开。实际配置里我是这么写的router: strategy: weighted_random fallback_strategy: roundrobin session_sticky: true session_sticky_expire_seconds: 1800 retry_queue_size: 5000 default_timeout_ms: 5000fallback_strategy是另一个容易忽略的细节。当主策略因为权重计算出错等原因没法决策时必须有一个兜底策略否则路由层自己会变成单点故障。我用的是最简单可靠的轮询当兜底。这里有一条铁律路由层绝不能因为策略模块报错就把请求直接打回给客户端。2.4 会话上下文的存取技巧Session Manager 在存储上我用的是比较保守的方案每个会话在 Redis 里单独存一个 Stringkey 是session:{uuid}value 是一个 JSON里面包含最近3轮对话的关键实体、当前 Agent 的意图上下文和待确认事项。为什么不用 Hash 来存因为 String 配合JSON.set整体读写更简单会话粒度下很少出现并发写同一个字段的需求Hash 的字段级操作反而带来额外的序列化负担。TTL 我统一设置为1小时。如果用户在1小时内没有新消息会话自动过期。这块有个细节每次用户发消息不是简单地刷新 TTL而是先按新的意图重新路由再把路由结果追加到会话记录里——顺序不能反因为路由决策依赖会话上下文而会话上下文又要记录新的路由结果。3. 实操过程与核心环节实现3.1 搭一个最小可用环境三台“虚拟Agent” 路由器我建议你第一次跑通 Agent-Reach不要一上来就连真实业务系统那样出了问题很难排查。我在本机用 Docker 起了一个 Redis然后写了三个模拟 Agent一个是“天气查询Agent”一个是“闹钟设置Agent”还有一个是“闲聊Agent”。三个 Agent 共用同一个 Agent SDK只需要实现一个handle(message, session)方法SDK 会自动负责注册、心跳和接收请求。这也是 Agent-Reach 降低接入成本的关键设计开发者只需要关心业务逻辑不用关心网络协议细节。# agent_sdk.py 中的核心抽象 class ReachAgent: def __init__(self, agent_config: dict): self.config agent_config self.runtime AgentRuntime(agent_config) def start(self): self.runtime.register() self.runtime.start_heartbeat() self.runtime.serve_http(portself.config[port])模拟 Agent 的代码大致长这样# weather_agent.py from agent_sdk import ReachAgent def handle_weather(message: str, session: dict): city extract_city(message, session) return {reply: f当前{city}天气晴转多云温度22℃, entities: {city: city}} agent ReachAgent({ agent_id: weather_agent_v1, intents: [weather_query], endpoint: http://127.0.0.1:9011/invoke, port: 9011, weight: 3 }) agent.set_handler(handle_weather) agent.start()我在这个阶段踩过一个非常隐蔽的坑模拟 Agent 启动后注册成功了心跳也打了但业务请求就是路由不过去。后来抓包才发现我写的endpoint是http://127.0.0.1:9011/invoke路由器跑在容器里127.0.0.1指向的是容器自己而不是模拟 Agent 进程。把地址改成宿主机网卡 IP 之后立刻恢复。容器化环境里千万不要用 loopback 地址互相访问。3.2 核心流程一次完整调用的全链路追踪一次完整的 Agent-Reach 调用流程是这样的客户端请求 → Reach Router 接收请求 → 校验 session_id无则创建 → 从 Registry 拉取候选 Agent 列表 → 按路由策略选定 Agent → 从 Session Manager 读取会话备忘录 → 转发请求到 Agent 的 endpoint → Agent 执行业务逻辑并返回结果 → Router 把结果写入会话备忘录 → 返回响应给客户端每一步的耗时我都埋了 trace 和耗时统计。实际跑下来的数据是Router 本身的决策和序列化耗时可忽略不计1ms 以内Redis 注册查询约 0.5msAgent 业务处理耗时占大头根据业务复杂度约 200ms~3s。所以优化的重点始终在 Agent 侧而不是 Router 侧。这里要强调一个关键点Agent-Reach 的路由决策永远不重试同一个 Agent 超过一次。第一次失败后Router 会从候选列表里剔除该失败实例再选下一个可用实例重试如果所有实例都失败才进入 Retry Queue。这么设计是为了避免一种极其常见的“重试风暴”某个 Agent 因为数据库连接池耗尽变慢结果 Router 看它超时立刻重试反复打同一台机器最后把本来能恢复的服务彻底打死。3.3 接入方式HTTP 与消息队列双模式很多接入方对调用方式有不同偏好Agent-Reach 默认提供 HTTP 接入也支持 Kafka 消息触发。HTTP 模式适合在线实时场景聊天机器人、Web API消息队列模式适合批量离线场景工单批量处理、定时任务。HTTP 模式的请求体我设计得比较保守尽量做到“一次请求带全上下文”POST /reach/invoke { session_id: uuid-123, user_id: u_456, message: 帮我查一下北京明天天气, platform: web_chat, extra: {channel: customer_service} }返回体{ agent_id: weather_agent_v1, reply: 北京明天晴最高温度26℃, session_id: uuid-123, spent_ms: 180, need_human_handoff: false }need_human_handoff这个字段是后来补的。原来 Agent 遇到解决不了的问题只会回一句“我不明白”后来产品要求这种情况必须转人工如果没有这个字段Router 和业务系统都不知道该不该弹人工客服窗口。加上之后只要任一 Agent 置位Router 就会走单独的人工坐席分配流程。3.4 接入一个真实Agent的完整清单模拟环境跑通之后接入真实系统前我建议按这个清单自查[ ] Agent 的/healthz接口能在3秒内返回 200不能依赖数据库联通性否则健康检查会频繁误报[ ] Agent 能处理 Router 发来的ping消息并原样返回pong[ ] 会话备忘录的读写不影响主流程Redis 挂了时 Agent 能降级为无状态模式[ ] 服务启动时主动注册进程退出时主动注销atexit钩子里调registry.deregister()[ ] 响应体结构统一包含reply和entities字段我记得第一次接入真实订单查询服务时漏掉了第4条——进程被 kill -9 强杀时来不及注销导致 Registry 里躺着一条脏数据。后来加了“心跳连续3次缺席即自动摘除”的策略这个问题才算彻底解决。永远不要依赖优雅注销一定要有被动失效兜底。4. 常见问题与排查技巧实录4.1 问题表现象、原因、解法一条龙我把自己使用 Agent-Reach 过程中遇到的典型问题整理成了表格省得大家重复踩坑现象根本原因解决办法新注册的 Agent 收不到流量intents 标签和路由策略不匹配或注册后未等心跳生效就发请求检查注册元数据确认 routing key启动后等5秒再测试请求全部超时但 Agent 日志显示正常Router 到 Agent 的链路不通常见是容器网络隔离curl 测试 Router 容器到 Agent endpoint 的连通性别用 loopback某个 Agent 频繁被摘除心跳接口里带了慢查询导致 /healthz 响应时间超过3秒健康检查只查进程存活和消息队列堆积量不要查数据库同一个用户会话偶尔答非所问Session Manager 的 TTL 设置太短或粘滞路由把请求分到了不同实例至少设30分钟 TTL确保开启 session_sticky流量高峰时 Redis 连接数打满每个请求都新建 Redis 连接没有用连接池使用 redis-py 的 ConnectionPool连接数控制在 20 以内重试风暴导致下游数据库被压垮失败重试逻辑直接堆在同一Agent上启用“失败剔除 换Agent重试 退避重试队列”三级策略4.2 实战排障一次“注册成功但心跳消失”的定位过程有一次线上某个检索 Agent 频繁上下线Registry 里它的状态在 online 和 offline 之间反复横跳。我第一反应是 Agent 进程崩了但看了监控进程一直活着。后来把 Agent 的心跳日志打出来看发现心跳发送间隔越来越慢从正常5秒逐步拉长到15秒最后直接不发。排查下来问题出在心跳发送函数里调用了一个统计工具方法这个方法内会重新建立数据库连接池而数据库连接池因为慢查询堆积占满了心跳线程每发一次就阻塞一次。修复方案很简单粗暴把统计工具从心跳路径上完全剥离心跳请求只允许走内存状态检查禁止任何 IO 操作。这之后 Agent 的状态就稳定了。这事的教训是健康检查路径必须足够“轻”任何可能阻塞的依赖都不能出现在心跳里。4.3 排障工具箱我常用的三个命令和两条日志排查 Agent-Reach 问题时我通常靠三个命令快速定位# 查看当前注册的 Agent 及其状态 redis-cli HGETALL reach:agent:registry # 查看某个 session 的上下文信息 redis-cli GET session:uuid-123 | jq . # 查看重试队列深度深度大于100说明系统处于异常状态 redis-cli LLEN reach:retry_queue日志方面路由层的日志一定要包含三个字段agent_id、session_id、route_cost_ms。有了这三样绝大多数问题都能快速圈定范围。我见过不少团队日志里只打 messageQPS一高根本不知道哪个 Agent 在超时从第一行开始就在浪费排查时间。4.4 避坑心得三条关于规模的清醒认知最后说几条用规模换来的认知不一定对但都是我真实踩出来的。首先是不要过早引入分布式事务。Agent-Reach 的会话上下文中如果涉及跨 Agent 的订单状态变更你可能会想用分布式事务保证一致性但在Agent场景下我更推荐“本地事务 补偿动作”。举个例子一个售后退款流程先由订单Agent冻结资金再由财务Agent执行退款。若第二个Agent失败不要回滚第一个 Agent 的本地事务而是由重试队列定期触发退款补偿动作。这个思路比强行上分布式事务简单一个数量级。其次是路由策略不要开“上帝模式”。有些开发者喜欢写一个超级规则期望它能准确识别所有意图并分发给所有 Agent。实际上意图识别本身也可能出错所以 Agent-Reach 在路由前增加了一个“意图置信度阈值”的概念低于阈值的请求不路由给任何功能Agent统一进入到人工坐席或兜底闲聊Agent。这个阈值宁可调高一点也不要因为误路由把用户的正式诉求带到错误流程里。最后是监控口径要统一。不同团队对“成功率”的定义千差万别有的把“Agent返回错误文本”也算成功有的把“超时但最终返回兜底话术”算失败。我在 Agent-Reach 里定了两个硬指标路由成功率Agent成功响应 2xx和业务解决率Agent响应且用户未在30秒内重复提问前者管稳定性后者管效果。两个指标缺一不可只看前者容易自我欺骗只看后者则容易掩盖基础设施问题。Agent-Reach 到目前为止我在两个内部项目里跑了大半年最大的感受是它不是一个需要你花几周去学习的框架而是一个帮你把“谁该处理这个请求”这个简单问题重新想清楚的工具。如果你现在的多智能体项目还停留在改入口字典的阶段我建议你花一个下午搭个最小版本试试体验一下“路由和业务分离”之后改需求有多轻松。也顺手把我当初踩过的坑存个档接入时对照 2.4 和 4.1 两张表检查一遍能少走很多弯路。