ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent-Reach:多智能体协作中触达层架构的设计与实战

Agent-Reach:多智能体协作中触达层架构的设计与实战 1. Agent-Reach要解决什么多智能体协作的触达困境如果你最近在折腾AI Agent你多半会遇到一个尴尬的场面模型智商看起来在线工具函数定义得也没毛病但Agent就是“够不着”外部世界。要么是工具描述太模糊导致参数幻觉要么是多Agent之间互相等对方回话等到超时再要么是接了一个新数据源之后整个调用链路乱成一锅粥。我去年年底开始做一个内部项目代号就叫Agent-Reach核心目标只有一个把Agent的“触达能力”做成一个独立、可治理、可观测的分层而不是让每个Agent拿着不同协议各玩各的。先说结论Agent-Reach不是一个魔法框架它解决的是三件特别具体的事。第一让Agent能够稳定、可控地调用外部工具和数据源不管那东西是REST API、数据库、内部RPC还是网页爬虫。第二让多个Agent之间可以互相发现、互相发消息而不是靠死板的“你调完我再调”的线性编排。第三也是我最看重的让所有触达行为都有日志、有审计、有限流出问题时能在十分钟内定位到是Agent决策错了还是触达层挂了。这个项目适合谁参考如果你的团队正在做多Agent系统、智能客服、自动化工作流或者你只是在自己捣鼓一个想接十几个工具的Agent原型Agent-Reach这套设计思路都有直接可抄的部分。我会把踩过的坑、最终留下的设计、以及几个压箱底的排查技巧都摊开讲。大家看完不用照着我的代码抄但架构分层和那几张排查表建议直接拿走。2. 整体架构与核心设计触达层的分层思路2.1 为什么单独做一个触达层而不是让Agent直接调API很多初学Agent的人第一版都是这么写的系统提示词里塞一堆工具说明然后让大模型输出一个JSON代码拿到JSON之后直接requests.post调外部API。demo阶段跑得飞起等到工具数量超过10个就开始崩。崩的方式五花八门Agent自作主张添参数、工具响应太长把上下文撑爆、两个工具返回的数据格式不一致导致后续步骤全部混乱。本质问题在于Agent的“意图决策”和“外部触达”被揉在了一起模型既要判断该干什么又要负责处理协议细节而这两件事根本是不同层面的复杂度。Agent-Reach的做法是在最上层Agent和底层工具之间嵌一个“触达层”。触达层对Agent暴露的界面极其简单你告诉我你想做什么我给你一篮子可用的操作你挑一个然后把你从对话里抽出来的参数交给我。至于这个操作背后是HTTP还是gRPC、是查MySQL还是调Python脚本、要不要先刷新令牌、返回结果应该截断多少都是触达层的事。Agent只负责两件事选动作、填参数。这么拆之后有个立竿见影的好处模型需要关心的内容变少了。一个只暴露了5个精选动作的Agent比一个暴露了30个杂七杂八函数定义但上下文窗口撑爆的Agent决策准确率高得多。我实测过工具描述文本总量从6000 token压到1200 token之后工具选择准确率反而从78%升到94%。这就是触达层的价值之一替Agent挡住了噪声它不接触底层世界的混乱自然就更专注。2.2 Agent-Reach的四个核心组件整个触达层的运行时由四个组件构成它们各司其职缺一不可。能力注册中心。这是所有工具的“户口本”。每个外部能力进来之前都要在这里登记唯一标识、名称、描述、参数Schema、调用地址、鉴权方式、超时阈值、限流等级。Agent要向某个工具伸手先得在注册中心查到这个工具的“户口信息”而不是把API地址硬编码在提示词里。这个设计让新增一个工具就像在淘宝开店一样上架之后全系统立即可用。路由引擎。它负责把Agent的意图和参数翻译成真实的外部调用。路由引擎拿到Agent选定的工具ID之后会做三件事校验参数合法性、补齐默认值、执行上下文装配比如带上调用者的身份信息、trace ID。然后它按照注册中心里存储的地址发起实际调用。路由引擎是整个触达层的心脏它不能挂挂了所有Agent瞬间变“植物人”。所以我们给它配了独立进程和降级策略后面第5章会详细讲。协议适配器。这是Agent-Reach最辛苦的一层。外部世界五花八门——有老旧的SOAP接口、有标准的RESTful API、有基于SSE推送的服务、也有MCP协议的工具服务器。协议适配器的作用是把这些不同的传输方式统一封装成内部标准的“触达请求”和“触达响应”。你接到一个新协议就写一个适配器实现了两个函数send_request、parse_response就能接入。目前Agent-Reach自带REST、gRPC、MCP、数据库Query四种适配器覆盖了九成场景。任务队列与状态存储。Agent调用外部工具大多数是同步的但多Agent协作里会出现大量异步场景你派Agent A去处理一批文件结果它要花三分钟那中间这段时间Agent A的状态怎么办、结果往哪放所以我们设计了任务队列每个触达动作都是一个可追踪的任务状态会经历PENDING、RUNNING、SUCCEEDED、FAILED、TIMEOUT、DEAD_LETTER六个阶段。状态存储用的是简化版的outbox模式每一条任务记录包括输入快照、输出快照、耗时、错误信息。有了这东西你才能事后回答“昨天下午那个Agent到底调用了几次支付接口”。3. 核心实现工具接入、函数调用与MCP适配3.1 从零注册一个工具关键是把描述写“透”我们拿一个最常见的场景举例给Agent接入一个“查询订单物流”的工具。很多人第一版工具描述是这么写的获取订单物流信息。参数order_id必填字符串。这种描述模型不瞎猜才怪。它不知道order_id什么格式、不知道返回内容长什么样、不知道这个工具适不适合当前用户的问题。我建议按下面的模板来写并且在Agent-Reach的注册中心里强制执行工具名称: query_logistics 工具描述: 根据订单号查询物流轨迹。当用户询问“我的包裹到哪了”“发货没有”“物流进度”时使用。 该工具仅适用于已支付且发货的订单未发货订单请调用query_order_status不要用本工具。 一次仅可查询一个订单多订单请拆成多次调用。 参数说明: order_id: 字符串类型必填用户下单单号通常为以字母“DD”开头的18位字符串。从用户输入中抽取不要自行编造。 返回值说明: 返回包含trace_nodes数组每个节点的status表示节点状态PENDING未到达、IN_TRANSIT运输中、DELIVERED已签收。这段描述看起来啰嗦但每一句都有用。第一句告诉模型“什么时候该用”第二句做了工具边界划分防止它拿错工具第三句限定了调用频率最后参数和返回值说明直接降低幻觉概率。这里的实操心得是工具描述每多100 token单次决策质量就高一点但上下文成本也会涨。我的平衡线是每个工具描述控制在150~250 token之间清晰交代“触发条件、边界条件、参数格式、返回格式”四个要素即可多余的营销话术全删。注册完成之后Agent-Reach会自动从描述里提取关键词建索引。这样当用户消息进来时路由引擎可以先做一个粗筛只把和当前用户问题相关的3~5个工具定义塞进提示词而不是每次把所有工具全部倒给模型。这一步能把token消耗直接砍掉一大半。3.2 Function Calling循环一个可靠的回环实现工具定义好之后要让Agent跑起来核心是一个带循环的Function Calling实现。我直接贴一个简化但生产可用的伪代码大家感受一下触达层在其中的位置def agent_with_reach_loop(user_message, tools): messages [{role: user, content: user_message}] for turn in range(6): # LLM根据系统提示工具schema决策是直接回答还是调用工具 resp llm.chat(messages, toolstools, tool_choiceauto) msg resp.message messages.append(msg) if not msg.tool_calls: return msg.content # 模型觉得信息够了直接回答 # ---- 从这里开始进入Agent-Reach触达层 ---- for tc in msg.tool_calls: tool_name tc.function.name args json.loads(tc.function.arguments) # 触达层核心先校验、再装配、再执行 validated, error reach_layer.validate_args(tool_name, args) if error: messages.append({ role: tool, tool_call_id: tc.id, content: f参数错误: {error}. 请修正后重试. }) continue result reach_layer.execute(tool_name, validated, contexttrace_ctx) # 触达层还会做一件事结果摘要。太长就截断成适合放回上下文的样子 messages.append({ role: tool, tool_call_id: tc.id, content: result.summarized_text }) # ---- 触达层结束下一轮循环让模型决定下一步 ---- return 达到最大循环次数任务未完成。这个循环里有三个细节值得展开。首先是参数校验必须先于真实调用。reach_layer.validate_args会拿注册中心里的JSON Schema去校验模型填的参数类型不对、枚举值不在范围内、必填项缺失一律在触达层拦下把错误信息返回给模型让它自己改。这比直接拿着坏参数去请求外部API、然后面对一屏的500错误要优雅得多。其次是结果摘要。外部API返回的JSON可能巨大一个物流接口返回20个节点每个节点200字描述那就是4000字。直接全塞回上下文两轮之后模型就“失忆”了。Agent-Reach对每个工具都配置了一个summary策略比如物流查询只提取最近三个节点、字段名保持原样。这一步表面上是省token实际上是在保上下文质量。我见过太多Agent表现不稳定不是模型不行而是被塞了太多垃圾中间结果。第三是6轮的循环上限。模型有时候会在一个简单任务上反复横跳调了查询工具、又说要调另一个工具、然后又回来调第一个。6轮上限保证了系统在任何情况下都不会死循环烧钱。真实生产环境我可以负责任地说95%的任务在3轮内就能结束。3.3 MCP适配让Agent触达长尾工具集单独说一下MCPModel Context Protocol适配因为这是现在接入Agent外部工具最热的方案。MCP的思路是把“工具提供方”抽象成一个个MCP Server通过stdio或SSE这两种通道用JSON-RPC格式暴露工具列表和工具调用。Agent-Reach支持MCP之后你的Agent就不再局限于自己团队的API整个开源生态里别人写好的MCP Server你接上就能用。我实现MCP适配器的代码结构大概长这样class McpAdapter(BaseAdapter): protocol mcp async def send_request(self, tool_name: str, args: dict, endpoint: str): async with stdio_client(StdioServerParameters(commandendpoint, args[])) as client: session await client.get_session() await session.initialize() tools await session.list_tools() # 只暴露当前请求相关的工具给上层 target_tool next(t for t in tools.tools if t.name tool_name) result await session.call_tool(tool_name, argumentsargs) return self._parse_content(result.content) def _parse_content(self, content_list): # MCP返回的是content数组里面有text和image两种类型 texts [c.text for c in content_list if c.type text] return \n.join(texts)这里要注意一个坑MCP Server列表里可能有十几个工具但Agent-Reach注册中心里只登记了你允许Agent调用的那三个。Adapter在执行前必须做一次白名单过滤绝不能把MCP Server的所有工具无脑暴露给Agent。一个小型MCP Server往往捆绑了二十多个函数里面可能有一半是你根本不想让Agent碰的。用注册中心的白名单卡一道安全性和决策质量都有保障。另外一个实操细节MCP通过stdio连接时子进程的生命周期很烦。如果不显式管理Agent每次调用都会拉起一个Node进程两分钟之后系统里躺着几百个僵尸进程。Agent-Reach的解决方案是连接池复用每类MCP Server只起一个常驻子进程所有请求通过同一个session走。这个优化能把调用延迟从800ms压到150ms左右且进程数恒定。4. Agent间互连触达能力延伸到协作场景4.1 让Agent能互相“看到”注册、发布与发现如果触达层只处理Agent到外部工具的方向那它充其量算个好用的API网关。Agent-Reach真正有意思的地方在于把Agent本身也当作一种“可触达的资源”。也就是说Agent A需要某个能力时它不一定非得调外部API也可能直接把任务递给Agent B。为了支持这个我们在触达层之上加了一层轻量级的Agent互连机制。实现思路不复杂模仿服务注册与发现每个Agent实例启动时把自己的能力标签与回调地址注册到中心例如“负责售后”“负责价格计算”“掌握数据分析”。别的Agent发起协作请求时触达层根据能力标签做匹配找到合适的接收方把任务消息放进接收方的信箱。整个过程对发起方是透明的——它觉得自己只是调了一个叫“agent:after_sales”的工具实际触达层在背后做了一整套服务发现和消息路由。一个特别需要注意的点不要让Agent直接暴露内部地址。所有互连消息必须经过触达层转发你才能在每个环节插入身份校验和审计日志。如果A和B开了一条直连通道一旦上线之后出了数据问题你连消息路径都说不清楚。4.2 回调、任务移交与结果回传我用一个真实跑通的业务场景来演示互连的价值。假设我们有一个客服助手系统前端主Agent负责接待用户后台有一个专门处理退款的Agent B。用户说“我要退掉那双41码的鞋订单号是DD20250118001”主Agent判断这属于退款Agent的职责于是发起触达调用目标: agent:refund_agent 动作: create_refund_task 参数: {order_id: DD20250118001, reason: 尺码不合适, expected_amount: 399}触达层收到这个调用后从注册中心找到退款Agent的地址把任务写进它的队列然后立即给主Agent返回一个PENDING状态的结果“退款任务已创建任务编号TASK-8823正在等待审批。”这时候主Agent就能放心地回复用户“收到我们正在加急处理您的退款”。关键机制在回传。退款Agent完成审批后不可能直接去改主Agent的内存变量。它的做法是生成一个callback事件通过触达层的回调通道发回给主Agent的事件信箱。主Agent想不想处理这个回调取决于它自己有没有订阅。我们用了一个极简的发布订阅模型Agent在启动时声明自己关心哪些事件类型比如“refund.completed”“refund.rejected”触达层负责把事件投递到对应的信箱。这样一来Agent之间的协作是松散耦合的谁挂了都不会拖垮整条链。这种设计的另一大好处是时间线清晰。每一次互连动作都会产生一条结构化事件记录包含“谁发起的、发给谁、任务状态、回传内容”。出问题的时候拽出这条时间线就能做故障复盘完全不用靠猜。4.3 Agent互连的失败语义超时与重试Agent之间协作最大的坑是超时。Agent B是个大模型应用它处理退款审批可能要思考好几秒如果主Agent同步等B的结果整体响应时间会飙到10秒以上这在客服场景里完全不可接受。所以Agent-Reach的互连默认用异步模式主Agent发出触达调用后立刻返回后续通过事件轮询或回调来收敛结果。但异步就带来了新问题如果退款Agent收到任务后进程崩溃了任务就丢了。我们的补救方案有两层。第一层是任务持久化所有互连请求落地到消息表接收方处理完才标记completed没处理完的实例重启后会重新拉取。第二层是重试与死信发送方可以配置重试策略默认最多重试3次间隔分别是5秒、30秒、2分钟。超过3次还失败任务进入死信队列由人工介入排查。这里有一个教训别把重试间隔设成固定值瞬时故障用短间隔分布式毛刺用中长间隔阶梯式重试的恢复率比固定间隔高出三成以上。5. 安全与治理触达层的权限边界与限流降级5.1 最小权限与工具白名单触达层能力越强风险越大。一个能自由调外部API、能连数据库、还能给别的Agent派活的层如果权限控制做得稀烂那就是给攻击者递刀。Agent-Reach从第一天起就把安全设计摆在功能之前这里分享两个让我半夜睡得着觉的机制。第一是最小权限的鉴权模型。每个Agent实例在触达层都有一张身份卡标明它允许调用的工具范围、允许访问的数据域、以及每个工具的配额。例如客服主Agent被授权调用query_logistics和create_refund_task但绝不允许直接调用delete_order。这个权限在调用链上逐级传递A给B派任务时B收到的令牌里依然带着A的权限边界不能越权调用A碰不到的资源。实现时用的是类似JWT的结构触达层在路由前验签、验权限不合法直接拒绝。第二是危险工具的二次确认机制。有些工具本身风险极大比如“批量删除用户数据”“修改订单金额”。Agent-Reach允许注册中心把这些工具标记为SENSITIVE标记后路由引擎不会直接把调用请求发出去而是先落到一个pending状态由人工审批接口决定放行还是拒绝。我们上线初期有一次Agent误判用户意图差一点触发批量邮件删除就是被这个机制拦下来的。那一瞬间我就明白了Agent做得越自动化越需要有人工的刹车闸。5.2 限流、降级与熔断防止触达层被拖死触达层是Agent的双手但它自己要避免成为瓶颈。我设计了三个递进层次的保护策略。限流跑在最前面。每个外部工具都设置独立的QPS上限比如query_logistics限300次/分钟超出后触达层直接返回“工具繁忙请稍后重试”绝不把流量打到后端API。这个限流是基于令牌桶实现的每个工具一个桶简单可靠。降级跑在中间。假如某个外部API的响应时间持续超过5秒触达层会自动启动降级策略对于可降级的调用比如查询类工具改为返回最近一次的成功缓存对于不可降级的调用比如支付、退款直接快速失败并且给Agent返回一个明确的错误信息让它向用户说明“服务当前繁忙”。缓存降级需要非常谨慎只有注册中心里显式标记cacheable的工具才会启用。熔断跑在最外圈。我们给每个外部服务维护了一个连续失败计数器5分钟内失败率达到50%就打开熔断开关后续请求直接短路不再真实调用外部服务。熔断打开后每30秒放一个探测请求过去如果成功就关闭熔断让流量恢复。这个机制在依赖的上游系统抖动时保护了触达层自身的稳定性。我自己遇到过的最悬一次外部物流接口因对方升级挂了两个多小时靠熔断降级组合拳整个客服系统愣是没崩用户侧看到的只是“物流信息暂时无法获取请稍后再试”。5.3 审计日志每条触达都有迹可循最后一块安全拼图是审计。Agent-Reach每处理一次触达请求都会生成一条不可篡改的审计记录字段包括发起Agent ID、目标工具/Agent ID、请求参数摘要、返回结果摘要、耗时、状态、trace ID。这些日志会异步写入独立的审计存储和业务日志分开避免互相淹没。有人可能觉得这样太重但请相信我一旦系统里跑着十几个Agent、每天产生几十万次工具调用没有审计日志你将完全无法定位问题。有一次用户投诉“订单重复退款了”我们查了20分钟没头绪最后是靠审计日志里同一笔订单两次被提交到退款工具追溯回去发现两个Agent同时接了同一用户的消息才定位到是消息分发层出了重复消费。没有审计日志这问题根本无从查起。安全三轮车缺一不可权限、保护、观测。只做权限不做限流触达层会被人打挂只做保护不做审计出了问题就成悬案。6. 踩坑实录Agent-Reach上线过程中最典型的5个问题6.1 工具突然“失联”排查下来是上下文太长现象Agent连续跑几个小时之后某些工具调用率直线下降甚至明明该调query_logistics的时候模型开始胡诌一个不存在的工具名。原因这是上下文污染问题。多轮对话累积了大量历史早期塞进系统提示里的工具description可能已经被其他内容“挤出”了有效注意力区域。模型不是不记得有工具是工具的“存在感”被稀释了。排查办法在Agent-Reach的观测面板上看最后一轮实际喂给模型的系统提示长度和内容如果工具描述已经因为截断策略丢了一半那就是这里的问题。我的解决办法是给每个会话加了“压缩锚点”每次轮转时把工具列表挪到系统提示最前部并且用固定分隔符强调“以下是最新可用工具请严格基于它们做选择”。上线这个改动之后长会话工具调用准确率稳了很多。6.2 多个Agent同时抢一个任务数据库唯一索引重了现象偶尔会出现同一退款任务被创建两次业务侧收到重复工单。原因多Agent实例同时轮询同一个任务队列且判断“这个任务还没被领走”存在时间窗口两个实例同时通过了检查于是各建了一张工单。排查触达层的任务状态更新必须走条件更新不只是SELECT再UPDATE而是直接用原子条件UPDATE比如UPDATE tasks SET statusLOCKED, owneragent_a WHERE task_id? AND statusPENDING如果影响行数为0说明任务已经被别人领走了当前Agent应该放弃。这是一个经典的分布式锁坑Agent系统也不能幸免。6.3 MCP Server连不上却查不到任何报错现象MCP适配器配置好之后调用一直超时但日志里没有任何异常堆栈。原因MCP stdio启动的子进程是懒加载的第一次调用时进程可能因为环境变量缺失或启动参数不对直接挂了错误信息写到stderr里没被捕获。排查给MCP适配器加一个“探测连接”钩子在注册中心登记工具时就主动去拉起一次子进程并调用list_tools失败的话立即把stderr的文本作为错误返回。这样配置错误在登记阶段就暴露而不是等到真实用户请求时才卡住。6.4 同步等待太多导致Agent响应时长失控现象Agent回复用户非常慢平均8秒起步用户流失率明显上升。原因一查时间线发现Agent在几轮Function Calling里全是同步调用每一轮都要等外部API完整返回三轮下来就是6~9秒。排查把所有可以异步执行的工具重新设计了调用链。查询类工具确实需要同步拿结果但“创建任务”“发送通知”“触发审批”这类操作全部改为异步模式Agent发出调用后立刻返回“已受理”再通过事件回调用回调收敛最终结果。改造完平均首字节响应时间从8秒压到了2.2秒用户体验天翻地覆。6.5 日志太全反而找不到需要的那一条现象每个触达动作都打了日志一天几十GB但出问题时检索“某个订单在哪个环节被谁处理过”搜出来的日志散落四个系统拼不出一条完整时间线。排查问题不在日志量在于缺一个统一的trace视角。后来Agent-Reach引入了全局trace ID从用户消息进入系统的那一刻生成每次触达动作都带上这个ID。排查问题变成了“输入trace ID拽出一棵调用树”不夸张地说定位问题的平均时长从小时级降到了分钟级。这个投入绝对值得。7. 写在最后一点个人的经验总结Agent-Reach这个名字我现在回头看起的还挺贴切。做Agent系统的人很容易把目光放在模型智商、Prompt技巧、RAG效果这些“大脑”层面但真正决定一个Agent系统能不能在生产环境活下来的往往是它“手够不够长”“触达稳不稳”。我在这个项目里最深的体会是Agent工程的核心不是让模型更聪明而是把聪明模型的能力安全地、可控地送到真实世界里去。如果你也要做类似的事情我建议从最小闭环开始先不碰多Agent互连先把“一个Agent调三个外部工具”这件事做扎实做好参数校验、结果摘要、超时重试、审计日志再一步步加MCP、加Agent互连、加分布式任务。触达层这种底层设施最重要的不是功能多强而是在交付第一天就能让人放心。我自己在项目里还留了几个未来想补的方向比如让Agent通过触达层的能力索引主动发现新工具以及把权限模型进一步细化到字段级别。这些等后续版本跑得更稳定了再写出来跟大家细聊。如果你正在被工具调用不准、Agent互相等死、接口一抖全链路瘫痪这些问题折磨那这套设计思路应该能给你一些启发。别急着堆更多Agent先把触达这件事理顺系统自己会变稳。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进