1. 为什么在AI Agent满天飞的阶段,还要自研一个“触达控制层”
先说说我做Agent-Reach的起因,或者说,我在生产环境里被揍了半年之后总结出来的血泪教训。
这两年AI Agent的落地速度很快,从年初还在跑Demo,到年中就开始被推到线上处理真实业务。我的团队负责的是公司内部的智能运维平台,Agent要主动去调内部API、查数据库、发工单、操作工单系统、甚至偶尔要直接执行一些变更脚本。听起来没有多复杂,但真正上了生产之后,我们被一个又一个“意外”反复摩擦:Agent在测试环境跑得丝滑如德芙,一放到生产环境就开始胡言乱语——不是模型不聪明,而是它“触达”外部系统的那条链路根本控不住。
Agent-Reach这个项目,说白了就是一个跑在Agent和外部工具之间的控制层。它不改造模型,不干预Prompt,而是专门管住Agent“伸手去够外部世界”的那一下——你要调用哪个工具、以什么参数调用、调用之后返回了什么、以及这次调用有没有超出我能接受的安全边界。我把这套东西叫作“触达控制层”,它解决的问题很具体:让Agent在真实系统里“够得着”该够的东西,同时“够不着”不该够的东西。
市面上做Agent编排的框架不少,LangChain、LlamaIndex、Coze,包括各家的Workflow产品,都能把工具挂给Agent。但这些框架解决的是“能调”,在“可控地调”这件事上它们给不了你要的承诺。比如工具鉴权、调用熔断、返回内容脱敏、异常调用审计、跨服务链路追踪——这些在生产环境里每一个都不能少的东西,框架层面基本处于“你自便”的状态。
所以Agent-Reach不是要替代谁,它是作为一个独立服务,夹在Agent与后端系统之间,做四件事:权限把关、调用熔断、内容清洗、全量审计。如果你也在做Agent的生产落地,被工具调用失控、服务间调用链路追查困难、或者安全审计不过审这些问题卡住,这个项目的设计思路和踩坑记录,应该能帮你省掉不少弯路。
2. Agent-Reach的核心架构:把“触达”变成一条可审计、可熔断的链路
2.1 整体分层:调度层-策略层-执行层-审计层
Agent-Reach的架构不花哨,四个层次,从接收到响应一共就这么深。
调度层负责接收Agent发来的工具调用请求。这些请求来自哪里无所谓,可能是LangChain的Tool节点,可能是自研框架的Function Call出口,甚至可能是别人写死的HTTP回调。调度层做的是统一收口——把不同来源、不同协议的调用请求,转换成内部统一的ReachRequest格式,然后往下送。
策略层是灵魂所在。每个工具在注册进Agent-Reach的时候,都要绑定一个或多个策略模板。策略模板里定义了谁能调、什么时候能调、调用频率上限、参数校验规则、结果返回规则。这一层不直接执行调用,它只做“放行”或“拒绝”的决定,所以慢也不怕,它和真正执行逻辑是完全隔开的。
执行层才是真正去调目标服务的地方。这里支持HTTP、gRPC、数据库查询、Redis操作、Shell脚本(受限模式)几种常见的触达类型。执行层有自己的一套超时控制和重试机制,而且重试次数是硬限制,不像有的框架默认重试三次,Agent在连环失败之后会把错误信息当工具参数再传给下一个工具,这种错误传导在生产环境里能把你气死。
审计层把每一次调用的全链路信息记录下来:入参、出参、耗时、鉴权结果、熔断原因、响应内容摘要、以及响应内容里命中了哪些敏感规则。数据落到独立的审计存储里,不跟业务数据混在一起。
这四个层次之间通过简单的内部接口通信,不搞复杂的消息队列,除非你有跨机房容灾需求。单个Agent-Reach实例每秒处理几百个调用请求没有任何压力,瓶颈一般出在目标系统那边。
2.2 反向通道(Reverse Channel)的设计思路
这里要展开说一下Agent-Reach里我觉得最有意思的一个设计——反向通道。
常规的工具调用模式是Agent通过HTTP去请求工具服务,拿到结果再返回给模型。问题是:如果Agent部署在企业内网,工具服务也在内网,这套模式没问题。但Agent如果是跑在外部托管的SaaS上,或者Agent部署在私有云、目标服务在公有云那边,跨网络调用就会很别扭——你总不能为了一个Function Call把内部API直接暴露到公网。
Agent-Reach的反向通道是这样的:Agent侧只保持一个长连接连到Agent-Reach,而且这个连接只做出站连接,不允许入站穿透。当Agent需要调用某个工具时,它把意图通过长连接发给Agent-Reach,Agent-Reach那边处理完,再把结果通过同一条长连接推回来。也就是说,Agent永远不需要知道目标服务长什么样,它只跟Agent-Reach这一个中间人打交道。
这个设计的好处非常直接:目标服务可以完全不暴露公网地址,Agent-Reach只需要能访问到目标服务的内网地址就行。Agent侧的网络策略可以收紧到只放行Agent-Reach这一个出站域名,其他的全关。安全评审的时候,这个架构能省掉你一半的解释工作。
另外一个隐藏的好处是:因为调用路径上多了一层完整的数据落盘,Agent如果试图通过工具调用做越权操作,审计日志里可以把它的“犯罪现场”还原得清清楚楚——先试图调什么、参数是什么、被哪一条策略拦住了,全都有据可查。
2.3 用“函数注册表”约束Agent能力边界
工具注册不只是把服务地址和API文档填进去那么简单。我在项目里定义了一套函数注册表(Function Registry),核心是同一种工具只允许注册一次,但可以挂多个不同的“暴露口径”。
举个例子:我们内部有个订单查询服务,底层接口能查到订单的所有信息,包括客户手机号、身份证号、支付渠道这些敏感字段。但如果把整个接口原封不动注册给Agent,那就是等着出事。
函数注册表在注册时做了一件关键的事:按角色拆接口。同一个底层服务,注册成两个不同的“函数”——query_order_summary只返回订单编号、订单状态、金额、时间这类可展示字段;query_order_detail多返回一些物流和商品明细,但仍然不返回手机号、身份证号;而包含完整敏感信息的查询,直接被Agent-Reach层面拦截,除非带上审批流的令牌。
这样做的好处是Agent在决策的时候拿到的是一份被切过的工具清单,而不是底层原始API列表。Agent能“看到”哪些工具,和底层系统“实际开放”哪些能力,两件事彻底解耦。你后续加接口、改权限,只需要调整注册表,不需要重新部署Agent端。
这个思路,比我一开始设计的“直接在Prompt里告诉Agent什么不能调”靠谱太多了。Prompt约束本质上是概率性的,模型心情不好就当没看见;函数注册表是硬约束,函数根本不在列表里,Agent就是想调也无从下手。
3. 工具供给与筛选器:如何让Agent“触达”得准、也给得稳
3.1 采纳窗口与重排序(Reach Scoring)
工具数量一多,新的问题又来了:Agent面前摆着两百多个函数,模型参数上下文再大,也不可能把所有工具描述都塞进去。
OpenAI的Function Calling、Anthropic的Tool Use、以及各家开源模型的工具调用,都有工具数量或者描述长度方面的限制。实际用下来,单次传给模型选择的工具数量超过三四十个,模型的选择准确率就开始明显下降,经常出现“有正确工具不用、偏去编一个不存在工具名”的幻觉情况。
Agent-Reach在调度层做了两件事来缓解这个局面:
第一个是采纳窗口(Admission Window)。根据当前任务的上下文关键词、用户身份、历史调用路径,把两百多个函数先粗筛成十几个候选函数,只把这批候选函数的描述注入给模型。别人家的做法可能是让模型先“想”需要什么工具,再走工具查询流程——这套路理论上很优雅,但在实际生产里增加了交互轮次,也增加了模型胡说八道的空间。我的做法朴素一点:用一套倒排索引加规则引擎,直接粗筛,宁可多给一两个没有用的候选,也尽量不错杀真实需要的工具。
第二个是重排序(Reach Scoring)。粗筛之后,再对十几个候选做一次精细评分。评分维度包括:该工具在当前用户角色下的历史使用次数、该工具与当前请求文本的语义相似度(我们本地跑了个小Embedding模型算的)、该工具最近一周的调用成功率、以及该工具的“平均响应体量”——响应体量越大的工具评分稍微降一点,因为大响应体容易撑爆上下文。
打个比方:采纳窗口相当于先帮你从书架上挑出可能相关的十本书,重排序则是按照“你平时的阅读习惯”和“最近读过的内容”把书排个序,让模型第一眼就看到最该拿的那本。
3.2 实践的筛选配置示例
下面是Agent-Reach里一段实际的工具筛选策略配置,格式是YAML,我简化了字段:
tool_group: order_ops admission_window: max_candidates: 18 min_score: 0.35 filters: - by: user_role allow: [ops_admin, order_cs] deny: [auditor, external] - by: task_phase phase: [pre_sale, after_sale] match: both reach_scoring: weight_historical_usage: 0.35 weight_semantic_similarity: 0.40 weight_call_success_rate: 0.15 weight_response_payload_size: 0.10 decay_half_life_days: 14 hard_quit: enabled: true quota_per_minute: 60 quota_per_hour: 300注意那个hard_quit字段,它是我后来加进去的一个“强制熔断”开关。不管模型觉得调用有多合理,如果某个工具在一分钟内的调用次数超过60次,Agent-Reach直接拒绝后续调用,并且返回一个固定错误码给Agent——“工具过载,请稍后重试”。这个设计是为了防止Agent陷入死循环风暴:生产环境里真出现过Agent因为一次API返回格式变化,开始疯狂重试同一批调用,每秒能打出几十个请求,硬生生把下游服务打挂了。有了这两层限流,起码Agent再怎么发疯,都在你画的圈圈里发疯。
3.3 保留“人工强制开关”
筛选器再聪明,也会有误判。Agent-Reach里有一个设计,我觉得值得任何一个搞AI基础设施的人抄走:所有工具都支持一个override开关,开关一旦被人工打开或关闭,优先级最高,任何评分算法都改不了。
比如某个工具因为语义相似度评分低,总是排不进候选列表,但运营团队就是天天用它。分析下来发现是因为工具名里用的业务缩写和日常叫法不一致,语义相似度模型导出来的分数一直偏低。这时候与其去调Embedding模型或者改工具名,不如直接在筛选配置里加一行:
pin: - tool_name: query_refund_status reason: "业务常用词与工具名语义偏离,锁定为常驻候选" enabled: true被pin住的工具,永远出现在候选列表里,不参与评分竞争。反过来,如果某个工具因为历史原因总是被误选,也可以用suppress把它从候选里摘掉。这就像是给自动推荐系统装了一个“人治大于法治”的扳手,关键时刻真的能救命。
4. 落地部署:服务接入、网络边界与安全兜底
4.1 以Sidecar方式接入
Agent-Reach的接入方式我定为Sidecar模式——每个业务方Agent旁边部署一个Agent-Reach实例,而不是所有Agent共享一个大集群。
这个选择被不少人问过,为什么不集中部署一套中心化服务,省成本也方便运维。答案是:中心化服务一旦挂了,所有Agent全部瘫痪。Agent在生产环境里已经被业务方骂得够多了,我不想再承担一个“Agent本身没出问题、但是控制层把全局拖垮”的事故。
Sidecar模式虽然部署成本高一些,但故障爆炸半径小:某个业务方的Agent-Reach实例崩了,只会影响那一个业务方。而且Sidecar和Agent同Pod部署,网络链路长度几乎忽略不计,调用延迟增加基本在1ms到3ms之间,这对大多数工具调用来说是可以接受的。
每个Sidecar实例启动时,会从配置中心拉取自己负责的那部分函数注册表和策略配置。配置变更走灰度发布,不一次性全量更新,就算某条策略写错了,也只影响一个集群。
4.2 网络策略与出网管控
网络层面是安全评审的重灾区。Agent-Reach对网络的要求可以简单概括成三句话:
- 业务方Agent只连自己的Sidecar Agent-Reach,不直接触达任何内部系统。
- Sidecar Agent-Reach通过内部服务名访问目标服务,不允许跨网段直连数据库。
- 所有对外部系统的调用,统一走一个独立的出口网关,出口网关上有全量流量审计。
在企业内网环境里,这套网络模型最大的价值是:你把“谁在调什么”从一个需要事后翻日志的问题,变成了一个事前就定义清楚的结构。网络团队只需要维护Agent-Reach出口网关那几条规则,不需要为每一个Agent开网络白名单。
另外提一个细节:Agent-Reach的Sidecar进程本身是只读挂载运行,运行目录没有任何写权限,临时文件一律写到内存盘。这个细节看似多余,但实际上挡住了好几类提权攻击的路径——万一Agent被恶意Prompt注入诱导去操作Agent-Reach自身文件系统,它连写文件的权限都没有。
4.3 权限模型与密钥管理
Agent-Reach的权限模型是三层:用户身份层、Agent身份层、工具权限层。
用户身份层指业务发起人的身份,比如一个客服人员发起“查订单”动作,这个人的角色是order_cs。Agent身份层指这个动作是被哪个Agent处理的,比如售前咨询Agent和售后工单Agent属于不同身份。工具权限层才是真正决定“能不能调”的那层:一个工具的调用,必须同时满足“发起人角色允许”和“Agent身份允许”。
密钥管理上,我强烈建议别把密钥写进Agent的环境变量。Agent-Reach里有一个专门的密钥保险箱组件,所有需要访问的第三方服务密钥都存到保险箱里,Agent-Reach启动时通过内部认证拉到内存,进程退出即销毁。密钥不出内存、不落磁盘、不进日志。审计日志里对密钥的引用全部脱敏成****。
这套权限模型实际跑下来有个意外收获:安全事故定责容易多了。以前出问题还要查日志看到底是谁的Agent在乱调API,现在每一个触达动作都带着完整的身份链路——人是哪个、Agent是哪个、工具是哪个、审批流水号是多少,安全团队的同事拿到审计日志基本不需要再来找我问话。
5. 打通链路追踪与复盘:出了问题,五分钟定位到具体动作
5.1 以Reach-ID为中心的追踪体系
Agent的一次完整任务,从接收用户请求、规划步骤、调用工具、拿到返回、生成回答,整个过程里会涉及多次工具调用。Agent-Reach为每一次工具调用生成一个全局唯一的Reach-ID,而且这个Reach-ID会和Agent侧的业务请求ID绑定。
具体做法:Agent侧发起第一次工具调用时,把业务请求ID放进自定义Header带给Agent-Reach;Agent-Reach生成的Reach-ID再传给目标服务;目标服务如果产生了回调或者异步任务,也要求透传这个Reach-ID。这样,一次业务请求的所有触达动作,都可以通过业务请求ID反查到一串Reach-ID,再用Reach-ID反查每一次调用的明细。
这套追踪体系,让“Agent到底干了什么”从一个模糊概念变成了一个可查询的列表。有一次线上事故,业务方投诉说Agent把一张本应该只读的订单状态表给改了。我直接用订单ID反查,发现确实有一个Reach-ID对应的调用走的是写操作。再往下追,发现是那条任务链里Agent错误地复用了工具链——它先调了查询接口,拿到订单编号后,又按照“工单更新”的流程模板试图执行“订单更新”动作,表单里恰好把两个工具都暴露给了同一个Agent角色。整个链路在五分钟内定位清楚,该补的策略直接补上。
5.2 触发器的定义与抽样
审计日志如果全量保存,存储成本很快会让你肉痛。Agent-Reach做了一个叫Trigger的东西,把审计分成两层:全量明细层和按需深挖层。
全量明细层记录基础字段——时间、Agent身份、用户角色、工具名、调用结果、耗时、返回码。这些字段体积小,长期保存也不占太多空间。按需深挖层记录完整入参出参和策略命中明细,只在满足Trigger条件时才写入。
Trigger条件可以配置,下面是我生产环境里常用的一组:
triggers: - name: sensitive_data_access condition: tool_group in [order_detail, member_profile, finance_payment] - name: denied_attempt condition: policy_decision == deny - name: anomalous_sequence condition: call_count_in_5min >= 30 - name: slow_query condition: tool_latency_p99 >= 5000ms这样设计的好处是:日常排查90%的问题只翻明细层就行,深挖层只在异常场景才产生数据量。存储成本大概能压到全量存储的十分之一。
5.3 复盘报告模板
每次事故复盘,团队最烦的就是“口述事情经过”。Agent-Reach带了一个简单的复盘报告生成器,把一次事故关联的Reach-ID序列拉出来,自动生成链路线——先调了什么工具、结果如何、第二步又调了什么、策略在哪个环节生效或失效、最终影响面是什么。
我常用的复盘报告结构是:
- 事件背景和影响范围
- 涉及的业务请求ID和Reach-ID列表
- 按时间排序的调用链明细
- 策略命中记录(哪些调用被拦截、哪些放行)
- 根因判断和修正动作
这套东西出来后,复盘会从“大家围在一起猜”变成了“对着链路线逐段看”,效率提了不是一点半点。
6. 上线两个季度后,我踩过的坑和我还在做的事
6.1 坑一:工具函数返回体过大,直接把模型上下文撑爆
Agent-Reach上线后的第一个大坑,居然发生在我最自信的执行层。
我们有个报表查询工具,正常情况返回几千个字的JSON没什么问题。但有一次业务方在Agent里配置了一个“导出全部订单明细”的意图,底层工具直接把三个月的数据一次性返回了,响应体量达到十几兆。Agent-Reach本身没被压垮,但把数据转发回Agent之后,模型上下文的Token直接超限,整个会话卡死,后续所有请求都在排队等重试。
这个坑的教训是:除了在注册表里定义返回值结构,还必须限制返回体量上限。我在Agent-Reach的执行层加了一个max_response_payload字段,默认5MB,超过就自动截断,并附带一个truncated标志位提醒Agent“数据被截断,如需完整数据请使用分页查询”。同时做了一个分页工具的标准模板,遇到大结果集先用分页拉前N条,给模型一个“数据概览”,如果模型确实需要更多再发起后续分页调用。
6.2 坑二:重试机制误伤了正常的长耗时任务
重试机制是个双刃剑。最初Agent-Reach对超时调用统一重试一次,本意是想提高容错率。结果发现很多长耗时工具——比如数据仓库查询,本身要跑几十秒——被误判为超时,然后Agent-Reach又自动重发了一遍。于是同一个查询被触发两次,数据仓库那边瞬时负载翻倍,查询互相争抢资源,最终全部超时,业务端看到的现象就是Agent一直在转圈然后报错。
后来我改了重试策略:对SELECT类的只读查询可以重试,对写操作一律不自动重试,超时就返回给Agent自己去判断。长耗时操作还要区分是“等待型”还是“执行型”——等待型可以改成轮询任务状态模式,执行型则干脆调高超时阈值。改完之后,误触发的重试基本消灭,数据仓库那边的负载也明显降了下来。
6.3 坑三:Agent会“钻沙箱空子”,用多个低危操作拼成一次高危操作
这是我在安全层面遇到的最棘手的一个问题——组合型攻击。单个工具调用看,每一个都人畜无害:查一次订单状态、查一次库存数量、再查一次会员等级、发一条工单消息,每一个都在自己的正常权限范围内。但如果把这些操作串起来,就能拼出一条越权数据获取链。
Agent有时候不会刻意去攻击你,但模型在生成路径时天然会选择“达到目标的最短路径”,这条最短路径可能就会无意间踩到组合敏感区。我目前的应对方案是在Agent-Reach策略层加了一个简单的序列规则引擎:如果同一个业务请求ID下,某个用户角色在短时间内触发了超过N个不同的敏感只读工具,系统自动把后续调用降级为人审模式——工具照常调用,但结果会被脱敏后再返回。比如手机号变成138****1234,详细地址变成街道级别模糊信息。模型拿到的是一份“不太好用但能完成一般任务”的数据,如果确实需要精确数据,就必须走人工审批流。
说实话这个方案还不完美,组合型调用模式千变万化,规则永远会有漏网之鱼。但作为对抗模型不可预测性的一道防线,先挡住80%的常见模式,剩下的靠审计日志慢慢补规则,是目前性价比最高的做法。
6.4 还在验证的方向
Agent-Reach目前的版本担得起“稳定可用”这四个字,但有几条线我还在延伸验证。
一是多模态触达的接入。现在我们内部在做一些图像识别的Agent场景,工具返回的不再是纯文本,而可能是图片、PDF、音视频。Agent-Reach目前的返回值处理和脱敏逻辑是针对文本设计的,直接套在图片上会出问题——比如一张截图里可能含有敏感信息,但脱敏规则看不见。这个方向需要引入视觉模型做内容审核,成本高一些,所以还在做取舍。
二是多Agent协作时的触达仲裁。多个Agent协作处理一个复杂任务时,每个Agent各自有自己偏好的工具路径,Agent-Reach目前是各自为战,还没有做跨Agent的调用冲突检测。假设两个Agent同时拿着同一个订单ID去执行退款操作——一个走自动退款流程,一个走人工审核流程——彼此不知道对方的存在,就可能出现重复退款或者状态覆盖。这个问题我计划在下个版本里做基于业务实体维度的锁机制,但这需要业务方配合定义“实体维度”,推进没有我想象的顺利。
回到最开始那个问题:Agent落地,最难的不是模型变聪明,而是让它的每一次伸手都可预期、可控制、可追溯。Agent-Reach这个名字其实有两层意思——让Agent能够“够到”它需要的世界,也让运维人员能够“够到”Agent背后每一个动作的真相。如果说有什么经验最值得分享,那就是:给模型设定边界这件事,靠写在Prompt里的“请勿”是没有用的,要把边界做成基础设施的一部分,让它根本没得选。