
Agent-Reach 是我最近在公司内部推动完成的一套多 Agent 任务触达系统。项目名字起得很直白让 Agent 真正“够得着”目标而不是只在模型演示里跑出漂亮结果。做这个项目的背景很现实——团队里半年攒了十来个 Agent 原型能力都有但一到真实场景就翻车有的任务找不到合适的 Agent 接有的 Agent 接住了但结果没人验证有的链路一断就直接失败用户只能重新提交。折腾两个月后我基本确定了一个判断现在缺的不是更强的单个 Agent而是一层能让 Agent 被正确调度、被稳定覆盖、失败后能自动补位的中间系统。Agent-Reach 就是为了补上这层。1. 为什么会做 Agent-Reach先讲清项目定位1.1 起点一堆能跑的 Agent却一个都用不上最开始的情况相信不少团队都遇到过。各个小组各自训练或封装了一批 Agent有做合同要素抽取的有做客服话术生成的有负责工单分类的还有做 SQL 生成和数据分析的。每一个单独拿出来都能在测试集上展示不错的例子可真要接到业务流程里问题立刻暴露出来。最大的问题不是效果差而是“没人管”。请求来了不知道该发给哪个 AgentAgent 返回的结果对不对没人把关调用失败后也没有重试或者换路线的机制。结果就是明明负责同一类任务的 Agent 有两三个但线上成功率反而比单个模型直出还要低因为每次失败都是硬失败没有任何冗余设计。我记得有一次上线一个智能客服意图识别模块A Agent 的准确率测试时是 88%B Agent 是 86%。按说两个都能用但线上实际跑下来A Agent 经常遇到一些看起来像闲聊、实际上该走下单流程的语句一句话都接不住B Agent 虽然综合准确率低一点但对这种边界语句反而更稳。如果把请求硬编码给 A就会漏掉将近 15% 的流量。当时那个项目就是因为没有“第二选择”上线第一周就被业务方找了三次。Agent-Reach 的出发点就是把这种“没人管”的状态收拾干净。它不关心你底层用的是哪个大模型、是开源还是商业 API它关心的是另外三件事请求来了能不能分配到合适的 Agent分错了能不能在结果验证阶段被发现发现之后有没有备选 Agent 顶上来。1.2 项目定位不造新 Agent造覆盖这是 Agent-Reach 和其他“框架型”项目最大的区别。很多项目喜欢自己定义一套 Agent 运行时把底下的模型全都封装一遍最后你会发现团队原来那些 Agent 反而迁不进去。我给 Agent-Reach 定的边界很窄它是一个调度和治理层不做模型推理不重新发明 Agent。整个系统站在所有 Agent 的上层通过统一入口接收请求然后根据请求内容、Agent 能力标签、历史成功率、当前负载这四个要素做路由决策。Agent 本身只需要实现一个非常简单的接口比如run(task)返回一个结果对象剩下的超时、重试、回退、结果校验全部由 Agent-Reach 接管。这个定位带来的好处非常直接。第一接入成本低团队里任何一个 Agent只要包一层薄薄的适配器就能挂到系统里不需要重写内部逻辑。第二替换成本也低如果某个 Agent 效果不行我直接把它从路由表里摘掉补一个新的线上几乎无感。Agent-Reach 不是要选出一个冠军 Agent而是要让整个池子里每个 Agent 都物尽其用。1.3 名词拆解Reach 的三层含义项目名里的“Reach”不是我随手取的它对应我们实际在解决的三个问题。第一层是可达性。一个 Agent 必须要能被稳定地调用到而不是今天能用、明天因为超时配置不对就断了。这层靠的是超时管理、连接池、容错机制。第二层是覆盖面。请求的分布是不均匀的有些高频任务多个 Agent 都能做有些低频任务可能只有一个 Agent 能接。覆盖面指的是无论请求长什么样路由系统都能找到一个可用的执行者并且不会把所有流量都压在一两个 Agent 身上。第三层是兜底。没有人能保证 Agent 不犯错Reach 的核心价值在于错误发生后还有一条“逃生通道”。一个 Agent 不行就换下一个一条链路断了就走备用链路确保业务不能因为单点失败而中断。所以如果你问我 Agent-Reach 到底是什么我会说它是一套让 Agent 集群可靠输出结果的调度保障系统核心衡量指标只有一个请求的最终触达成功率。2. 架构核心设计把“触达”变成一条可复用的链路2.1 旧单链路 vs 新多链路在 Agent-Reach 之前接入流程基本都是“一请求一 Agent”的单链路模式。请求进来直接丢给指定的 Agent拿结果返回。这种方式在 demo 阶段看不出问题因为 demo 里每个 Agent 都是精心挑过输入的几乎不会失败。但生产环境不是这样。生产环境有输入扰动、有模型抖动、有时效性要求、有超时等待。单链路模式的致命缺陷在于它把“Agent 会不会成功”这个概率问题直接放大成了“整个流程会不会成功”的可用性问题。举个例子。假设一个 An Agent 成功率是 90%这看起来不低。但如果这条链路是唯一的那么整体成功率就是 90%剩下的 10% 用户全部会直面失败。你引入第二个同样成功率的 Agent 做回退整体成功率会变成 90% 10% × 90% 99%这就完全不一样了。Agent-Reach 做的就是把这种简单的概率叠加变成默认机制而不是靠业务方自己去写 try-catch。所以在我设计的架构里请求和 Agent 之间永远不是一对一的关系而是一条路由链。一个请求会对应一个主 Agent以及若干备用 Agent。主 Agent 挂了或者结果校验不通过自动切换下一个。用户的体验始终是“被接住了”而不是“报错了再试一次”。2.2 三个关键模块调度器、路由表、回退器Agent-Reach 的核心由三个模块组成每个模块的职责都很纯粹。调度器是入口它接收所有请求把请求解析成标准任务格式然后对任务做语义归一化。比如“帮我看看这个合同里赔偿金额是多少”和“提取合同中的违约赔偿数字”这两句话在业务上是一个意图调度器会把它们映射到同一个任务标签contract_penalty_extract而不是看字面意思去匹配。路由表是决策中心它记录了“任务标签 → Agent 候选列表”的映射关系。每一个任务标签后面跟着一串 Agent带有序号和权重。这里有个重要的设计细节路由表里的 Agent 不只按能力排序还要按照实时状态做筛选。一个 Agent 如果当前负载过高即使它是第一候选调度器也会把它临时往后挪让空闲的 Agent 先顶上。回退器是最容易被忽略、但实际价值最大的模块。它负责两类工作一是异常回退也就是 Agent 调用超时、报错、返回空值的时候自动触发下一个候选二是质量回退也就是 Agent 正常返回了但结果校验不通过这时候同样要触发下一个候选。很多系统只做了前者漏了后者结果就是 Agent 每次都吭哧吭哧返回一大堆内容业务方用不了线上还看不出来。2.3 为什么必须加回退链我见过不少做 Agent 工作流的团队架构图上画着一堆节点看起来花团锦簇但一问回退策略答案往往是“回退我们还没想这个”。这其实是个很危险的事因为在大模型时代失败不是异常失败是常态。模型推理不是确定性计算。同一个输入温度调到 0也可能因为模型版本更新、量化精度变化、上下文窗口截断而输出完全不同的结果。再加上外部 API 的限流、网络抖动Agent 的失败率在线上和离线根本不是一个量级。没有回退链就相当于把所有失败集中爆发在用户那一端。回退链设计上要遵循一个原则备选 Agent 不能是主 Agent 的“复制品”。如果两个 Agent 用的是同一个底座模型、同一套 prompt、同一个 RAG 源那它们大概率会犯同样的错误回退毫无意义。所以我要求在路由表里配置回退 Agent 时必须保证至少一个维度是不同的要么换模型厂商要么换提示词策略要么换检索策略。只有这样回退才可能纠正真正的问题。另外回退链也不是越长越好。我测试过五级回退和三级回退五级的优势非常有限反而会拉高整体响应延迟因为每一级都要消耗一次模型调用时间。我的经验是常规任务配置两级到三级就足够了极端重要任务可以配四级再往下基本都是浪费资源。2.4 触达策略不是轮询是分级下派一开始我把回退实现成了简单的顺序轮询主 Agent 失败就找下一个再失败接着往下找。后来发现这样做有两个毛病。第一个毛病是慢。每一级都等完整超时才切换最坏情况下一个请求要等几十秒才能到最后的兜底 Agent体验非常差。第二个毛病是盲目。有些 Agent 的历史成功率是有明显差别的轮询把每一个候选摆在同等位置上等于放弃了历史数据这个重要信号。Agent-Reach 的最终策略是分级下派。我先给候选 Agent 分档主档是历史成功率高且延迟表现稳定的 Agent次档是成功率中等但能力覆盖面广的 Agent末档是专门用来兜底的通用 Agent。调度时优先压主档但如果主档 Agent 当前繁忙或刚刚出现过失败会直接把请求降级到次档不用等超时。这本质上是把实时状态和历史统计结合起来做决策而不是机械地按顺序试。为了支持这个策略路由表里每个 Agent 都会保存一个滑动窗口内的历史成功率、平均延迟、最近五分钟的失败次数。调度器在做决策时把这些信息读出来综合算一个得分再结合当前负载来决定优先级。这套机制跑下来整个系统的平均响应时间比最开始的无脑轮询降了大概 35%成功率反而还升了。3. 核心实操流程请求从进门到完成触达3.1 第一步统一入口与语义归一化Agent-Reach 的所有流量都通过一个统一的 HTTP 接口进入对外表现就是一个标准 API。调用方不需要知道自己是在调用哪个 Agent只需要提交业务请求。这种设计大大简化了上游接入业务系统只需要对接一个接口Agent 层面的调整对它们完全透明。请求进来之后的第一个工序是语义归一化。这不是个花哨的 NLP 任务我更愿意叫它“打标签”。我会准备一份任务标签表每条标签对应一类业务场景然后先用轻量级规则或者小模型做初筛只有规则无法判定的请求才送到大模型做意图识别。为什么不用大模型全量做因为成本。Agent-Reach 要处理的请求量不小如果每个请求都先送大模型做一轮意图识别再送 Agent 做一轮任务执行延迟和成本都翻倍。轻量规则可以挡掉 60% 以上有明确关键词的请求剩下 40% 再交给模型整个入口的延迟就能控制在两百毫秒以内。归一化完成后请求会变成一条标准任务记录包括任务标签、输入参数、优先级、请求来源、超时要求。这条记录后续会在整个链路里被反复引用也是日志追踪的唯一标识。3.2 第二步路由决策是怎么做出的路由决策是 Agent-Reach 最核心的一步。前面归一化只解决了“这个请求想干什么”路由决策要回答的是“谁来干”。决策过程分三层。第一层是硬匹配。任务标签去路由表里找对应的 Agent 候选组如果连候选组都没有说明这个任务还没有被支持直接返回“无法处理”。第二层是软过滤。把候选组里当前处于不健康状态的 Agent 剔除比如刚才连续失败三次、正在被限流、或者加载超时的。第三层是加权选择。在剩下的 Agent 里按照优先级、历史成功率、平均延迟、当前并发数综合打分分最高的作为主执行者其他按分数顺序排列作为回退序列。这里有个细节值得说明路由决策必须非常快因为它本身不消耗模型调用只是读配置和做内存计算。如果发现路由决策比 Agent 执行还慢那一定是设计上出了问题。我自己的实现里这一层平均耗时不超过五毫秒基本等于纯查表。3.3 第三步执行、验真、形成反馈Agent 执行是链路里最不可控的一环所以 Agent-Reach 在这里做了双保险。第一重保险是自动超时。每个任务在调用 Agent 时都会带上一个明确的超时时间不同类型的任务超时时间不一样。比如短文本分类任务我给三秒长文档提取任务我给十五秒。到了时间还没返回直接按照失败处理进入回退序列。有人担心强行超时会导致 Agent 后台任务没有停止、造成资源泄漏这个担心是对的。所以我还要求 Agent 适配层在捕获超时后主动调用一次 cancel 方法通知 Agent 释放资源虽然不一定能做到完全干净但总比干等好。第二重保险是结果验证。Agent 返回后不会直接透传给调用方而是先经过一个验证器。验证规则分两类一类是通用规则比如返回必须是合法 JSON、不能为空、不能包含错误码另一类是任务定制规则比如合同金额提取必须是一个数字、工单分类必须落在预定义类别里。只有通过验证的结果才会被标记为“触达成功”否则走质量回退。验真这一步是 Agent-Reach 和简单 API 网关最大的区别。很多系统只做了“Agent 调用了”这个层面的监控但“Agent 调用成功”和“业务真正被满足”是两码事。加验证器之后你能同时看清两层数据Agent 的物理成功率和任务的业务成功率。后者才是真正需要优化的指标。3.4 我在项目里持续追踪的数据点这套系统上线后我监控看板上常驻几组数据每一组都能回答一个关键问题。第一组是触达成功率统计口径是“收到请求并返回了通过验证的结果”除以总请求数。这是核心北极星指标。第二组是回退率也就是最终成功的结果里有多少是从备用链路来的。回退率不是越低越好太低说明所有流量都压在同一个主 Agent 上潜在风险大太高说明主 Agent 选型可能有问题。我自己的经验是回退率控制在 5% 到 15% 之间比较健康。第三组是平均链路长度也就是一个请求从开始到成功实际经过了几级 Agent。这个值如果在大多数情况下是 1说明路由决策准确如果经常是 2 甚至 3说明路由需要优化。第四组是结果拒绝率即主 Agent 正常返回但被验证器拦下的比例。这个值如果偏高说明验证规则可能太严格或者主 Agent 的能力确实存在短板。这四组数据之间是有联动关系的。某个阶段我发现回退率持续走高顺着数据一查原来是某个新版本的基础模型对特定输入经常输出空列表验证器全给拦了。如果只盯着最终成功率完全发现不了这个底层问题。4. 最小可用版本实操我如何在本地跑通 Agent-Reach4.1 环境与选型如果你也想快速复刻一个最小可用的 Agent-Reach不需要搞特别重的架构。我刚开始做原型的时候只用了一台普通开发机Python 3.10 环境核心依赖只有fastapi、asyncio和一个内存版本的路由表连数据库都没有。Agent 这边先做了两个 mock 实现一个模拟稳定 Agent一个模拟偶发超时的 Agent用于验证回退逻辑。等到逻辑跑通之后再逐步加配置中心、持久化、真实 Agent 接入。我强烈建议任何人在做这类系统时都先从一个最小闭环开始不要上来就搞 K8s、消息队列、注册中心这些东西。调度逻辑没验证之前基础设施再完备也白搭。4.2 数据结构设计最小版本需要三个核心数据结构Agent 注册信息、路由规则、任务记录。Agent 注册信息描述一个 Agent 的基本能力我放在一个 dataclass 里字段不算复杂但每个都有用from dataclasses import dataclass, field from enum import Enum import datetime class AgentHealth(Enum): HEALTHY healthy DEGRADED degraded DOWN down dataclass class AgentUnit: agent_id: str name: str task_tags: list[str] # 能处理的业务标签列表 priority: int 1 # 默认优先级 timeout_seconds: float 10.0 # 调用的超时控制 health: AgentHealth AgentHealth.HEALTHY consecutive_failures: int 0 last_called_at: datetime.datetime | None None路由规则描述任务标签和 Agent 候选之间的关系。我直接用字典保存简单直观ROUTES { contract_extract: [agent_a, agent_b, agent_fallback], ticket_classify: [agent_c, agent_a], sql_generate: [agent_d, agent_fallback], }任务记录则是每一条请求在系统内的完整流转痕迹dataclass class TaskRecord: task_id: str tag: str payload: dict status: str pending # pending | running | success | failed attempts: list[dict] field(default_factorylist) final_result: dict | None None4.3 核心代码骨架下面这截代码是整个调度器的核心循环逻辑上就三步取出路由链逐个尝试验证通过即返回。import asyncio import logging import random # 模拟 Agent 执行 async def call_agent(agent: AgentUnit, payload: dict) - dict: if agent.health AgentHealth.DOWN: raise RuntimeError(agent is down) # 模拟偶发超时 await asyncio.sleep(random.uniform(0.2, 1.5)) if random.random() 0.2: raise TimeoutError(simulated timeout) return {agent_id: agent.agent_id, content: processed okay} # 简单结果校验 def validate(result: dict) - bool: if not result: return False if content not in result or not result[content]: return False return True # 带回退链的调度 async def execute_with_backup(task: TaskRecord, chain: list[str], agents: dict[str, AgentUnit]): for idx, agent_id in enumerate(chain): agent agents[agent_id] try: result await asyncio.wait_for( call_agent(agent, task.payload), timeoutagent.timeout_seconds, ) except asyncio.TimeoutError: logging.warning(f[{task.task_id}] agent {agent_id} timeout) task.attempts.append({agent: agent_id, status: timeout}) continue except RuntimeError as e: logging.warning(f[{task.task_id}] agent {agent_id} error: {e}) task.attempts.append({agent: agent_id, status: error}) continue if validate(result): task.status success task.final_result result task.attempts.append({agent: agent_id, status: success}) return result else: task.attempts.append({agent: agent_id, status: invalid_result}) logging.info(f[{task.task_id}] agent {agent_id} returned invalid result) task.status failed return None这段代码看起来简单但基本覆盖了核心要点asyncio.wait_for做超时控制attempts 记录完整的调用链validate 负责结果验真。真正上生产时你只需要把这段逻辑替换成消息队列驱动、重启恢复、分布式锁等加强版本思路是完全一致的。4.4 本地沙箱验证代码写完以后我在本地建了一个沙箱场景做验证。设置了三个 Agentagent_a模拟高质量 Agent成功率 95%速度较快agent_b模拟备选成功率 85%偶尔超时agent_fallback模拟兜底 Agent成功率 99% 但速度很慢。然后往系统里灌了 1000 个模拟请求。跑完以后看数据非常有意思如果不看回退主 Agent 单跑成功率差不多是 95%但加上验证器之后实际触达成功率在 90% 左右因为还有一部分返回了但内容不对。启用了回退链后最终成功率稳定在 99.5% 以上平均链路长度是 1.15。这个验证说明两件事第一回退链确实能把成功率拉起来第二验证器消耗掉的一部分成功率恰恰说明单看 Agent 请求成功率会产生幻觉必须加结果校验。5. Agent-Reach 常见故障与排查实录5.1 响应超时连锁反应Agent-Reach 上线初期最典型的问题就是超时配置不合理。有的 Agent 实际执行需要二十秒我给它配了五秒超时导致大量请求被误判失败触发回退回退 Agent 也不一定能处理最终成功率反而不如单 Agent 直连。排查思路是看 attempt 记录。当时我拉了两小时的日志发现大量 attempt 的 status 都是 timeout而且都集中在同一个 Agent 上。再一看这个 Agent 的平均服务耗时已经涨到十几秒了原因是对接的第三方知识库接口变慢。解决方式分两步先把超时时间调整到跟真实 P95 延迟匹配避免误杀然后去推动这个 Agent 的内层接口做缓存和优化。这里想提醒一句超时时间不是越短越好。它必须基于 Agent 的真实性能数据来定而且要做动态调整。Agent-Reach 每次调用都会记录耗时分布超时设置应该跟着这个分布走比如max(平均耗时 × 3, 平均耗时 10秒)。5.2 语义路由匹配失败另一个高频问题是任务标签匹配不上导致请求走到“无法处理”分支。有一次同事新上线了一个 Agent能力标签写的是stock_analysis但业务方实际提交的请求经过归一化后打的标签是market_trend_analysis俩标签八竿子打不着新 Agent 完全接不到流量。这种问题的根源是标签体系没有统一治理。Agent 注册时的能力标签和前端请求归一化的任务标签必须来自同一份字典。我在修复这个问题之后加了一个强制约束所有 Agent 在注册时必须从全局标签库中选择能力不能自己造新词。同时加了一个离线任务定期扫描路由表找出“有 Agent 挂了但没在库里的标签”和“有标签但没有任何 Agent 覆盖”的洞输出成报表。5.3 结果“假触达”这类问题最隐蔽。表面上看 Agent 返回了 200验证器也没有报错但实际结果是一堆模板套话或者错误的空值。比如让 Agent 提取一份合同里的违约金比例它返回了一个句子“根据合同条款违约金比例见附件”内容非空但根本不是可用的结构化数据。后来我在验证器里加了一层 schema 校验。针对这种结构化抽取类任务要求返回的字段必须符合预设的 JSON Schema包括字段名、类型、数值范围。比如违约金这个字段定义为number类型范围 0 到 100单位是百分比。这样一旦 Agent 返回的是句式文本校验直接失败走回退链路。对于无法用 schema 严格约束的开放式任务我加了一个置信度阈值低于阈值的也要走回退。这个改动的启发是结果验证的严苛程度要跟任务性质挂钩。越结构化的任务校验规则越严越开放的任务宁可多花一次回退的代价去换更安全的输出。5.4 并发挤压导致挂起还有一次线上的表现是请求量一大整个链路突然变得极其缓慢但看每个 Agent 的响应时间又都正常。排查到最后发现是某个外部接口服务限流了。平时 QPS 没问题一旦 Agent-Reach 发起并发的模型调用立刻触发限流大量的请求卡在等待上超时连坐回退链又叠加更多调用压向同一个外部服务最终雪崩。这个问题的解法分两层。第一层是整体限制 Agent-Reach 对下游的并发数用一个信号量把它控制在外部服务的 QPS 阈值以下超出的部分排队或者拒绝。第二层是把回退链的调用做退避处理比如主 Agent 失败后等三百毫秒再调用备用 Agent避免同一时间窗口内对下游造成二次冲击。6. 复盘与扩展Agent-Reach 这一步走完后的体会6.1 我最大的体会覆盖面赢在系统层而不是模型层做完 Agent-Reach 之后我有个很强的感受在单体 Agent 推理能力接近的背景下拉开差距的东西不是模型本身的聪明程度而是系统层面对覆盖面的控制。一个成功率 90% 的 Agent加上合理的回退链和结果校验能被系统撑到 99% 以上的可用性不同风格的 Agent 配合能覆盖单一 Agent 的盲区。这就像运维上的一个常识服务器故障是必然的但业务不中断是可以靠高可用架构实现的。Agent 也一样模型出错是必然的但业务不出错是可以靠调度系统实现的。6.2 项目后续可以扩展的三个方向如果继续往下做我觉得有三个方向价值最大。第一个是可视化触达链路。当前 only 通过日志能看 attempt 记录不够直观。可以把每个请求的调用链、每层决策的原因、每次回退的触发点画成一条链路图这对于排查路由错误和 Agent 效果问题会非常高效。第二个是基于反馈动态调整路由权重。现在的方案是每五分钟统计一次重算权重其实可以更实时。比如用滑动窗口在线更新发现某个 Agent 最近五次里有三次失败立刻把它的权重降下来让流量自动迁移到健康 Agent。这其实就是简单的自适应负载均衡。第三个是多目标路由。现在主要看成功率和延迟未来可以加入成本维度。同一个任务可能有高成本高质量 Agent 和低成本中质量 Agent 可选路由系统可以根据当前预算和业务要求在成本和质量之间做权衡。这个对追求 ZJB总体拥有成本的团队特别有价值。6.3 给想入坑的人一个最小切入建议最后给想自己做这套东西的同学一个建议不要一开始就照着生产环境的标准去设计。先用最快的时间把最小闭环跑通我上面给的代码骨架已经足够用了。在这个模型里你只需要做到三件事统一入口、路由标注、回退执行。做完之后立刻把真实 Agent 接进来看它能不能覆盖住你的核心场景。然后再一点一点加结果验证、动态权重、并发控制、监控看板。很多团队第一步就没走对上来就搭了一堆基础设施结果调度核心逻辑还没验证后面想改都难。Agent-Reach 这类系统的命门不在于技术复杂度而在于你是否把“触达”这件事当成一个完整链路去设计。链路在系统就稳。