资讯动态

Agent-Reach:为大模型Agent构建统一触达层,解决工具调用与治理难题

发布时间:2026/10/25 18:26:13 来源:尧图企业网站定制
大概半年前我开始把手里几个基于大模型的Agent从能聊天往真干活方向推。第一批接入的公司内部系统只有七八个跑了一个月很顺利。等接到二十多个系统的时候问题就来了——同一个查询Agent常常需要连续调三四个工具每个工具返回的格式都不一样有的还要轮询等异步结果。最夸张的一次一个Agent因为等一个慢接口超时把整条任务链卡了将近四分钟。我那时一度怀疑自己是不是把Agent做成了个只会说不会做的半成品。后来我把触达层单独抽了出来做了个叫Agent-Reach的中间件。简单说它就是夹在LLM和外部系统之间的一层统一触达协议无论背后是内部API、SaaS服务、数据库还是老的遗留系统Agent都通过同一套方式去够到它们。这篇文章就把这段过程完整拆一遍它到底解决什么问题、架构怎么设计、关键实现长什么样、实测效果如何以及路上踩过的几个大坑。如果你也在做智能体编排、Function Calling集成或者准备把Agent接到更多业务系统上这里面的教训应该能帮你省掉不少弯路。1. 为什么我决定做Agent-ReachAgent触达外部系统时的三座大山1.1 工具散落异构每个系统都有自己的方言做Agent的首选方案很直接给LLM配几十个Function让它自己选。一开始确实能跑但很快我就发现真正困难的不是让模型理解用户意图而是让它稳定地触达那些外部系统。拿我手头的系统举例报销系统是老的SOAP接口知识库用得是GraphQL监控平台是REST带WebSocket推送身份信息在LDAP里还有一个团队自研的管理后台走的是内部RPC。Agent要触达这些系统就得面对各不相同的鉴权方式、参数格式、返回结构、错误码语义。有的系统用Bearer Token有的用签名头还有的直接需要在Header里塞一个内部票据。为了让Agent够到这些服务我一开始在代码里写了几十种分支逻辑每接入一个新系统就要复制粘贴一堆适配代码。任何一个系统升级接口Agent这边就要跟着返工。这种散落异构带来的直接后果是Agent的核心逻辑被无关的适配细节淹没。一个负责查询报销单的函数的真实业务逻辑可能只有十行但为了兼容返回格式、处理超时、拼接鉴权头代码膨胀到两百行。这种代码写多了以后Agent的扩展速度会急剧下降连我自己都不想再往里加新东西。Agent-Reach的第一个目标就是把触达这件事从业务逻辑里剥离开让Agent只关心要什么不关心怎么连。1.2 上下文被工具描述吃光30个工具之后的Prompt失控第二个让我头疼的问题来自LLM本身。做Agent的人都绕不过Function Calling而Function Calling需要把工具的描述塞进Prompt。刚开始只有10个工具时工具的JSON描述加起来也就几千个Token模型选得很准。等工具数量涨到30个以上时问题开始出现工具描述在Prompt里占了将近两万Token不仅挤占了用户对话和上下文的空间更严重的是模型开始忽略一些工具甚至频繁选错工具。我印象很深的一次调试用户问把上周的发布报告发给王工模型没有调用联系人查询工具而是直接调用了创建工单工具因为它觉得关键词发布报告和某工单模板更相关。这就是典型的工具描述过载导致的指令漂移。后来我分析当工具清单过长时模型对每个工具真实语义的注意力会被稀释它更倾向于根据描述里最显眼的那些词做判断。两万Token的工具描述还会让后续的每次对话都背负巨大的计算开销单轮延迟跟着涨。要解决这个问题最优雅的做法不是逼模型在几十个工具里做选择题而是让Agent在调用前先做一次工具筛选只把相关的少数几个工具暴露给模型。这个思路如果靠业务代码自己实现每个场景都要写特殊的筛选逻辑而且很难通用。Agent-Reach把这件事做成了目录服务的默认行为后面我会详细说。1.3 故障扩散全联通不等于高可用第三个问题是在Agent触达的系统多了以后才暴露出来的。Agent一次任务往往要串行或并行调用多个工具链路拉长后任何一环慢下来都会被成倍放大。举个例子某个查询需要先查组织架构拿到团队ID后再查项目列表最后再去监控系统拿接口调用数据。这个过程在模型层面可能只要几秒钟的思考但实际执行会因为三四个外部请求的往返而显著变慢。有一次监控平台一个接口响应正常但它的网关偶发卡顿导致每次Agent执行到那一步都要卡住十几秒。用户体感就是从秒回变成了不知道什么时候会回。更糟的是这类问题很难定位。因为Agent是自由发挥地调用工具每次调用的路径都不一样传统的告警规则很难覆盖所有可能的链路组合。我们需要的是一个能统一做超时控制、重试、限流、熔断和链路追踪的触达层而不是让每个连接器自己各管各的。Agent-Reach把所有这些治理能力下沉到了中间层连接器只负责一件事接上目标系统做完一次请求返回结果。2. 核心架构把触达抽象成可插拔的协议层2.1 四个核心组件各管一摊Agent-Reach不是一个Agent框架也不做模型调度。它只负责Agent触达外部系统这一段。整个设计围绕四个组件展开职责非常清楚组件职责核心产出Access Protocol统一定义一次触达请求和响应的数据结构标准的AccessRequest/AccessResponseAsset Catalog工具注册、能力描述、动态发现、健康检查分组后的工具Schema列表Router按模型的调用意图路由到具体连接器一次实际的外部系统调用Guard限流、超时预算、重试、熔断、审计每次调用的治理决策与日志这四个组件加在一起目标就一句话Agent只需要说我要触达某件事Agent-Reach负责完成如何触达。模型侧不用再关心目标系统在哪、用什么协议、要不要鉴权这些全部由中间层吞掉。在设计上我把Access Protocol放在最底层因为它是整个方案的共同语言。只要连接器都遵守这套协议Agent-Reach的Router和Guard就能对任意连接器做统一治理。这也是为什么我坚持所有连接器必须继承同一个基类而不是各自返回自由格式的对象。2.2 为什么选择自描述工具而不是直接把OpenAPI交给模型你可能会有疑问现在很多系统都提供OpenAPI文档为什么不把这些文档直接交给模型让它自己去调呢我试过踩过坑才最终回到自描述工具这条路上来。问题出在两点。第一OpenAPI文档是为开发者设计的里面包含了大量的枚举、错误码、分页参数、鉴权说明一个稍微复杂点的系统文档可能有几百KB直接塞给LLM既不现实也会严重污染上下文。第二模型的Function Calling能力面对的是精简、明确、单用途的工具描述而不是完整的企业级API面。把OpenAPI原样交给模型模型很大概率会在参数映射上出错比如把客户ID和订单ID搞混。Agent-Reach的做法是让每个连接器在Asset Catalog里注册一份高度精炼的工具描述只留下模型真正需要的信息工具名、一句话说明、参数结构和返回值结构。这些描述由接入方维护但它不是从OpenAPI自动生成的而是基于实际Agent调用场景手工提炼的。听起来麻烦但这是保证模型调用准确率的关键一步。工具描述写得越聚焦模型选得越准。我在内部还规定一个工具的description不超过64个Token参数说明里每个字段的description不超过20个Token。超了就要求精简。这个约束一开始团队成员觉得太苛刻后来大家发现这反而倒逼出了更好的Schema设计——如果你没法用一句话讲清楚这个工具是干什么的大概率是你对这个工具的功能定位还没想清楚。2.3 目录服务与动态发现让连接器即插即用Asset Catalog在实现上借鉴了微服务里的注册中心思路。每个连接器在进程启动时向Catalog注册注册信息包含工具Schema、健康检查地址、限流配置、鉴权类型和依赖标签。Catalog对外提供两类查询接口全量查询给运维和调试用的能看到当前注册了哪些工具、各自状态如何。按相关性查询给Agent编排用的传入用户请求和上下文返回一组合适的工具子集。按相关性查询是规避工具描述过载的关键。具体做法是先按权限过滤用户没有权限的工具直接不返回这也能防止模型尝试调用不该调的东西再做语义检索。我给每个工具描述离线生成了向量索引查询时用用户请求的embedding做topK召回再叠加关键词加权。召回数量设了上限默认12个。这样无论底层已经注册了50个还是200个工具模型每次看到的都只是经过筛选的、和当前请求真正相关的一小部分。动态发现还有个好处是支持的灰度发布。新工具接进来后可以先把状态置为影子模式只记录不生效链路验证通过后再切到正式模式。这个能力在传统系统对接时代需要额外的开关和发布流程在Agent-Reach里就只是Catalog里的一个状态字段。3. 工程实现从Tool Schema到连接器适配的关键细节3.1 用一份YAML描述一个工具的全生命周期Agent-Reach的每个工具在Catalog里都是以YAML配置形式存在的。这里我放一个真实示例是某个工具接入时的配置version: 1.0 id: crm_search_contacts name: search_contacts description: 根据姓名、部门或邮箱搜索CRM客户联系人信息 type: tool input_schema: type: object properties: keyword: type: string description: 联系人姓名、邮箱或手机号支持模糊匹配 department: type: string description: 可选按部门过滤 required: [keyword] output_format: type: json fields: - contact_id: string - name: string - department: string max_output_size: 2048 runtime: connector: crm.http endpoint: /api/v1/contacts/search method: POST timeout_ms: 3000 retry: 2 rate_limit: capacity: 20 refill_per_sec: 5 auth: type: gateway_sso scopes: [crm:read] fallback: - type: mock data_path: /etc/agent-reach/mock/crm_search_contacts.json这份YAML覆盖了一个触达调用的完整生命周期。模型侧只需要看到name、description、input_schema这部分Router要的是runtimeGuard要的是rate_limit、timeout_ms、retry运维要的是fallback和auth。所有角色都从同一份配置里取自己关心的字段不需要各自维护一份真相源。有个细节我要强调下max_output_size这个字段一开始我没加后来被大规模返回打爆过上下文才补上的。有的工具返回的列表动辄几百条记录如果全部塞进模型上下文既浪费Token又干扰判断。加了上限之后连接器可以对返回结果做截断或者汇总保证模型拿到的始终是精华版数据。这个字段对控制成本帮助非常大。3.2 连接器基类把后端适配收敛到统一接口所有接入Agent-Reach的工具都必须实现这样一个基类import asyncio from abc import ABC, abstractmethod from dataclasses import dataclass dataclass class AccessRequest: action: str params: dict trace_id: str auth_context: dict deadline: float dataclass class AccessResponse: success: bool data: object None error_code: str message: str class BaseConnector(ABC): def __init__(self, name: str, config: dict): self.name name self.config config abstractmethod async def execute(self, request: AccessRequest) - AccessResponse: 执行一次触达调用必须全程异步并且遵守 request.deadline。 abstractmethod def health_check(self) - bool: 返回当前连接器是否可用。AccessRequest里的deadline是Agent-Reach超时控制的关键。Router在派发请求时会根据Guard算好的超时预算设置一个绝对时间戳连接器在执行过程中必须主动检查这个deadline。这样一次触达调用不会因为某个第三方SDK内部的重试机制而无限制地拖下去。异步是另一条硬性要求。所有连接器的execute都必须是async函数因为Agent一次任务常常会并行触达多个工具如果某个连接器用了同步阻塞调用整个事件循环就会被卡住。这也是我在评审连接器代码时最先检查的部分。3.3 把目录动态编译成Function Calling参数有了Catalog里的工具Schema下一步就是把它们动态编译成LLM能识别的Function Calling参数。核心代码其实很薄def build_tool_schemas(catalog_items: list[CatalogItem]) - list[dict]: schemas [] for item in catalog_items: schemas.append( { type: function, function: { name: item.function_name, description: item.description, parameters: item.input_schema, }, } ) return schemas真正的难点在调用前的工具筛选。我在Router层实现了一个select_tools函数它的输入是用户请求和用户上下文输出是一组适合当前场景的CatalogItemasync def select_tools(query: str, user_context: dict, catalog: Catalog) - list[CatalogItem]: # 第一步按权限过滤用户没有权限的工具一律不给模型看到 permitted catalog.filter_by_scope(user_context[scopes]) # 第二步如果过滤后的工具少于12个直接全部返回 if len(permitted) 12: return permitted # 第三步按语义相关性打分取前12个 scores [(item, scoring(query, item)) for item in permitted] scores.sort(keylambda x: x[1], reverseTrue) return [item for item, _ in scores[:12]]请注意我把权限过滤放在相关性检索之前。这背后的逻辑是模型只能从给它看到的东西里做选择。如果一个无权限的工具出现在Prompt里模型很可能因为描述匹配而尝试调用然后被权限错误打回浪费一整轮往返。在触达层把这种失败提前拦截掉比事后再做权限校验要高效得多。3.4 限流、超时与重试的组合拳Guard组件的核心是三个策略的组合令牌桶限流、全局超时预算、指数退避重试。限流用的是令牌桶算法。每个连接器在Catalog里配置了capacity和refill_per_secRouter派发前先尝试取令牌取不到就直接返回限流中错误并附带建议的等待时间。这里的关键是限流也要做成统一逻辑不能每个连接器自己实现一套。超时预算是Guard最微妙的部分。一个Agent任务可能先后触达5个工具如果每个工具各设3秒超时最坏情况就是串行等待15秒。我在Router层给整个任务链设了一个总预算例如8秒。每次调用子工具前Guard会把剩余预算分配下去子工具的deadline就是当前时间加剩余预算。这样即使中间某个工具异常慢也不会拖垮整条任务链因为它的deadline会随着时间推移越来越短最终被强制打断。重试默认开一次且只在超时和网络类错误上重试业务错误码一律不重试。重试时使用指数退避基础间隔200毫秒最多延迟1.6秒。这些参数都能在Catalog里按工具单独覆盖但默认值已经能覆盖大部分场景了。4. 实测数据从10个工具扩展到50个工具稳定性表现如何4.1 三个月的观察稳定性指标的明显变化Agent-Reach稳定跑了三个月我拿接入前后的关键指标做了对比。这里的接入前指Agent直接连各个系统、每个连接器自己管超时重试的阶段接入后指全部被Agent-Reach统一治理的阶段。指标接入前接入后单次工具调用平均延迟2.1s1.2sP99延迟8.7s2.4s工具调用成功率94.2%99.1%工具描述占用的上下文Token约26000约4500新增一个工具的平均耗时约2天含联调约1小时平均延迟下降的核心原因有两个工具描述变短之后模型决策更快这是第一层慢接口不再拖垮整体链路这是第二层。P99从8.7秒降到2.4秒是最直观的用户体感改善——之前用户经常投诉转圈半天没结果接入后这类投诉几乎绝迹。成功率从94.2%提到99.1%靠的不是某个单独的技巧而是超时、重试、熔断、降级这些治理能力的合力。尤其是把业务错误和系统错误在AccessResponse里明确区分开之后排错效率高了很多。系统错误可以安全重试或触发熔断业务错误直接返回给模型让它调整策略两者不再混为一谈。4.2 一个完整的触达链路从自然语言到结构化结论我们看一个具体的例子。用户问本月团队报销单里有没有金额异常的条目这个请求经过Agent-Reach的完整链路大概是这样的第一步Agent-Reach收到请求后先做工具筛选。它从Catalog里召回了4个工具查组织架构、查报销单列表、查审批流信息、查核算规则。query这个请求的embedding和工具描述的相似度排序中这4个排在最前面。其他四十六个工具完全没有出现在Prompt里模型也不会被干扰。第二步Router按照工具间的依赖关系编排执行顺序。先查组织架构拿到当前用户的团队ID再用团队ID查报销单列表拿到所有报销记录接着并行触发审批流和核算规则两个查询拿到审批节点和异常判定规则。第三步Guard在每次调用前都检查剩余超时预算。第一次调组织架构花了0.8秒剩余预算6.2秒第二次查报销单花了1.1秒剩余5.1秒两个并行调用各分配约2.5秒预算。如果其中一个慢接口超时了它不会拖死另一个查询因为两个调用各自有独立的deadline。第四步所有结果以统一格式组装成一个结构化JSON交给模型生成最终答案本月报销单共37笔发现2笔金额超出阈值详情如下……整个过程从请求到返回结论耗时大概4秒。这在接入前是不可想象的。4.3 数字之外的额外收益接入和维护体验比数字更让我满意的是开发体验的变化。接入一个新系统平均只需要一个小时大部分时间花在写YAML配置和联调返回格式上不再需要为了每个系统写一套自定义适配代码。团队内部的分工也清晰了业务方只需要提供工具描述和接口文档Agent-Reach的连接器模板由平台组维护大家各司其职。另一个意外收益是审计变得完整了。以前Agent调了个什么接口、用了谁的凭据、返回了什么数据要查代码日志才能勉强拼出来。现在所有触达调用统一走Guard每次调用都自动记录调用者身份、目标系统、参数摘要、返回状态和执行耗时。审计日志不再是额外工作而是Agent-Reach的默认副作用这对安全合规部门来说是个相当实用的能力。5. 排错实录三个让我头疼的问题与最终解法5.1 工具描述太长导致的指令幻觉最早把工具全量塞进Prompt的时候我从20个工具增加到了30个然后观察到一种诡异的现象模型会频繁调用一些和用户请求关联很弱的工具。比如用户说查一下客户张明的订单模型在调用search_customer之后还会额外调用update_contact_tag把张明的标签改成了订单查询。这类操作带有副作用而且完全没有必要。我一开始以为是模型随机行为排查完才发现是工具描述过载导致的指令幻觉当System Prompt里的工具定义太多、太冗长时模型会错误地把一些描述片段理解为隐含指令。定位过程比较繁琐。我先打印出了每次完整请求里tools参数的大小发现光工具定义的JSON就有约8万字符折合两万多Token。然后我做了个对照实验只保留6个相关性最强的工具同样的用户请求模型行为完全恢复正常不再出现多余调用。这个对照实验确认了根因。最终解法就是我前面说的动态工具筛选以及给工具描述设定Token上限。此外我还在Prompt末尾加了一句硬性约束你只能调用上述工具列表中的函数不要调用未列出的函数。听起来很基础但它显著减少了模型的自由发挥倾向。5.2 一个慢接口拖垮整条链路第二个坑是慢服务导致的级联阻塞。某天线上反馈Agent处理报销类查询明显变慢单轮耗时从3秒涨到了12秒。打开链路追踪的瀑布图一看问题不在模型推理而在一个遗留系统提供的导出报销明细接口上。那个接口的正常响应就要3-5秒偶尔会拖到8秒以上。更麻烦的是Agent在调用这个慢接口时是并行发起多个子调用的。标准做法里并行调用应该各自独立但当时我的实现是把多个子调用的await放在同一个TaskGroup里任何一个子调用失败都必须等全部子调用结束才能继续。asyncio.TaskGroup在Python 3.11里的语义决定了一旦某个子任务抛异常它会等待所有子任务结束再统一抛出。这就导致一个慢接口不仅自己慢还绑架了其他所有子调用的返回。解法的核心是引入独立的任务隔离和全链路超时预算。每个子调用不再是整体等齐的语义而是各自带上独立的deadline谁先完成谁先返回慢的那个单独超时结果标记为失败或者走降级分支。同时用asyncio.timeout给整条任务链套一个总预算避免多个子调用的超时相互叠加。改完之后同样场景的P99延迟稳定在2.4秒左右慢接口不再产生一拖全拖的效果。5.3 权限模型不统一导致调用频繁失败第三个坑在权限层。Agent接入的系统越多权限模型的差异就越明显有的系统要Basic Auth有的要求JWT Bearer还有内部系统直接认LDAP账号甚至有一个遗留后台要求自己拼签名。最初的设计是每个连接器自己保存一套凭据结果用户换岗位后有的连接器还在用旧的账号调用导致一批Agent任务在触达那几步时频繁报401。排查过程很痛苦因为错误发生在不同系统里报错信息五花八门有的是Unauthorized有的是token expired有的是干脆返回一个空列表根本看不出权限问题。后来我调整了设计把所有外部系统的凭据从连接器里剥离出来统一放到Agent-Reach的凭据仓库里管理。连接器在execute时不再接触任何密钥只从auth_context里取一个user_ref由网关层在做鉴权时统一映射到目标系统的凭据。用户在离职或变更权限时只需要在凭据仓库里改一条映射所有连接器立刻生效。这个改动还顺带解决了一个安全隐患连接器代码仓库里不需要再存放任何真实凭据即使代码泄露密钥也不会随之泄露。每天定时轮换凭据也变成了一件可以在网关层统一执行的事而不是逐个系统去改。6. 个人经验落地Agent-Reach的几条实操建议6.1 别贪多先从三个高频但简单的场景切入如果你也想在自己的Agent体系里引入类似的触达层我的建议是千万别想着一次接入几十个工具。先选两三个高频、逻辑简单、结果可验证的场景跑通这能帮你尽早验证架构方向对不对又不会在排查问题上分走大量精力。我当时先选了三个场景查联系人、查报销单、查监控大盘。这三个场景覆盖了三种典型模式简单查询、带依赖的查询、跨系统聚合查询。把这三个场景跑稳定之后后续扩展更多工具就只是工作量问题而不是架构问题。6.2 把输出约束写进Tool Schema比事后截断更省心工具的max_output_size这个字段是我强烈建议保留的。它不只是一个数字更是一种设计约束接入方在写工具描述时就必须想清楚这个工具最多返回多少数据而不是等到运行时被大量返回撑爆了上下文才想起来做限制。我在实际使用中发现给max_output_size配上自动摘要效果更好。超过上限时连接器先把返回结果压缩成关键字段再返回给模型。比如查联系人列表超过50条时自动聚合成共找到137位联系人其中前50位如下这样的格式既保住了关键信息又不会污染上下文。6.3 可观测性必须从第一天就做而不是出了问题再补Agent类系统的排错天然比普通Web服务难因为模型行为带有不确定性。同一个用户请求两次调用的工具可能都不一样。在这种系统里没有链路追踪几乎等于盲人摸象。我要求所有触达调用都必须带trace_id一条完整链路里的所有外部调用都共享这一个ID。Guard每次介入限流、重试或者熔断都会记录事件并和trace_id关联。这样排查一个问题时我可以直接画出完整的时间线用户在什么时候发出请求模型选了哪些工具每个工具的执行时长、成功与否、是否触发了重试、是否被限流。没有这套基础设施那些偶发的Agent故障根本无从下手。开发Agent-Reach这段经历给我最大的一个感受是Agent能不能在真实业务里落地瓶颈往往不在模型聪明不聪明而在于它能不能可靠地够到那些系统。触达层看起来是最不性感的部分但它决定了Agent从Demo到生产的距离。每次新接一个系统只需要写一份YAML配置而不是重新造一遍轮子的时候我就觉得当时抽出来的这层没有白做。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑