用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
三、核心依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-actuator
com.networknt
json-schema-validator
org.springframework.boot
spring-boot-starter-jdbc
org.postgresql
postgresql
runtime
生产项目可以替换Schema验证库,但必须使用确定性验证,不能只让模型自己判断参数是否合法。
四、定义工具风险等级
package com.zyentor.toolgateway.definition;
public enum ToolRiskLevel {
LOW,
MEDIUM,
HIGH,
CRITICAL
}
推荐含义:
| 等级 | 示例 | 策略 |
|---|---|---|
| LOW | 查询天气、公开资料 | 自动执行 |
| MEDIUM | 查询内部库存 | 权限校验后执行 |
| HIGH | 取消订单、发送邮件 | 用户确认 |
| CRITICAL | 退款、删除数据、修改权限 | 二次认证与人工审批 |
风险等级必须由工具所有者配置,不能让模型动态决定。
五、定义ToolDefinition
package com.zyentor.toolgateway.definition;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.Duration;
import java.util.Set;
public record ToolDefinition(
String name,
String version,
String description,
JsonNode inputSchema,
ToolRiskLevel riskLevel,
Set requiredPermissions,
boolean idempotent,
boolean requiresConfirmation,
Duration timeout,
int maxResultBytes
) {
}
每个工具除了名称和描述,还必须包含:
版本
参数Schema
风险等级
所需权限
是否幂等
是否需要确认
超时
最大返回值
六、工具注册表
package com.zyentor.toolgateway.definition;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ToolRegistry {
private final Map definitions =
new ConcurrentHashMap();
public void register(ToolDefinition definition) {
String key = key(
definition.name(),
definition.version()
);
ToolDefinition existing =
definitions.putIfAbsent(key, definition);
if (existing != null) {
throw new IllegalStateException(
"工具已经注册:" + key
);
}
}
public ToolDefinition get(
String name,
String version
) {
ToolDefinition definition =
definitions.get(key(name, version));
if (definition == null) {
throw new ToolNotFoundException(
name,
version
);
}
return definition;
}
public Collection list() {
return List.copyOf(definitions.values());
}
private String key(String name, String version) {
return name + ":" + version;
}
}
生产环境还应防止同名不同语义工具,并支持:
Active
Deprecated
Disabled
Removed
生命周期。
七、定义执行请求
package com.zyentor.toolgateway.api;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record ToolExecutionRequest(
@NotBlank
String requestId,
@NotBlank
String conversationId,
@NotBlank
String toolName,
@NotBlank
String toolVersion,
@NotBlank
String idempotencyKey,
@NotNull
JsonNode arguments,
String approvalId
) {
}
请求中不应让客户端直接传:
tenantId
userId
permissions
这些字段必须从认证上下文读取。
八、定义执行上下文
package com.zyentor.toolgateway.execution;
import java.util.Set;
public record ToolExecutionContext(
String requestId,
String conversationId,
String tenantId,
String userId,
Set permissions,
String clientId,
String sourceIp
) {
}
上下文应由网关从:
- JWT;
- OAuth Token;
- API Gateway Header;
- 服务身份;
中解析,并进行签名校验。
九、参数Schema验证
@Component
public class ToolArgumentValidator {
private final JsonSchemaFactory schemaFactory =
JsonSchemaFactory.getInstance(
SpecVersion.VersionFlag.V202012
);
public void validate(
ToolDefinition definition,
JsonNode arguments
) {
JsonSchema schema = schemaFactory.getSchema(
definition.inputSchema()
);
Set errors =
schema.validate(arguments);
if (!errors.isEmpty()) {
throw new InvalidToolArgumentsException(
errors.stream()
.limit(10)
.map(ValidationMessage::getMessage)
.toList()
);
}
}
}
必须限制:
Schema大小
Schema深度
参数大小
数组长度
字符串长度
验证时间
错误数量
避免恶意Schema和超大参数消耗资源。
十、权限与白名单策略
@Component
public class PermissionService {
public boolean hasAllPermissions(
ToolExecutionContext context,
ToolDefinition definition
) {
return context.permissions().containsAll(
definition.requiredPermissions()
);
}
}
策略决策:
public enum PolicyDecision {
ALLOW,
REQUIRE_CONFIRMATION,
REQUIRE_APPROVAL,
DENY
}
@Component
public class ToolPolicyEngine {
private final PermissionService permissionService;
public ToolPolicyEngine(
PermissionService permissionService
) {
this.permissionService = permissionService;
}
public PolicyDecision decide(
ToolExecutionContext context,
ToolDefinition definition
) {
if (!permissionService.hasAllPermissions(
context,
definition
)) {
return PolicyDecision.DENY;
}
return switch (definition.riskLevel()) {
case LOW, MEDIUM ->
definition.requiresConfirmation()
? PolicyDecision.REQUIRE_CONFIRMATION
: PolicyDecision.ALLOW;
case HIGH ->
PolicyDecision.REQUIRE_CONFIRMATION;
case CRITICAL ->
PolicyDecision.REQUIRE_APPROVAL;
};
}
}
Prompt中的“请谨慎使用”不能替代策略引擎。
十一、确认和审批需要分开
用户确认
用户本人确认当前动作:
取消订单A1001,是否确认?
人工审批
由拥有审批权限的其他人批准:
退款金额超过5000元,需要财务审批
状态:
public enum ApprovalStatus {
PENDING,
APPROVED,
REJECTED,
EXPIRED,
CANCELLED
}
审批记录必须绑定:
工具名称
参数Hash
申请人
审批人
有效期
业务对象
参数变化后,旧审批不得继续使用。
十二、幂等设计
模型可能因为:
- 网络超时;
- 流式断开;
- 重试;
- Tool Calling循环;
- 用户重复点击;
重复发起同一工具。
数据库表:
CREATE TABLE tool_idempotency (
tenant_id VARCHAR(64) NOT NULL,
idempotency_key VARCHAR(200) NOT NULL,
tool_name VARCHAR(100) NOT NULL,
arguments_hash VARCHAR(64) NOT NULL,
status VARCHAR(30) NOT NULL,
result_json TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
PRIMARY KEY (tenant_id, idempotency_key)
);
规则:
同一幂等键+同一参数
→ 返回原结果
同一幂等键+不同参数
→ 拒绝
不能只使用Redis短缓存处理付款、退款等关键业务。
十三、定义ToolExecutor
package com.zyentor.toolgateway.execution;
import com.fasterxml.jackson.databind.JsonNode;
public interface ToolExecutor {
String toolName();
String toolVersion();
JsonNode execute(
ToolExecutionContext context,
JsonNode arguments
);
}
示例订单查询:
@Component
public class QueryOrderExecutor
implements ToolExecutor {
private final OrderService orderService;
private final ObjectMapper objectMapper;
@Override
public String toolName() {
return "query_order";
}
@Override
public String toolVersion() {
return "1.0";
}
@Override
public JsonNode execute(
ToolExecutionContext context,
JsonNode arguments
) {
String orderId = arguments
.required("orderId")
.asText();
OrderSummary result = orderService.query(
context.tenantId(),
orderId
);
return objectMapper.valueToTree(result);
}
}
十四、执行器注册表
@Component
public class ToolExecutorRegistry {
private final Map executors;
public ToolExecutorRegistry(
List executorList
) {
this.executors = executorList.stream()
.collect(Collectors.toUnmodifiableMap(
executor -> key(
executor.toolName(),
executor.toolVersion()
),
Function.identity()
));
}
public ToolExecutor get(
String name,
String version
) {
ToolExecutor executor =
executors.get(key(name, version));
if (executor == null) {
throw new ToolExecutorNotFoundException(
name,
version
);
}
return executor;
}
}
十五、审计事件
public record ToolAuditEvent(
String auditId,
String requestId,
String conversationId,
String tenantId,
String userId,
String toolName,
String toolVersion,
String argumentsHash,
ToolRiskLevel riskLevel,
PolicyDecision policyDecision,
String approvalId,
String executionStatus,
long durationMs,
String resultHash,
Instant occurredAt
) {
}
审计日志不建议直接保存完整敏感参数。
可以保存:
参数Hash
脱敏摘要
业务对象ID
完整敏感内容放到受控业务系统中。
十六、完整ToolExecutionService
@Service
public class ToolExecutionService {
private final ToolRegistry toolRegistry;
private final ToolExecutorRegistry executorRegistry;
private final ToolArgumentValidator argumentValidator;
private final ToolPolicyEngine policyEngine;
private final ApprovalService approvalService;
private final IdempotencyService idempotencyService;
private final ToolAuditService auditService;
public ToolExecutionResponse execute(
ToolExecutionContext context,
ToolExecutionRequest request
) {
long start = System.nanoTime();
ToolDefinition definition = toolRegistry.get(
request.toolName(),
request.toolVersion()
);
argumentValidator.validate(
definition,
request.arguments()
);
PolicyDecision decision = policyEngine.decide(
context,
definition
);
if (decision == PolicyDecision.DENY) {
throw new ToolAccessDeniedException();
}
if (decision == PolicyDecision.REQUIRE_CONFIRMATION) {
return ToolExecutionResponse.confirmationRequired(
request.requestId(),
buildConfirmation(definition, request)
);
}
if (decision == PolicyDecision.REQUIRE_APPROVAL) {
approvalService.assertApproved(
request.approvalId(),
context,
definition,
request.arguments()
);
}
return idempotencyService.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")
public class ToolExecutionController {
private final ToolExecutionService service;
private final CurrentUserService currentUserService;
@PostMapping
public ToolExecutionResponse execute(
@Valid
@RequestBody
ToolExecutionRequest request,
HttpServletRequest httpRequest
) {
CurrentUser user = currentUserService.requireUser();
ToolExecutionContext context =
new ToolExecutionContext(
request.requestId(),
request.conversationId(),
user.tenantId(),
user.userId(),
user.permissions(),
user.clientId(),
httpRequest.getRemoteAddr()
);
return service.execute(context, request);
}
}
十九、返回协议
public record ToolExecutionResponse(
String requestId,
String status,
String code,
String message,
JsonNode data,
ConfirmationPayload confirmation,
String businessResultId
) {
}
状态建议:
SUCCESS
FAILED
DENIED
CONFIRMATION_REQUIRED
APPROVAL_REQUIRED
IN_PROGRESS
二十、如何与Spring AI接入
Spring AI中的工具不直接执行核心业务,而是调用网关:
@Tool(
description = "取消指定订单。高风险动作,可能需要确认。"
)
public ToolExecutionResponse cancelOrder(
CancelOrderArguments arguments,
ToolContext toolContext
) {
return gatewayClient.execute(
buildRequest(arguments, toolContext)
);
}
模型收到:
CONFIRMATION_REQUIRED
后向用户展示确认内容,而不是绕过网关执行。
二十一、测试重点
至少覆盖:
未知工具
禁用工具
Schema错误
无权限
确认未完成
审批过期
幂等重复
幂等参数冲突
执行超时
结果过长
敏感字段脱敏
审计写入失败
业务执行成功但响应中断
高风险工具要做并发幂等测试。
二十二、生产环境还需要补齐
- OAuth与服务身份;
- 数据库事务;
- Outbox事件;
- 熔断;
- 超时;
- 限流;
- 多区域幂等;
- Secret管理;
- OpenTelemetry;
- 审批通知;
- 工具版本灰度;
- Schema兼容检查;
- 工具停用开关。
总结
AI工具网关的核心不是“把函数统一放到一个接口”,而是建立确定性控制面:
白名单
+Schema校验
+权限
+风险策略
+确认与审批
+幂等
+脱敏
+审计
模型负责提出动作,网关负责判断动作是否允许、是否安全,以及能否被可靠地执行一次。
延伸阅读
如果你正在关注企业级 AI 应用、Spring AI、RAG、Agent 与 MCP 工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。