先说结论:Agent-Reach 不是一个花哨的 Agent 框架,也不是又一个 ChatBot 套壳。我把它做成了一个偏底层的"能力发现与路由"服务。刚跑通第一个版本时,身边同事的反应是"这不就是个注册中心加语义搜索吗",但实际用了两个多月后发现,真正让多 Agent 系统稳定协作的,恰恰就是这种不起眼的基础设施。如果你正在为调度器乱派任务、Agent 互相抢占、新技能上线后没人调用这些问题头疼,那这篇文章值得你花十分钟看完。
1. 先说痛点:Agent 多了之后,"任务该找谁"变成第一问题
我最初的想法很简单,Agent 系统嘛,核心是让模型能调用不同工具、不同专家、不同流程。早期模型少、接口少,靠手写 if-else 分发完全够用。可一旦 Agent 数量超过三五个、任务类型超过十几种,问题就变了。
1.1 从一次线上事故说起:能力路由失灵的代价
有一次我把一个"行业研究报告生成"的任务交给我自己搭的调度器,结果它把任务派给了一个只会做数据清洗的 Agent,原因仅仅是那个 Agent 的 system prompt 里出现了"报告"两个字。那次事故的后果不是任务失败那么简单,而是连累了一批下游数据任务,整个过程持续了三小时。我复盘时发现,问题不在调度器本身,而在调度器完全不知道"哪个 Agent 到底擅长做什么"。
这件事给了我一个很直接的启发:Agent 系统真正稀缺的不是模型推理能力,而是对自身能力的"可发现性"。一个能力再强但无法被正确调用的 Agent,跟不存在没有区别。
1.2 单体 Agent 到多 Agent 的失控
很多团队做 Agent 走了这条路:先是一个大 Agent 包打天下,什么任务都塞到同一个上下文里。然后发现上下文越来越长、幻觉越来越多,于是拆分出多个专项 Agent。拆完又发现新的问题——任务怎么分流?能力怎么避免重复建设?谁来决定这个请求该给谁?
这很像一个公司从一个人干所有事,变成几百个人的大部门。人多了以后,最重要的不是每个人多能干,而是有没有一份清晰的"通讯录"和"分工表"。Agent-Reach 的定位就是这张表和这本通讯录。
1.3 为什么不做成简单的 if-else 分发
有朋友问,直接在调度器里写死映射关系不行吗?比如"提到数据分析就调 data_agent"。这在静态场景下确实可以,但一旦 Agent 的能力发生变化、或者来了一个新任务类型、或者某个 Agent 挂了需要临时顶替,写死的逻辑立刻变成维护噩梦。更麻烦的是,自然语言的表达千变万化,同一个意图可能隔几天换一种说法,if-else 根本覆盖不过来。
所以我决定做一个独立层,让 Agent 通过声明式的方式注册自己的能力,让调用方通过语义化描述来检索目标 Agent。这个思路本质上和微服务架构里的服务注册与发现是一样的,但 Agent 的复杂之处在于:服务的接口是固定的,Agent 的能力是模糊、动态、需要理解的。
2. Agent-Reach 的总体设计:注册、发现、路由三层分离
Agent-Reach 从架构上拆成了三个明确的模块,彼此之间通过事件和 API 通信,互不耦合。整个链路只有一条:Agent 启动时注册能力,任务到达时查询能力,调度器拿到候选后做路由决策。
2.1 核心组件和职责边界
我在设计时坚持了一个原则:Agent-Reach 只负责"谁有能力",不负责"任务怎么执行"。执行流程、参数校验、结果回传这件事,全部留给上层的调度器(Orchestrator)去管。这样做的原因是边界清晰,Agent-Reach 的代码量可以控制在很小的范围内,出错的概率也大幅下降。
三个核心组件分别是 Registry(注册中心)、Matcher(能力匹配器)、Router(路由裁决器)。Registry 管存储和健康状态,Matcher 管语义理解和评分,Router 管最终选谁以及降级策略。数据流向是单向的,不搞复杂回调。
2.2 协议与注册信息设计
Agent 注册时提交的信息是一种 JSON 格式的能力描述,我用一个叫 capability 的顶层字段来包裹。里面包含能力 ID、名称、描述、输入参数 Schema、输出格式声明、健康检查地址、权重和标签。注册完成后返回一个 capability_id,后续心跳续租都靠这个 ID。
{ "capability_id": "cap_report_engine_001", "agent_id": "agent_industry_report", "name": "行业研究报告生成器", "description": "根据指定行业和主题生成结构化的分析报告,包含市场概况、竞争格局、发展趋势等章节,支持 docx 和 pdf 导出", "input_schema": { "industry": "string", "theme": "string", "depth": "basic|deep" }, "output_format": ["docx", "pdf"], "health_check": "/healthz", "ttl": 60, "weight": 1.0, "tags": ["report", "research", "industry"] }一个容易被忽略的细节是 description 字段。它不仅是给人看的,更重要的是它会被后续的语义匹配用作索引依据。description 写得越具体、越贴近实际任务的自然表达,匹配准确率就越高。这一点在后面踩坑部分我会详细说。
2.3 为什么用声明式能力描述而不是函数签名
早期我试过直接把函数的签名和 docstring 当作 Agent 能力的描述。函数签名确实严谨,比如generate_report(industry: str, theme: str) -> Report,但对大模型来说信息密度太低,完全没表达"能处理什么、适合用来做什么、产出是什么样"。更关键的是,实际调用 Agent 的场景中,用户不会用函数思维描述需求,他们会说"帮我写一份关于新能源汽车行业的深度报告"。
所以 Agent-Reach 必须提供一种"面向自然语言表达"的能力描述方式,而不是"面向编译器"的接口描述。声明式能力描述介于两者之间,既保留了结构化的参数约束,又给语义理解留了充分的空间。
3. 能力描述协议的设计:如何让"人的需求"与"Agent 的能力"对上
这一块是整个 Agent-Reach 里我迭代最多、也最有心得的部分。表面上看它只是一个 JSON 格式的声明,实际上它决定了后续匹配效果的上限。
3.1 能力描述的结构设计
description 字段我建议写成一小段"完整的话",不要用关键词堆砌。比如描述一个客服 Agent,不要写"客服、售后、退款、问题处理"这种碎片词,而要写成"面向平台用户的在线客服助手,可以处理订单查询、退换货申请、物流跟踪、发票咨询等售后场景,并支持将复杂问题转接人工"。原因是向量编码对完整句子更友好,碎片词会让 Embedding 混入大量歧义信息。
我把能力描述拆成三层:语义标签(tags)、功能描述(description)、使用场景(scenarios)。tags 是给机器快速过滤用的,description 是给语义匹配用的,scenarios 是给模型推理时参考的。三层叠加之后,召回的准确性比只用单一描述高不少。
3.2 语义匹配与向量召回的结合
我尝试过的方案一:纯关键词搜索。描述里出现"报告"才能搜到报告 Agent,无法处理"写一份行业分析"这种同义表达。方案二:纯向量召回。能处理同义词,但会把"幸存者偏差"这种统计术语误召回给心理学 Agent。最后我采用了两级召回:
先用 tags 做粗筛,把明显无关的 Agent 去掉。然后对粗筛结果和 query 一起做向量化,用余弦相似度排序。tags 相当于设置了一个硬边界,向量负责在边界内做软排序。这个混合策略的效果是最稳的。
def match(query, top_k=5): rough = registry.search_by_tags(query_tags(query)) if not rough: rough = registry.all_alive() query_vec = embed(query) scored = [ (cap, cosine(query_vec, cap.embedding)) for cap in rough ] scored.sort(key=lambda x: x[1], reverse=True) return scored[:top_k]3.3 打分排序和置信度阈值
只靠相似度分数不够,因为某些场景下所有候选的分数都很低,这时硬选一个高分的很容易出问题。我给 Agent-Reach 加了一个置信度阈值(默认 0.62),低于阈值的请求直接判定为"无匹配能力",返回给调度器一个明确的错误信号,调度器可以走兜底流程,比如问用户、或者调用一个通用大模型。
注意:阈值不是一个需要经常调整的参数。我建议你观察一段时间的匹配分数分布特征,找"分数明显下降的拐点"作为初始阈值,而不是按直觉拍脑袋设定。拐点太高的代价是大量任务匹配不到 Agent,拐点太低的代价是误召回会显著增加。
还有一个细节是热门数据和冷门数据的分数差。热门场景的描述通常比较泛,差不多什么 query 都能撞上;冷门场景描述比较具体,匹配分数反而很高。在排序时我给泛化的热门 Agent 加了一个惩罚系数,避免它长期霸榜。
4. 路由服务的关键实现:注册中心的健康状态与语义路由逻辑
架构好说,真正写代码时坑不少。Agent-Reach 的注册中心不是简单的 CRUD,它承担着 Agent 健康状态维护、能力版本管理、路由权重调整三件事。我在这里重点说一下我认为最有价值的两个模块。
4.1 注册中心的健康状态管理
Agent 和普通微服务最大的区别是:进程活着不代表能力可用。底层模型 API 挂了、上下文窗口满了、依赖的数据库连不上了,都会导致 Agent 无法完成实际任务。所以 Agent-Reach 的健康检查设计成了两级:一级是 HTTP 层 liveness 检查,确认进程还在;另一级是自定义的 readiness 探针,由 Agent 自己上报"是否准备好接收新任务"。
async def heartbeat_loop(agent): while True: await asyncio.sleep(agent.ttl / 3) ok = await agent.check_readiness() registry.report(agent.cap_id, round(ok, 3))readiness 返回的不是一个布尔值,而是一个 0 到 1 之间的浮点数。比如某个 Agent 当前负载已经 90%,它可以上报 0.1,让 Router 在分配任务时自动降低它的优先级。这个设计是我在排查一次"明明 Agent 健康却频繁超时"的问题后加上的,效果立竿见影。
4.2 语义路由的裁决逻辑
Router 拿到 Matcher 给出的候选列表后,不会直接取第一名,而是会综合三件事:能力匹配分数、Agent 当前健康度、Agent 最近一小时的任务成功率。成功率会通过一个滑动窗口实时统计,避免某个 Agent 因历史表现好而永远被优先选中,哪怕它最近已经连续失败。
裁决公式非常简单,就是一个加权求和:
score = match_score * 0.6 + readiness * 0.25 + success_rate * 0.15这个公式是第一个版本上线时的权重,后来基本没改过。原因很简单,从透出数据来看,它已经能让大多数任务落到正确且有能力的 Agent 上,折腾权重带来的收益远不如把描述质量和健康上报做扎实。如果你是从零开始搭,也别在这上面花费太多精力,先把链路跑通再调。
4.3 降级与重试策略
不管匹配和路由做得多好,总有可能出现所有匹配的 Agent 都不健康的情况。Agent-Reach 的做法是分级降级:
第一级:去掉 readiness 和 success_rate 权重,只按 match_score 排序,尝试抢救性调用。第二级:从能力的同类标签池中寻找替代 Agent,比如专门做行业报告的 Agent 挂了,就尝试调用通用研究 Agent。第三级:放弃匹配,返回"no capable agent available"。
我要特别强调重试策略的细节。第一次路由失败后,重试请求必须打上一个 marker,让 Router 知道这是重试,而不是新请求。不然每次失败都原样重来,会把最健康的 Agent 也拖垮。我在实际运维中遇到过这种雪崩,非常恐怖。
5. 我在调通整个链路时踩过的坑
写代码的过程还算顺利,真正折磨人的是链路调通后的各种诡异现象。这些坑每一个都是线上实际遇到并逐步定位的,记录在这里,希望你能少走弯路。
5.1 Agent 注册后"幽灵失联"
现象是 Agent 明明注册成功,心跳也正常,但任务量一大就偶尔出现某个 Agent 长时间没响应,看监控却一切正常。排查了很久才发现问题出在 asyncio 的事件循环被阻塞了。我的 Agent 里有一段同步的模型调用,没包线程池,直接把事件循环卡死,心跳自然发不出去。这不是 Agent-Reach 本身的问题,但它暴露了一个架构原则:所有注册和心跳上报路径上的 IO 操作都必须是非阻塞的,否则健康状态本身就失去了意义。
5.2 同义词和中英文混合查询导致的召回漂移
这是匹配模块最典型的坑。我在测试时发现,输入"退款怎么办理",匹配到的结果竟然是"法务纠纷咨询"Agent,原因是我在描述里用了"售后争议处理",而向量模型认为"退款"和"争议"在语境上很接近。这个问题的根源不在模型,而在描述没有覆盖常见意图的表达方式。
我的解决办法是在 Agent 的 scenarios 字段里补充真实用户说过的说法,比如把"退款、不想用了、退货、怎么把钱拿回来"这类表达都列进去。这本质上是给向量召回加了人工种子,成本不高收益非常明显。
5.3 过载时的级联超时
某个 Agent 因为外部 API 变慢,单个任务的响应时间从 2 秒涨到 20 秒,但它的健康状态仍然上报为 1.0,于是 Router 以更高的概率把新任务派给它,导致它越来越慢,最终引发连环超时。这个问题让我意识到,readiness 上报不能只看"能不能干活",必须把当前队列长度和最近响应时间也算进去。
修改后,readiness 的计算方式是base - penalty,如果当前排队任务数超过阈值直接打五折。这样 Router 在裁决时会本能地避开已经处于重负载的 Agent。
5.4 静态注册与动态能力的矛盾
还有一个很隐蔽的问题:Agent 通常是在启动时一次性注册全部能力的,但实际运行中有些能力会因为配置变化而临时不可用。比如一个多模态 Agent,在图片生成 API 欠费后仍会注册图片生成能力,导致任务被路由过来后才能发现失败。
方案是在注册协议里支持临时"禁用某个能力"的接口。Agent 运行中可以主动调用/disable或者/enable来更新能力状态,Registry 同步变更记录的版本号,Router 缓存靠轮询感知变更。这其实蛮像注册中心的配置热更新,但在 Agent 场景里很多人会忽略。
6. 实测效果与当前局限:Agent-Reach 到底解决了什么问题
上线两个多月,Agent-Reach 的数据表现让我比较满意,但距离一个真正成熟的多 Agent 基础设施还有很长的路。把真实数据和不足都摊开说说。
6.1 一组对比数据
我把 Agent-Reach 上线前后两周的数据做了一下对比:
| 指标 | 旧方案(if-else 分发) | Agent-Reach |
|---|---|---|
| 任务路由准确率 | 72.4% | 93.8% |
| 平均任务首次成功率 | 61.2% | 85.7% |
| 调度器代码量 | 1800 行 | 400 行 |
| 新增 Agent 上线耗时 | 1-2 天 | 10 分钟 |
这个对比里最让我意外的是调度器代码量从 1800 行降到 400 行。因为所有分发逻辑都收敛到了 Agent-Reach 的注册、匹配、路由三层里,上层不再需要关心"该调用谁",只需要关心"调用什么能力"。架构边界清晰之后,每个模块的代码量都自然下降了。
6.2 当前局限与可以扩展的方向
Agent-Reach 现在最大的短板是:它只解决"找得到"问题,不解决"叫得动"问题。也就是说,即使找到了正确的 Agent,也不保证这个 Agent 能按预期完成工作。Agent 内部规划质量、工具调用准确率、上下文管理这些决定服务质量的因素,依然需要靠每个 Agent 自身的能力来保障。
另一个局限是,当 Agent 能力描述本身写得不好时,任何匹配算法都救不回来。文案质量成了整个系统准入门槛,很考验每个 Agent 的维护者。我的计划是把能力描述生成做成交互式引导,Agent 接入的时候通过一段对话逐步生成高质量描述,而不是让开发者手动写。
再往下走,我打算做的是给匹配结果增加可解释性。现在只返回一个分数,用户和调度器都很难判断"为什么选它"。如果能在路由时输出三个理由,比如"匹配度最高""健康度最优""历史成功率领先",整个系统的可观测性会好很多,排障效率也会大幅提升。
6.3 我对这类基础设施的最终看法
Agent-Reach 这个项目最让我受益的一点,是我终于意识到多 Agent 系统不是"把多个 Agent 拼在一起就能变强",而是"每个 Agent 都必须能被准确理解、可靠地发现、合理地调度"。这三个词听上去朴素,做起来每一项都需要独立的机制去支撑。
如果你也在搭多 Agent 系统,我的建议很简单:先把五六个 Agent 跑通业务闭环,再去搞路由优化和语义匹配;但一旦 Agent 超过十个,能力注册和发现就必须提上日程,否则你会栽在我文章开头说的那种事故里。基础设施的价值,永远是在出问题之前看不出来,出问题之后追悔莫及。