ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

AI代理统一触达层:Agent-Reach中间件架构设计与实践

2026/10/8 11:47:56 拓冰建站 浏览量
AI代理统一触达层:Agent-Reach中间件架构设计与实践 去年年初的时候我在团队内部做了一个叫 Agent-Reach 的小项目。起因其实特别朴素当时我们已经上了不少 AI 代理Agent用来处理客服、催收、数据整理、工单分配这些事情但是随着代理数量变多问题也冒出来了——每个代理都是一个个“孤岛”它们能说话但够不着彼此也够不着我们公司已有的那套老系统。有的代理能调 CRM有的只会查订单有的能发邮件但没法访问内部知识库。遇到一个稍微复杂点的需求就得靠人工把多个代理的结果拼起来或者直接写死脚本来回搬运数据。Agent-Reach 这个名字起得挺直白就是把“代理的触达范围”这件事做成一个独立的中间层。它不做决策不替你写提示词也不负责训练模型它只解决一个问题让任何一个代理能以统一的方式触达到它该触达的系统、工具和另一个代理。这篇文章就把这个项目从设计到落地的过程完整拆给你看适合正在做多代理系统、或者准备把 AI 代理接入公司内部系统的人参考。1. 项目背景当 AI 代理需要“伸手够到”更多系统时1.1 为什么单代理能干活但干不了复杂的活先说我观察到的现象。单个代理跑一个简单任务比如“根据订单号查询物流状态”效果通常不错。你给它一个工具函数它通过 function calling 就能调通。但是一旦任务变成“查询物流状态如果超时了自动给客户发一封安抚邮件同时通知仓储系统检查库存”单个代理就力不从心了。不是模型能力不够而是它缺“触达”的通道。你要让代理发邮件得接邮件服务要查仓储得连 WMS仓库管理系统要通知客服又得接工单系统。这些系统的协议不一样有的走 HTTP有的走消息队列有的只提供数据库只读账号。如果把所有连接逻辑都堆在代理的代码里代理不仅会变得越来越臃肿而且每接入一个新系统都要重新发版、重新测试极其痛苦。更麻烦的是不同代理用的模型不一样有的用开源模型跑本地有的调用云端 API它们的工具调用格式、上下文长度限制、对报错信息的理解能力都不同。你没法保证每个代理都能正确处理外部系统返回的各种异常响应。1.2 Agent-Reach 想解决的三个核心痛点我当时给这个项目定了三个核心目标后来越做越觉得这三条是刚需第一统一连接层。所有外部系统都通过 Agent-Reach 暴露成标准化的“能力”代理不需要知道对方的 API 长什么样只需要按 Agent-Reach 定义的协议发起请求。第二智能路由。系统根据任务内容、代理的能力标签、当前负载情况把请求送给最合适的代理去处理。这解决了“哪个代理该干这个活”的问题。第三可靠性和可观测性。外部调用一定会失败网络一定会抖动第三方接口一定会超时。Agent-Reach 负责把这种失败对代理的冲击降到最低同时把每一次调用的链路数据都记录下来方便排查问题。打个比方如果每个代理是商店里的售货员Agent-Reach 就是商店里的内部电话系统——售货员不需要知道货仓在哪个位置也不需要会开叉车他只需要拿起电话说一句“我要三号货仓的商品”剩下的路由、调度、送达都由电话系统完成。2. 总体架构与边界它不是一个 Agent而是一张“触达网”2.1 组件的边界哪些事归 Agent哪些事归 Reach做这个项目的时候我反复提醒团队一件事Agent-Reach 要克制不能什么都往里装。它只负责“触达”不负责“思考”。具体来说我们定义了这样一条清晰的边界Agent 负责理解用户意图、拆解任务、生成中间步骤、决定下一步要调用什么能力。Agent-Reach 负责能力注册、请求路由、协议转换、负载均衡、超时重试、熔断降级、权限校验、链路追踪。这条边界带来一个好处Agent 可以随时换模型甚至换掉整个 Agent 框架只要它还按 Agent-Reach 的协议发请求就行。而 Agent-Reach 这一侧也可以在不通知 Agent 的情况下替换背后的某个第三方服务比如把短信服务商从 A 换成 B代理完全无感。2.2 数据面与控制面分离架构上我参考了服务网格的思路把 Agent-Reach 拆成了控制面Control Plane和数据面Data Plane两部分。控制面负责管理能力注册中心、路由规则配置、代理状态监控、权限策略管理。数据面负责执行接收代理发来的调用请求根据控制面下发的路由表把请求转发给目标能力提供方然后把响应原路返回。这个分离在项目早期看起来有点过度设计但等到接入的代理超过二十个之后好处就非常明显了。你可以独立升级数据面不影响控制面的配置下发你也可以单独给控制面做备份和高可用数据面即使全部重启路由配置也不会丢。数据面的核心是一个轻量级的转发引擎我们用 Go 写的因为并发模型适合处理大量长连接和异步回调。控制面则是按需加载的模块Python 写的方便快速迭代业务逻辑。2.3 一次完整调用的流转路径我举个例子假设有一个代理接到了一个用户请求“帮我查一下订单 ORD-2024-1023 的物流如果三天没更新就发短信提醒。”这个请求在第一层代理里被拆成两个动作查询物流、根据条件发短信。代理先去查物流状态这一步会走 Agent-Reach代理发出一个标准化的请求体包含目标能力标识、输入参数、调用方身份→ Agent-Reach 数据面收到 → 权限校验 → 根据能力标识查找路由表 → 发现“物流查询”能力由供应链系统的 REST 网关提供 → 数据面发起实际的 HTTP 调用 → 供应链系统返回结果 → 数据面把结果包装成统一响应格式 → 返回给代理。整个过程对代理来说就是“我调用了一个函数得到了一个结果”。它完全不知道背后还有路由查找、协议转换、网络重试这些事。3. 连接器体系让每个外部系统都有统一“插座”3.1 连接器的抽象接口设计Agent-Reach 的核心抽象是连接器Connector。每个外部系统接入的时候我们只需要写一个连接器把它注册到控制面之后所有代理都能通过这个连接器访问该系统。连接器接口在我们项目里长这样type Connector interface { // 能力标识例如 order.query CapabilityID() string // 执行调用ctx 用于传递超时和取消信号 Execute(ctx context.Context, req *ReachRequest) (*ReachResponse, error) // 健康检查控制面会定期调用 HealthCheck(ctx context.Context) error }这个接口故意设计得特别窄。我知道很多人喜欢把连接器做得很重加一堆钩子函数、事件回调、配置热更新。但我的经验是连接器越薄越好它只做三件事参数转换、发起调用、结果包装。其他事情比如重试、熔断、限流全部放在数据面的框架层处理不要写进连接器内部。3.2 三种常见连接器的实战写法我们实际项目里主要写了三类连接器第一类是 REST 连接器对接公司内部的 OpenAPI 网关。这类最简单因为大部分系统都暴露 HTTP 接口。我们在连接器里做的事情就是把 Reach 的请求体映射成目标接口的 JSON 结构处理认证签名然后解析响应。type RESTConnector struct { endpoint string apiKey string httpClient *http.Client } func (c *RESTConnector) Execute(ctx context.Context, req *ReachRequest) (*ReachResponse, error) { // 1. 构造目标请求 targetReq, err : buildTargetRequest(req) if err ! nil { return nil, err } // 2. 加上认证头 targetReq.Header.Set(X-API-Key, c.apiKey) // 3. 执行调用 resp, err : c.httpClient.Do(targetReq.WithContext(ctx)) if err ! nil { return nil, err } defer resp.Body.Close() // 4. 包装响应 return wrapResponse(resp) }第二类是消息队列连接器对接 Kafka 和 RabbitMQ。有些系统不接受同步调用只接受发消息。这种连接器处理的方式是把调用请求转成消息发出去然后监听响应主题用 correlationId 关联请求和响应。这里有个小坑我们后面踩坑部分会细说。第三类是数据库连接器。主要用于那些完全没有任何 API 的老旧系统只能给一个只读数据库账号。这种连接器需要特别小心我们会强制用只读账号并且在 SQL 模板层面做白名单校验只允许执行预先定义好的查询语句不允许代理自由传 SQL。3.3 认证与密钥管理连接器接入的时候认证是个容易被忽略的坑。尤其当你代理很多、系统很多的时候绝对不要把密钥写死在配置里。Agent-Reach 的密钥管理做了三层第一层密钥统一存放在独立 secrets 服务里数据面启动时从远程拉取不落盘。第二层每个连接器可以绑定多个密钥版本轮换时不需要停服。第三层权限校验前置到控制面代理只能访问它被授权的那些能力授权粒度甚至能细到“只允许读取不允许写入”。这一点我觉得是 Agent-Reach 最容易被低估的价值。因为代理本身是被大模型驱动的模型的输出有不确定性你真的不能保证它不会搞出一次意外的删除操作。所以权限这东西不是在代理那一层约束而是在触达层强制约束。4. 路由与调度怎么把任务准确送到能干的代理手里4.1 能力声明与动态路由Agent-Reach 的路由分为两级第一级是“任务应该找哪个代理”第二级是“代理想调用的能力应该找哪个系统”。第一级路由依赖代理的能力声明。每个代理在接入 Agent-Reach 时都需要提交一份能力描述用 Tags 或者 Semantic Descriptor 表示。例如agent: customer-service-01 capabilities: - tag: order.query weight: 80 - tag: after-sales.handle weight: 60 - tag: email.send weight: 30当数据面收到一个请求控制面会根据请求头里的目标标签结合代理的在线状态、当前负载、历史成功率算出路由分数然后选一个最优代理转发。第二级路由简单一些就是根据能力标识查注册表找到对应的连接器实例。如果同一能力有多个实例就按健康状态和权重做负载均衡。4.2 优先级、超时与预算控制多代理系统里调度不是简单的“转发”还要懂得什么时候该抢资源、什么时候该等待。Agent-Reach 在请求里加了一个优先级字段分为高、中、低三档。高优先级请求比如用户投诉升级可以插队甚至可以抢占低优先级任务的配额。中优先级是普通业务请求。低优先级是后台批量任务比如夜间数据同步、日志分析。优先级之外预算控制也很关键。每个代理每天能调用的外部系统次数是有限额的比如客服机器人一天最多发 2000 条短信。如果超额控制面会直接拒绝请求而不是让代理硬调避免月底收到巨额账单。4.3 重试、熔断与降级外部系统不稳定是常态所以 Agent-Reach 的数据面内置了完整的容错机制重试只对幂等请求自动重试比如查询类接口。非幂等操作下单、转账不自动重试宁可报错让上层代理决定怎么处理。熔断如果某个连接器连续失败超过阈值数据面会进入熔断状态直接快速失败不再实际调用。降级当核心系统不可用时可以切换到一个预设的降级响应。比如物流查询失败时降级逻辑可以返回“物流信息暂时无法获取请稍后查询”而不是把超时异常直接甩给代理。这些机制单独看都是老生常谈但在 AI 代理场景里有个特殊之处代理是语言模型驱动的它不像传统程序那样有严格的状态机。如果你的容错机制只是简单返回错误码代理可能不理解甚至自己脑补出一个错误的处理结果。所以 Agent-Reach 的错误响应里必须带走人类能读懂的提示信息而且要标准化的模版方便代理理解。提示容错逻辑一定不能直接透传第三方系统的原始报错。模型拿到“Internal Server Error”这种信息很容易做出错误的补救动作。最好统一转成“服务暂时不可用建议稍后重试”这样的语义化提示。5. 配置与部署实践5.1 最小可用配置示例Agent-Reach 的整体配置我用的是 YAML核心配置可以浓缩成这样一个例子# Agent-Reach 控制面配置 control_plane: host: 0.0.0.0 port: 8080 storage: type: postgres dsn: postgres://reach:password127.0.0.1:5432/reach_db connectors: - name: order-query type: rest endpoint: https://internal.example.com/api/orders/query auth: type: api-key key_ref: secrets/order-api-key health_check: path: /healthz interval: 30s timeout: 5s - name: email-sender type: rest endpoint: https://mail.example.com/v1/send auth: type: oauth2 token_ref: secrets/mail-token timeout: 10s retry: max_attempts: 3 backoff: exponential routing_rules: - match: capability: order.query route_to: connector: order-query weight: 100这个配置里面藏着几个注意事项key_ref引用的不是密钥本身而是密钥存储服务的引用这样可以避免密钥跟着配置文件走。timeout必须比代理侧设置的超时短。如果代理等待响应的时间是 10 秒你的连接器超时设置 5 秒这样代理好歹能收到一个超时错误而不是挂在那边毫无反应。retry.backoff选择 exponential 指数退避避免重试风暴把下游系统打死。5.2 水平扩展与状态存储Agent-Reach 的数据面是无状态的这一点让我在部署时省了不少心。任意多个数据面实例可以同时运行请求通过负载均衡器分发。因为无状态所以滚动升级很容易新版本实例起来后旧实例慢慢摘流量就行。但控制面是有状态的它要存路由规则、连接器注册信息、代理状态。我们用了 PostgreSQL 做存储并用 etcd 做配置分发。控制面在高可用模式下部署了三个实例通过 etcd 的 leader 选举机制保证只有一个实例在写其他实例作为后备。代理和 Agent-Reach 之间的通信我用的是 gRPC 双向流。代理发起调用时建立一条长连接请求和响应都在这条连接上跑。这样做的好处是省去了每次握手的时间而且支持服务端主动推送比如 Agent-Reach 可以主动告诉代理“某个连接器现在不可用请走降级逻辑”。5.3 监控面板与日志追踪没有监控的中间层就是定时炸弹。Agent-Reach 的监控体系分三块第一块是实时指标用 Prometheus 采集Grafana 展示。重点指标包括每秒请求数、P95 延迟、成功率、熔断器状态、连接器健康分。第二块是链路追踪对接 Jaeger。每个请求从代理发出到最终响应都会生成一条 trace包含路由决策、连接器调用、重试尝试等 span。出了问题直接从 trace 里看到底卡在哪一环。第三块是审计日志。所有经过 Agent-Reach 的调用请求都要记录调用方、目标能力、请求参数摘要、响应状态。这既是排查问题的依据也是合规审计的要求。6. 真实环境踩坑与取舍6.1 冷启动路由震荡Agent-Reach 上线后的第一个问题是路由震荡。具体表现是新接入一个代理实例后刚开始有一段时间流量特别不均匀有的实例被打满有的实例空转。排查后发现原因不复杂路由评分里的“历史成功率”和“负载”在一开始是空的控制面给所有新实例的初始分数都一样但一旦某个实例先被选中执行了一个慢请求它的负载分就高了然后控制面一段时间内就不选它了导致流量全部涌向剩下那个实例。解决方法是加了一个预热机制。新实例在启动后前 60 秒内只分配少量测试请求等它的指标数据积累到一定数量才恢复正常权重。这个机制看着不起眼但真正解决了多实例部署时的冷启动问题。6.2 模型网关超时吞噬的连锁反应第二个坑属于典型的“看不到根因”。有一段时间客服代理经常出现整体响应变慢但我们查 Agent-Reach 的指标连接器调用成功率很高延迟也不高。最后发现卡点根本不在 Agent-Reach而在于代理自己使用的模型网关。我们的代理会先调用大模型决定下一步动作这一步偶尔会超时。按常规逻辑这次代理调用就失败了应该报错。但问题是代理框架内部默认对大模型超时做了重试重试期间用户请求还在等待等模型终于返回了代理又回头去调 Agent-Reach而这个时候用户已经等得不耐烦了。这给了我们一个很重要的教训Agent-Reach 的容错体系不能独立设计必须和代理侧的模型调用策略对齐。最终我们把 Agent-Reach 的超时设置、重试次数、降级时机做成了可配置项并且提供一个“建议默认值”不同代理可以根据自己的模型网关特点覆盖配置。6.3 测试环境与生产环境的验证建议最后聊一点测试心得。Agent-Reach 这种中间层项目最怕的是在测试环境一切正常一上生产就出问题。我们的经验是必须做三类测试第一类是契约测试。每个连接器都要有一个 Mock 版本返回预设响应用来验证 Agent-Reach 的协议转换和容错逻辑是不是正确。第二类是混沌测试。定期在测试环境杀掉一个连接器实例、模拟数据库延迟、注入网络丢包看 Agent-Reach 是不是真的能按预设策略降级。我们有一次混沌测试真的发现Kafka 连接器在消费者组重平衡的时候会丢消息后来通过调整提交偏移量的逻辑修复了。第三类是生产灰度。不要把 Agent-Reach 一步到位接入所有代理先挑一两个低风险代理跑一周真实流量对比接入前后的成功率指标没问题再逐步放开。7. 这个项目后续还能怎么扩展Agent-Reach 做到现在最初的目标都实现了但我也看到很多可以继续做的方向。我这里说一点实际操作中的体会Agent-Reach 这一类触达层真正的价值不在于某个单独的技术点而在于它把“代理调用外部系统”这件事从临时的、一次性的代码变成了可持续治理的基础设施。目前我们正在探索的扩展方向有这几个第一把连接器的编排能力做得更强。现在的连接器还是一对一的调用下一步想支持 on behalf of 的链式调用——一个代理需要先查 A 系统再带着 A 系统的结果去调 B 系统。用 Agent-Reach 做编排代理就不用自己维护中间状态了。第二增强语义路由。目前路由主要靠标签匹配对代理能力描述的理解还是太浅。下一步准备把每个代理的能力描述转成向量用语义相似度做兜底路由。这样即使代理没有显式声明某个标签只要它的能力描述里有类似含义也能被正确匹配到。第三引入请求级沙箱。有些外部系统不能在测试环境模拟比如真实短信发送。我们想在 Agent-Reach 里加一个沙箱模式把请求复制一份发到真实系统但是把真实的副作用改成模拟输出这样既验证了链路又不会产生实际成本。最后再分享一个运维层面的小技巧Agent-Reach 的日志要按“请求”维度聚合而不是按“连接器”维度聚合。如果你只在连接器层面看日志那么当一个任务涉及多个连接器调用时你很难还原整个故事线。我们后来把每个请求的 traceId 作为主键所有日志和指标都挂在这个 traceId 下面排查问题的速度快了非常多。