用Spring Boot搭建AI工具执行网关:白名单、审批、幂等与审计完整实战

文章摘要

直接把订单、退款、邮件、文件删除等业务方法暴露给大模型,会把模型的不确定性带入真实业务系统。更稳妥的做法是建立独立工具执行网关:模型只能提出工具名称和参数,网关负责身份校验、工具白名单、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校验 +权限 +风险策略 +确认与审批 +幂等 +脱敏 +审计

模型负责提出动作,网关负责判断动作是否允许、是否安全,以及能否被可靠地执行一次。