
过去半年我大部分时间都在跟一个问题较劲Agent 能做什么不该由模型的“临场发挥”说了算而是该由一层叫 Agent-Reach 的触达治理机制说了算。项目最初只是内部客服助手的一次权限事故复盘现在整理成一套相对完整的思路——面向智能体Agent的统一触达控制层。它要解决三件事明确 Agent 能碰哪些工具和数据、每次触碰留下完整记录、不该碰的任何提示词都拦在门外。如果你正在做 Agent 类项目或者正为“权限到底怎么给”头疼这篇文章里的模型、配置和踩坑记录可以直接抄作业。1. 为什么 Agent 落地最先撞墙的是“触达半径”1.1 从一次失败的联调说起模型很强系统很脆事情是这样的。我们给公司客服团队做了一个订单助手让大模型通过函数调用去查订单、改地址、算运费。开发环境评测指标漂亮得很对话自然度、任务完成率都过了验收线。结果上线第一个晚上就出了岔子用户在一段多轮对话里被误导Agent 跟着上下文执行了一个删除注释接口的调用。数据有备份没酿成大祸但整个项目被安全团队要求即刻下线整改。复盘的时候我们盯着调用日志看了很久发现根子不在模型也不在用户的恶意输入而在最初架构设计只考虑了“模型能不能完成这个任务”完全没考虑“模型以什么身份、在什么范围内完成”。常规的做法是给模型塞一长串 system prompt告诉它“你是客服助手只能查订单不能删数据”。可问题在于大模型是概率系统prompt 里的规则写得再硬在复杂上下文、工具描述模糊、用户刻意诱导的时候依然可能被绕过。那次事故之后我下定决心Agent 的能力边界必须从“自然语言约束”变成“运行时强制管控”。1.2 触达半径的三个维度工具、数据、身份我把“Agent 到底能碰到什么”拆成三个维度这也是 Agent-Reach 最底层的思考框架。工具维Agent 能调用哪些 API、插件、内部服务。这是最浅层、也最容易被关注到的一层。数据维工具返回结果里Agent 能读取哪些字段。很多团队只控制了接口级权限却忘了控制字段级权限。比如订单接口能返回收货人手机号客服 Agent 确实需要它来完成业务但 BI 分析助手就没必要读取优衣库订单里的用户手机号。身份维Agent 代表谁、以谁的权限在执行操作。同样是改地址客服机器人以普通客服身份只能改备注以主管身份才能改收货人信息。一个 Agent 背后可能串联多个人触达控制必须跟身份系统打通。这三个维度叠加起来就是“触达半径”。用户问一句“帮我看看快递到哪了”背后其实是工具查询权限、字段读取权限、当前会话身份三者的乘积。三者都满足请求才放行任何一个不满足直接拒绝。1.3 为什么不建议把权限约束寄托在 Prompt 上我知道很多团队到现在还在靠 Prompt 管权限理由也很实际省事不引入额外基础设施。但实测下来Prompt 约束至少有四个绕不过去的问题。第一Prompt 是建议而不是禁令。模型看到“不要调用删除接口”和真正面对一个看起来很像“正常操作”的删除请求时并没有硬性的校验链路。第二工具多了之后 Prompt 会失控。当 Agent 能触达十几个工具时每个工具边界都要在 Prompt 里描述一遍上下文长度吃紧描述之间的冲突也会增多。第三提示注入防不住。网页内容、邮件正文、聊天消息里都可能藏着恶意指令它们诱导模型去调用本来不该调用的工具。只靠 Prompt 约束等于把安全寄托在模型对闲聊内容的分辨能力上。第四审计困难。Prompt 方式下模型确实“拒绝了”还是在“假装拒绝然后又悄悄调用”很难从外部感知。运行时管控则不同每一次触达都有策略引擎的裁决记录。所以 Agent-Reach 的第一条设计原则很简单权限规则从模型提示词里拿出来移到独立的运行时决策层里。模型可以有“建议”但不能有“决定权”。2. Agent-Reach 的核心模型触达清单、策略引擎、双层校验2.1 Reach Manifest把能力变成可枚举、可校验的清单动手做 Agent-Reach 之前我先问自己一个问题现在有多少工具是 Agent 真正能触达的结果团队内部没有一个人能完整列出来。文档散落在各个服务仓库里有些工具连调用方都不知道存在。所以第一件事不是写策略而是建立一份“触达清单”我叫它 Reach Manifest。你可以把 Reach Manifest 理解成 Agent 世界的路由表加说明书每个工具一条记录描述清楚名称、功能、入参、出参、调用方式、权限要求。有了它Agent 框架才知道有哪些工具可选策略引擎才知道要对哪些对象做裁决安全审计才知道该记录哪些行为。下面是我们给订单查询工具写的 Manifest 示例id: order.query name: 按订单号查询订单 description: | 仅用于按订单号查询订单详情。 用户询问“我的订单到哪了”“查一下单号 xxx”时使用。 如果用户没有提供订单号不要猜测请求用户提供。 type: http.get endpoint: https://api.internal.example.com/orders/{order_id} version: 1.2.0 input_schema: order_id: type: string required: true max_length: 32 pattern: ^[A-Z0-9]{8,32}$ output_schema: order_id: string order_status: string logistics_trace: array receiver_name: string receiver_mobile: string auth: scope: order:read identity: customer_service_robot这段 Manifest 的要点在于description 里明确写了“如果用户没有提供订单号不要猜测”。你可能会觉得这是废话但后面我会专门讲工具描述写不清楚导致的误调用远比想象中多。input_schema 里加了 pattern 校验把明显非法或可疑的入参在源头挡住。output_schema 则告诉我们这个工具会返回收货人手机号后续策略引擎可以基于此做字段级拦截。2.2 ReachPolicy默认拒绝一切显式放行才生效触达清单建好之后下一步是定义策略。我们这里最核心的一条原则是默认拒绝一切显式放行才生效。换句话说不在白名单列表里的工具任何 Agent 都调不动调用了也直接返回拒绝不存在“试试看”的余地。策略引擎的匹配逻辑很简单拿到 Agent 的调用请求后先看目标工具是否在 Manifest 里不在直接拒再看策略列表里有没有匹配的 allow 规则没有则拒。只有既在清单里、又精确命中 allow 规则请求才会被放行。一个实际的策略文件长这样policies: - id: p-default effect: deny description: 兜底策略所有未命中的请求一律拒绝 - id: p-cs-order-query effect: allow matcher: tool_id: order.query identity.roles: [customer_service_bot] session_env: production constraints: rate_limit: 60/min output_filter: deny_fields: [receiver_mobile] - id: p-cs-order-update-addr effect: allow matcher: tool_id: order.update_address identity.roles: [customer_service_supervisor] constraints: condition: current_hour 9 current_hour 22这个例子里有意思的是 p-cs-order-query 规则虽然查询接口会返回手机号但策略引擎在输出端做了字段过滤把 receiver_mobile 字段从结果里剥掉。客服机器人仍然可以查订单状态但拿不到用户手机号——这正好呼应前面说的数据维控制。另一条改地址的规则只允许主管角色在早 9 点到晚 10 点之间执行既做了角色控制也做了时间段控制。2.3 双层校验Agent 侧先判断网关侧最终把关策略引擎放在哪儿直接决定安全性。我们最初的方案是把策略校验做成 Agent 框架里的一个回调函数Agent 调用工具前会先跑一遍校验。上线前脑子里过了一遍攻击模型就发现问题如果攻击者通过提示注入让 Agent 内部的“判断逻辑”出现偏差那回调函数本身就可能被绕过。所以最终做成了双层校验。第一层是 Agent 侧校验主要为了省成本。模型在生成 function call 的时候我们可以把策略提示词注入一下让它不要生成明显违规的调用。这层不是安全边界只是降低无意义调用的概率。第二层才是真正的安全边界也就是 Agent-Reach 网关。所有 Agent 的工具调用请求统一走一个 HTTP 入口网关侧独立加载 Manifest 和 Policy不依赖任何模型名、不依赖 Agent 框架、不依赖模型上下文。哪怕模型这一侧被攻击者完全控制了到了网关这里依然拿策略裁决结果说话。我拿订单查询请求来演示整个链路async def handle_tool_call(ctx, tool_name, arguments): # 第一道本地缓存快检用于减少无效的远端调用 if not await local_cache_check(ctx, tool_name, arguments): audit.log(ctx, decisiondeny, reasonlocal_precheck_failed) raise ToolRejected(tool call blocked by precheck) # 第二道策略引擎实时裁决 decision policy_engine.evaluate( tool_idtool_name, identityctx.identity, environmentctx.environment, argumentsarguments, ) if not decision.allowed: audit.log(ctx, decisiondeny, reasondecision.reason) raise ToolRejected(decision.reason) # 通过裁决后生成幂等键并派发真实请求 replay_id make_idempotency_key( ctx.conversation_id, ctx.turn_id, tool_name, hash(arguments) ) resp await dispatcher.dispatch(tool_name, arguments, replay_idreplay_id) # 输出过滤器按策略剥离敏感字段 filtered_resp output_filter.apply(resp, decision.output_rules) audit.log(ctx, decisionallow, result_summaryfiltered_resp) return filtered_resp网关层把权限裁决、幂等控制、输出过滤、审计日志一次做完。Agent 那边对这套链路几乎无感知它只是在原来的 function calling 流程里多走了一个网关地址而已。3. 实操把一个内部订单工具接入 Agent-Reach3.1 选型与整体链路不绑定具体 Agent 框架真正动手接入前最常被问的问题有三个这个方案支持 LangGraph 吗跟 Dify 能配合吗我们自研的 Agent 框架行不行我的回答统一是Agent-Reach 不关心你的 Agent 框架它只工作在工具调用这一条链路上。你只要能把 function calling 的 HTTP 请求从“直接打到业务系统”改成“先打到 Agent-Reach 网关”接入就算完成一半。整个链路是这样的Agent 框架识别到工具调用意图 → 构造 ToolCall 请求并携带身份上下文 → 发往 Agent-Reach 网关 → 网关做策略裁决 → 放行后转发到内部服务 → 返回结果经过输出过滤 → 交回 Agent。这个设计的好处是以后换 Agent 框架甚至换模型治理层完全不动。坏处是如果你的 agent 是纯本地进程、连 HTTP 都不想多走一跳那 Agent-Reach 会引入一点网络开销——这个取舍我认为值得安全边界不应该跟应用进程绑定在一起。接入一个内部工具建议按下面五个步骤来。3.2 编写 Reach Manifest 并注册到网关写清楚工具的功能边界和输入输出格式。这一步不要嫌麻烦Manifest 的质量直接决定策略质量和误调用概率。我们内部有个不成文的规定描述里必须包含“适合使用该工具的场景”和“不适合使用该工具的场景”只写“查询订单”四个字是会被打回重写的。注册动作很简单把 Manifest 文件推到配置仓库网关监听变更并加载到内存。我们用的是 Git 仓库加 GitOps 流程任意一个 Manifest 改动都要经过 MR 评审这样避免有人悄悄给工具放开权限。3.3 改造 Agent 的 function calling 流程以 OpenAI 风格的 function calling 为例你原来可能是这样定义工具的tools [ { type: function, function: { name: order_query, description: 查询订单, parameters: {...} } } ]改造后工具定义可以保持不变但在处理模型返回的 tool_call 时不再直接执行本地函数而是把调用信息拼接成标准请求发给 Agent-Reach 网关async def execute_tool_call(tool_call, identity_context): request { tool_id: tool_call.function.name, arguments: json.loads(tool_call.function.arguments), identity: identity_context, trace_id: generate_trace_id(), } response await reach_gateway.call(request) return response这里有几个容易踩的细节我提前说一下identity_context 不能只传一个用户名至少要包含 role、department、source_system 等字段因为策略引擎要拿这些字段做匹配。trace_id 必须透传否则后续审计日志无法把一次会话的全部工具调用串起来。网关调用必须设置超时不要跟着业务系统的超时时间走。我们最开始没单独设置业务接口偶尔慢了Agent 的整个线程就被卡住后面的问题我会单独讲。3.4 配置策略并做最小权限测试首次配置核心工具的时候我强烈建议宁可少放行也不要一次开太多。比如订单助手首批只开三个工具order.query、order.update_address、logistics.track。剩下的工具全部处于 deny 状态。配置完策略后在测试环境跑一个最小验证集包含四类用例正常查单期望返回结果并记录 allow 日志。调用未授权工具期望返回 ToolRejected。越权请求比如客服机器人调 order.update_address期望被角色规则拒绝。带敏感字段输出的请求验证输出过滤是否真的把手机号剥掉了。这一步别偷懒后面所有排错都是建立在“策略确实生效”这个前提上的。3.5 上线前的验证清单我把正式上线前要过一遍的检查项整理成了一张表团队每次接新工具都拿它逐项打钩。检查项操作方式期望结果触达清单完整拉取所有 Manifest 与实际 API 文档比对无遗漏、无冗余默认拒绝生效调用一个未注册的随机工具 id返回拒绝审计有日志白名单精确只放行必要的少量工具每一条 allow 都有业务依据角色边界有效用不同角色身份调用同一工具低角色被拒绝高角色放行输出过滤有效构造含手机号、地址等敏感字段的响应网关返回中字段已剥离幂等键正确同一会话内重复提交相同写请求业务系统不重复执行跟踪日志齐全检查 trace_id 关联的调用链一次会话内全部 tool_call 可回溯这套清单看起来繁琐但真正跑完之后Agent 的行为会变得非常可预期。这也是我们后来敢把更多内部系统交给 Agent 的原因——不是对模型的可靠性有了信心而是对控制层的可靠性有了信心。4. 实测中踩过的三个坑4.1 工具描述模糊Agent 选了错误的工具第一个坑在工具数量少的时候完全没暴露。我们只有三个工具时模型几乎不会选错。后面把工具扩展到十几个问题立刻来了。某个工具叫 order.getManifest 描述就五个字“获取订单信息”。结果用户问“帮我查最近一个订单到哪了”模型没有选 logistics.track也没选 order.list_recent而是选了 order.get还往 order_id 参数里塞了个“latest”。真实业务系统不认这个值直接报参数错误。一开始我以为这是模型理解能力的问题后来查看 GPT 这类模型对工具选择的机制才发现模型的 function calling 决策在很大程度上依赖工具 description 的质量。模糊描述等于把选择权交给模型猜猜错概率自然高。我们最后总结出的写法是每个工具的描述都要包含“何时使用”和“何时不使用”两个部分必要时给出负面例子。现在 order.get 的 description 是这样的按订单号精确查询订单详情。仅当用户提供了完整订单号时使用。 如果用户没有提供订单号应使用 order.search_by_recent 列出最近订单 或主动向用户索要订单号。不要猜测、拼接或虚构订单号。这个改动上线之后order.get 的误调用率下降了将近六成。老老实实把描述写清楚是性价比最高的一次优化。4.2 长任务缓存策略险些绕过权限改动第二个坑出在性能优化上。Agent-Reach 网关刚上线那会儿我们发现一个多轮会话里同一个工具要被调用几十次每次都做完整策略匹配有点浪费就在网关里加了一层“会话级缓存”同一个会话内同一个工具一旦被放行后续半小时内默认放行。这个优化本身没什么问题但它带来了一个隐患权限配置是动态的。某天下午安全团队把某个工具的角色范围收紧紧接着线上一个长会话里的旧请求因为命中了缓存依然沿用半小时前的放行决定钻了空子。虽然事件没造成损失但这事让我出了一身冷汗。修复就是不搞长时效缓存改成每次调用都实时校验。为了性能我们只保留了一个很弱的内存缓存而且用工具 URI 加身份上下文的共同哈希作为键任何策略版本变更都会让构建缓存键的依赖版本号变化从而自动失效。安全优先于性能这是治理层该有的立场。4.3 循环调用导致费用暴涨网关侧需要幂等约束第三个坑是费用问题。某个 Agent 对接了外部物流查询接口外部接口偶尔抽风返回值结构跟预期不一致。模型拿到的“result”看起来像是正常响应但里面内容不合法于是它认为任务还没完成开始反复调用同一个工具每次失败重试都会产生一笔外部 API 费用。一个小时下来账单数字相当壮观。排查的时候我在审计日志里看到同一 trace_id 下十分钟内出现了二十多次相同参数的工具调用第一反应是模型死循环了第二反应是网关缺少防护。修复分三块网关侧引入幂等键同一个会话内相同参数重复提交时除非业务必须刷新数据否则直接返回上一次结果或拒绝。写操作一律不重放读操作降级为间隔放开。给写类工具加单会话调用次数上限超过阈值直接冷却返回“请新开会话重试”。把错误类型分类。可重试的错误超时、限流允许模型重试不可重试的错误参数非法、业务规则不允许直接返回明确提示并在返回信息里带上“不要再尝试”的语义标记。改完之后循环调用基本绝迹。我后来把这三板斧当成了默认配置只要接入新工具就直接套上。5. 从“能触达”到“触达得合理”审计与扩展方向5.1 审计日志是策略优化的数据源很多人把审计日志当成合规要求出事了再翻。但 Agent-Reach 跑了一段时间后我发现审计日志最大的价值是策略优化的数据源。每周拉一次日志做两个维度的分析一是被拒绝的调用二是高频调用的工具。被拒绝的调用很有意思比如某个客服 Agent 持续被 order.query 拒绝看日志发现是身份上下文丢了请求里没有带角色字段策略引擎把空身份当成了低权限。这往往不是工具的权限问题而是上游 Agent 框架忘记透传身份的问题。这一类 root cause 在日常联调里是肉眼发现不了的只有靠日志统计暴露。高频调用的工具则是另一个信号。某周日志显示内部搜索工具被调用了四万多次其中有三成次数是同一个用户场景下反复触发。分析后发现模型对某个指令总是先生成一次搜索、再生成一次查询多了一次多余调用。我们在 Manifest 描述里加了“优先查询、搜索是兜底”的说明调用次数立刻降了下去。审计日志里藏着大量这类优化线索不利用起来太可惜。5.2 三个值得继续做的方向Agent-Reach 目前在公司内部已经覆盖客服机器人、运维助手、数据分析助手三块场景但它距离我理想中的“触达治理平台”还有距离我自己列了三个值得继续投入的方向。一是和公司身份系统彻底打通。现在身份上下文还需要调用方显式传递一旦哪个 Agent 忘了带权限就会变得模糊。更彻底的做法是网关直接对接单点登录从令牌里解析身份和角色调用方根本碰不到身份字段。二是动态预算与成本治理。工具调用不只是权限问题还是成本问题。每个工具可以配成本档位策略引擎按会话、按天、按团队分别计算预算超了自动降级或拒绝。这部分我们已经做了雏形下一步想把它跟告警系统串起来。三是跨 Agent 的动态触达。现在每个 Agent 的触达范围是静态配置的。设想一下一个用户问客服机器人“有没有权限能帮我申请加急”机器人应该动态地把会话升级到另一个有更高权限的 Agent 手里而不是自己拿到那个权限。这个方向把“触达治理”从单 Agent 扩展到了 Agent 协作网络里是我想继续深挖的。最后再分享一点个人体会。Agent-Reach 真正让我受益的并不是那套策略引擎的代码而是它逼着我们回答了以前从没认真想过的问题这个 Agent 到底要替谁做事、需要哪些最小权限、做完事如何被证明是合规的。在模型能力快速迭代的今天把这些问题想清楚比追求让 Agent“更聪明”更优先。权限治理不是一个静态配置它会随着业务接入、角色调整、异常事件不断演进。你需要的不是一份写得完美的策略而是一套能让你持续调整策略、并且每次调整都可以被审计的基础设施。Agent-Reach 给了我们这个基础设施这也是它在团队里留下来、并持续迭代的原因。