文章摘要
直接把订单、退款、邮件、文件删除等业务方法暴露给大模型,会把模型的不确定性带入真实业务系统。更稳妥的做法是建立独立工具执行网关:模型只能提出工具名称和参数,网关负责身份校验、工具白名单、JSON Schema验证、风险分级、人工确认、幂等执行、结果脱敏和审计。本文使用Spring Boot实现一个可运行的轻量级工具网关,并给出ToolDefinition、PolicyEngine、Approval、Idempotency和Audit的核心代码。
一、为什么需要工具执行网关
最简单的Agent工具调用:
大模型 → 直接调用业务方法 → 返回结果Demo阶段很方便,进入生产环境后会暴露多个问题:
- 模型可能选错工具;
- 参数可能缺失或格式错误;
- 用户没有工具权限;
- 同一动作可能重复执行;
- 高风险操作缺少确认;
- 工具返回敏感数据;
- 无法追踪谁在什么时候做了什么;
- 工具升级后Schema不兼容;
- 服务异常时模型反复重试。
工具执行网关把链路改为:
模型生成Tool Call → 工具网关接收 → 身份与白名单 → Schema校验 → 风险策略 → 审批或确认 → 幂等执行 → 结果脱敏 → 审计 → 返回模型二、项目结构
ai-tool-gateway ├── pom.xml └── src/main/java/com/zyentor/toolgateway ├── api │ ├── ToolExecutionController.java │ ├── ToolExecutionRequest.java │ └── ToolExecutionResponse.java ├── definition │ ├── ToolDefinition.java │ ├── ToolRiskLevel.java │ └── ToolRegistry.java ├── execution │ ├── ToolExecutor.java │ ├── ToolExecutionService.java │ └── ToolExecutionContext.java ├── policy │ ├── ToolPolicyEngine.java │ ├── PolicyDecision.java │ └── PermissionService.java ├── approval │ ├── ApprovalService.java │ └── ApprovalStatus.java ├── idempotency │ └── IdempotencyService.java └── audit ├── ToolAuditEvent.java └── ToolAuditService.java三、核心依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>com.networknt</groupId><artifactId>json-schema-validator</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency></dependencies>生产项目可以替换Schema验证库,但必须使用确定性验证,不能只让模型自己判断参数是否合法。
四、定义工具风险等级
packagecom.zyentor.toolgateway.definition;publicenumToolRiskLevel{LOW,MEDIUM,HIGH,CRITICAL}推荐含义:
| 等级 | 示例 | 策略 |
|---|---|---|
| LOW | 查询天气、公开资料 | 自动执行 |
| MEDIUM | 查询内部库存 | 权限校验后执行 |
| HIGH | 取消订单、发送邮件 | 用户确认 |
| CRITICAL | 退款、删除数据、修改权限 | 二次认证与人工审批 |
风险等级必须由工具所有者配置,不能让模型动态决定。
五、定义ToolDefinition
packagecom.zyentor.toolgateway.definition;importcom.fasterxml.jackson.databind.JsonNode;importjava.time.Duration;importjava.util.Set;publicrecordToolDefinition(Stringname,Stringversion,Stringdescription,JsonNodeinputSchema,ToolRiskLevelriskLevel,Set<String>requiredPermissions,booleanidempotent,booleanrequiresConfirmation,Durationtimeout,intmaxResultBytes){}每个工具除了名称和描述,还必须包含:
版本 参数Schema 风险等级 所需权限 是否幂等 是否需要确认 超时 最大返回值六、工具注册表
packagecom.zyentor.toolgateway.definition;importorg.springframework.stereotype.Component;importjava.util.Collection;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;@ComponentpublicclassToolRegistry{privatefinalMap<String,ToolDefinition>definitions=newConcurrentHashMap<>();publicvoidregister(ToolDefinitiondefinition){Stringkey=key(definition.name(),definition.version());ToolDefinitionexisting=definitions.putIfAbsent(key,definition);if(existing!=null){thrownewIllegalStateException("工具已经注册:"+key);}}publicToolDefinitionget(Stringname,Stringversion){ToolDefinitiondefinition=definitions.get(key(name,version));if(definition==null){thrownewToolNotFoundException(name,version);}returndefinition;}publicCollection<ToolDefinition>list(){returnList.copyOf(definitions.values());}privateStringkey(Stringname,Stringversion){returnname+":"+version;}}生产环境还应防止同名不同语义工具,并支持:
Active Deprecated Disabled Removed生命周期。
七、定义执行请求
packagecom.zyentor.toolgateway.api;importcom.fasterxml.jackson.databind.JsonNode;importjakarta.validation.constraints.NotBlank;importjakarta.validation.constraints.NotNull;publicrecordToolExecutionRequest(@NotBlankStringrequestId,@NotBlankStringconversationId,@NotBlankStringtoolName,@NotBlankStringtoolVersion,@NotBlankStringidempotencyKey,@NotNullJsonNodearguments,StringapprovalId){}请求中不应让客户端直接传:
tenantId userId permissions这些字段必须从认证上下文读取。
八、定义执行上下文
packagecom.zyentor.toolgateway.execution;importjava.util.Set;publicrecordToolExecutionContext(StringrequestId,StringconversationId,StringtenantId,StringuserId,Set<String>permissions,StringclientId,StringsourceIp){}上下文应由网关从:
- JWT;
- OAuth Token;
- API Gateway Header;
- 服务身份;
中解析,并进行签名校验。
九、参数Schema验证
@ComponentpublicclassToolArgumentValidator{privatefinalJsonSchemaFactoryschemaFactory=JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);publicvoidvalidate(ToolDefinitiondefinition,JsonNodearguments){JsonSchemaschema=schemaFactory.getSchema(definition.inputSchema());Set<ValidationMessage>errors=schema.validate(arguments);if(!errors.isEmpty()){thrownewInvalidToolArgumentsException(errors.stream().limit(10).map(ValidationMessage::getMessage).toList());}}}必须限制:
Schema大小 Schema深度 参数大小 数组长度 字符串长度 验证时间 错误数量避免恶意Schema和超大参数消耗资源。
十、权限与白名单策略
@ComponentpublicclassPermissionService{publicbooleanhasAllPermissions(ToolExecutionContextcontext,ToolDefinitiondefinition){returncontext.permissions().containsAll(definition.requiredPermissions());}}策略决策:
publicenumPolicyDecision{ALLOW,REQUIRE_CONFIRMATION,REQUIRE_APPROVAL,DENY}@ComponentpublicclassToolPolicyEngine{privatefinalPermissionServicepermissionService;publicToolPolicyEngine(PermissionServicepermissionService){this.permissionService=permissionService;}publicPolicyDecisiondecide(ToolExecutionContextcontext,ToolDefinitiondefinition){if(!permissionService.hasAllPermissions(context,definition)){returnPolicyDecision.DENY;}returnswitch(definition.riskLevel()){caseLOW,MEDIUM->definition.requiresConfirmation()?PolicyDecision.REQUIRE_CONFIRMATION:PolicyDecision.ALLOW;caseHIGH->PolicyDecision.REQUIRE_CONFIRMATION;caseCRITICAL->PolicyDecision.REQUIRE_APPROVAL;};}}Prompt中的“请谨慎使用”不能替代策略引擎。
十一、确认和审批需要分开
用户确认
用户本人确认当前动作:
取消订单A1001,是否确认?人工审批
由拥有审批权限的其他人批准:
退款金额超过5000元,需要财务审批状态:
publicenumApprovalStatus{PENDING,APPROVED,REJECTED,EXPIRED,CANCELLED}审批记录必须绑定:
工具名称 参数Hash 申请人 审批人 有效期 业务对象参数变化后,旧审批不得继续使用。
十二、幂等设计
模型可能因为:
- 网络超时;
- 流式断开;
- 重试;
- Tool Calling循环;
- 用户重复点击;
重复发起同一工具。
数据库表:
CREATETABLEtool_idempotency(tenant_idVARCHAR(64)NOTNULL,idempotency_keyVARCHAR(200)NOTNULL,tool_nameVARCHAR(100)NOTNULL,arguments_hashVARCHAR(64)NOTNULL,statusVARCHAR(30)NOTNULL,result_jsonTEXT,created_atTIMESTAMPNOTNULL,updated_atTIMESTAMPNOTNULL,PRIMARYKEY(tenant_id,idempotency_key));规则:
同一幂等键+同一参数 → 返回原结果 同一幂等键+不同参数 → 拒绝不能只使用Redis短缓存处理付款、退款等关键业务。
十三、定义ToolExecutor
packagecom.zyentor.toolgateway.execution;importcom.fasterxml.jackson.databind.JsonNode;publicinterfaceToolExecutor{StringtoolName();StringtoolVersion();JsonNodeexecute(ToolExecutionContextcontext,JsonNodearguments);}示例订单查询:
@ComponentpublicclassQueryOrderExecutorimplementsToolExecutor{privatefinalOrderServiceorderService;privatefinalObjectMapperobjectMapper;@OverridepublicStringtoolName(){return"query_order";}@OverridepublicStringtoolVersion(){return"1.0";}@OverridepublicJsonNodeexecute(ToolExecutionContextcontext,JsonNodearguments){StringorderId=arguments.required("orderId").asText();OrderSummaryresult=orderService.query(context.tenantId(),orderId);returnobjectMapper.valueToTree(result);}}十四、执行器注册表
@ComponentpublicclassToolExecutorRegistry{privatefinalMap<String,ToolExecutor>executors;publicToolExecutorRegistry(List<ToolExecutor>executorList){this.executors=executorList.stream().collect(Collectors.toUnmodifiableMap(executor->key(executor.toolName(),executor.toolVersion()),Function.identity()));}publicToolExecutorget(Stringname,Stringversion){ToolExecutorexecutor=executors.get(key(name,version));if(executor==null){thrownewToolExecutorNotFoundException(name,version);}returnexecutor;}}十五、审计事件
publicrecordToolAuditEvent(StringauditId,StringrequestId,StringconversationId,StringtenantId,StringuserId,StringtoolName,StringtoolVersion,StringargumentsHash,ToolRiskLevelriskLevel,PolicyDecisionpolicyDecision,StringapprovalId,StringexecutionStatus,longdurationMs,StringresultHash,InstantoccurredAt){}审计日志不建议直接保存完整敏感参数。
可以保存:
参数Hash 脱敏摘要 业务对象ID完整敏感内容放到受控业务系统中。
十六、完整ToolExecutionService
@ServicepublicclassToolExecutionService{privatefinalToolRegistrytoolRegistry;privatefinalToolExecutorRegistryexecutorRegistry;privatefinalToolArgumentValidatorargumentValidator;privatefinalToolPolicyEnginepolicyEngine;privatefinalApprovalServiceapprovalService;privatefinalIdempotencyServiceidempotencyService;privatefinalToolAuditServiceauditService;publicToolExecutionResponseexecute(ToolExecutionContextcontext,ToolExecutionRequestrequest){longstart=System.nanoTime();ToolDefinitiondefinition=toolRegistry.get(request.toolName(),request.toolVersion());argumentValidator.validate(definition,request.arguments());PolicyDecisiondecision=policyEngine.decide(context,definition);if(decision==PolicyDecision.DENY){thrownewToolAccessDeniedException();}if(decision==PolicyDecision.REQUIRE_CONFIRMATION){returnToolExecutionResponse.confirmationRequired(request.requestId(),buildConfirmation(definition,request));}if(decision==PolicyDecision.REQUIRE_APPROVAL){approvalService.assertApproved(request.approvalId(),context,definition,request.arguments());}returnidempotencyService.executeOnce(context.tenantId(),request.idempotencyKey(),request.toolName(),request.arguments(),()->executeActual(context,request,definition,decision,start));}}十七、结果大小和脱敏
执行成功后不能直接把所有结果返回模型。
先处理:
字段白名单 敏感字段脱敏 最大字节数 分页 结果摘要例如客户对象只返回:
{"customerId":"C1001","name":"张**","level":"VIP","status":"ACTIVE"}不要返回:
- 身份证号;
- 完整手机号;
- 密码Hash;
- 银行卡;
- 内部备注;
- 数据库技术字段。
十八、Controller
@RestController@RequestMapping("/api/tool-executions")publicclassToolExecutionController{privatefinalToolExecutionServiceservice;privatefinalCurrentUserServicecurrentUserService;@PostMappingpublicToolExecutionResponseexecute(@Valid@RequestBodyToolExecutionRequestrequest,HttpServletRequesthttpRequest){CurrentUseruser=currentUserService.requireUser();ToolExecutionContextcontext=newToolExecutionContext(request.requestId(),request.conversationId(),user.tenantId(),user.userId(),user.permissions(),user.clientId(),httpRequest.getRemoteAddr());returnservice.execute(context,request);}}十九、返回协议
publicrecordToolExecutionResponse(StringrequestId,Stringstatus,Stringcode,Stringmessage,JsonNodedata,ConfirmationPayloadconfirmation,StringbusinessResultId){}状态建议:
SUCCESS FAILED DENIED CONFIRMATION_REQUIRED APPROVAL_REQUIRED IN_PROGRESS二十、如何与Spring AI接入
Spring AI中的工具不直接执行核心业务,而是调用网关:
@Tool(description="取消指定订单。高风险动作,可能需要确认。")publicToolExecutionResponsecancelOrder(CancelOrderArgumentsarguments,ToolContexttoolContext){returngatewayClient.execute(buildRequest(arguments,toolContext));}模型收到:
CONFIRMATION_REQUIRED后向用户展示确认内容,而不是绕过网关执行。
二十一、测试重点
至少覆盖:
未知工具 禁用工具 Schema错误 无权限 确认未完成 审批过期 幂等重复 幂等参数冲突 执行超时 结果过长 敏感字段脱敏 审计写入失败 业务执行成功但响应中断高风险工具要做并发幂等测试。
二十二、生产环境还需要补齐
- OAuth与服务身份;
- 数据库事务;
- Outbox事件;
- 熔断;
- 超时;
- 限流;
- 多区域幂等;
- Secret管理;
- OpenTelemetry;
- 审批通知;
- 工具版本灰度;
- Schema兼容检查;
- 工具停用开关。
总结
AI工具网关的核心不是“把函数统一放到一个接口”,而是建立确定性控制面:
白名单 +Schema校验 +权限 +风险策略 +确认与审批 +幂等 +脱敏 +审计模型负责提出动作,网关负责判断动作是否允许、是否安全,以及能否被可靠地执行一次。