资讯详情 Agent-Reach:为AI Agent打造可靠的触达基础设施
📅 2026/10/26 15:18:29
大概半年前我被一个看似普通的需求折腾得够呛公司内部要做一个能查订单、能发通知、能跟别的系统对话的助手。第一版我的思路很直接把大模型塞进去就完事。模型本身表现相当不错生成的话术一套一套的挑不出毛病。结果一到真实环境就露馅了——它在我面前信誓旦旦地说您的订单已发货预计明天送达实际上底层接口根本没调通数据是从一个已经废弃的服务里拿到的。那次事故让我彻底想明白一件事做Agent的时候生成能力只是最外面那层皮真正决定Agent能不能用的是它能不能够得着外部世界——工具接口、业务数据、其他Agent。也是从那时候起我开始动手做Agent-Reach。Reach就是触达这个项目解决的是Agent最容易被低估、但翻车率最高的那段路从模型的意图到外部世界的确定性动作到底由谁来保障。这篇文章是我在Agent-Reach上的完整总结包括这个项目为什么会出现、核心设计怎么拆、一套可以直接抄走的接入示例以及我在真实环境里踩过的几个大坑。适合正在做Agent应用、或者打算把Agent接进内部系统的朋友参考——尤其是那些已经发现模型很聪明但系统总是莫名其妙出错的人。1. Agent-Reach的诞生先说清楚Agent最大的痛点在哪里1.1 从一次模型什么都会、落地全翻车的现场说起先还原一下那次事故的细节。我们的场景很简单用户问帮我查一下订单OW-20240618的状态Agent按计划应该调用一个订单查询接口拿到数据后格式化回复。表面上链路是通的Agent正确识别了意图选对了工具发出了HTTP请求。问题出在请求的目标上。当时那个订单服务经历过一次迁移旧域名虽然还能解析但返回的已经是另一个项目的占位数据——状态永远是已发货物流单号永远是一个假号码。Agent并不知道自己拿到的是垃圾数据它只觉得自己完成了任务于是很有底气地编了一段合情合理的回复。最可怕的是整个过程中没有任何一个环节报警。大模型不会因为数据可疑而拒绝回答工具调用层只关心HTTP状态码是不是200业务方看结果的时候又只盯着话术是否通顺。等我们发现真实数据早就对不上了已经过去了两周多。这次踩坑让我意识到Agent系统里有一个长期被当成理所当然的部分就是工具调用和外部系统触达。大家默认模型会选对工具、默认接口会正常返回、默认拿到数据就能直接用但这些假设在真实环境里几乎全部会失效。Agent-Reach这个项目就是冲着这些默认会失效的假设去的。1.2 触达和生成是两码事为什么Reach会单独成为一个问题大模型的本质是一个生成器它根据上下文预测下一段最合理的文字。生成这个动作本身是概率性的同一个问题换一种问法、换一次采样回答可能就变了。但触达完全不是概率性的——调一个下单接口发出消息更新一条数据库记录这些动作的结果必须是确定性的、可验证的。生成要的是像触达要的是对。我见过太多团队把这两件事混在一起处理Agent的Prompt里写了一大堆如果调用失败请重试注意接口返回格式然后把工具调用的重试逻辑、超时控制、数据校验全部压给模型去临场发挥。这就像让一个文科生去写底层通信协议——不是说一定不行是这种方式完全没法保证质量和可维护性。触达涉及的是网络、鉴权、数据格式、一致性、多服务协作这些全是传统工程问题。模型可以在触达失败后生成一段抱歉的话但它不能让请求变成功。所以在Agent-Reach里我把触达单独抽象出来作为一层基础设施来做模型只负责表达意图触达层的连接器、路由、重试、熔断、观测全部由工程手段保障。1.3 我决定做的不是框架而是一层连接基础设施市面上其实不缺Agent编排框架有轻量级的、有全家桶的我也用过好几个。我的感受是编排框架解决的是让模型跑流程、调工具的问题但当工具多了、系统复杂了以后真正缺的不是怎么让模型调工具而是调工具这段路怎么才能不出事。所以Agent-Reach从第一天起就不是一个Agent框架它是一层连接基础设施。核心只做四件事连接器管理统一封装外部API、数据库、其他Agent的调用屏蔽协议和鉴权差异。路由决策根据意图、标签、权限选择合适的连接器避免模型在几十个工具里乱选。执行保障超时控制、重试、幂等、熔断、降级让每一次调用都有据可依。观测记录每一次触达都留下结构化日志出问题能定位做报表有数据。它不被任何特定编排框架绑架既可以被LangChain这类流程调用也可以被我自己的一个简单循环调用甚至可以两个Agent之间直接互调。边界划得很清楚Agent负责想和说Agent-Reach负责够得着和靠得住。2. Agent-Reach的核心设计把触达拆成四个可度量、可运维的环节2.1 连接器层先解决够得着的问题Agent要触达的每个外部能力在Agent-Reach里都被封装成一个连接器。连接器是一个很薄的适配层它要做的事情是把外部世界的复杂性隔离在统一接口后面。我看过很多人写Agent工具函数上来就直接在函数里写HTTP请求然后把requests库的Response对象原样返回给模型。你猜模型拿到的是什么一个status_code、一个可能被截断的响应体、一堆头信息。模型得自己猜这个返回到底代表成功还是失败猜错了就翻车。Agent-Reach的连接器不一样它强制要求返回值是一个结构化的ActionResult{ status: ok, # ok / failed biz_code: 0, # 0表示业务成功非0表示业务层失败 data: {...}, # 经过清洗后的业务数据 error: None, # 失败时的具体错误信息 meta: { source_ts: 2025-06-18T15:30:00Z, # 数据产生时间防止模型拿旧数据当新数据 latency_ms: 87, # 本次触达耗时 idempotency_key: a92f03... # 幂等键 } }我把这个结构称为触达协议。它的核心思想是连接器不仅要负责把请求发出去还要负责判断这次触达到底算不算成功。HTTP 200只代表网络层通了业务层成没成是另一回事。这事绝对不能交给模型去判断。下面是我常用的一个连接器骨架agent_reach.register(order.query) class OrderQueryConnector: def __init__(self, endpoint: str, timeout_ms: int 3000): self.endpoint endpoint self.timeout timeout_ms async def execute(self, request: ActionRequest) - ActionResult: # request里带有action_id、params、trace_id、agent_id async with aiohttp.ClientSession() as session: try: async with session.post( self.endpoint, json{order_id: request.params[order_id]}, timeoutaiohttp.ClientTimeout(totalself.timeout), ) as resp: raw await resp.json() except asyncio.TimeoutError: return ActionResult.failed(timeout, order.query timeout) # 业务层校验很多接口哪怕失败也会返回200 if raw.get(code) ! 0: return ActionResult.failed(fbiz_error_{raw.get(code)}, raw.get(message)) # 数据清洗只把模型真正需要的字段返回出去 data { order_id: raw[data][order_id], status: raw[data][status], tracking_no: raw[data].get(tracking_no, ), } return ActionResult.ok(data)注意代码里的几个关键点超时设置是针对触达的第一个保障biz_code的校验是把假成功掐死在连接器这一层data字段只保留必要字段防止上下文被无关数据撑爆。这些细节单拎出来都不难但组合在一起就是触达和裸调HTTP的区别。2.2 路由层让正确的工具被正确的Agent用上工具少的时候把全部工具函数塞给模型随便选就行。可一旦工具数量超过十个、几十个模型选错工具的概率会直线上升——不是它变笨了而是描述相似的工具太多了。Agent-Reach的路由层解决的就是这个问题。它把模型选工具这件事从自由发挥变成半自动决策。常用的路由策略有三类路由策略适用场景说明标签路由工具数量中等、分类明确按order/read、order/write这类标签圈定工具子集语义路由工具数量大、命名混乱用embedding计算意图与工具描述的相似度取TopK权限路由多租户、多Agent先按Agent的权限范围过滤可用工具集我的经验是标签路由是主力语义路由做兜底权限路由在任何场景下都不能省。配置上大概是这个感觉routes: - name: query_order match: labels: [order, read] semantic: 查订单、订单状态、物流信息、签收时间 connector: order.query timeout_ms: 3000 retry: max_attempts: 3 backoff: [200, 600, 1800] circuit_breaker: error_threshold: 0.4 cooldown_ms: 30000路由层在真正调用连接器之前先把候选集合缩小。比如用户问订单状态模型只需要在orderread这两个标签下的三个工具里做选择而不是在包含库存、售后、支付、物流HR等在内的三十个工具里大海捞针。这一步做完模型选错工具的概率会下降一个量级。还有一个很多人忽略的细节路由结果本身也要记录。我在观测数据里专门加了一个routed_to字段用来还原当时这套配置把这个问题路由到了哪里。排查Agent为什么调用了一个奇怪的接口之类的问题时这个字段能帮你快速界定是路由配错了还是模型在路由层之外乱调了。2.3 执行与重试把一次调用变成一次有保障的触达连接器把外部系统包了起来路由把工具选择范围缩到最小接下来真正动手调用的时候还有一个更麻烦的问题哪怕是同一个接口我上次调用成功了这次调用可能就超时了。触达不能靠运气必须有执行保障机制。Agent-Reach的执行保障包含四个层次我按优先级排一下超时控制。每个连接器都有独立的超时时间初始值建议3000ms写操作可以放宽到5000ms。没有超时的触达是事故温床一个慢接口能把整个Agent卡到天荒地老。重试。超时类错误、网络抖动类错误才值得重试业务失败比如余额不足绝对不能盲目重试。重试策略我默认用指数退避按[200, 600, 1800]毫秒的间隔试探三次避免流量瞬间打爆下游。幂等。重试必须配合幂等不然重试就是制造重复订单的工具。我给每次触达生成一个idempotency_key由trace_id、action_id和重试序号共同计算。下游支持幂等键时直接传给它不支持的话就在连接器内部做去重记录。熔断与降级。这层是给下游已经半死不活的场景准备的。某个连接器在30秒内错误率超过40%开关就打开后续触达直接走降级路径比如返回缓存数据、返回明确的故障文案不再去蹂躏下游。这套机制跑起来之后我再也没有遇到过Agent因为一个超时问题反复重试同一个接口把下游拖垮的惨剧。模型也不需要靠Prompt去理解怎么重试了它在触达层看到的结果永远只有两个形态成功的结构化数据或者失败的结构化错误。至于怎么重试成功的那是Agent-Reach的事不是模型该操心的事。2.4 观测层不盯着链路就别谈可靠性我见过太多的Agent项目上线之后只有一个最终回复是否成功的指标中间发生了什么完全是一团黑盒。模型为什么选了这个工具那次调用到底超时了没有重试了几次拿到的数据是哪个时间点的这些问题全都没有答案。Agent-Reach的观测层把每一次触达都记录成一条结构化的trace核心字段包括字段含义排查价值trace_id一次触达的唯一标识把Agent思考过程、路由结果、连接器执行串起来agent_id发起触达的Agent多Agent场景下定位是哪个角色惹的祸intent模型识别出的意图判断意图识别阶段是否就偏了routed_to路由到的连接器名称判断是路由配错还是模型乱选statusok/failed快速汇总成功率error_typetimeout/biz_error/network/empty嫌status不够细就靠它分类latency_ms连接器实际耗时延迟优化最直接的依据我自己的使用习惯是每次迭代都导一份这样的数据按error_type分组看看这周翻车翻在哪。有一周数据出来特别有意思错误类型里占比最高的不是timeout也不是network而是empty——接口返回成功但是查不到数据返回了一个空列表。这说明我们的数据覆盖出了问题跟模型一点关系都没有。如果没有观测层这种问题大概率会被当成模型答错了来处理那就真的跑偏了。观测数据还有一个高阶用法喂回给路由层。如果大量查订单的请求都被路由到了物流查询说明两个连接的语义描述太接近路由配置需要调整。观测不只是为了事后复盘它本身可以驱动配置迭代。3. 动手实录用Agent-Reach接一个查订单→发通知的真实场景3.1 场景拆解与最小配置理论讲再多不落地全是空谈。下面我把自己实际跑通过的一个场景完整拆一遍你们拿去可以直接参考。场景是这样用户问帮我看看我的订单到哪了顺便发个短信通知我。这个需求拆开看包含两个动作查订单状态发通知。对应到Agent-Reach里就是两个连接器order.query和notify.sms。某一次实际处理过程会比较典型Agent先确定意图是查询订单且需要通知然后通过路由选到order.query连接器拿到订单状态后再调用notify.sms发送短信。这中间涉及的问题包括参数从哪来、下游接口用什么鉴权、失败的时候怎么办。这些我都写进配置。3.2 自定义连接器的接入流程我假定你们公司已经有一个内部订单服务它真正的业务逻辑是post一个order_id进去、返回订单状态。下面这个连接器就是Agent-Reach里最标准的一个形态agent_reach.register(notify.sms) class SmsNotifyConnector: def __init__(self, endpoint: str, api_key: str): self.endpoint endpoint self.api_key api_key async def execute(self, request: ActionRequest) - ActionResult: payload { phone: request.params[phone], content: request.params[content], } headers {X-API-Key: self.api_key} try: async with aiohttp.ClientSession(headersheaders) as session: async with session.post( self.endpoint, jsonpayload, timeoutaiohttp.ClientTimeout(total5000), ) as resp: result await resp.json() # 关键很多短信服务在欠费、模板不对时也会返回200 if result.get(code) ! 0: return ActionResult.failed( fsms_biz_{result.get(code)}, result.get(message, unknown sms error) ) # 只返回模型需要的最小信息 return ActionResult.ok({msg_id: result[data][msg_id]}) except asyncio.TimeoutError: return ActionResult.failed(timeout, sms provider timeout) except aiohttp.ClientError as exc: return ActionResult.failed(network, str(exc))这段代码能跑但我更想让你们注意几个设计上的细节。第一短信发送是一个不可逆的写操作重试策略务必保守——如果短信服务方不支持幂等键宁可不要自动重试。我在这个连接器上直接把max_attempts配成了1反正没发出用户会再问一次总比发三条一模一样的短信强。第二返回给模型的内容只保留msg_id不把整个短信服务商的响应体丢给模型不然Agent会从里面读出一些奇怪的东西来。3.3 路由、超时和熔断的具体参数连接器写完后接下来就是路由和执行策略的配置。注意这一层才是Agent-Reach的核心价值所在每次让连接器裸奔的时候都会出事。# config.yaml connectors: order.query: endpoint: http://order-svc.internal/query timeout_ms: 3000 retry: max_attempts: 3 backoff: [200, 600, 1800] circuit_breaker: error_threshold: 0.4 cooldown_ms: 30000 notify.sms: endpoint: http://sms-provider.internal/send timeout_ms: 5000 retry: max_attempts: 1 circuit_breaker: error_threshold: 0.2 cooldown_ms: 60000 routes: - name: query_order match: labels: [order, read] semantic: 订单状态物流什么时候到发货了吗 connector: order.query - name: send_notification match: labels: [notify, write] semantic: 发短信发通知提醒我 connector: notify.sms参数看着简单但每个都是踩坑换来的。order.query是查询接口幂等天然安全所以允许三次重试但每次重试间隔要拉开——200毫秒、600毫秒、1800毫秒加起来约2.6秒加上首次调用的3秒超时用户最多等大约5.6秒。对于查订单这种场景这个延迟基本可接受。notify.sms我单独做了更严的熔断设置错误阈值降到0.2、冷却时间拉到60秒。原因是短信发送一旦连续失败往往意味着服务商那边出大事了这时候疯狂重试只会让用户收不到短信还白白烧钱。按我的经验对写操作和花钱类操作熔断阈值应该比对查操作更严格。3.4 跑通后的观测演示配置完、跑通一次完整流程后我会看观测数据。一条典型的成功trace长这样trace_id: tr_20250618_9f2a agent_id: customer_service intent: query_order_and_notify routed_to: order.query - notify.sms segment1: order.query statusok latency_ms87 segment2: notify.sms statusok latency_ms420 final_answer: 您的订单已由上海分拨中心发出预计明天送达已发送短信通知。这条trace一眼能看出两件事查订单很快但发短信相对慢占了总耗时的绝大部分。如果担心整个请求太慢优化的方向就应该是短信这道环节而不是去调大模型的Prompt。这就是观测层的意义它把模糊的Agent好慢变成了具体的notify.sms平均耗时420ms。我实际跑下来还有一些细节值得注意短信连接器第一次调用耗时3000毫秒以上一开始我以为都这样后来发现是因为短信服务商的沙箱环境冷启动慢。预热的经验是把短信服务商那边的连接预热一下首次调用前先发一条测试短信整体延迟就能降到百毫秒级别。4. 实测中踩过的坑触达失败往往不是模型的错4.1 假成功HTTP 200但业务失败这个坑我在开头那次事故里就踩过后来在Agent-Reach上又遇到一次只是形态换了。之前有一个库存查询接口设计得很不规范参数传错了、没有库存、接口内部异常全部返回HTTP 200区别只在响应体里的code字段。第一版我的连接器只看了HTTP状态码等于把所有异常全放行了。结果模型拿到code500库存充足的数据之后给用户回复了有货。这是一个非常伤害信任的错误。解决方案就是我在连接器里加的那行biz_code校验。现在所有连接器都强制遵守一个约定HTTP层面只代表网络层结果业务是否成功必须看响应体里约定的业务码。没有业务码、含义不清楚的返回一律按失败处理宁可得不到数据也不能把脏数据当成真的喂给模型。4.2 Agent在同一工具上无限循环调用有一次做客服Agent用户问的是我的优惠券为什么用不了。Agent第一次调用coupon.query拿到了一个异常状态码按说应该返回给用户该券已过期这类话。但那个模型的决策出了问题它觉得是自己调用次数不够于是反复调用同一个工具一次、两次、三次……直到token几乎耗尽才停下来。表面看是模型的错其实根子还在触达层。Agent-Reach后来加了两个防护一是每个连接器记录调用结果的指纹同样的params和同样的结果在短时间内重复出现时直接给模型附加一条警告该结果与您上次调用的结果完全一致重复调用可能无法解决问题二是给单个连接器设置最大调用次数默认5次超了就强制终止这条触达分支。这个坑让我意识到触达层不仅要管怎么成功还要管怎么停下来。4.3 上下文里塞满了JSON模型反而不会用了这是我自己设计翻车的一个坑。早期版本里查询连接器会把接口返回的完整JSON直接当作data返回给模型我心想多给点信息模型能看得更准。结果恰恰相反。有个订单接口的返回体特别庞大包含几十个字段、嵌套了三层的物流轨迹还有一堆ARRAY类型的重复数据。模型在写总结的时候被那些无关字段严重干扰把签收已超时说成了正在派送中因为它的注意力被某个状态字段吸引了。后来我在连接器里加了一个返回字段白名单机制每个连接器声明它允许返回给模型的字段白名单之外的一律裁掉。同时给长文本字段提供摘要选项。这之后模型的准确率肉眼可见地提升了。4.4 多Agent互等分布式触达里的死锁Agent-Reach支撑过两个Agent协同的场景一个客服Agent需要从客户数据Agent那边拿用户画像客户数据Agent在拿不到某条数据时又会反过来请求客服Agent提供补充信息。听起来很绕实际运行的时候出现了更尴尬的事两个Agent通过共享连接器互相等待对方的返回各自的超时设置又不一致最后整条链路卡住双方都触发了重试把系统拖到崩溃边缘。这个问题本质上是分布式系统里的死锁发生在Agent世界里时特别隐蔽。后面我在Agent-Reach里引入了一个规则多Agent协作时整个触达链路上必须携带全局trace_id并且每条子触达的超时时间必须小于链路剩余预算。两个Agent之间的互调请求都走同一个连接器由连接器记录谁在等谁一旦发现循环等待立刻给其中一个发送超时中断。做了这个之后类似死锁场景再没出现过。5. 触达质量的评测思路不要只看任务完成率5.1 触达成功率要拆成三个阶段看跟传统服务不一样Agent触达一个目标是个多阶段链路每一环都可能出错但它们出错的诱因完全不同。我在给Agent-Reach做评测的时候把触达拆成了三个阶段分别统计成功率阶段含义常见失败原因归因对象意图识别用户输入能否被正确理解为可执行意图Prompt不合适、语义模糊模型层路由命中正确触达是否被路由到正确连接器路由规则冲突、相似描述、标签配错配置层执行成功连接器调用下游是否顺利返回有效数据超时、下游故障、数据缺失基础设施层只看总任务完成率很容易掩盖问题。比如总成功率是85%你根本不知道是意图识别错得多还是路由错得多更不知道怎么优化。拆分后就通透多了明明是路由环节的问题你反而去调Prompt等于白忙活。我给自己定了一个及格线意图识别95%路由命中95%执行成功98%。执行成功率要求最高因为这里是纯工程问题理论上最可控。5.2 延迟预算多Agent协作下如何分层分配触达一旦涉及多Agent协作延迟问题就会被放大。单个Agent查一次订单只要300ms但如果它还要问另一个Agent要数据、再写回记录总耗时可能就变成自身触达对方触达通信时间的叠加。我在项目里给多Agent协作设了一套延迟预算公式明确一条完整触达的总预算比如5秒。预算分配意图识别阶段500ms、路由阶段300ms、执行阶段4200ms。执行阶段内部再按依赖链拆分每跳分配1200ms左右剩下的500ms留作缓冲。关键原则每一跳的超时时间都要严格小于链路剩余预算保证整条链路不会因为某一跳迟迟不返回而崩溃。这套东西在单一Agent场景下价值有限但到了多Agent协作它就特别重要。没有预算分层系统就像没有红绿灯的路口谁都想先走结果谁都走不了。5.3 可回滚性触达之后的事往往比触达本身更关键最后一个评测维度也是我最想强调的触达不只是把请求发出去、拿回响应这么简单它往往伴随状态改变比如发了短信、下了订单、更新了记录。这些后果无法撤回的时候触达的可靠触达标准就不只是一句成功了而是成功且没有副作用。Agent-Reach对流式写操作引入了一份事件日志每次触达在真正执行前先记录一条pending事件执行结束后把事件标记为done或failed。后续如果下游系统不一致排查人员拿这份日志就能知道某个动作到底执行没有。没有这个设计的时候我们遇到过一次短信发送状态不明的问题服务商那边超时了但短信其实发成功了用户收到两条通知还以为是系统抽风。有了事件日志和幂等键这类问题从怎么查都查不清变成了查一下日志就能给出结论。写在最后的实际体会Agent-Reach做到现在我最大的体会是大模型确实让Agent的脑子变聪明了但手脚还是那套传统分布式系统的问题——超时、重试、鉴权、数据格式、链路追踪一个都少不了。很多Agent项目翻车不是模型不够强而是触达这段路从来没人认真铺过。如果你现在正在做Agent我的建议很直接把触达层单独拎出来设计哪怕第一版只做连接器和日志这两件事后面都会帮你躲掉无数个它在胡说八道的深夜。等工具数量多起来了再把路由、重试、熔断逐层加进去。Agnet-Reach这个名字我很喜欢因为它时刻提醒我Agent的价值从来不在生成而在触达。