Agent-Reach:多Agent协同的智能体统一调度平台实践

发布时间:2026/10/9 11:12:48
Agent-Reach:多Agent协同的智能体统一调度平台实践 前两个月我们团队上线了第17个AI智能体。按理说项目多了是好事但那一刻我反而心里发凉17个Agent分属四个小组有的用LangChain写的有的是Dify托管的还有两个是直接裸调大模型API——没有统一入口没有统一协议出了问题要在几套日志里来回翻业务方想调用某个能力得先问十个人才知道该打哪个地址。于是我们动手做了一个内部项目就是标题里这个Agent-Reach。Agent-Reach的定位很简单一个面向智能体触达与调度的统一平台。它把散落在各处的Agent收编到一套标准协议下对外暴露一个稳定的服务入口对内负责任务路由、状态追踪、超时重试和失败回调。它不解决单个Agent“聪不聪明”的问题解决的是多Agent协同过程中最折磨人的工程混乱。这篇文章我把整个项目的设计思路、核心代码、踩坑记录和上线运维经验完整写一遍。适合正在做Agent落地、做团队AI能力中台、或者已经发现Agent一多就开始失控的团队参考。不吹不黑技术点都不高深但每一条设计背后都是实际换来的教训。1. 为什么Agent越多越失控从17个智能体到一套标准入口1.1 智能体落地真正的瓶颈不在模型过去一年大模型能力进化速度快得离谱但真正在企业里把Agent用起来卡点反而不是模型的推理能力。我在几个项目里都见到同一个现象单点Demo跑得很漂亮一旦要接入真实业务问题全冒出来——这个Agent走HTTP那个Agent走WebSocket这个返回JSON那个只吐字符串这个同步返回结果那个要轮询半个小时才能拿到答案。本质上每个Agent内部用什么框架、什么提示词策略、什么记忆机制那是各组的自由。但Agent与外部世界之间必须有一层稳定、统一、可观测的交互边界。没有这层边界多Agent就不用谈协作光联调就能耗掉团队大半时间。生活里有个很好的类比手机充电口。如果每个厂商各搞一个接口用户出门得带一堆线体验稀碎。Type-C能普及不是因为它充电速度碾压一切而是因为它是统一标准。Agent-Reach想做的就是智能体世界的Type-C接口。1.2 Agent-Reach一个统一触达层所以Agent-Reach从一开始就没打算做“又一个Agent框架”。它不是LangChain的替代品也不碰提示词编排和RAG它是站在所有Agent之上的一层调度与触达底座。名字里的Reach是“触达”的意思包含三层含义让业务方能够触达每个Agent的能力让Agent能够触达它需要的工具和数据让一条任务能够被稳定地触达正确的出口并拿到结果。项目核心就做四件事统一接入、统一消息、统一路由、统一追踪。接入统一之后上游业务方不再需要关心某个能力到底由哪个Agent提供、跑在什么框架里只需要按Agent-Reach的规范发一条消息剩下的路由和调度全部交给平台。这样做的直接收益是新Agent上线从按周计算缩短到按天计算——因为接入方不需要再等业务方改联调代码。1.3 适合谁用不适合谁用这个设计不是万能的。如果你的场景只是“一个Agent 一个应用”没有必要引入Agent-Reach直接调API更省事。但如果你有超过三个Agent要被多个业务方共享或者已经开始出现这几个问题没有统一入口、问题难以定位、每个Agent各自做鉴权和限流、状态无人追踪那Agent-Reach这套思路就非常对症。我们团队当时还有一种特殊需求部分Agent由外包组开发代码质量参差不齐。统一触达层相当于把质量边界卡在了适配器上内部实现再乱只要外部协议规范风险就可控。这个点在后来的运维中帮了我们大忙。2. Agent-Reach的核心设计做“调度与触达”的薄层2.1 四层结构与消息流转Agent-Reach整体分四层接入层、触达协议层、调度层、治理层。接入层给各Agent提供SDK和适配器负责建立连接、注册能力、上报心跳。触达协议层定义统一的消息模型、回调规范、错误码体系是所有Agent都遵守的“普通话”。调度层负责任务队列、路由选择、超时控制、重试策略、限流熔断。治理层负责鉴权、审计、可观测指标、日志追踪。一条消息的流转大致是这样业务方调用Agent-Reach的入口API平台先做鉴权然后根据请求里的意图找到哪个Agent有能力处理把消息投递到该Agent对应的队列接着触发适配器把平台协议转换成Agent内部能理解的格式Agent处理完后再按回调规范把结果返回平台平台最终把结果同步给上游或者通过回调通知。这个过程中每跳一步都会打点记录这也是后面排查问题的底气所在。2.2 统一消息模型是地基整个平台最核心的部分不是代码而是消息模型。模型定好了后面所有环节都顺模型定烂了后面所有环节都在还债。我们最终采用的请求消息结构大致长这样{ schemaVersion: 1.0, traceId: 4f3a2b1c-xxxx, correlationId: order-10086, messageType: task.request, target: { agentId: ticket-classifier, ability: classify }, payload: { ticketTitle: 无法登录账号, ticketContent: 一直提示密码错误, channel: app }, timeoutMs: 10000, callback: { type: http, url: https://openapi.example.com/callback, authToken: xxx }, metadata: { priority: high, source: crm-system, userId: u_10086 } }几个看似不起眼其实很关键的字段traceId一次请求在平台内部流转的唯一标识所有日志、指标都会带上它没有它排查问题就是灾难。correlationId业务侧自己的请求号用来做幂等和结果对账。后面讲到重复执行问题时会发现这个字段多重要。target.agentId ability不是让调用方手动指定某个具体Agent而是声明“我想做什么”由平台根据能力注册表来路由。callback异步任务的归宿。真正生产环境里同步等结果很容易被慢Agent拖垮异步回调才是稳妥路径。响应消息也做了统一无论底层Agent返回什么格式对外都包装成统一结构里面包含状态码、业务数据、耗时、Agent元信息。这一步是兼容性的核心。2.3 适配器为什么不让Agent直接对外裸奔曾经有一个组直接把他们的Agent接口地址发给了业务方对方很高兴但我们立刻叫停了。原因很简单一旦业务方直接调AgentAgent这边的接口签名调整就没法做出问题也没人能在中间拦截更别说限流和审计了。所以Agent-Reach强制要求每个Agent通过适配器接入。适配器干的事情就三件协议转换、鉴权透传、状态上报。内部定义一个最小的适配器接口各Agent实现起来成本很低class AgentAdapter: agent_id: str capabilities: list[str] health_status: str def invoke(self, request: UnifiedRequest) - UnifiedResponse: ...实际落地时我们提供了三种内置适配器类型适配器类型适用场景实现成本HTTP JSON大多数内部HTTP服务低消息队列订阅高吞吐异步场景中函数直调Agent与平台同进程部署极低平台和Agent之间的鉴权我们统一用AK/SK签名避免直接在请求里暴露密钥。适配器里还要上报心跳平台根据心跳判断Agent是否健康健康的节点才参与路由。2.4 动态路由、超时与熔断路由是Agent-Reach最像“大脑”的地方。每个Agent注册时会提交一份能力清单平台维护一个能力到Agent实例的路由表。路由时先按能力过滤出候选集再按负载策略选一个。负载策略我们支持几种轮询、最小连接数、一致性哈希。不同场景用不同策略无状态类Agent用轮询或最小连接数有状态Session场景用一致性哈希保证同一个会话尽量落在同一个实例上。超时和熔断必须一起说。Agent处理任务超过timeoutMs后平台会先把这条任务标记为超时再决定是否重试。如果某个Agent连续一段时间内超时率超过阈值平台会对它进行熔断暂停流量分发等健康检查恢复后再放量。这个机制挽救了线上很多次事故。熔断阈值不能拍脑袋定我们采用动态统计窗口每分钟统计一次最近5分钟的失败率超过50%就熔断30秒30秒后自动半开试探。参数可以调但默认值经过了多次压测验证大部分场景下够用。3. 手把手接入你的第一个Agent协议、路由与重试3.1 第一步给Agent写一份能力清单manifestAgent接入Agent-Reach的第一步不是写代码而是写清它自己能干什么。我们设计了一份manifest文件每个Agent目录下必须放一份apiVersion: agent-reach/v1 kind: AgentManifest metadata: name: ticket-classifier version: 2.1.0 spec: displayName: 工单分类智能体 owner: 客服中台组 capabilities: - name: classify description: 对工单内容分类并给出置信度 inputSchema: ticketTitle: string ticketContent: string channel: string outputSchema: category: string confidence: float limits: maxConcurrency: 50 timeoutMs: 8000 endpoints: invoke: http://ticket-classifier.internal:8080/reach/invoke health: http://ticket-classifier.internal:8080/reach/health这份manifest有三层价值。第一层平台靠它构建路由表第二层接入方可以通过平台的目录页看到所有可用能力不用再打听第三层它强制各组在开发Agent时先把输入输出想清楚这个前置约束本身就能减少很多烂接口。3.2 第二步实现统一触达入口平台侧提供统一入口API所有请求先进这里。我们用FastAPI实现了一个精简版入口核心逻辑很有意思from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel app FastAPI() class ReachRequest(BaseModel): trace_id: str target_ability: str payload: dict timeout_ms: int 10000 app.post(/v1/reach) async def reach(req: ReachRequest, background_tasks: BackgroundTasks): # 1. 鉴权省略 # 2. 校验能力是否已注册 if req.target_ability not in ability_registry: return {code: 404, msg: ability not registered} # 3. 生成任务ID并落库 task_id create_task(req) # 4. 异步投递任务避免阻塞入口 background_tasks.add_task(dispatch_task, task_id, req) return {code: 202, taskId: task_id}注意这里的返回码是202而不是200。为什么因为Agent-Reach默认采用异步处理模型业务方提交请求平台确认已经接收但处理结果通过回调通知。刚开始有同事不理解觉得同步拿结果更简单但实际跑下来同步调用对上游超时容忍度要求太高一旦Agent慢一点上游链路全线阻塞。事实证明202配合回调是最稳的落地姿势。你可以在状态查询接口里查结果也可以等平台回调两套机制都保留。3.3 第三步路由配置与重试参数路由配置落在平台的一份路由表里我们按能力维度管理routes: - ability: ticket.classify strategy: least_conn candidates: - agent: ticket-classifier weight: 100 - agent: ticket-classifier-v2 weight: 30 # 新版本灰度 - ability: ticket.summarize strategy: round_robin candidates: - agent: summarize-agent weight: 100 retry: maxAttempts: 3 backoffMs: [1000, 2000, 4000] retryableErrors: [TIMEOUT, SERVICE_UNAVAILABLE, INTERNAL_ERROR]参数看着简单里面全是当时的坑。重试次数别瞎设三下就完事得结合Agent的平均耗时来算。我们有个Agent平均处理需要6秒第一轮压测把超时设成3秒结果每次都是假超时重试两次之后请求堆成山。重试的退避策略我们采用指数退避加抖动第一次失败等1秒第二次等2秒第三次等4秒每次加一个0到500毫秒的随机量。为什么要加随机抖动因为如果所有客户端都在精确的1秒、2秒、4秒重试会产生同频共振瞬间打爆后端的恢复窗口。3.4 第四步接入工单分类Agent一个完整案例第二步的入口只是骨架真正要跑通还需要一个真实Agent。我拿客服中台组的工单分类Agent做例子。这个Agent内部用LangChain实现输入工单的标题和正文输出分类结果。接入前它对外暴露的是乱七八糟的自定义接口接入Agent-Reach只花了不到半天。具体步骤是写manifest文件在Agent服务里加一个/reach/health健康检查端点写了个60行左右的适配器把Agent-Reach的统一请求转成该Agent内部期望的ClassifyRequest再把内部的ClassifyResult转回统一响应结构然后在平台控制台里注册能力、配置路由压测验证切流量。上线后客服系统调用Agent-Reach发一条工单分类请求流程是平台收到请求根据target.ability找到ticket.classify路由项用最小连接数策略选出一台健康的实例投递到队列适配器拿到之后调起内部AgentAgent开始执行最后适配器把结果回传给平台平台调用业务方填的回调地址把分类结果和置信度送回去。整个过程业务方全程只用跟Agent-Reach打交道。后来客服系统说“我想把工单紧急程度识别也加上”我们没有改一行业务代码只是又在平台注册了一个新Agent的能力并加了路由就上线了。3.5 压测时最该盯的三个指标接完第一个Agent别急着上线先压测。压测过程中我后面发现三个指标联动性极强入口并发、Agent平均耗时、超时时间。第一个指标是入口并发。上传压测脚本时我们一开始用了1000并发结果把只有50路并发上限的Agent打爆了平台里全是被拒绝的任务。后来把平台侧的并发限制调到Agent上限的80%才稳下来。第二个指标是Agent平均耗时。这个必须统计P95而不是平均值因为平均值掩盖长尾长尾是超时的元凶。第三个指标是超时时间。建议初始设置为P95耗时的两倍再加1秒。比如P95是3秒超时就设7秒。太紧会产生大量误杀太松又会让用户在错误面前等太久。我们给每个新接入的Agent都建了一张基线表记录P50/P95/P99、平均耗时、超时率、重试率。这张表是后面调优的决策依据也是最容易被团队忽视的资产。4. 上线后最容易踩的五个坑排查实录与对策4.1 任务一直Pending谁的问题现象业务方提交任务后状态一直是PENDING超过一分钟也没变成RUNNING。排查思路分三步走。第一步看路由表确认这个能力有没有被注册是不是该Agent组把manifest版本搞错了第二步看健康状态如果健康检查失败平台不会把流量发过去所以任务会一直卡着第三步看队列水位如果某个Agent的队列满了新任务会排队等待。我们有一次就是因为Agent节点内存泄漏健康检查还能通过因为心跳是单独的线程但实际已经无法处理新任务任务全部堆积。后来把健康检查从“进程活着”升级为“能成功处理一个探测请求”才算彻底解决。4.2 回调丢失别让结果石沉大海异步模型最怕的就是结果丢了。最早我们只在Agent处理完成后回调一次后来线上出现了几次回调失败导致业务方一直等不到结果的事故。分析日志发现回调URL偶发超时、Agent服务重启导致回调没发出去原因五花八门。对策是给回调加了两层保险。第一层回调失败自动重投递最多五次重试间隔是30秒、1分钟、2分钟、5分钟、10分钟。第二层平台保留任务的最终结果业务方可以用taskId主动查询。重试还失败就触发告警人工介入。说实话重投递也会有重复通知所以回调报文里必须带taskId和correlationId业务方侧要做一个幂等判断同一条任务的结果重复收到时只处理一次。4.3 重试导致的重复执行幂等是底线这是我在Agent-Reach里踩过最深的坑没有之一。有个自动化Agent负责发邮件通知。某次它执行超时了平台按策略重试了一次结果两次执行都成功了用户收到了两封一模一样的邮件投诉立刻过来。超时重试造成的重复执行本质上是老的经典问题“将军和传令兵”问题。你无法区分“执行成功了但回执丢了”和“根本没执行”。所以重试的前提必须是平台能确认Agent没有执行。我们的解决办法是所有写入类Agent必须实现幂等。平台在请求里带上correlationIdAgent在自己的存储里按这个字段做去重。重试前平台也会先发一个查询请求确认结果如果确认已处理就不再重试。async def dispatch_with_deduplication(task, agent): if await agent.has_processed(task.correlation_id): result await agent.fetch_result(task.correlation_id) return complete_task(task, result) return dispatch_and_wait(task)这个思路说起来简单但推下去才发现不是每个Agent都能提供“按ID查询处理结果”的接口。我们在适配器规范里把这个接口设成了必选项不实现不允许上线。强制几次之后大家都习惯了。4.4 多轮会话上下文串线灰度期出现过一次诡异的Bug客服系统里A用户的问题分类结果里混着B用户的工单内容。排查下来发现罪魁祸首是Agent内部为了省事把上下文存成了全局变量。多个并发请求进入同一个Agent进程变量被互相覆盖输出自然就串了。这跟平台没关系但问题在Agent-Reach体系里更明显——因为平台把并发拉上来了过去Demo单线程跑问题暴露不出来现在并发一高就爆。对策分两端。平台端我们要求所有有状态会话的请求必须带sessionId并且路由策略必须是一致性哈希保证同一会话落在同一实例。Agent端必须把上下文存储切到Redis或数据库任何内存全局变量都不允许。4.5 一个真实排查案例只见入站不见出站最后分享一个排查全过程。某天业务方反馈晚间高峰期工单分类任务大量超时日志里只看到Agent-Reach收到请求但看不到Agent侧有任何处理痕迹。我们打开链路日志先用traceId在Agent-Reach侧找到路由记录发现消息确实已经投递给ticket-classifier-v2这个实例。接着去v2实例查日志结果一条请求日志都没有。问题指向两个方向要么是网络层面丢包要么是适配器没有把请求转发到内部端口。继续排查发现v2的适配器里有个配置项连接池大小设置过低只有2。高峰期连接池排队适配器只是在等空闲连接而Agent内部完全没收到请求。定位后把连接池调到50请求吞吐立刻恢复。这种问题如果没有统一的日志追踪体系靠人肉对着两套日志找人非常痛苦。最后我们给所有Agent的经验是排查一定要先看“请求有没有到达”再看“Agent有没有处理”最后看“结果有没有回来”三步定位法屡试不爽。5. 可观测性设计让每一次触达都有迹可循5.1 用状态机管理任务生命周期可观测性的基础是状态清晰。Agent-Reach给每个任务定义了一套状态机任何时刻都知道任务处于哪个阶段状态含义RECEIVED平台已收到请求ROUTED已完成路由选择DISPATCHED已投递给AgentRUNNINGAgent正在处理RETRY_WAIT等待下一次重试SUCCEEDED处理成功FAILED处理失败TIMEOUT超时未完成可能后来才返回CANCELLED用户取消状态机最大的价值不是好看而是让运维人员和业务方在同一个语境下对话。业务方说“任务卡住了”我们问“卡在哪个状态”他答不上来但我们一步步查状态快照就能定位。状态迁移的每一次流转都会写事件日志附带耗时和节点信息。我们内部把这个叫“任务旅程”查一次完整旅程95%的问题都能定位到具体环节。5.2 日志规范每一条日志都带traceId没有规范的日志系统多Agent排查就是灾难。Agent-Reach上线早期我们吃过这个亏后来定了一条铁律平台和Agent的所有日志必须包含traceId、agentId、taskId三个字段并且统一输出为JSON格式方便采集检索。一个标准日志长这样{ ts: 2025-01-17T10:23:45.123Z, level: INFO, traceId: 4f3a2b1c-xxxx, agentId: ticket-classifier, taskId: task_10086, event: dispatch.success, costMs: 234, message: task dispatched to agent }日志是给机器读的不是给人读的所以格式统一比内容华丽重要得多。后续接ELK或者Loki做检索时这个规范帮了大忙。5.3 发布、灰度与降级别让Agent升级拖垮线上Agent迭代不可避免但直接换版本上线风险太大。Agent-Reach支持路由权重灰度新版本Agent先注册路由权重设成10%观察半小时指标正常后再逐渐加码。回滚也简单把路由权重调回0流量全部回到旧版本。遇到Agent故障要下线时平台支持“维护模式”Agent会主动摘除流量而不是被反复打满。这个操作必须自动通知到调度层否则消息会一直向不健康的实例投递。至于降级这块是运维里最容易被忽略的。线上流量超过平台处理能力时我们宁可快速拒绝一部分任务告诉业务方“服务繁忙请稍后重试”也不要把所有请求都收下来排队因为排队只会让所有请求都超时。限流策略我们用的是令牌桶核心原则是保住了部分请求的确定性好过让全部请求一起失败。Agent-Reach并没有多高深的技术核心就是把混乱变成统一。我在整个项目里最大的体会是Agent本身不是问题Agent之间缺乏契约才是问题。模型能力再强如果接入、路由、追踪都是一团乱麻规模化落地就无从谈起。如果你也在做Agent落地建议不要一上来就搞复杂的自动编排。先把触达层做好让每个Agent都能被稳定地调用、稳定地追踪、稳定地失败重试后面再谈编排只是时间问题。最后再分享一个小技巧把异步回调当作系统的第一公民来设计。很多人习惯把所有调用都做成同步等待觉得简单但Agent场景下耗时不可控同步模型会放大多Agent的整体延迟。从第一天起就用“202 回调 可查询”这个组合后面会少走很多弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询