AI Agent风控闸门:基于Spring Boot的可审计安全过滤层设计
发布时间:2026/9/2 5:19:18 作者:尧图编辑部 阅读量:1,286

先还原一个典型的故障现场。一个负责执行交易策略的 AI Agent 收到一条文本信号“market risk high, sell 500 ETH”。由于上游模型对字段做了错误映射Agent 将风险级别解析成了 LOW并直接向交易系统提交了名义金额 120 万美元的卖单。看起来每一步都符合逻辑但整条链路缺少一个“人能够插手的确认点”。这类问题在 AI Agent 自动化流程中越来越常见尤其是涉及资金、权限、生产变更的场景。本文要分享的是一套可复用的 AI Agent 风控闸门设计方案Agent Guard。这套方案的核心目标是在 Agent 与真实执行系统之间增加一层可审计、可规则校验、可人工审批、可熔断的安全过滤层。文章会从事故复盘讲起再给出一套基于 Spring Boot 的最小可运行实现包含完整代码、配置、接口调用示例和常见问题排查思路。内容兼顾新手的理解成本和有经验开发者的落地方案适合正在做 Agent 开发、Agent 安全、自动化交易风控或者大模型工具调用的读者。1. 事故复盘AI Agent 为什么会移走 120 万美元1.1 一条被“误读”的风险信号很多 Agent 任务并不是端到端生成一段文本而是通过“感知信号 → 理解语义 → 选择动作 → 调用工具”的链路执行。交易类 Agent 的输入往往来自实时风控信号、舆情分析、行情策略模型等。每一次输入都可能包含多个字段比如风险等级、置信度、方向、金额、时间戳。只要某一个字段在解析阶段被错误映射Agent 后续的动作就会在错误的上下文上继续推演。在我们复盘的这个案例中原始文本是market risk high, sell 500 ETH。Agent 的工具调用层应该把风险等级识别为 HIGH但由于关键词提取规则不完整模型把risk high映射成了 LOW。随后 Agent 根据 LOW 风险、较高置信度的判断直接提交了卖单。从 Agent 内部看它确实“按规则执行”了但从全局看这是对真实风险信号的一次严重误读。1.2 常见的 Agent 风险信号误读根因从大量 Agent 落地项目看风险信号被误读的原因通常不是模型能力不够而是流程设计缺少校验。常见根因包括字段语义从文本到结构化数据转换时使用了有歧义的映射规则。比如risk high和high risk被不同正则覆盖。Agent 在调用外部工具时工具返回结果超时或格式异常Agent 走了兜底分支把风险字段默认成“安全值”。上游信号缺少统一 schema不同来源分别使用 high、severe、critical、低风险等表达方式Agent 没有做归一化。Agent 的执行权限过大遇到疑似风险信号时默认“继续执行”而不是“暂停确认”。缺少人工审批环节所有判断完全由模型完成没有人在最后一步复核。这些问题不是单点技术 bug而是整条自动化链路的治理缺失。单纯提高模型精度并不能完全解决因为模型永远可能出现概率性误判。我们需要的是一层独立于模型推理的确定性防护机制。1.3 缺失的一环Agent Guard 风控闸门如果把 Agent 看成“大脑”执行系统看成“双手”那么当前很多项目缺少的是“脊椎里的刹车系统”。Agent Guard 就是这样一个刹车系统。它不替代 Agent 做交易决策也不替代模型理解语义而是在 Agent 提交动作之前做几件确定的事重新检查原始信号与结构化字段是否一致运行一组可配置的规则判断是否允许自动执行对高风险或超阈值操作强制进入人工审批对审批过程、校验结果、最终决策做全量审计在异常情况下直接拦截或熔断。这样即使 Agent 误读了风险信号风控闸门也能在最后一公里发现问题。2. 方案设计Agent Guard 风控层2.1 整体架构为了便于理解我们把方案设计成一个独立的 Spring Boot 服务。Agent 不再直接把交易指令发给交易系统而是先调用 Agent Guard 的接口。Agent Guard 完成校验、审批、审计之后只返回一个决策结果。真正连接交易系统的执行服务再根据决策结果决定是否继续。整体链路如下AI Agent ↓ 提交 TradeSignal Agent Guard Service ↓ 规则校验 RiskSignalValidator ↓ 高风险/超阈值 ManualApprovalService ↓ 人工审批 AuditLogService ↓ 返回 AgentDecision 执行服务模拟这个架构最大的好处是Agent 的逻辑和风控逻辑彻底解耦。风控规则变更时不需要重新发布 AgentAgent 模型升级时风控能力不会受到模型概率影响。2.2 核心组件方案中主要包含以下组件TradeSignal风险信号实体包含原始文本、解析后的风险等级、置信度、交易方向、金额等字段。AgentDecision风控决策结果包含审批状态和原因。RiskSignalValidator核心规则校验器负责归一化风险等级、检查置信度、检查重复信号、检查金额上限。ManualApprovalService人工审批服务维护一个待审批队列。AuditLogService审计日志服务记录每一次信号提交和审批操作。AgentGuardService编排以上所有服务对外提供统一入口。设计上保持每个组件职责单一。规则校验器只做判断不负责数据库持久化审批服务只负责状态流转不负责具体执行审计日志独立记录不影响主流程。这样后续把内存实现替换成 Redis、PostgreSQL、消息队列时改动范围会更小。2.3 决策流转流程一次完整的风控决策会经历以下状态Agent 提交 TradeSignal。如果风控总开关关闭直接返回 AUTO_EXECUTED方便测试环境调试。如果命中硬性违规规则返回 BLOCKED并记录违规原因。如果风险等级为 HIGH 或 CRITICAL但未命中硬性规则进入 PENDING。如果金额超过自动执行上限进入 PENDING。如果普通信号通过全部规则返回 AUTO_EXECUTED。PENDING 状态由审批人执行 approve 或 reject审批结果重新返回给调用方。这个流程把“硬拦截”和“人工确认”分开。硬拦截解决明显错误场景人工确认解决风险边界场景。3. 环境准备与项目结构3.1 技术选型说明为了跑通完整示例我们选择 Java 17 Spring Boot 3。这个组合在 Agent 后端服务中比较常见依赖管理和工程结构都比较成熟。示例使用 Spring Web 对外提供 REST 接口使用内存数据结构模拟数据库。生产环境建议替换为 Redis PostgreSQL 更稳妥。版本需要根据实际项目调整。本文示例以 Spring Boot 3.2.4 为例重点演示配置和代码思路如果你使用的是其他 Spring Boot 3.x 版本修改父版本号即可。3.2 项目目录项目结构如下trading-agent-guard/ ├── pom.xml └── src/main/java/com/example/guard/ ├── GuardApplication.java ├── config/ │ └── GuardProperties.java ├── model/ │ ├── AgentDecision.java │ ├── ApprovalStatus.java │ ├── ApproveRequest.java │ └── TradeSignal.java └── service/ ├── AgentGuardService.java ├── AuditLogService.java ├── ManualApprovalService.java └── RiskSignalValidator.java这个结构足够清晰后续如果要接入数据库只需新增 repository 层并对 service 内部实现做替换。3.3 引入 Maven 依赖pom.xml 需要引入 Spring Boot 基础 Web 依赖和参数校验依赖。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.4/version relativePath/ /parent groupIdcom.example/groupId artifactIdtrading-agent-guard/artifactId version1.0.0-SNAPSHOT/version nametrading-agent-guard/name descriptionAI Agent risk guard demo/description properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project这里没有引入数据库依赖是因为示例用内存保存待审批队列和审计日志。实际项目中待审批状态必须持久化否则服务重启会导致审批丢失。4. 核心代码实现4.1 配置类首先创建一个配置类用来读取 application.yml 中的风控参数。这样规则阈值不需要改代码通过配置即可调整。package com.example.guard.config; import org.springframework.boot.context.properties.ConfigurationProperties; ConfigurationProperties(prefix app.guard) public class GuardProperties { /** * 小于等于该金额的普通信号允许自动执行单位美元 */ private double autoExecuteMaxUsd 100_000; /** * Agent 给出的置信度底线 */ private double confidenceThreshold 0.8; /** * 同一个 signalId 在窗口内不能重复执行单位秒 */ private int duplicateWindowSeconds 300; /** * 风控总开关测试时方便快速关闭生产不建议关闭 */ private boolean enabled true; public double getAutoExecuteMaxUsd() { return autoExecuteMaxUsd; } public void setAutoExecuteMaxUsd(double autoExecuteMaxUsd) { this.autoExecuteMaxUsd autoExecuteMaxUsd; } public double getConfidenceThreshold() { return confidenceThreshold; } public void setConfidenceThreshold(double confidenceThreshold) { this.confidenceThreshold confidenceThreshold; } public int getDuplicateWindowSeconds() { return duplicateWindowSeconds; } public void setDuplicateWindowSeconds(int duplicateWindowSeconds) { this.duplicateWindowSeconds duplicateWindowSeconds; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled enabled; } }这里的关键点是不要把业务阈值硬编码在代码里。Agent 项目的风控规则一定会根据线上情况调整如果每次调整都要发版效率太低。启动类需要开启配置属性扫描。package com.example.guard; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; SpringBootApplication ConfigurationPropertiesScan public class GuardApplication { public static void main(String[] args) { SpringApplication.run(GuardApplication.class, args); } }4.2 风险信号与决策模型TradeSignal 使用 Java record 定义。它代表 Agent 上报给风控服务的原始信号。package com.example.guard.model; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import java.time.Instant; public record TradeSignal( NotBlank String signalId, NotBlank String rawText, NotBlank String source, NotBlank String riskLevel, Min(0) double confidence, NotBlank String symbol, NotBlank String action, Positive double notionalUsd, Instant occurredAt ) { }字段说明signalId信号唯一 ID用于幂等和防重。rawText原始信号文本供规则校验器重新解析。riskLevelAgent 解析出的风险等级。confidenceAgent 对当前决策的置信度。notionalUsd名义金额单位美元。action交易方向比如 BUY 或 SELL。这里特别强调 rawText。只保存结构化字段是不够的因为事故的根因就是结构化字段被 Agent 转错了。保留原始文本可以让风控层在规则校验时做二次验证。AgentDecision 代表风控决策结果。package com.example.guard.model; public record AgentDecision( String tradeId, String signalId, String action, String symbol, double notionalUsd, ApprovalStatus status, String reason ) { }审批状态使用枚举package com.example.guard.model; public enum ApprovalStatus { PENDING, APPROVED, REJECTED, BLOCKED, AUTO_EXECUTED }审批请求体定义如下package com.example.guard.model; public record ApproveRequest( String operator, String reason ) { }在真实项目中operator 应该是登录用户的 ID不能由前端随便传。这里为了示例简洁直接用字符串表示操作人。4.3 规则校验器RiskSignalValidator 是整个风控层的核心。它负责对信号做确定性校验不依赖大模型能力。即使 Agent 误读了风险字段规则校验器也能通过 rawText 进行二次识别。package com.example.guard.service; import com.example.guard.config.GuardProperties; import com.example.guard.model.AgentDecision; import com.example.guard.model.ApprovalStatus; import com.example.guard.model.TradeSignal; import org.springframework.stereotype.Service; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; Service public class RiskSignalValidator { private final GuardProperties properties; private final MapString, Instant executedSignals new ConcurrentHashMap(); public RiskSignalValidator(GuardProperties properties) { this.properties properties; } public AgentDecision evaluate(TradeSignal signal) { ListString violations validate(signal); if (!violations.isEmpty()) { return new AgentDecision( UUID.randomUUID().toString(), signal.signalId(), signal.action(), signal.symbol(), signal.notionalUsd(), ApprovalStatus.BLOCKED, String.join(; , violations) ); } String normalizedRiskLevel normalizeRiskLevel(signal.rawText(), signal.riskLevel()); if (isHighRisk(normalizedRiskLevel) || signal.notionalUsd() properties.getAutoExecuteMaxUsd()) { return new AgentDecision( UUID.randomUUID().toString(), signal.signalId(), signal.action(), signal.symbol(), signal.notionalUsd(), ApprovalStatus.PENDING, needs manual approval ); } markExecuted(signal.signalId()); return new AgentDecision( UUID.randomUUID().toString(), signal.signalId(), signal.action(), signal.symbol(), signal.notionalUsd(), ApprovalStatus.AUTO_EXECUTED, auto executed ); } public ListString validate(TradeSignal signal) { ListString violations new ArrayList(); String normalizedRiskLevel normalizeRiskLevel(signal.rawText(), signal.riskLevel()); if (signal.confidence() properties.getConfidenceThreshold()) { violations.add(confidence too low); } if (isHighRisk(normalizedRiskLevel) !isHighRisk(signal.riskLevel())) { violations.add(parsed risk level conflicts with raw text); } if (isHighRisk(normalizedRiskLevel) signal.notionalUsd() properties.getAutoExecuteMaxUsd()) { violations.add(high risk signal exceeds auto execute limit); } if (signal.notionalUsd() 0) { violations.add(notional must be positive); } if (isDuplicate(signal.signalId())) { violations.add(duplicate signal in window); } return violations; } public void markExecuted(String signalId) { executedSignals.put(signalId, Instant.now()); } private boolean isDuplicate(String signalId) { Instant executedAt executedSignals.get(signalId); if (executedAt null) { return false; } return executedAt.plusSeconds(properties.getDuplicateWindowSeconds()).isAfter(Instant.now()); } private String normalizeRiskLevel(String rawText, String parsedRiskLevel) { if (rawText null) { return parsedRiskLevel null ? UNKNOWN : parsedRiskLevel; } String text rawText.toLowerCase(); if (text.contains(high risk) || text.contains(severe) || text.contains(critical) || text.contains(stop loss triggered)) { return HIGH; } if (text.contains(low risk) || text.contains(no risk)) { return LOW; } return parsedRiskLevel null ? UNKNOWN : parsedRiskLevel; } private boolean isHighRisk(String riskLevel) { return HIGH.equalsIgnoreCase(riskLevel) || CRITICAL.equalsIgnoreCase(riskLevel); } }这里最核心的规则是如果原始文本包含high risk等关键词但 Agent 解析出的 riskLevel 却是 LOW那么直接判定为违规。这样即使 Agent 上游模型出错风控层也能通过原始文本发现冲突。另外置信度低于 0.8 的信号不会自动执行。因为低置信度本身就说明 Agent 对当前决策把握不足这种情况更适合交给人工判断。4.4 人工审批服务ManualApprovalService 通过内存队列模拟一个待审批列表。生产环境请替换成数据库持久化并且增加幂等约束。package com.example.guard.service; import com.example.guard.model.AgentDecision; import com.example.guard.model.ApprovalStatus; import org.springframework.stereotype.Service; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; Service public class ManualApprovalService { private final MapString, AgentDecision pendingDecisions new ConcurrentHashMap(); public void submit(AgentDecision decision) { if (decision.status() ApprovalStatus.PENDING) { pendingDecisions.put(decision.tradeId(), decision); } } public OptionalAgentDecision getPendingDecision(String tradeId) { return Optional.ofNullable(pendingDecisions.get(tradeId)); } public OptionalAgentDecision approve(String tradeId, String operator) { AgentDecision decision pendingDecisions.get(tradeId); if (decision null) { return Optional.empty(); } AgentDecision approved new AgentDecision( decision.tradeId(), decision.signalId(), decision.action(), decision.symbol(), decision.notionalUsd(), ApprovalStatus.APPROVED, manual approved by operator ); pendingDecisions.remove(tradeId); return Optional.of(approved); } public OptionalAgentDecision reject(String tradeId, String reason) { AgentDecision decision pendingDecisions.get(tradeId); if (decision null) { return Optional.empty(); } AgentDecision rejected new AgentDecision( decision.tradeId(), decision.signalId(), decision.action(), decision.symbol(), decision.notionalUsd(), ApprovalStatus.REJECTED, manual rejected: reason ); pendingDecisions.remove(tradeId); return Optional.of(rejected); } }审批通过后风控服务会调用markExecuted把 signalId 加入到已执行集合中防止同一个信号在重复窗口期内再次被提交。4.5 审计日志服务审计日志是风控系统最重要的能力之一。出现问题时我们需要能够还原完整链路Agent 提交了什么、规则如何判断、审批人是谁、最终决策是什么。package com.example.guard.service; import com.example.guard.model.AgentDecision; import com.example.guard.model.TradeSignal; import org.springframework.stereotype.Service; import java.time.Instant; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; Service public class AuditLogService { private final ListString logs new CopyOnWriteArrayList(); public void record(TradeSignal signal, AgentDecision decision) { logs.add(timestamp Instant.now() signalId signal.signalId() riskLevel signal.riskLevel() confidence signal.confidence() notionalUsd signal.notionalUsd() decision decision.status() reason decision.reason()); } public void recordApproval(AgentDecision decision) { logs.add(timestamp Instant.now() tradeId decision.tradeId() signalId decision.signalId() decision decision.status() reason decision.reason()); } public ListString list() { return List.copyOf(logs); } }这里为了示例简单使用字符串拼接日志。生产环境建议使用结构化日志并写入独立审计表或日志系统方便后续检索和告警。4.6 风控编排服务AgentGuardService 是门面类Agent 只通过它提交信号不需要关心内部细节。package com.example.guard.service; import com.example.guard.config.GuardProperties; import com.example.guard.model.A