首页 / 资讯中心 / 文章详情

Agent工具接入安全设计:Schema、鉴权、确认与兜底策略

Agent工具接入安全设计:Schema、鉴权、确认与兜底策略 ★ FEATURED ARTICLE
1. 从一次线上事故说起Agent 到底“读”到了什么去年冬天我负责的一个内部运维 Agent 出了事。这个 Agent 的功能很简单接收用户自然语言指令调用几个内部工具完成服务器巡检、日志拉取、配置查询。上线两周一直很稳直到某天凌晨它把一个本不该执行的“重启服务”指令真的执行了。复盘时我们发现问题不在模型本身也不在提示词。问题出在工具 Schema 的描述上。那个重启工具的 Schema 里service_name字段的类型是string描述写的是“要操作的服务名称”。模型在解析用户那句“帮我看下订单服务是不是卡了不行就重启一下”时把“订单服务”填进了service_name然后直接调用了重启接口。它没有先查询状态因为 Schema 里没有告诉它“这个工具是破坏性操作调用前需要确认”。这件事让我彻底意识到一个被很多人忽略的事实Agent 真正读的不是你的提示词而是你给它的工具 Schema。提示词决定它“想做什么”Schema 决定它“能做什么”以及“怎么做”。而安全设计恰恰就藏在这两者的交界处。这篇内容适合正在做 Agent 开发、准备给 Agent 接工具、或者已经上线了 Agent 但心里没底的同学。我会从四个维度拆解给 Agent 接工具时的安全设计Schema 的语义边界、鉴权的粒度控制、执行前的确认机制、以及失败后的兜底策略。每个维度都会给出具体的 Schema 写法、代码示例和踩坑经验尽量让你看完就能直接改自己的项目。2. Schema 不只是类型声明它是 Agent 的行为契约2.1 为什么type: string是危险的开始很多人写工具 Schema 时习惯性地把字段类型定义得尽可能宽泛。service_name用stringcommand用stringpath用string。从程序角度看没问题但从 Agent 角度看这等于给了它一张空白支票。Agent 的决策逻辑是根据用户意图在可用工具中选择最匹配的一个然后从用户输入中抽取参数填充 Schema。如果 Schema 对参数的约束只有“字符串”那模型就会把任何看起来像字符串的东西塞进去。用户说“把那个卡住的服务重启一下”模型可能填service_name: 那个卡住的服务也可能填service_name: 订单服务甚至可能填service_name: 卡住的服务。它不知道你的服务名有固定枚举值因为 Schema 没告诉它。正确的做法是凡是能枚举的绝不用自由字符串。服务名、环境名、操作类型、日志级别这些都应该用enum约束。这样模型在填充参数时会被强制在有限集合内选择而不是自由发挥。{ name: restart_service, description: 重启指定的服务。这是一个破坏性操作调用前必须确认服务当前状态。, parameters: { type: object, properties: { service_name: { type: string, enum: [order-service, user-service, payment-service, gateway], description: 要重启的服务名称必须是枚举值之一 }, environment: { type: string, enum: [staging, production], description: 目标环境。production 环境的重启需要额外审批 }, reason: { type: string, maxLength: 200, description: 重启原因用于审计日志 } }, required: [service_name, environment, reason] } }注意description里的那句话“这是一个破坏性操作调用前必须确认服务当前状态。”这不是写给开发者看的是写给模型看的。模型在决定是否调用这个工具时会参考 description 里的语义信息。你写得越明确它越不容易误判。2.2 用description给工具打“危险标签”Schema 的description字段是 Agent 安全设计里最被低估的武器。很多人把它当成注释随便写实际上它是模型理解工具意图的主要来源。我现在的习惯是在 description 里明确标注三类信息。第一这个工具是只读还是写入还是破坏性。第二调用前需要满足什么前置条件。第三调用后会产生什么副作用。这三条信息会直接影响模型的决策链。比如一个查询日志的工具description 可以写“只读操作安全。返回指定服务在指定时间范围内的日志摘要。不会修改任何状态。”而一个删除文件的工具description 应该写“破坏性操作。删除指定路径下的文件不可恢复。调用前必须确认路径正确且用户已明确授权。”实测下来这种标注方式能显著降低模型误调用破坏性工具的概率。因为模型在规划步骤时会倾向于先调用只读工具获取信息再决定是否调用写入工具。你在 description 里把“只读”和“破坏性”区分得越清楚它的规划就越合理。2.3 参数校验不能只靠模型Schema 层要兜底模型不是万能的。即使你写了enum模型仍有可能在极端情况下生成不在枚举范围内的值。这时候如果后端不做校验直接拿这个值去执行就会出问题。我的做法是Schema 定义和后端校验用同一份定义。在 Python 里可以用 Pydantic在 Node.js 里可以用 Zod把 Schema 定义一次既导出给 Agent 用又在工具执行入口做校验。这样模型传进来的参数如果不符合 Schema会在执行前被拦截而不是等到执行时才报错。from pydantic import BaseModel, Field from enum import Enum class Environment(str, Enum): staging staging production production class RestartServiceParams(BaseModel): service_name: str Field(..., description服务名称) environment: Environment Field(..., description目标环境) reason: str Field(..., max_length200, description重启原因) validator(service_name) def validate_service_name(cls, v): allowed {order-service, user-service, payment-service, gateway} if v not in allowed: raise ValueError(fservice_name must be one of {allowed}) return v这层校验看起来多余因为 Schema 里已经写了enum。但实际跑下来模型偶尔会生成一些“看起来像枚举值但拼写略有差异”的内容比如order_service而不是order-service。如果没有这层校验这个错误会一路传到执行层变成一次失败的调用。有了这层校验它会在参数解析阶段就被拦截Agent 可以收到明确的错误信息并重新尝试。3. 鉴权不是“有就行”Agent 调用工具的权限粒度设计3.1 Agent 的鉴权和用户的鉴权是两回事很多团队给 Agent 接工具时直接复用了用户的 Token。用户登录后拿到一个 TokenAgent 拿着这个 Token 去调用所有工具。这看起来很方便实际上是个巨大的安全隐患。原因很简单用户的权限范围通常远大于 Agent 当前任务所需的权限。一个用户可能是管理员能删除数据库、能重启生产环境、能修改配置。但当他让 Agent “查一下今天的订单量”时Agent 只需要一个只读的订单查询权限。如果 Agent 拿着管理员的 Token一旦模型被诱导或者出现幻觉它就能执行远超当前任务范围的破坏性操作。正确的做法是Agent 调用工具时使用独立的、最小化的服务身份。这个身份只拥有当前任务所需的权限任务结束后权限即回收。用户身份只用于“发起任务”和“审批高风险操作”不直接传递给工具执行层。3.2 按工具粒度分配权限而不是按 Agent 粒度我见过一些实现给每个 Agent 分配一个角色这个角色拥有一组工具的权限。比如“运维 Agent”拥有所有运维工具的权限。这比复用用户 Token 好但还不够细。更安全的做法是按工具粒度分配权限并且支持动态收窄。每个工具在注册时声明自己需要的权限Agent 在执行任务时根据任务类型申请对应的权限集合。比如一个“巡检任务”只需要read:logs、read:metrics、read:config三个权限那 Agent 在执行这个任务时就只能调用需要这三个权限的工具。即使模型试图调用restart_service也会因为权限不足被拒绝。# 工具权限声明示例 tools: - name: query_logs required_permissions: [read:logs] - name: query_metrics required_permissions: [read:metrics] - name: restart_service required_permissions: [write:service, approve:production] - name: delete_file required_permissions: [write:filesystem, approve:destructive] # 任务权限配置 tasks: inspection: allowed_permissions: [read:logs, read:metrics, read:config] remediation: allowed_permissions: [read:logs, read:metrics, write:service] requires_approval: [write:service]这种设计的好处是权限边界非常清晰。Agent 能做什么不取决于模型有多“聪明”而取决于当前任务被授予了什么权限。模型再怎么会规划也突破不了权限层。3.3 鉴权绕过那些容易被忽略的路径鉴权绕过是 Agent 安全里最隐蔽的坑。我踩过的一次是工具 A 需要权限read:config工具 B 需要权限write:config。但工具 A 的实现里有一个参数叫config_path用户可以传入任意路径。模型在调用工具 A 时如果传入了一个指向敏感配置的路径就能读到本不该读的内容。这不是模型的问题是工具实现的问题。但 Agent 场景放大了这个问题的危害因为模型会“创造性地”使用参数。用户说“帮我看看数据库连接配置”模型可能调用query_config并传入config_path: /etc/db/credentials。如果工具没有对路径做白名单校验这个请求就会成功。我的经验是所有工具的参数只要涉及资源定位路径、URL、ID都必须做白名单或格式校验。不能假设模型会传入“合理”的值。模型只负责生成参数不负责判断参数是否安全。安全判断必须在工具执行层做。4. 执行前的确认机制让 Agent 学会“停下来问一句”4.1 破坏性操作必须有人工确认环节Agent 最大的风险不是它做错了而是它做对了但你不希望它做。用户说“订单服务好像有问题”Agent 查到订单服务响应时间偏高然后自动重启了服务。从技术角度看这个决策链是合理的。但从业务角度看重启生产环境的订单服务需要审批不能由 Agent 自主决定。所以破坏性操作必须有人工确认环节。这个确认不是简单地弹个窗而是要包含足够的信息让用户做出判断Agent 打算做什么、为什么这么做、影响范围是什么、有没有替代方案。实现上我通常会在工具 Schema 里加一个requires_confirmation标记Agent 在执行这类工具前会先输出一个确认请求包含工具名、参数、以及模型给出的理由。用户确认后Agent 才真正调用工具。{ name: restart_service, description: 重启指定的服务。破坏性操作需要人工确认。, requires_confirmation: true, confirmation_template: 即将重启服务 {service_name}环境{environment}。原因{reason}。此操作会导致服务短暂不可用预计影响时间 30 秒。是否确认, parameters: { ... } }confirmation_template是关键。它让模型在请求确认时能生成一段人类可读的说明而不是干巴巴地列出参数。用户看到“即将重启订单服务预计影响 30 秒”比看到{service_name: order-service, environment: production}要直观得多。4.2 确认机制不能只靠模型“自觉”有些实现把确认逻辑写在提示词里“在执行破坏性操作前必须先询问用户。”这不可靠。模型可能在多轮对话中“忘记”这个规则或者在用户说“别问了直接做”时被绕过。更可靠的做法是在工具执行层强制拦截。所有标记为requires_confirmation的工具在执行前必须检查当前会话是否有一个有效的确认令牌。这个令牌由用户确认操作生成绑定到具体的工具和参数。没有令牌工具直接拒绝执行不管模型怎么请求。def execute_tool(tool_name, params, session): tool registry.get(tool_name) if tool.requires_confirmation: token session.get_confirmation_token(tool_name, params) if not token or not token.is_valid(): return { status: confirmation_required, message: tool.confirmation_template.format(**params), token_request: generate_token_request(tool_name, params) } # 执行工具 return tool.execute(params)这样即使模型试图跳过确认执行层也会拦住它。模型收到confirmation_required的响应后会把这个信息传递给用户等待用户确认。用户确认后系统生成令牌模型再次调用工具时带上令牌才能执行。4.3 确认信息的粒度太粗和太细都不好确认信息太粗用户看不懂只能盲目点“确认”。确认信息太细用户被大量技术细节淹没同样做不出判断。我的经验是确认信息应该包含“做什么、为什么、影响什么”三个要素用业务语言而不是技术语言表达。比如“重启 order-service”是技术语言“重启订单服务预计影响 30 秒”是业务语言。后者更容易让用户理解后果。如果影响范围更大比如“重启网关服务预计影响所有 API 请求 2 分钟”那确认信息里应该明确写出这个影响让用户意识到这不是一个小操作。另外确认信息里应该包含模型给出的理由。用户看到“因为检测到订单服务响应时间超过阈值”这个理由才能判断这个重启是否合理。如果模型给不出理由或者理由很牵强用户可以选择拒绝。5. 失败与异常Agent 执行出错后的兜底策略5.1 工具调用失败时Agent 不应该“自己想办法”Agent 调用工具失败时常见的错误处理方式是让模型“重试”或者“换一种方式”。这在只读场景下没问题但在写入或破坏性场景下非常危险。模型可能因为一次超时就换了一个更激进的工具来达到目的。比如查询日志超时了模型可能转而调用“重启服务”来“解决”问题。这不是它恶意而是它在追求任务完成率。所以破坏性工具的失败必须直接上报不允许模型自动重试或切换工具。我的做法是在工具 Schema 里加一个failure_policy字段。只读工具可以设为retry或fallback写入工具设为abort破坏性工具设为abort_and_alert。执行层根据这个策略决定失败后的行为。{ name: restart_service, failure_policy: abort_and_alert, description: ... }abort_and_alert意味着工具执行失败后立即终止当前任务链向用户和运维通道发送告警不允许模型继续尝试其他工具。这能防止模型在失败后“病急乱投医”。5.2 超时和幂等Agent 场景下的特殊考量Agent 调用工具的超时设置和普通服务调用不一样。普通服务调用超时通常设 3-5 秒但 Agent 场景下模型生成参数、网络传输、工具执行加起来可能超过 10 秒。如果超时设得太短工具可能实际上执行成功了但 Agent 收到了超时错误然后重试导致重复执行。对于写入类工具必须保证幂等性。比如“重启服务”这个操作重复执行两次和一次的效果应该是一样的。实现上可以用一个请求 ID 来去重Agent 每次调用工具时生成一个唯一 ID工具执行层记录已处理的 ID重复请求直接返回上次的结果。def execute_with_idempotency(tool_name, params, request_id): if request_id in processed_requests: return processed_requests[request_id] result execute_tool(tool_name, params) processed_requests[request_id] result return result这样即使 Agent 因为超时重试也不会导致服务被重启两次。5.3 审计日志出了事能查清楚Agent 的审计日志和普通系统日志不一样。普通日志记录“谁在什么时候做了什么”Agent 日志还需要记录“模型为什么这么做”。具体来说每次工具调用应该记录会话 ID、用户 ID、任务类型、模型生成的参数、模型的推理理由如果有、工具执行结果、是否经过人工确认、确认人是谁。这些信息在出事后复盘时至关重要。比如那次重启事故如果我们有完整的审计日志就能看到模型是在什么上下文下决定调用重启工具的它的推理链是什么哪个环节出了问题。没有这些日志复盘就只能靠猜。{ timestamp: 2025-01-15T03:22:17Z, session_id: sess_abc123, user_id: user_456, task_type: remediation, tool_name: restart_service, params: { service_name: order-service, environment: production, reason: 响应时间超过阈值 }, model_reasoning: 检测到订单服务 P99 响应时间超过 2 秒持续 5 分钟符合重启条件, confirmation: { required: true, confirmed_by: user_456, confirmed_at: 2025-01-15T03:22:15Z }, result: success, duration_ms: 2340 }这份日志不仅能用于复盘还能用于优化。比如你发现模型经常在某个场景下误判就可以针对性地调整 Schema 的 description 或者增加前置检查工具。6. 四个安全设计的组合拳一个完整的工具接入清单6.1 工具注册时的安全检查清单在把一个工具接入 Agent 之前我会过一遍这个清单。每一条都对应上面四个维度中的一个或多个。检查项对应维度具体要求参数是否可枚举Schema 语义能用 enum 的不用 string能用 number 的不用 stringdescription 是否标注危险性Schema 语义明确标注只读/写入/破坏性说明前置条件和副作用是否有独立权限声明鉴权粒度每个工具声明所需权限不继承用户全量权限资源定位参数是否白名单鉴权粒度路径、URL、ID 等参数必须校验格式和范围是否需要人工确认确认机制破坏性操作必须标记 requires_confirmation确认信息是否包含理由确认机制confirmation_template 包含做什么、为什么、影响什么失败策略是否明确兜底策略破坏性工具设为 abort_and_alert不允许自动重试是否幂等兜底策略写入类工具必须支持幂等防止重试导致重复执行是否记录审计日志兜底策略记录模型推理理由、确认信息、执行结果这个清单看起来长但实际过一遍也就几分钟。比起出事后复盘的成本这几分钟非常值得。6.2 从“能跑”到“敢用”渐进式接入策略如果你现在有一个已经跑起来的 Agent想加上这些安全设计不建议一次性全改。我的经验是分三步走。第一步先把所有工具的 Schema 补上enum和description标注。这一步改动最小但效果最明显。很多误调用问题在这一步就能解决。第二步加上权限层和确认机制。先给破坏性工具加确认再逐步收窄权限。这一步可能需要改一些工具的实现但不需要改 Agent 的核心逻辑。第三步完善审计日志和失败策略。这一步是长期工程可以随着工具迭代逐步完善。每一步之间留一周左右的观察期看看有没有新的问题出现。这样渐进式地改风险可控团队也容易接受。6.3 一个容易忽略的点工具之间的依赖关系最后说一个容易被忽略的点工具之间可能有依赖关系。比如“重启服务”依赖于“查询服务状态”的结果。如果 Agent 先调用了查询工具发现服务正常就不应该再调用重启工具。但如果查询工具返回了错误Agent 可能误以为服务异常然后调用重启。这种依赖关系需要在 Schema 层面表达出来。我通常会在工具 description 里写明前置条件“调用此工具前必须先调用 query_service_status 并确认状态为 unhealthy。”同时在 Agent 的规划逻辑里把这类工具设为“需要前置工具成功执行”才能调用。{ name: restart_service, description: 重启服务。前置条件必须先调用 query_service_status 并确认状态为 unhealthy。, preconditions: [ { tool: query_service_status, condition: result.status unhealthy } ] }执行层在调用restart_service前会检查当前会话中query_service_status的执行结果。如果不满足前置条件直接拒绝执行。这样即使模型“忘记”了前置条件执行层也会拦住它。这四个安全设计——Schema 语义边界、鉴权粒度、确认机制、兜底策略——不是孤立的。它们是一套组合拳共同构成 Agent 工具接入的安全底座。单独用其中一个效果有限四个一起用才能真正让 Agent 从“能跑”变成“敢用”。我在实际项目里踩过的坑基本都能归到这四个维度里。Schema 写得太宽导致误调用权限给得太大导致越权没有确认机制导致意外执行失败策略不明确导致连锁反应。每次出事之后回头看都是这四个维度里某一个没做到位。所以现在我的习惯是每接一个新工具先过一遍 6.1 的清单再想想这个工具和现有工具之间有没有依赖关系。这套流程跑下来新工具接入的安全问题基本能在上线前发现而不是等到线上出事才补救。
阅读完成 · 觉得有帮助?
咨询建站