Agent-Reach:多智能体协作与触达的协调层设计

发布时间:2026/10/27 4:45:39
Agent-Reach:多智能体协作与触达的协调层设计 一年多前我负责的企业智能助手平台里Agent 的数量终于突破了两位数。表面上每个智能体都能独立完成自己的任务但客户真正问起来的时候你会发现它们根本不“通气”用户问一句“我的订单为什么还没发货”客服 Agent 答不上来因为查不了订单订单 Agent 查到了却没权限通知用户就算查到了也想通知它又不知道该走邮件还是走短信。这种各自为政的状态正是我做 Agent-Reach 的起点——一个专门负责解决“智能体之间如何协作、如何触达外部世界”的协调层。Agent-Reach 这个名字有两层含义一是让每个 Agent 的能力可以被其他 Agent “触达”二是让 Agent 集群真正能够“触达”用户和外部系统。它不是又一个大模型应用而是一个介于智能体与业务系统之间的调度与通道层。如果你正在做的事涉及多个 Agent、需要它们协同处理真实业务请求或者你只是想让自己的智能体项目不再只是“聊天玩具”那么这篇文章里关于架构设计、路由策略和踩坑记录的部分应该能帮你省下不少时间。1. 智能体数量上去了协作却倒退了项目的缘起1.1 我在实际项目中遇到的“智能体孤岛”问题我最初接手这个平台的时候代码库里已经有五个独立开发的功能模块一个负责意图识别一个负责知识库问答一个负责工单处理一个负责数据分析还有一个负责消息触达。每个模块单独跑都表现不错一旦串起来就出问题。典型的场景是用户说“我要投诉物流顺便看看我最近三个月退款率”按照业务拆解这应该是一个“投诉数据分析”的组合任务。但实际系统只把文本交给意图识别模块识别出“投诉”意图然后走工单流程。退款率那个需求直接被丢弃了。这是“智能体孤岛”最典型的表现——每个 Agent 只知道自己的职责边界不知道旁边还有谁也不知道如何把多余的需求转交出去。你可能会说让大模型根据用户话语自动调用工具不就行了我也这么试过但在真实生产环境里把十几个工具的调用逻辑全部塞进一个会话的上下文里很快就会发现两个问题一是上下文被撑爆成本直线上升二是模型在复杂任务里出现工具调用幻觉明明没有某个权限却偏要调用某个 Agent 的能力。还有一个更隐蔽的问题工具调用是一次性的它不关心“调用之后呢”。给用户发出一个通知通知有没有送达创建一张工单工单后来有没有被处理——这些闭环逻辑单靠 tool calling 根本管不了。1.2 为什么单靠提示词无法解决协作触达我也见过不少团队试图用“提示词工程”解决协作问题比如在系统提示词里写“你可以参考以下 Agent 的能力列表自行判断交给谁处理”。这个方案在小规模演示时能跑通但生产环境根本不可控。原因有三个第一模型对能力的边界判断不稳定同样的请求今天路由给 A Agent明天就路由给 B Agent。第二能力列表是静态的新 Agent 上线、旧 Agent 下线提示词不一定能及时同步。第三提示词只解决了“决定交给谁”没有解决“怎么交、交到之后怎么收”。一个 Agent 需要另一个 Agent 的处理结果它该同步等待还是异步轮询超时怎么办这些是工程问题不是语义问题。Agent-Reach 的核心思路就是把“谁来做”这个语义决策和“怎么做、怎么触达”这个工程执行彻底剥离开。语义决策交给路由引擎工程执行交给调度器和通道层。这样每个 Agent 只需要向注册中心声明“我能做什么”然后从调度器接收清清楚楚的任务做完以后把结果交回给调度器由调度器统一负责闭环触达。Agent 之间不需要知道彼此的存在也从根本上避免了调用链蜘蛛网。1.3 Agent-Reach 想要达成的目标清单在设计之前我给这个项目定了三个目标后面所有决策都围绕这三个目标展开。第一接入成本必须低任何一个普通 Agent只要实现一个最小的任务处理接口就能被纳入整个协调网络不需要理解全局路由细节。第二触达必须可追踪所有发给用户或外部系统的消息都要有状态已发送、已读、已回调、已失败每个状态都要有记录否则出了问题根本没法向客户解释。第三路由必须有兜底判断不了、匹配不到、执行失败的任务要有清晰的处理路径绝不能静默丢失。2. Agent-Reach 的核心架构与设计取舍2.1 四个核心组件注册中心、路由引擎、通道层、调度器Agent-Reach 的完整架构由四个组件组成每个组件拆开看都很朴素关键在它们之间的协作约定。注册中心Registry是所有 Agent 能力的登记处。每个 Agent 启动时把自己支持的任务类型、输入输出格式、可用通道、期望的 SLA 上报给注册中心。注册中心维护一张能力表不仅是简单的列表还包含能力之间的依赖关系。比方说“查物流”依赖“订单系统”Agent那注册中心会记录这种依赖路由时优先保证依赖链完整。路由引擎Router拿到用户请求后结合注册中心的能力表、历史路由反馈和当前的通道健康状态决定把任务交给哪个 Agent。路由策略不只有一种后面我会专门讲多级兜底设计。通道层Reach Channel是 Agent-Reach 里“Reach”的直接体现。它把所有触达手段抽象成统一接口——无论是发短信、邮件、IM 消息、Webhook 回调还是创建工单、调用外部 API在通道层看来都是一种“触达动作”。每个通道独立部署、独立维护健康状态互不拖累。调度器Orchestrator负责任务状态机的流转接收到任务、路由、派发、等待 Agent 处理、收集结果、触发触达、处理回调、处理超时与重试。调度器没有业务逻辑它只保证状态不丢、不重、不死。2.2 为什么选择“中心化编排 去中心化执行”架构设计阶段我纠结过一个问题要不要做成完全点对点的网格也就是每个 Agent 直接与其他 Agent 通信就像微服务里的服务间调用。后来很快否掉了这个方案。Agent 集群的特点是成员变化频繁今天新增一个数据分析 Agent明天下线一个外呼 Agent如果每个 Agent 都要维护全局路由表那每一次变更都要波及全网对运维来说是个灾难。中心化编排的好处在于所有路由决策、状态管理、重试策略都集中在一个地方逻辑没有散落出去排查问题时有一个明确的入口。代价是调度器可能成为瓶颈。但我在设计时做了两个缓冲区来缓解一是调度器本身无状态状态全部存在 Redis 里可以水平扩展二是通道发送这种耗时操作全部异步化调度器只负责出任务、收结果不阻塞等待通道响应。这样一来实测单机调度器能撑住的吞吐量远超业务需求。2.3 Agent 间的通用消息协议一切皆信封让不同 Agent 协作最重要的不是选什么语言、什么框架而是定义一套统一的“任务信封”。我在 Agent-Reach 里定义了如下核心字段{ task_id: 7f3a..., trace_id: c9d1..., intent: query_refund_rate, payload: { user_id: U_1024, start: 2025-01-01, end: 2025-03-31 }, callback: { channel: http, endpoint: ... }, ttl: 30, priority: 5 }每个字段都有它存在的理由。task_id用于幂等去重确保同一任务不会被两个 Agent 重复执行。trace_id贯穿整个链路排查问题时可以从用户请求一路追到通道发送记录。intent是任务类型的机器可读标识不是自由文本而是一个先在注册中心登记过的枚举值。payload是业务参数。callback用于异步结果回收Agent 处理完任务后不是直接回答用户而是把结果按 callback 交给调度器由调度器决定后续闭环动作。ttl是有效期防止任务在系统里无限堆积。这套约定的精髓在于Agent 不需要关心任务是谁发来的也不需要关心结果在后续如何被使用。它只需要保证“我声明过能处理 intent X那么我会在 ttl 时间内按照协议返回结果”。这种解耦使得新 Agent 接入时几乎不用理解全局逻辑。3. 触达通道层让智能体真正“够到”用户和工具3.1 通道抽象把短信、邮件、Webhook、工单统一成一个动作通道层是我花费时间最多的部分因为触达这件事表面上简单实际上坑很深。我们日常说的“给用户发个通知”在企业系统里可能意味着用户手机上应该收到带验证码的短信技术负责人邮箱里应该收到一份带附件的日报外部 SaaS 系统应该收到一条用于自动创建订单的 Webhook内部工单系统应该被插入一张新工单。这些触达方式协议各不相同、响应机制也完全不同但在 Agent-Reach 看来它们都是同一个动作send(message)。我为每个通道定义了三个核心方法send(message)发送一条消息返回一个通道侧的消息 ID。ack(send_result)处理发送回执例如 IM 通道的已读回执、短信通道的运营商回执、邮件通道的退信回执。callback(payload)处理来自外部系统的异步回调例如用户回复了消息或者工单状态发生了变更。有了这三板斧任何新的触达渠道都能通过实现一个接口接入。项目中途有个新需求通过企业微信应用消息触达内部员工。开发同学只花了一个下午就写完了企业微信通道的适配器然后注册到通道层Agent 侧一行代码都没改。3.2 通道注册与健康检查机制通道层只定义接口是不够的更重要的是它的可用性治理。早期版本里某个短信通道偶尔出现延迟但由于没有健康检查机制调度器依然会把任务分配过去导致用户长时间收不到验证码。后来我在通道层加了三级健康状态健康Health、降级Degraded、熔断Open。每个通道启动时会上报自己的元信息包括支持的消息类型、限流目标、最大的发送超时时间。通道层每 30 秒探测一次各通道的连通性并对最近一分钟的发送成功率进行滚动统计。成功率低于 80% 标记为降级低于 50% 自动熔断。熔断后的通道会在 30 秒内不接收新任务同时路由引擎会得到反馈把本应走该通道的任务切到备用通道。这张表是我在实际运维中发现最重要的指标指标健康阈值降级阈值熔断阈值发送成功率≥98%≥80%80%P95 延迟≤800ms≤2000ms2000ms回执到达率≥90%≥70%70%熔断不是最终目的最终目的是尽快恢复。所以熔断期间通道本身还会继续接收健康检查请求连续三次健康检查通过后通道自动回到健康状态无需人工干预。3.3 回调编排触达之后的异步闭环“发出去之后呢”这个问题最容易被忽略但恰好是 Agent-Reach 和普通消息推送工具拉开差距的地方。举个例子用户通过客服 Agent 发起退款申请Agent 处理完创建了退款工单然后通过邮件触达用户“您的退款申请已受理”。到这里流程并没有结束三天后用户回复邮件追问“退款怎么还没到账”这时需要有一个机制把这封回信重新变成一个任务路由给退款状态查询 Agent。这个能力在 Agent-Reach 里叫作“回调编排”。每个触达动作都可以绑定一个回调意图外部系统的任何响应都会被打包成一个新事件进入调度器的队列。调度器会先查这个事件属于哪个 trace_id找到原始任务的上下文然后把事件作为新任务的 payload重新走一遍路由。这个机制极大地减少了状态管理的负担。普通的消息推送工具只负责“发出去”用户回复了跟它没关系。但在 Agent-Reach 里一次触达是一个完整会话的起点用户的每一条后续反馈都不会丢。3.4 通道层接入的最小代码示例我用 Python 写了一个最小的通道适配器来演示接入过程。这也是后来给团队新成员的第一份入门文档。# reach/demo_notify_channel.py from reach.sdk import ChannelBase, Message, DeliveryReceipt class NotifyChannel(ChannelBase): property def channel_type(self): return notify def send(self, message: Message): # 此处对接真实的推送服务 remote_id push_service.submit( user_idmessage.to_user, contentmessage.content, prioritymessage.priority ) return DeliveryReceipt(sentTrue, remote_idremote_id) def ack(self, remote_id: str): state push_service.query(remote_id) return state # delivered / read / failed def callback(self, event: dict): # 用户点击、回复等异步事件统一包装 return self.new_callback_event( event_typeevent[type], payloadevent[data] )接入之后还需要在启动脚本里注册from reach.sdk import start start(registry_urlhttps://registry.internal/reach, channels[NotifyChannel()])通道层会把这个 Agent 的通道能力上报注册中心。路由引擎在派发任务时就会知道这个 Agent 不仅会处理退款查询还具备触达用户的手段。这里有个细节通道类型和 Agent 能力是两个维度一个会话类 Agent 可以不具备发送通道只负责生成回复内容而通知类 Agent 可以没有复杂的语义理解能力只负责把文本送到指定用户。两者通过任务信封组合在一起。4. 路由策略如何把任务交给最合适的 Agent4.1 基于能力声明加意图匹配的两段式路由最初我天真地以为路由就是把用户请求丢给大模型让它判断交给谁。实测下来准确率不低但延迟和成本都不好看。后来采用了“两段式路由”第一段是精确匹配第二段是语义兜底。精确匹配依赖一个注册表里面记录了每条意图到 Agent 的映射关系。比如intent query_refund_rate映射到data_analyzer这种映射关系稳定、快速、可查。但用户的话术千变万化不可能每个意图都被准确识别所以第二段语义兜底就很重要当精确匹配失败时路由引擎会把用户请求向量化与注册中心里每个 Agent 的能力描述做一次相似度召回取 Top 5再由一个小型排序模型结合历史路由反馈进行打分。两段式的好处很明显精确匹配的延迟稳定在 10ms 以内覆盖了大约 70% 的流量语义兜底慢一些但在 300ms 左右能完成覆盖剩下 30%。两者的比例可以根据业务需求调节如果新意图频繁出现就把新映射直接插入注册表让精确匹配的覆盖率逐步上升。4.2 多级兜底链路精确路由、语义路由、人工转交不管路由引擎设计得多聪明总会有匹配不到或者拿不准的时刻。Agent-Reach 里有一条铁律任何任务都不允许无下落。路由链路的最后一级一定是人工转交。完整的兜底链路是这样的用户请求进入路由引擎后首先匹配精确映射表如果没有精确命中走语义召回加打分如果打分结果超过阈值就派发给对应 Agent如果得分都不高再尝试一次模糊意图分类看能不能归并到一个已有的通用意图上如果还不行就把任务标记为UNROUTABLE进入人工队列。我见过很多项目在这个环节选择让大模型“再想想”结果是不仅延迟飙升还经常把不相关的 Agent 拉进来。倒不如明确承认“当前系统处理不了”直接转人工。在产品侧人工转交不一定意味着用户体验失败因为转交的同时会把用户请求、已尝试过的意图映射、相关日志都打包给人工坐席让坐席能在完整上下文里介入。4.3 路由结果的缓存与反馈回写路由引擎还有一个非常实用的机制路由结果缓存。同样一个请求“我要投诉物流”第一次出现时系统的处理路径是“意图识别→精确匹配→派发给投诉 Agent”耗时可能有几百毫秒。但这条路径会被缓存下来第二次再有用户说类似的话直接命中缓存路由时间降到个位数毫秒。更关键的是反馈回写。Agent 执行任务之后会返回一个执行结果报告标注任务是否完成。路由引擎会把“用户请求→路由结果→任务完成情况”的元组写回反馈表。时间长了这张反馈表就成为一个真正贴近你业务的路由偏好库。有些请求表面上是 A 意图但 A 类 Agent 经常执行失败B 类 Agent 反而成功率很高反馈表会把这条信息记录下来逐步调整路由权重。这个机制有点类似推荐系统的隐式反馈但它调整的对象是路由决策而不是用户兴趣。4.4 冷启动阶段的规则补全新引入的 Agent 经常面临没有历史反馈的问题这时候向量召回很容易不靠谱。我的做法是要求每个 Agent 在注册时提供一份“能力种子规则”里面写明自己最擅长处理的三种请求场景并配上至少三个示例问题。路由引擎启动时会先用这些种子规则构建一份初始的精确映射表。虽然覆盖面有限但能保证 Agent 刚上线的第一天不会完全无人问津。等线上真实请求积累一段时间后种子规则的权重会被真实反馈逐渐替代。举个例子接入了 一个新的报表 Agent它的种子规则可能是“支持按时间范围统计退款率、转化率、客单价”。第一个真实用户请求可能措辞是“帮我看看这两个月退款情况”向量召回能match到但如果用户说的是“这波活动亏没亏”向量召回就未必能 match。经过几次人工转交和反馈回写后路由引擎会慢慢学会把“亏没亏”这类话术也归到这个 Agent。冷启动期一般需要一到两周之后路由准确率会进入一个比较稳定的平台期。5. 实测表现与踩坑记录5.1 压测数据与线上指标Agent-Reach 在测试环境跑了两轮压测。第一轮是纯调度压力模拟每秒 2000 个任务进入调度器Agent 侧用 Mock 实现单机调度器把任务正常派发出去P95 延迟稳定在 34ms没有出现积压。第二轮是完整链路压测加上真实通道层、Redis 状态存储和回调模拟吞吐下降到每秒 900 个任务瓶颈不在调度器而在 Redis 的写入竞争。后来把状态做了分片按task_id的哈希拆到四个 Redis 实例吞吐恢复到 3500 每秒已经远远超过我们线上最高峰的峰值。上线后观察到的核心指标大概是这样指标结果精确路由命中率73%语义路由兜底命中率22%人工转交比例5%平均端到端处理延迟1.8s触达成功率97.4%回调闭环率88.6%端到端延迟 1.8 秒里其实大部分来自 Agent 自身调用大模型的时间调度和路由贡献了不到 200ms。这说明 Agent-Reach 作为协调层本身没有引入明显的性能损耗。5.2 我踩过的三个坑第一个坑是会话状态不同步。上线早期用户在一个会话里连续问了三个问题分别路由给三个不同 Agent。问题来了第二个 Agent 拿不到第一个 Agent 产生的上下文回答完全脱节。我在设计任务信封时只考虑了单个任务的完整性忘了考虑跨 Agent 的会话连续性。后来在调度器里增加了一个“会话快照”机制每个任务派发时除了带参数还附带一个会话上下文的只读快照Agent 对快照有任何修改都通过回调写回调度器由调度器合并更新全局会话状态。这个设计既避免了多个 Agent 同时写同一份状态造成的竞争又保证了每个 Agent 都能看到之前发生了什么。第二个坑是通道重试风暴。某个短信通道出现故障发送超时调度器的重试机制被触发。按照我最初的设计重试次数上限是 3 次间隔是 1 秒、5 秒、10 秒。但问题是积压的超时任务太多同一时间几百个任务触发重试直接把本来已经喘不过气的短信通道彻底压垮也把上游运营商接口打到了限流。这是我第一次深刻理解“重试风暴”的可怕。修复方式是引入熔断器连续失败超过阈值后通道标记为熔断所有进入该通道的新任务立即走备用通道或排队等待熔断期间不允许任何重试。把“重试”优先级从“完成任务”降为“恢复通道”。第三个坑是意图漂移。同一个用户请求在大模型版本升级前后意图解析结果会发生变化。最典型的是“查物流”升级前被归类为query_logistics派发给物流 Agent升级后归类为query_order派发给订单 Agent但订单 Agent 根本没有物流查询能力于是任务失败。这个坑很难通过测试提前发现因为大模型底座的概率性行为就摆在那里。解决办法是给意图解析结果加一个置信度阈值低于阈值的请求不强求归类而是走“候选集合并派发”模式同时派发给两个 Agent先返回有效结果者胜出另一个结果进缓存备用。虽然浪费了一点计算资源但用户体感的大幅提升值得。5.3 为什么这些问题单靠单元测试发现不了回头看这三个坑有一个共性它们都发生在多个组件的交互边界上不是单个组件内部逻辑能暴露的问题。单元测试能保证一个 Agent 输入 X 输出 Y但保证不了两个 Agent 之间的上下文衔接能保证通道发消息的代码逻辑正确但保证不了它在外部分子故障时的行为。这也是为什么我建议凡是做多 Agent 协作系统的项目一定要在测试环境模拟“下游全面变慢”和“某个 Agent 悄悄下线”这类混沌场景而不是只测正常路径。我在 Agent-Reach 里专门建立了一个故障注入测试套件每周跑一次随机故障演练。这比任何静态检查都能更快地暴露设计缺陷。6. 关于扩展方向和个人体会6.1 多租户隔离与权限边界目前 Agent-Reach 部署在一个企业客户内部使用但我已经在考虑把它产品化成多租户服务。多租户带来的最大挑战不是技术上做不做得到而是权限边界怎么定义。每个租户的 Agent 名录不一样能力声明不一样路由策略也不一样。如果只是把数据隔离做好、让 Task Envelope 加上租户 ID那还比较简单真正的难点在于路由引擎里如果使用了共享的大模型做意图识别如何保证一个租户的数据不被另一个租户的任务影响。这部分其实和数据隐私架构强相关也是我的下一步研究方向。6.2 触达成本控制触达动作是有实际成本的发短信要花钱、外呼要花钱、OCR 能力调用也花钱。早期我们没有成本维度路由引擎只关心“能不能完成”不关心“花多少钱能完成”。后来计费部门找到我说某个渠道的费用超标才发现需要把成本预算纳入路由决策。在设计上每个通道可以上报一个成本系数路由引擎在候选 Agent 打分时把“完成任务”和“控制成本”做一个加权平衡。优先级高的任务不惜成本普通任务优先走低成本通道。这个改动不难但收益非常直观。6.3 最后想分享的两条设计原则一是不要把业务状态放进 Agent 的内存里。Agent 会下线、会升级、会被流量打死它的一切记忆都要放在外部存储里通过调度器统一读写。这样任何 Agent 出现问题拉起一个新实例就能无缝接管。二是先保证不丢消息再追求智能路由。哪怕路由判错了只要任务没有丢就有机会通过人工转交或者后续调度补偿回来一旦任务静默丢失连排查入口都没有。这两条原则帮我在无数次反问“为什么这个任务没闭环”的时候能迅速定位到问题在哪一环。如果你也在搭多智能体系统我强烈建议你先把触达通道和闭环追踪做扎实再折腾花哨的路由策略。触达能力是智能体从“演示玩具”走向“生产工具”的那道门槛。Agent-Reach 这套设计算不上多高深但它是被真实的业务请求和客户投诉一点一点打磨出来的希望这些记录能让你少走几步弯路。