
先说结论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_k5): 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(keylambda x: x[1], reverseTrue) 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 超过十个能力注册和发现就必须提上日程否则你会栽在我文章开头说的那种事故里。基础设施的价值永远是在出问题之前看不出来出问题之后追悔莫及。