ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Agent-Reach:构建多Agent协作的统一触达与调度层

Agent-Reach:构建多Agent协作的统一触达与调度层

做Agent开发三年多,我最深的体会不是模型能力进步多快,而是“Agent之间的串联”这件事,远比想象中难。公司业务线陆续上线了客服、数据分析、知识库检索、流程审批等多个Agent,各自跑得挺好,可一旦涉及协作——比如用户问一句“帮我查一下上个季度的退款率,顺便给客服团队写一份改进建议”——就发现这些Agent根本连不起来。有的只暴露HTTP接口,有的走内部RPC,有的干脆把上下文锁在本地内存里,外部根本拿不到。

我做的Agent-Reach,就是为了解决这个问题而生的一个统一触达与调度层。它不是某个具体的业务Agent,而是所有Agent的“连接中枢”:负责把散落在各处的Agent纳入一套统一协议,提供动态注册、智能路由、流程编排和全链路追踪能力。换句话说,Agent-Reach解决的是“让Agent能够触达彼此、触达业务系统、触达外部世界”这件事。

这篇文章我会把Agent-Reach从设计到落地的完整过程拆开来讲:为什么需要它、核心模块怎么设计、哪些实现细节决定了成败、上线后踩了哪些坑,以及最后沉淀下来的实测数据。无论是正在做Agent平台化的技术负责人,还是准备给自己的Agent加协作能力的开发者,这篇文章应该都能给你一些启发。

1. 从“七个Agent八个协议”说起:为什么要做这个连接层

1.1 我遇到的真实困境

年初我们做过一次内部盘点,当时的状况可以用四个字概括:各自为政。客服Agent基于大语言模型做了意图识别,对外暴露WebSocket接口;数据分析Agent封装了一堆数据查询能力,走的是gRPC;知识库Agent提供的是REST API;还有流程审批Agent,干脆嵌在OA系统内部,只支持消息队列异步触发。

业务方提需求从来不考虑技术细节。他们只问一句:能不能让所有Agent像一个人那样协同工作?我当时的回答是“能”,但回去一看代码就发现,每个Agent的接入方式、参数格式、错误码定义、上下文传递机制完全是各写各的。客服Agent的“会话ID”和数据分析Agent的“请求ID”根本不是一个概念,连最基本的链路追踪都做不了——出了问题,只能一个Agent一个Agent查日志,靠猜。

这个痛点不是买一个开源编排框架就能解决的。市面上的Agent编排框架,比如LangGraph、AutoGen这些,核心面向的是“开发一个新Agent时内部的节点编排”,而不是“把已有的、异构的、独立部署的Agent统一纳管”。我需要的是中间的一层——一个连接不同协议、统一消息格式、屏蔽底层差异、让Agent之间可以通过语义而不是硬编码来找到彼此的调度层。

1.2 Agent-Reach的定位:触达层,不是业务层

这里有一个重要的边界:Agent-Reach本身不跑任何业务逻辑,也不替Agent做决策。它只做三件事。

第一,接入与转换。每个Agent注册自己的协议类型、消息格式、回调地址,Agent-Reach负责把这些异构协议翻译成内部统一格式。第二,寻址与路由。一个请求进来,Agent-Reach根据意图、能力标签、负载状态,决定把它路由到哪个Agent,而不是在业务代码里写死“先调A再调B”。第三,编排与追踪。当任务需要多个Agent协作时,Agent-Reach负责定义流程、传递上下文、收集结果、记录全链路信息。

这个设计理念后来被证明是对的。因为把业务决策留在Agent侧,Agent-Reach就能保持轻量;把连接和调度收拢到中间层,业务方就不需要关心各个Agent的通信细节。有点像企业里的网关:你不让网关替你写业务代码,但所有流量都过网关。

1.3 为什么不用现成的服务网格或消息队列

有同事提过:这不就是服务网格加消息队列吗?直接用Istio加Kafka不就完事了?这个思路方向是对的,但实际落地会发现两个问题。

服务网格解决的是“服务间通信”的标准化,但它的设计单位是“服务”,不是“能力”。而Agent暴露出来的不是单一的RPC接口,而是一组能力描述——比如“我可以分析数据”“我可以查知识库”“我可以写邮件”。这些能力需要被描述、被发现、被按语义路由,而传统的服务发现机制是基于服务名的精确匹配,满足不了这种“模糊寻址”的需求。

消息队列能解决异步解耦,但解决不了编排问题。Kafka能保证消息不丢,但它不会帮你判断“这个任务该先调谁、失败后要不要重试、上下文怎么在不同Agent之间传递”。编排是有状态的,消息队列本质上是无状态的管道。

所以Agent-Reach最终没有做成一个单纯的中间件,而是做成了一个“有状态的路由编排平台”——消息管道只是它的一部分能力,能力注册中心、上下文管理、流程状态机这三块才是核心。

2. Agent-Reach的整体架构:连接器、路由总线与编排核心

Agent-Reach的架构可以用一句话概括:周边是连接器,中间是路由总线,核心是编排引擎。任何消息进来,先过连接器转换格式,再进路由总线寻址,如果涉及多步协作,就交给编排引擎负责状态推进。

2.1 连接器层:让异构Agent能“讲同一种语言”

每个Agent接入Agent-Reach之前,都要实现一个连接器。连接器可以理解成Agent的翻译官,把Agent原本的协议翻译成统一的平台消息格式,同时把平台下发的指令翻译回Agent能理解的参数。

我基于抽象基类统一了连接器的生命周期,核心结构如下:

class BaseConnector(ABC): # 每个连接器必须声明自己代理的Agent信息 @abstractmethod def register(self) -> AgentInfo: """向Agent-Reach注册能力描述、协议类型、健康检查地址""" # 统一入站接口:外部请求进入Agent-Reach后调用 @abstractmethod async def on_request(self, ctx: RequestContext) -> RequestContext: """将平台标准消息转换为目标Agent的实际请求""" # 统一出站回调:Agent执行完毕后的结果回传 @abstractmethod async def on_response(self, resp: AgentResponse) -> AgentResponse: """将Agent返回的结果转换为平台标准响应""" # 健康检查 @abstractmethod async def health_check(self) -> HealthStatus: """对应Agent是否存活、是否已过载"""

实际接入时,HTTP型Agent只需要实现一个把JSON消息转成POST请求的逻辑,gRPC型Agent则需要做proto到统一消息的映射,早期这部分的开发量最大。踩坑之后我的建议是:第一版先别追求全协议覆盖,把HTTP和WebSocket这两种最常见的接好,跑通再扩展。

2.2 路由总线:从“写死调用关系”到“按能力语义寻址”

Agent-Reach路由总线的核心是能力注册表。每个Agent注册时,除了元数据之外,还要声明自己的能力标签。比如数据分析Agent对应的标签是[data-analysis, sql-query, report-generation],客服Agent对应[customer-service, intent-detect, faq]。

调用方不再关心“我该调哪个Agent”,而是描述“我要做什么”。Agent-Reach根据能力标签匹配候选Agent,再结合权重和负载状态做最终选择。

比如一个自然语言请求进来,Agent-Reach会做一次意图分类,得到一个能力需求,比如“查询数据并生成报告”。这个需求会被转换成请求,内部通过哈希一致性路由分发到对应能力的Agent上。不同Agent对同一条能力请求的响应能力会被注册时的preference字段标识,路由时结合实时健康数据选择。

这个“语义路由”设计一开始大家觉得虚,但实际跑起来后收益非常明显。最直观的好处是新增一个Agent不需要改任何调用方代码,只要往注册表里登记,流量就能自动分配过去。我们第一次把智能报表Agent接入生产环境时,整个接入过程不到20分钟——这在以前至少需要协调各业务线改代码,排期两周起步。

2.3 编排引擎:把串行协作变成有状态流程

单Agent请求路由到Agent就算完事了,但真实的协同任务往往需要多个Agent依次执行。比如前面提到的“查退款率并生成改进建议”这个任务,实际流程是:客服Agent先做意图识别,识别出“需要数据分析”,然后数据查询Agent去BI平台拉退款率数据,再把数据传给生成Agent撰写分析报告,最后报告通过消息通知推送给客服团队。

Agent-Reach的编排引擎用DAG来定义这类流程。每个节点对应一个Agent的一个能力调用,节点之间通过边依赖来表达先后顺序。引擎维护每个执行实例的完整状态,包括当前执行到哪个节点、每个节点的输入输出、失败时的重试次数。

流程定义走的是可视化配置加JSON描述的方式。比如上面那个任务,流程定义大致如下:

{ "flow_id": "refund-analysis-v1", "nodes": [{ "id": "intent-detect", "agent": "customer-service-agent", "capability": "intent-detect", "next": ["data-fetch"] }, { "id": "data-fetch", "agent": "data-agent", "capability": "sql-query", "next": ["report-generate"] }, { "id": "report-generate", "agent": "gen-agent", "capability": "report-generation", "next": ["notify"] }, { "id": "notify", "agent": "notify-agent", "capability": "message-push", "next": [] }] }

引擎执行过程中所有节点状态和输入输出快照会记录下来,这样当某个节点失败时,我们不仅能准确知道失败位置,还能拿到完整的中间结果决定是重试还是降级。

这套状态机制设计并不复杂,但极其重要。没有它,你面对的就是几十个Agent、上百条调用链在故障时一片混沌,连问题都定位不了。

3. 关键实现里的那些细节:从协议设计到全链路追踪

3.1 统一消息协议的取舍

Agent-Reach的消息格式参考了主流的RPC规范,上层用JSON承载业务数据,头部统一注入路由元数据。下面这个结构基本贯穿了所有Agent间的通信:

{ "trace": { "trace_id": "550e8400-e29b-41d4-a716-446655440000", "session_id": "sess-8f9a", "seq": 23 }, "source": "customer-service-agent", "target": "data-agent", "capability": "refund-rate-analysis", "payload": { "start_date": "2025-01-01", "end_date": "2025-03-31", "dimension": "by_team" } }

这里有一个很关键的设计:trace层从业务payload中独立出来,并且整个链路中每次转发都要原样保留,只允许追加不允许改写。这是吸取了早期一个血的教训——某次客服Agent在处理会话时,随手覆盖了上游传过来的session_id,导致下游所有Agent拿到的都是新ID,一次多轮对话彻底断链,用户被迫重复了三次诉求。

业务payload则尽量保持精简。不要把Agent内部的思考链、候选结果、中间日志全塞进消息体里,这些信息又大又敏感,而且会拖垮路由性能。我们后来单独设计了一个附件存储,大体积上下文走对象存储,消息体里只带引用ID。

3.2 能力注册与心跳机制:让Agent“活着”且“可发现”

Agent-Reach注册中心存储两类信息:一类是相对静态的能力元数据,如能力标签、接口协议、超时级别、支持的输入参数模式;另一类是动态的运行时状态,如当前负载、最近一次心跳时间、健康评分。

每个Agent启动后,连接器会调用注册接口上报前一类信息,随后每5秒上报一次心跳。心跳里除了“我还活着”,还要带上当前队列长度和处理耗时,这个信息对路由决策非常关键。

比如两个Agent都声明了>

返回列表