☰
多智能体系统调度实战:Agent-Reach注册路由与执行网关设计
2026/10/6 14:43:52 网站建设 项目流程

最近半年我和团队一直在做多智能体系统的产品化落地,项目名叫 Agent-Reach。它是我们内部用来解决一个老大难问题的中间层:当 AI 智能体数量从两三个涨到十几个,分属不同小组维护、接口风格各异、能力边界模糊,调度逻辑变成一团乱麻时,你根本不知道该找哪个智能体干活。Agent-Reach 的核心思路很直接——把智能体之间的调用关系从硬编码的 if/else 里解放出来,做成一个带注册中心、路由引擎和执行网关的调度框架。这篇文章我会把整个项目从设计到踩坑落地的过程完整拆出来,包括能力描述怎么写、语义匹配怎么做、超时降级怎么配、性能和成本怎么控,希望对正在做类似多智能体系统的团队有点参考价值。


1. 当智能体数量超过五个,团队就开始抓狂

1.1 我遇到的实际问题

最开始我们其实没有多智能体,只有一个客服问答机器人。后来业务方陆续提出新需求,团队就顺手加了几个独立智能体服务:文档摘要、舆情监控、报表生成、运营分析。半年后数了数,线上已经有 9 个用不同技术栈实现的智能体,Python FastAPI 的、Node.js 的、还有两个是一个 LangGraph 工作流直接暴露成 HTTP 服务的。每个服务都有自己的一套 API 风格、参数定义和返回结构。

起初路由很简单,无非就是在调度代码里写一堆条件分支:

if "总结" in user_query: target_agent = doc_agent elif "舆情" in user_query: target_agent = social_monitor elif "报表" in user_query: target_agent = report_agent else: # 兜底:交给客服智能体,但也经常答非所问 target_agent = support_agent

这套代码一开始能跑,但随着智能体数量增长很快就失控了。典型场景是新来的同学问我想加一个“会议纪要智能体”,他要改的不仅是新服务本身,还要动调度服务里的 if/else、改一堆优先级判断、加一堆特例。时间一长,这个调度服务就被戏称为“祖传大泥球”——没人敢随意改,因为任何一次调整都可能影响线上流量。

更麻烦的是智能体之间的能力边界开始重叠。文档摘要智能体声称自己能做“总结”,舆情监控智能体也写了“总结”能力;报表生成智能体能“汇总数据”,运营分析智能体也能“汇总数据”。不同小组为了表现能力,能力描述写得一个比一个全,但真正执行时又会说“这不是我的职责”。这种情况不需要多,发生两次你就会意识到:缺一个统一的能力注册与路由层。

1.2 Agent-Reach 想解决什么

我当时想得很清楚,团队需要的不是再写一个“更聪明的 if/else”,而是一个能支撑智能体持续增长的基础设施。于是 Agent-Reach 的定位就变成了三层:

  • 注册中心:统一保存智能体的基本信息、能力描述、健康状态和版本号,新智能体接入只需要在这里做一次登记。
  • 路由引擎:根据用户请求或上层任务的描述,自动计算候选智能体列表并排序,不再依赖硬编码的条件分支。
  • 执行网关:统一负责调用转发、超时控制、重试、限流和结果校验,让每个智能体的内部实现细节不再暴露给上层。

一句话概括:Agent-Reach 做的事情就是“谁有能力谁上,谁最近稳定谁优先”,而调用方只要跟 Agent-Reach 对话就行。它不关心下游是一个 Python 服务还是一个 Node 服务,也不关心智能体内部用了什么模型。

这个抽象层的价值在智能体数量越过五六个之后会非常明显:新接入智能体时,注册一份能力描述就可以被路由到;路由策略的调整也只发生在路由引擎内部,不动任何业务方代码。到后面我们甚至没有智能体清单的硬编码,Agent-Reach 成了所有智能体的“唯一电话号码本”。

2. Agent-Reach 的总体设计与选型依据

2.1 架构总览:注册中心、路由引擎、执行网关

整体调用链长这样:

调用方 -> Agent-Reach API -> 路由引擎(打分排序) -> 执行网关(转发&容错) -> 目标智能体 ^ v 注册中心(能力清单) 健康检查/追踪/成本记录

注册中心是源头。每个智能体上线前必须提交一份能力描述文件(我下文会给出 Schema),Agent-Reach 启动时加载所有描述,之后每当有新的注册或版本更新,会对内存索引做一次热更新。注册中心还做一个轻量级健康检查:每 30 秒探测一次每个智能体的/healthz接口,状态不好的智能体会被标记为degraded,路由时降权。

路由引擎主要负责两件事:一是把任务请求映射成可计算的匹配信号;二是在匹配信号基础上加权打分,选出前 N 个候选智能体。这块是整个系统最核心也最容易被做复杂的地方,后面我会单独聊匹配算法。

执行网关则是一个比较通用的转发层。它拿到路由引擎给出的候选结果后,按照优先级依次调用智能体的 HTTP 接口,同时处理超时、熔断、结果格式校验,并把每次执行的 trace 信息写进日志。网关还承担限流职责——防止某个智能体因为粘性流量被打爆。执行网关的设计很大程度上是抄了微服务网关的思路,但简化了很多,因为智能体之间不需要微服务那样复杂的服务发现和负载均衡,候选集本来就可以由路由引擎动态生成。

2.2 为什么不用现成的编排框架

在动手写 Agent-Reach 之前,我们其实认真评估过一圈市面上的主流框架,比如 LangChain/LangGraph、AutoGen、CrewAI。它们的定位不同,用下来感受也不一样,我列个对比表说明当时的选择逻辑:

框架/方案擅长场景在我们项目里的问题
LangChain / LangGraph单个智能体内的工具调用、流程编排它更偏“一个智能体如何调工具”,而不是“多个独立服务如何互相发现”。我们这些智能体已经是以独立服务形态存在了,硬迁到 LangGraph 等于重写一遍
AutoGen多智能体对话、角色扮演它的核心模型是多智能体会话,适合研究原型。但生产环境里每个智能体都是有状态的 HTTP 服务,让它们在一个会话里互相喊话,运维责任太重
CrewAI角色分工的任务执行同样偏角色协作,但我们的智能体不是统一跑在 LLM 上下文里的“角色”,而是各自有独立技术栈和部署单元,这超出了它的设计边界
自研 Agent-Reach能力注册、路由、网关三层它解决的本质问题是“智能体可达性”,不是流程编排,所以可以和任何流程引擎共存

这个对比让我确定了关键判断:我们的问题不是“怎么写一个工作流”,而是“一群独立的智能体服务如何被稳定地调度”。这正是 Agent-Reach 要做的。它不是要替代 LangGraph,而是可以放在 LangGraph 前面或后面——上游做流程编排,到真正执行某个能力时,把执行请求交给 Agent-Reach 去找具体服务。

如果你们团队的情况是若干智能体都在同一个代码仓库里、由同一个人维护,那确实不需要这种面向服务的路由层。但如果你和我们一样面临着跨团队、跨技术栈、甚至跨部门的智能体协同,一个独立的注册与路由服务几乎是绕不开的。

3. 核心实现:能力描述、路由与容错的细节

3.1 能力描述文件的 Schema

路由能算得准,前提是信息进得来、进得来还得标准化。我们给每个智能体定义了一套能力描述文件,YAML 格式,必须和代码一起走版本评审。下面是一个简化示例:

id: doc-summarizer version: 1.2.0 display_name: 文档摘要智能体 endpoint: http://doc-agent.internal:8001/v1/execute health_check: http://doc-agent.internal:8001/healthz timeout_ms: 30000 capability: action: summarize domains: - document - report - email input: required: - content optional: - max_len - format output: content_type: text max_length: 2000 confidence_weight: 0.9 tags: - 摘要 - 总结 - 提炼 keywords: - 总结一下 - 太长不看 - 一句话概括 languages: - zh - en conditions: - "输入必须包含正文内容,不能仅传URL"

这里有几个设计时验证过很重要的点。

tags和keywords必须由智能体开发团队自己填,不能省。很多团队会觉得反正有 embedding 语义匹配了,关键词不需要。但我们实测下来,语义匹配对专有名词和缩略词非常不稳定,比如“PRD总结”和“文档总结”在向量空间里未必足够接近。而显式关键词是有确定性收益的,成本极低。这个字段千万不能省。

conditions一开始我也不太理解它的价值,后来栽过跟头才明白它有多重要。它相当于智能体预设的“前置条件说明”,路由引擎在匹配时会把任务输入与conditions做简单的规则校验,如果输入不符合条件,即使相似度很高也会被过滤掉。比如文档摘要智能体说自己不接受只传 URL,那路由引擎就不会把一个只有 URL 的任务路由给它,宁可交给下一个候选或进入人工复核。

字段里的confidence_weight是给路由引擎的一个先验权重。因为不同智能体对自身能力的描述有浮夸倾向,我们允许在接入时人工设置一个 0~1 的系数,但要求必须给出合理理由。这个系数最终会参与打分,但不是决定性的。

3.2 路由匹配算法:从精确匹配到语义匹配

路由引擎的打分我们分了三条线,分别覆盖确定性匹配、语义匹配和运行状态匹配,最后加权得总分:

def score_candidate(agent_desc, task): # 第一层:规则匹配,关键词/标签命中情况 rule_score = rule_match(agent_desc["keywords"], agent_desc["tags"], task.text) # 第二层:语义匹配,文本向量余弦相似度 semantic_score = semantic_match(agent_desc["capability"], task.embedding) # 第三层:状态因子,健康度越高的智能体分数越高 health_factor = agent_desc["health_factor"] # 0~1 final_score = ( 0.3 * rule_score + 0.5 * semantic_score + 0.1 * agent_desc["confidence_weight"] + 0.1 * health_factor ) return final_score

我解释一下权重为什么这样配。semantic_score权重最高,因为用户请求的表述千变万化,规则匹配永远会有漏网之鱼;但纯靠语义又容易误召,所以把规则分拉到 0.3 来稳定输出。confidence_weight和health_factor各占 0.1,主要起微调作用,避免某个智能体因为写了个高权重描述就一直霸榜。

这里有个容易被忽视的细节:任务文本的向量计算。我们一开始用的是简单的将整段任务描述整体向量化,后来发现效果不理想,因为一个请求里往往包含多个意图,比如“帮我把上周的日报总结一下,然后发给运营团队”这句话里既有摘要意图,又有发送消息意图。后来优化为用一个小模型先做意图分类和关键词抽取,再用抽取出来的关键片段去做向量匹配。这一步大概让整体精确率提升了 8% 左右,代价是路由时延增加约 30 毫秒,完全可接受。

候选集生成我们用的是 Top-K 方式,默认取前 3 名。最终判定规则是:

  • 最高分 ≥ 0.75:直接执行该候选。
  • 最高分在 0.55~0.75:将候选列表返回给上层,由上层决定是否确认。
  • 最高分 < 0.55:认为没有能力匹配的智能体,返回“未找到可用能力”,进入人工复核通道。

第一条阈值设得比较保守,是故意为之。宁可多让上层确认一次,也不要因为分数虚高就乱执行。毕竟在真实业务里,一个任务被错派给错误的智能体,浪费的不只是那一次调用成本,还可能引发连锁错误。

3.3 超时、重试与降级策略

路由找到候选只是开始,真正执行时各种幺蛾子比匹配不到能力还多。这里我列一套我们在 Agent-Reach 里沉淀下来的参数,不一定适用于所有人,但可以直接抄作业:

参数默认值说明
timeout_ms30000每个智能体端点上配置,长任务可以单独调大
max_retries2最多重试两次;只在任务幂等时才允许重试
retry_backoff指数退避,基础 1s,加随机抖动避免雪崩式重试打垮下游
circuit_breaker失败率 > 50% 且持续 60s 则熔断熔断窗口 30s,期间该智能体降权到 0.1
fallback按候选集依次尝试首选失败后,自动尝试第二候选

重试逻辑有几个坑。第一,不是所有智能体都幂等。比如“发送通知”这个动作,被重试一次就意味着发两条通知。我们在能力描述里增加了一个idempotent: true/false字段,网关读取后才能决定是否对失败请求做重试。第二,重试时一定要在请求头里带上同一个x-reach-request-id,让下游能识别这是同一次请求,方便做去重和排查。第三,指数退避不能只有固定倍数,要加抖动,否则多个并发请求的重试会形成节奏一致的流量波峰,非常容易把下游服务直接震挂。

降级路径我们也考虑过。如果候选集里所有智能体都不可用,Agent-Reach 不会直接返回失败,而是把任务放进一个manual_review_queue,同时给值班人员发一条通知。人工处理结果可以回填到路由日志里,作为后续调参的数据。这个“失败不落地”的设计在真实运营中很重要,因为多智能体系统里任何一个环节的抖动都可能让最终用户得到一个莫名其妙的错误,而我们希望至少保证有兜底。

4. 实测中的意外情况与排查过程

4.1 智能体互相“甩锅”,任务无人处理

上线后遇到的第一个棘手问题,不是匹配不上,而是匹配上了但智能体拒绝执行。有一次线上突然出现大量“任务失败”告警,业务方反馈说有些数据报告任务被自动路由到一个数据分析智能体,结果这个智能体返回的错误信息是:“本智能体仅处理结构化数据,无法处理非结构化文本,请转人工。”

我们追 trace 发现,路由打分环节这个智能体确实排名第一,因为它的能力描述里写了“数据分析”和“文本处理”两个 tag,conditions却没有限定输入格式。它的能力描述写得比实际执行能力宽泛,导致它自己给出的“能力承诺”和真实边界不一致。

根因很清楚:能力描述是智能体开发团队自己写的,人都有高估自己能力的倾向。我们的解法是在能力描述里增加了一个审核环节,要求每个新版本的能力描述必须附上“负例场景清单”——也就是这个智能体明确不处理什么。这个负例清单会参与路由过滤,比如数据分析智能体写明“不处理非结构化文本”,那路由时遇到非结构化文本任务就直接过滤掉。这个改动上线后,类似甩锅问题降低了七成。

4.2 语义匹配的误召率问题:两个智能体能力太像

另一个高频问题是语义匹配误召。运营团队有两个智能体,一个做“日报摘要”,一个做“周报复盘”。从用户视角看,两者的语义描述确实非常接近,向量相似度在 0.9 以上。结果出现一个需求“把本周的运营总结发我”,路由引擎连续几次都选中了日报摘要智能体,返回的内容是每日颗粒度,根本不是业务方想要的周报维度。

这个问题的本质是:语义空间里两个能力挨得太近,但实际执行语义差异很大。我们第一版想通过提高精确阈值来解决,但阈值调高了又会让许多本应正常命中但表述不那么标准的请求掉出候选集,影响体验。

后来我们的方案是在能力描述里增加action和domains两个维度作为硬过滤条件。action可以理解为一等能力分类,比如summarize、analyze、generate_report;domains是领域分类。路由时先按 action + domains 过滤,缩小候选范围,再做语义打分。只要日报和周报的 action/domains 配置不一样,哪怕语义接近也很难跨错。这本质上是“宁可多写一点结构,也不依赖纯向量”。

当然,这个方案需要每个智能体在接入时就把 action 归类想好。我给团队的建议是不要自创太多 action,系统预置一份常用分类表,比如summarize、analyze、search、generate、transform、notify,有特殊需求再走评审扩展。保持 action 集合小,误召率会低很多。

4.3 排查链路:把请求链路可视化出来

多智能体系统的排障比单体服务难一个数量级。尤其是在半夜告警的时候,你根本不知道一个任务到底经过了哪些智能体、在哪个环节卡住、哪个服务超时。上线早期我们遇到过这样的场景:明明网关日志显示已经调用了目标智能体,但目标智能体日志里根本没有这条请求,两边各说各话。

后来我们全面接入了 OpenTelemetry 风格的追踪。Agent-Reach 为每个路由请求生成一个全局唯一的trace_id,这个 ID 通过 HTTP header 传给下游智能体,要求所有接入方在日志里带上它。这样在日志平台里直接搜 trace_id,就能把一次任务的完整链路拉出来:路由打分用了多久、选中的是哪个候选、网关转发了几次、每次执行耗时多少、最终结果从哪里返回的。我们还搭了一个简单的可视化页面,按 trace_id 就可以链式查看各个智能体的调用顺序和时间线。

这个追踪体系上线之后,排障效率提升非常明显。有一次我们发现某个任务的实际耗时几乎是预期的一倍,通过 trace 看到是网关因为超时重试了两次,但重试都打到了同一个故障节点上。如果当时没有这条链路,这个排查过程至少要花半天。

需要提醒的是,能力接入的时候就要把 trace_id 传播的规范定下来,不要等出了问题再补。我们就是因为最开始接入的智能体没有统一规范,后来挨个改造接口和日志格式,花了很大力气。作为中间层,Agent-Reach 承担了唯一的上游入口,这让 trace_id 的传播规范变得非常简单:下游只需要把一个 header 解析出来打日志就行。

5. 性能与成本:扣细节的经验

5.1 延迟分布与瓶颈定位

Agent-Reach 第一版上线时的路由时延大概在 600ms 左右,听上去不多,但对上层服务来说这是纯新增的固定开销,用户体感非常明显。我们用性能分析工具打了一版延迟分布,发现大头在四处:

  • 任务文本向量化:约 180ms
  • 候选向量相似度计算:约 220ms
  • 健康检查同步请求:约 150ms(这个完全不该阻塞路由)
  • 其余序列化和网关转发:约 50ms

优化方案分三步。第一,把健康检查从同步请求改成异步缓存,网关侧每 30 秒拉一次各智能体状态,路由时直接读缓存,不再现场等探测结果;第二,把向量化结果按文本 hash 缓存起来,同一个任务反复出现时直接命中缓存;第三,也是最关键的——当候选智能体数量少于 50 个的时候,根本不需要上向量数据库,直接做暴力线性计算就好,反而比引入索引快得多。

优化后路由部分延迟稳定在 120ms 以内,网关转发部分另算。这个数据对大多数业务场景够用了。如果你预期智能体数量会快速涨到几百个,建议在三方存储里启用向量索引,但不要把路由延迟的预算设计得太紧张,毕竟它只是一个调度开销,不是执行业务逻辑本身。

5.2 并发控制与限流

Agent-Reach 作为中心调度层,还有一个很大的潜在风险:如果某个智能体非常热门,所有任务同时涌向它,会把下游直接打挂。我们虽然没有在企业级 API 网关上做那么复杂的流量治理,但还是实现了一个按智能体维度的令牌桶限流。

核心规则是每个智能体在注册时可以声明max_concurrency和max_qps。路由引擎在打分时会参考当前智能体的实时负载,如果负载已经超过阈值,该智能体会被降权,任务优先调度给次优候选,或者放入等待队列而不是直接往热点上堆。这套机制上线后,有好几次上游流量突增,下游智能体没有出现被打挂的情况。

实际配置中要注意,max_qps不宜设得太死,建议留 20%~30% 的缓冲余量。因为任务执行时长波动很大,同一个智能体处理 3 秒级任务和 30 秒级任务的负载差异巨大,纯 QPS 不够全面。我们在网关里统计了每个智能体当前正在执行的请求数(inflight),把这个数值和 QPS 一起作为限流依据,比只看一个指标靠谱得多。

5.3 成本台账:把每一次调用变成可审计的记录

做多智能体系统,老板最关心的问题之一就是成本。每个智能体背后可能是不同的模型、不同规格的 GPU 或 API 服务,调用一次到底花了多少钱,很多时候根本没数。Agent-Reach 从设计之初就要求网关在每次请求完成后记录一条成本明细,包括:调用的智能体 ID、模型名、输入 token、输出 token、执行耗时、重试次数、最终状态。

这些明细沉到日志系统里,我们每周会跑一张“智能体成本账单”报表,按智能体维度汇总总调用次数、总耗时、总 token 消耗。这个数据帮我们发现了不少问题:比如有个智能体执行成功率极高,但单位调用 token 消耗是其他智能体的三倍,因为它的系统提示词写了一万字。后来我们把提示词压到两千字以内,成本直接降了一半。没有成本台账,这种问题你根本定位不到。

有一点要特别提醒:路由引擎自身也有成本,主要体现在任务文本的向量化调用上。如果所有任务都走向量化,这部分费用会随着请求量线性增长。我们给路由引擎加了一个降级逻辑:如果任务文本能通过规则层的关键词匹配得出明确候选,就不再计算语义层分数,直接走快速通道。这个优化让路由引擎本身的成本下降了约 40%,且几乎没有影响路由准确率。

6. 一些可以少走弯路的建议

6.1 小规模团队的 Agent-Reach 适配清单

Agent-Reach 不是所有团队的必需品。如果你只有两个智能体,而且都由同一个后端服务维护,那完全没必要引入一个独立路由层,反而是过度设计。但从我实际经验来看,出现下面任意两条,就值得考虑:

  • 智能体数量 ≥ 5,且预计还会持续增加;
  • 智能体由不同小组或不同技术栈独立开发和部署;
  • 核心业务开始因为调度逻辑混乱而出现互相协调的问题;
  • 有人在你面前提“要是能自动找到能处理这个任务的智能体就好了”。

刚开始不要一上来就做全套。我建议的落地顺序是分三步走:第一步只做能力注册 + 基于关键词/标签的硬匹配,验证“把调度从代码里抽出来”这个思路是否成立;第二步补语义匹配和候选打分,让路由效果上一个台阶;第三步再上执行网关的重试、熔断、限流和成本台账,这时候系统已经能扛住生产流量了。一步到位反而容易因为太多不确定因素导致上线反复受阻。

6.2 后续可以扩展的方向

Agent-Reach 目前的形态已经能解决核心的“可达性”问题,但后面还有一些明确值得做的东西。比如路由引擎的人工反馈闭环:把人工复核队列里那些被纠正过的路由结果收集起来,定期重新衡量打分权重,甚至可以微调一个小模型来优化排序。又比如跨组织的智能体注册协议,让外部团队的智能体只要声明能力描述就能接入,这会极大提升生态扩展性。还有智能体之间的协商机制——如果某个任务需要多个智能体协同完成,Agent-Reach 负责把任务拆解后的子请求分别路由给对应服务,再把结果合并返回。

最后说一点个人心得。做这个项目时,最开始的挫折感很强,总觉得“智能体路由”听起来太基础,好像不值得做成一个独立系统。但真正跑起来以后我才发现,恰恰是这种基础能力最容易被忽视,也最容易成为瓶颈。一个多智能体系统能不能规模化,不取决于单个智能体有多聪明,而取决于它们之间能不能被高效、可靠地连接起来。Agent-Reach 的诞生就是这样朴素的结论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询