ARTICLE DETAIL

资讯详情

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

Agent-Reach:为AI Agent构建安全稳定的工具触达层

Agent-Reach:为AI Agent构建安全稳定的工具触达层

最近半年我一直在鼓捣一个叫 Agent-Reach 的项目,起因特别朴素:我手里那套 Agent 应用,模型聪明得不行,但一到“办事”环节就露馅。问它库存它随口编数字,让它走审批流程它说自己“没有权限”,跟它说给客户发一封邮件,它漂亮地写好草稿,然后就没有然后了。问题不在模型智商,而在于它根本“够不着”真实世界——没工具、没数据、没执行通道。Agent-Reach 就是我为解决这个问题单独加的一层触达网关,它专门负责让 Agent 真正摸到外部系统。这篇文章把它的设计思路、实现细节和踩坑经历完整梳理一遍,适合正在做 Agent 接入、工具调用、或者被“模型会想不会做”折磨的朋友。

1. 为什么一定要有“触达层”,而不是直接在 Agent 里堆工具

1.1 模型的“能”和“不能”之间,隔着一道执行墙

先想清楚一个基础问题:大模型到底能不能自己“做事”?答案是不能。模型的本质是一个概率化文本生成器,输入 token 输出 token,它没有能力去请求 HTTP 接口、读写数据库、发送消息。你可能说,“现在不是有 Function Calling 吗?模型可以调用函数啊。” 这个理解有个偏差,Function Calling 只是让模型学会了输出一种特殊文本——“我想调用某个工具,参数如下”,真正把这个请求翻译成一次真实系统调用的人,是外层框架。

打个比方,模型是大脑,外部系统是手和脚,Function Calling 只是大脑发出了“抬手”的神经信号,信号要传到胳膊上,中间还得有一整套神经束和肌肉系统,这套东西就是触达层。没有神经束,大脑再聪明也抬不起手。这也是很多 Agent 项目的通病:只在提示词和模型能力上下功夫,把工具调用当成一个 if-else 分支来写,最后模型“想得到”但“够不着”,或者“够得着”但“抓不稳”。

1.2 为什么叫 Reach:触达是 Agent 的核心短板

我并不缺推理能力,一个 Agent 项目做下来,真正卡住我的从来不是“模型不够聪明”,而是它对外部世界的可控触达太弱。我这里说的“触达”包括四类:工具触达(调用 API、执行脚本)、数据触达(查库、检索文档)、渠道触达(IM、邮件、站内信),以及 Agent 之间的触达。这四件事统称 Reach——一个 Agent 能“够到”多少东西、够得稳不稳、够得安不安全,直接决定它在真实业务里有没有用。

我见过不少团队花大力气调 Prompt、微调模型,把推理能力拉满,结果部署上线后 Agent 还是没法干活,原因很简单:它想调用一个订单服务,但订单服务的鉴权、限流、字段映射、错误处理全散落在业务代码里,换个场景就废了。推理能力和触达能力是两回事,现代 Agent 不缺前者,缺的是后者。Agent-Reach 这个项目就是专门补齐后者的。

1.3 和 Function Calling / Tool 方案的边界划分

很多现成框架也有工具管理能力,比如 LangChain 的 Tool 抽象,直接调 OpenAI Function Calling 也行。我的判断是:这些方案解决的是“单个工具怎么被调用”的问题,而 Agent-Reach 解决的是“一堆工具怎么被安全、稳定、可观测地触达”的问题。两者层次不一样,我用一张表来对比会清楚一些。

方案工具接入路由选择权限门禁审计观测适合场景
裸写 Function Calling手写模型自己选无无1~2 个工具,POC 验证
LangChain Tool统一接口模型从全部工具中选靠业务代码补靠框架日志中型 Demo,10 个左右工具
Agent-Reach 三层结构注册表统一管理两段式召回+模型精选内置白名单与审批全量审计生产环境,工具多、权限杂、需要稳定

核心结论是:工具一旦超过 20 个,让模型从全量工具列表里做选择,既浪费 token 又容易选错,而且权限、审计这种横切逻辑放在业务代码里根本无法复用。Agent-Reach 的定位就是“工具触达的基础设施”,把注册、路由、门禁、观测收敛到同一个层里,Agent 只负责“说我要什么”,Reach 层负责“安全地把它办成”。

2. Agent-Reach 只干三件事:注册、路由、门禁

2.1 工具注册:给每个能力一张“身份证”

Reach 层第一个要解决的,是怎么让系统认识一个工。我做的第一件事是定了一套工具注册规范——每个接入的能力都必须用一份 ToolSpec 描述自己,相当于给每个工具办一张身份证。字段包括:

  • name:全局唯一标识,比如order_track_query
  • description:一句话说清楚这个工具是干什么的,什么场景该用它
  • input_schema:入参的 JSON Schema,字段名、类型、必填项、枚举值
  • category:读操作 / 写操作 / 外部调用,不同类别走不同治理策略
  • risk_level:低 / 中 / 高,高风险工具需要额外审批
  • timeout_ms:超时时间,分级别各有建议值
  • idempotent:是否幂等,这直接决定后面能不能自动重试
  • auth_scope:调用这个工具所需的权限级别

代码里我大致是这样定义的,用 Pydantic 做校验,写起来比较干净:

from pydantic import BaseModel, Field from typing import Any, Dict, Optional class ToolSpec(BaseModel): name: str = Field(..., description="全局唯一工具名") description: str = Field(..., description="给模型看的一句话描述") input_schema: Dict[str, Any] = Field(..., description="入参 JSON Schema") category: str = Field(..., pattern="^(read|write|external)$") risk_level: str = Field(default="low", pattern="^(low|medium|high)$") timeout_ms: int = Field(default=5000) idempotent: bool = Field(default=False, description="是否天然幂等") auth_scope: str = Field(default="user", description="所需权限级别")

登记时我会强制要求每个工具必须填写描述字段,并且统一描述模板。别小看这个模板,后面模型选错工具,八成是描述没写好。模板很简单:做什么 + 什么时候用 + 千万不要用于什么场景。例如“查询订单物流状态。当用户询问包裹到哪了、单号追踪时使用。不要用于查询订单金额或售后进度。” 多写一句“不要用于”能显著降低模型误调用的概率。

2.2 两段式路由:先粗召回,再精选择

工具注册表建好之后,下一个核心设计是路由。大多数框架的做法是把所有工具的描述塞进 System Prompt,让模型自己挑。工具少还行,工具超过几十个的时候,光描述就能吃掉一大段上下文,而且模型在相似工具之间经常“脸盲”。我的做法是两段式路由:

第一段叫粗召回,不经过模型,用规则加向量相似度,从注册表里捞出一批候选工具,数量控制在五到七个。比如用户问“帮我查一下上个月的电费账单”,先通过关键词“账单”“电费”把可能相关的工具全捞出来,哪怕捞错也没关系,候选范围小,模型纠错成本低。第二段才把候选工具的 description 注入 Prompt,让模型从这几个里面做最终选择。

这个设计说白了就是不让模型做大海捞针式的选择题。模型不是搜索引擎,你让它从一百个选项里找最优解,它大概率会蒙。但如果先靠规则和向量把范围缩到五六个,模型的选择准确率会高很多。我做了个小实验,同一个用户请求,直接从 120 个工具里选,正确率只有 68%;先召回再选,正确率能到 92%,差异非常明显。用到的召回函数,大概是这个逻辑:

def recall_candidates(user_query: str, tool_registry: List[ToolSpec], top_k: int = 7): scored_tools = [] for spec in tool_registry: score = 0.0 # 关键词命中 query_tokens = set(user_query.split()) desc_tokens = set(spec.description.split()) score += len(query_tokens & desc_tokens) * 0.1 # 向量相似度(实际项目中用 embedding 计算) score += cosine_similarity(embed(user_query), embed(spec.description)) scored_tools.append((score, spec)) scored_tools.sort(reverse=True) return [spec for _, spec in scored_tools[:top_k]]

实际生产里向量相似度那步要用专门的 embedding 服务和向量库来做,性能比 Python 里逐条算好很多。但思路就是这样一个粗筛的动作,把最可能相关的工具先圈出来,后面模型再精简。

2.3 执行门禁:所有工具调用走同一道闸机

路由选好工具之后,真正执行之前还要过一道门禁。我在这里统一处理四件事:入参校验、权限校验、限流判断、输出脱敏。

入参校验用的是 ToolSpec 里的 input_schema,避免模型生成参数时手滑,比如把订单号传成手机号,这一步能挡掉一大半低级错误。权限校验是按 auth_scope 做的,用户只能调用允许范围内的工具,跨权限的调用直接拒绝,并且记入审计。限流判断是防止 Agent 在循环里把一个工具疯狂调几百次,每个工具都配了独立的 QPS 上限,超了就排队或者报错。输出脱敏则是等工具结果返回后,在还给模型之前把敏感字段处理掉。

整个执行链路是一个管道:入参校验 → 权限校验 → 限流判断 → 执行 → 脱敏 → 归一化返回。每个环节都是独立函数,可以单独测试和替换。这么做的好处是,Agent 业务代码里不用再掺任何权限判断和 try-catch 逻辑,所有工具调用看起来就像是调用本地函数一样干净。

3. 一个真实调用完整走一遍

3.1 场景:Agent 替用户查订单物流

理论说多了容易飘,我拿一个实际场景把链路走一遍。假设用户对 Agent 说了句:“我 3 月 8 号买的那单发货了没?” 这是一句很典型的日常表达,里面没有“查询物流”这样的关键词,也没有订单号,Agent 要做两件事:一是理解用户意图,二是在上下文中找到订单号。

Agent 的主循环接收到这句话之后,先解析出用户意图是“查询订单物流状态”,然后把意图和会话历史一起丢给 Reach 层——注意,这里并不是直接把整个对话发给所有工具,而是让 Reach 层先判断要哪些能力。

3.2 路由与参数补全

Reach 层收到请求后,先用粗召回逻辑在注册表里找候选。与“订单”“物流”“查询”相关的工具有五六个:订单查询、物流轨迹查询、售后进度查询、发票查询。模型从这几个候选中选中了“订单物流轨迹查询工具”,接下来要传参,但这个工具要求 order_id,用户那句话里没有。

这个地方就是 Reach 层最有价值的地方之一:参数补全。Reach 层在会话上下文里搜索“3 月 8 号”“那单”这些线索,从用户历史对话里找到了之前生成的订单号,把 order_id 自动补齐,然后才发起调用。没有这层逻辑,模型就只能傻傻地问用户“请提供订单号”,体验极差。

3.3 执行与归一化返回

订单服务返回的原始 JSON 大概长这样,里面字段非常多:

{ "status": 0, "data": { "order_id": "SO2025030800123", "order_status": "SHIPPED", "delivery_list": [ {"company_name": "顺丰速运", "tracking_no": "SF1234567890", "status": "IN_TRANSIT", "last_event": "已到达杭州转运中心"}, {"company_name": "顺丰速运", "tracking_no": "SF1234567891", "status": "OUT_FOR_DELIVERY", "last_event": "派送中,预计今天送达"} ], "customer_phone": "138****6789", "warehouse_remark": "内部备注,勿展示" } }

我是不可能把这段原始 JSON 直接扔回给模型的。一来字段太多,模型容易看晕;二来里面有内部字段,比如仓库备注,绝对不能带到模型上下文里。Reach 层会做一次归一化和裁剪,最后返回给模型的内容是简洁的:

订单 SO2025030800123 已发货。 物流轨迹: - 包裹一:顺丰速运 SF1234567890,运输中,已到达杭州转运中心 - 包裹二:顺丰速运 SF1234567891,派送中,预计今天送达

模型拿到这段干净、脱敏、结构化的内容之后,组织和润色给用户看,就非常轻松了。整个链路的要点是:模型不直接面对系统的大而全的返回,Reach 层把“世界本来的样子”翻译成“模型最容易消化的样子”,两边的工作量都降下来了,稳定性自然就上去了。

4. 实测里最容易翻车的三个地方

4.1 工具返回值撑爆上下文窗口

这是我在 Agent-Reach 上线的第一周就踩到的坑。有个查询报表的工具,一次返回了八千多字的 JSON,直接把模型的上下文窗口撑到极限,然后模型开始胡言乱语,把报表里的数字当成聊天素材,跟用户说“我发现你的店铺评分是 4.7”,用户一脸懵。

后续我加了一套返回值压缩策略:给每个工具的输出设一条 token 上限,超过阈值就不返回全文,而是返回“前 N 条原始记录 + 统计摘要”。比如一个工具返回近三个月的所有订单,50 条记录全部塞给模型没意义,模型真正需要的是“这三个月总共有 128 单,总金额 3.4 万,退款率 2.1%”这样的结论,加偶尔需要看几条例证。于是 Reach 层会截取前五条详情作为示例,用统计数字概括剩余部分,模型既有据可依,又不至于被淹没。

4.2 相似工具让模型“脸盲”

工具数量超过 50 个之后,新问题出现了:模型经常在相似工具之间选错。最典型的是“创建群聊”和“拉人进群”,描述都含“群”,模型在候选里看到两个工具,一半概率选错。我一开始以为是模型不行,后来排查发现是我描述写得有问题。工具描述没有区分边界,模型只能靠猜。

后来我给所有工具统一了描述模板,强制要求写“什么时候用”和“一定不要用于什么场景”,再把高度相似的工具做了合并。“创建群聊”和“拉人进群”本来就是同一个工具的两个动作,拆成两个工具纯属给自己找麻烦。合并之后,模型选择准确率明显回升。这个教训很实际:工具是否容易选错,不是模型单独决定的,工具本身的设计和描述占了一半因素。

4.3 自动重试引发的重复写操作

第三坑,也是最危险的一个坑:写操作被自动重试,导致重复发货、重复发消息。起因是某次外部接口网络超时,Agent 框架自带的重试机制自动把工具调用又执行了一遍,结果用户收到了两条一模一样的企业微信通知。用户没骂模型,骂的是我们系统“抽风”。

要根治这个问题,必须在工具注册表里明确声明幂等性。我给每个工具增加了一个属性:是否幂等。只有幂等标记为 true 的工具才允许开启自动重试,非幂等工具在超时之后只能走人工确认流程,或者改成“先查后写”模式——先调用一个查询工具去检查上一步操作到底成功没有,再决定要不要重试。写操作加上幂等键也很有效,每次请求生成一个 client_request_id,服务端靠这个 ID 去重,同一个请求重复提交只会生效一次。这是线上踩了坑才知道的东西,文档里很难提前学会。

5. 性能和安全的组合拳:两条生死线

5.1 超时分级和并发池隔离

Agent 调用工具的时候,最恶心的场景是某个工具卡住了,把整条 Agent 任务拖死。用户问一句话,Agent 在那等了几十秒才回,体验极其糟糕。我后来给工具做了超时分级,不同类别的工具配不同的超时时间。

工具类别建议超时说明
内部读操作(查库、查缓存)3~5 秒内部网络,通常很快
内部写操作(下单、审批、发消息)10~15 秒涉及事务,时间适当放宽
外部 API 调用15~30 秒受网络波动影响,但要设上限
AI 类工具(生成、翻译)30~60 秒依赖模型推理速度

光有超时还不够,并发也要隔离。我把读操作和写操作放进不同的线程池:读操作并发上限高,因为大多是无状态的查询,多来几个也不怕;写操作单独用一个小的线程池,防止某个慢事务把线程全部占满,导致其他读请求全部排队。这就像商场出入口和收银台,一个要求吞吐大,一个要求控制节奏,混在一起准出问题。

5.2 权限白名单和返回脱敏

安全这件事,我的原则是“默认拒绝”。用户能访问什么,由 Reach 层的权限表说了算,而不是由 Agent 自由发挥。每个工具声明自己的 auth_scope,用户再通过认证后得到一个角色,角色对应一组可访问的工具范围。比如普通用户能查自己的订单,但只有管理员能调退款审批工具。越权调用直接被拒,并且记录一条审计日志。

审计日志记账什么?入参全量记录,出参只记摘要,避免日志里堆满敏感数据,同时防止事后说不清楚。返回脱敏则是工具返回值在进入模型上下文之前的最后一道防线。手机号、身份证、Token、密钥这类信息,在返回给模型之前就替换成脱敏形式,例如138****6789。我甚至不允许模型上下文里出现真实的 API Key,因为没法保证模型不会在后续多轮对话中不小心把它复述出来。用正则和关键词规则做一套通用脱敏器,几百行代码就能覆盖大部分场景,成本不高,收益很大。

6. 下一站:让 Agent 触达的不只是工具

6.1 把 RAG 也当成一个“检索工具”

做了几个月之后,我发现 Reach 层的能力可以继续外延。很多团队单独搭一套 RAG 做知识库问答,再另搞一套 Agent 做工具调用,两套体系各管各的。我在 Agent-Reach 里做了一件事:把知识库检索也注册成普通的“检索工具”,挂在统一注册表下面。

这样一来,RAG 也天然获得了路由、限流、权限校验、审计观测。用户问“报销标准是多少”,Reach 层可以直接路由到知识库检索工具,走同一套执行门禁,不用再单独维护一套 RAG 的监控和权限逻辑。加上工具路由的两段式召回,甚至可以让模型自己决定:这个问题是查知识库还是查 API。把数据触达和工具触达统一到一个入口之后,整个系统的复杂度是下降的。

6.2 触达渠道:Agent 主动把结果送到人面前

另一个延伸方向是渠道触达。Agent 完成一个长任务之后,结果不能只停留在聊天窗口里,有时候需要主动推送到企业微信、邮件、或者站内信。这个“把结果送达用户”的动作,也属于 Reach 的定义范围。

我在 Reach 层加了一套渠道适配器,把 IM 机器人、邮件服务、短信网关都抽象成统一的“送达通道”,配好模板和退订规则。用户半夜发起一个批量数据处理任务,Agent 跑完自动把结果报表发到指定邮箱,这就是实实在在的“触达”能力。从工具触达,到数据触达,再到渠道触达,Agent 不再是一个被困在对话框里的聊天机器人,而是真正能“伸手”到用户身边的人。

6.3 跨 Agent 协作:从工具调用到 Agent 握手

最后我还在试验的,是 Agent 与 Agent 之间的触达。未来的场景里,一个客服 Agent 发现用户的投诉涉及售后赔付,它自己是无法完成赔付操作的,需要把一个带上下文的工单转给另一个有赔付权限的 Agent。这个过程中,Reach 层变成了两个 Agent 之间信任握手的边界。

认证方式、上下文归档、权限转移,这些都能在 Reach 层统一完成。Agent A 调用 Agent B,就像调用一个工具一样,只不过这个“工具”背后也是一个完整的 Agent。底层的注册、路由、门禁逻辑完全复用。这也是我把这个项目命名为 Agent-Reach 的原因:它最终要触达的,不只是一堆函数的端点,而是完整的人与系统。


这套东西做下来,我最大的感觉是:Agent 项目的复杂度从来不在模型,而在和真实世界的接缝处。模型再聪明,工具触达层如果是一团乱麻,项目一样跑不起来。如果你也在做类似的事,我的建议是先别急着堆工具,拿出一张纸,画一画你的 Agent 到底需要“摸到”哪些东西,再把它们收拢到同一个注册、路由、门禁体系里,把这些基础打扎实了再谈智能化。这样即便后面模型换了一代又一代,你的 Reach 层能一直用下去。

返回列表