第1974篇:私有化部署的合规考量——数据不出境的架构设计方案
第1974篇:私有化部署的合规考量——数据不出境的架构设计方案
前段时间帮一个金融客户做技术方案评审,需求很明确:要用 AI 能力,但用户的身份证号、账户信息、交易记录这些数据,必须只在公司自己的机房里处理,一个字节都不能出去。
这不是个别需求。《数据安全法》和《个人信息保护法》落地之后,医疗、金融、政务、教育这些行业都面临类似约束。AI 能力很香,但数据合规更重要。今天把"数据不出境"这个架构命题系统讲一遍。
为什么数据不出境是硬约束
很多开发者觉得"加密传输不就行了",但合规层面不是这么算的。
《个人信息保护法》第四十条明确规定:关键信息基础设施运营者和处理个人信息达到规定数量的个人信息处理者,向境外提供个人信息前,需通过国家网信部门组织的安全评估。
《数据安全法》第三十一条:关键数据境外提供安全评估不通过,就不能出境。
金融行业还有银保监会的具体要求:银行业的客户信息和数据,不得在境外存储和处理。
所以这不是技术选型问题,是法律红线。架构设计必须从这个约束出发,而不是事后打补丁。
数据分级:不是所有数据都需要留本地
做架构之前,先要搞清楚什么数据必须留本地,什么数据可以出去:
这个分级很重要。很多架构师一上来就全部本地化,成本直接上去了。其实很多通用能力(比如写文档、代码审查、通用问答)用的数据根本不涉及敏感信息,完全可以走公有云模型,成本低、效果好。
只有一级数据,才必须走本地部署的模型。
整体架构:双轨并行
这个架构的核心是 AI 网关 这一层,它负责:
- 识别请求中的敏感数据
- 决定路由到本地还是公有云
- 在必要时对数据做脱敏处理
敏感数据识别:这是最难的部分
如何自动识别请求中包含哪类数据?这是整个方案里最有挑战性的技术点。
方案一:规则引擎(快但不全)
@Component
public class SensitiveDataDetector {
// 预编译正则,提高性能
private static final Pattern ID_CARD_PATTERN =
Pattern.compile("[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]");
private static final Pattern BANK_CARD_PATTERN =
Pattern.compile("\\b\\d{16,19}\\b");
private static final Pattern PHONE_PATTERN =
Pattern.compile("1[3-9]\\d{9}");
private static final Pattern AMOUNT_PATTERN =
Pattern.compile("(人民币|¥|CNY|RMB)?\\s*\\d+(\\.\\d{1,2})?(元|万元|亿元)?");
private static final List<String> SENSITIVE_KEYWORDS = List.of(
"身份证", "护照", "银行卡", "账号", "密码", "账户",
"转账", "存款", "贷款", "征信", "医疗记录", "病历"
);
public DataSensitivityLevel detect(String content) {
// 检查高风险模式
if (ID_CARD_PATTERN.matcher(content).find() ||
BANK_CARD_PATTERN.matcher(content).find()) {
return DataSensitivityLevel.LEVEL_1_MUST_LOCAL;
}
// 检查关键词
boolean hasKeyword = SENSITIVE_KEYWORDS.stream()
.anyMatch(content::contains);
if (hasKeyword || PHONE_PATTERN.matcher(content).find()) {
return DataSensitivityLevel.LEVEL_2_DESENSITIZE;
}
return DataSensitivityLevel.LEVEL_3_PUBLIC;
}
public enum DataSensitivityLevel {
LEVEL_1_MUST_LOCAL, // 必须本地处理
LEVEL_2_DESENSITIZE, // 脱敏后可外发
LEVEL_3_PUBLIC // 可直接外发
}
}方案二:NER 模型识别(慢但准)
对于复杂文本,正则规则容易漏掉(比如"张先生的账户余额"——账户号没在文本里,但这条信息本身就很敏感)。用 NER(命名实体识别)模型更准确:
@Service
public class NERSensitiveDetector {
// 用本地轻量 NER 模型(可以用 Bert-base 微调)
@Autowired
private NERModel nerModel;
public SensitivityResult detectWithNER(String text) {
List<NamedEntity> entities = nerModel.extract(text);
boolean hasPerson = entities.stream()
.anyMatch(e -> e.getType() == EntityType.PERSON);
boolean hasOrganization = entities.stream()
.anyMatch(e -> e.getType() == EntityType.ORGANIZATION
&& isFinancialOrg(e.getText()));
boolean hasAmount = entities.stream()
.anyMatch(e -> e.getType() == EntityType.MONEY);
if (hasPerson && (hasAmount || hasOrganization)) {
return SensitivityResult.mustLocal("检测到人名+金额/机构的组合,属于高风险数据");
}
return SensitivityResult.safe();
}
private boolean isFinancialOrg(String text) {
return text.contains("银行") || text.contains("基金")
|| text.contains("证券") || text.contains("保险");
}
}实际项目建议:两种方法组合用。正则规则快,先跑一遍;遇到模糊情况再走 NER 模型。这样既保证速度,也保证准确率。
数据脱敏:二级数据的处理
二级数据(脱敏后可出境)的处理也有讲究,不是简单把名字打星号:
@Service
public class DataDesensitizer {
/**
* 智能脱敏:保留语义,替换敏感值
*/
public String desensitize(String text) {
// 手机号:保留前三位和后四位
text = ID_CARD_PATTERN.matcher(text).replaceAll(m ->
m.group().substring(0, 6) + "****" + m.group().substring(14));
// 银行卡:只保留后四位
text = BANK_CARD_PATTERN.matcher(text).replaceAll(m ->
"**** **** **** " + m.group().substring(m.group().length() - 4));
// 手机号:中间四位替换
text = PHONE_PATTERN.matcher(text).replaceAll(m ->
m.group().substring(0, 3) + "****" + m.group().substring(7));
return text;
}
/**
* 结构化数据脱敏
*/
public Map<String, Object> desensitizeMap(Map<String, Object> data) {
Set<String> sensitiveKeys = Set.of(
"idCard", "bankCard", "phone", "mobile",
"password", "realName", "name"
);
return data.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> sensitiveKeys.contains(e.getKey())
? maskValue(e.getValue())
: e.getValue()
));
}
private Object maskValue(Object value) {
if (value instanceof String s) {
if (s.length() <= 2) return "**";
return s.substring(0, 1) + "*".repeat(s.length() - 2) + s.substring(s.length() - 1);
}
return "****";
}
}脱敏的一个坑:脱敏后要验证语义是否还完整。比如你把客户名字全打星号,然后让模型分析"客户满意度",模型可能因为主语缺失而回答质量下降。好的脱敏是替换成假名,而不是打码:
@Service
public class PseudonymizationService {
private static final List<String> FAKE_NAMES = List.of(
"张三", "李四", "王五", "赵六", "陈七"
);
/**
* 假名化处理:替换成随机假名,保留语义完整性
*/
public PseudonymResult pseudonymize(String text, List<String> realNames) {
Map<String, String> nameMapping = new HashMap<>();
String result = text;
for (int i = 0; i < realNames.size(); i++) {
String fakeName = FAKE_NAMES.get(i % FAKE_NAMES.size())
+ (i / FAKE_NAMES.size() > 0 ? String.valueOf(i / FAKE_NAMES.size()) : "");
nameMapping.put(realNames.get(i), fakeName);
result = result.replace(realNames.get(i), fakeName);
}
return new PseudonymResult(result, nameMapping);
}
/**
* 模型返回后还原真实名字
*/
public String restore(String text, Map<String, String> nameMapping) {
String result = text;
for (Map.Entry<String, String> entry : nameMapping.entrySet()) {
result = result.replace(entry.getValue(), entry.getKey());
}
return result;
}
}本地模型部署:vLLM 方案
一级数据必须走本地模型。目前企业级本地部署主流选 vLLM(高吞吐)或 Ollama(轻量易用):
vLLM 部署(生产环境推荐):
# 使用 Docker 部署 vLLM
docker run --runtime nvidia --gpus all \
-p 8000:8000 \
-v /data/models:/models \
--ipc=host \
vllm/vllm-openai:latest \
--model /models/qwen2.5-7b-instruct \
--served-model-name qwen2.5 \
--max-model-len 8192 \
--dtype half \
--tensor-parallel-size 2 # 多 GPU 并行vLLM 最大的优势是原生支持 OpenAI API 格式,Spring AI 接入零成本:
spring:
ai:
openai:
api-key: "not-needed" # vLLM 本地不需要真实 key
base-url: http://your-vllm-server:8000
chat:
options:
model: qwen2.5
temperature: 0.7模型选型建议(按 GPU 资源):
| 可用 VRAM | 推荐模型 | 推理质量 |
|---|---|---|
| 8GB | Qwen2.5-7B-Instruct | 中等 |
| 16GB | Qwen2.5-14B-Instruct | 较好 |
| 24GB | Qwen2.5-32B-Instruct (INT4量化) | 好 |
| 48GB+ | Qwen2.5-72B-Instruct | 接近API级别 |
AI 网关的完整实现
把前面所有组件组合起来,构建统一的 AI 网关:
@Service
@Slf4j
public class ComplianceAIGateway {
@Autowired
private SensitiveDataDetector detector;
@Autowired
private DataDesensitizer desensitizer;
@Autowired
private PseudonymizationService pseudonymizer;
@Autowired
@Qualifier("localChatClient") // 本地 vLLM
private ChatClient localClient;
@Autowired
@Qualifier("cloudChatClient") // 阿里云 DashScope
private ChatClient cloudClient;
@Autowired
private AuditLogService auditLog;
public ComplianceResponse chat(ComplianceRequest request) {
String content = request.getMessage();
String requestId = UUID.randomUUID().toString();
// 1. 检测数据敏感级别
DataSensitivityLevel level = detector.detect(content);
log.info("请求[{}] 数据敏感级别: {}", requestId, level);
// 2. 审计日志(必须记录路由决策)
auditLog.record(AuditEvent.builder()
.requestId(requestId)
.userId(request.getUserId())
.sensitivityLevel(level)
.routingDecision(level == LEVEL_1_MUST_LOCAL ? "LOCAL" : "CLOUD")
.timestamp(Instant.now())
.build());
// 3. 根据级别路由
return switch (level) {
case LEVEL_1_MUST_LOCAL -> {
log.info("请求[{}] 路由到本地模型(含敏感数据)", requestId);
String response = localClient.prompt()
.user(content)
.call()
.content();
yield ComplianceResponse.local(response, requestId);
}
case LEVEL_2_DESENSITIZE -> {
log.info("请求[{}] 脱敏后路由到云端", requestId);
String desensitized = desensitizer.desensitize(content);
String response = cloudClient.prompt()
.user(desensitized)
.call()
.content();
yield ComplianceResponse.cloud(response, requestId, true);
}
case LEVEL_3_PUBLIC -> {
log.info("请求[{}] 直接路由到云端", requestId);
String response = cloudClient.prompt()
.user(content)
.call()
.content();
yield ComplianceResponse.cloud(response, requestId, false);
}
};
}
}审计日志:合规的技术证明
数据不出境不只是架构设计,还要有技术手段证明你做到了。审计日志是关键:
@Entity
@Table(name = "ai_audit_log")
public class AIAuditLog {
@Id
private String requestId;
private String userId;
private String sessionId;
@Enumerated(EnumType.STRING)
private DataSensitivityLevel sensitivityLevel;
private String routingDecision; // LOCAL 或 CLOUD
private boolean dataDesensitized;
// 不记录完整内容,只记录摘要和哈希
private String contentHash; // SHA-256 of request content
private int contentLength;
private Instant requestTime;
private Instant responseTime;
private long latencyMs;
private String modelUsed;
private int tokensUsed;
}
@Service
public class AuditLogService {
@Autowired
private AIAuditLogRepository repository;
@Async // 异步写入,不影响主流程性能
public void record(AuditEvent event) {
AIAuditLog log = new AIAuditLog();
log.setRequestId(event.getRequestId());
// ... 填充字段
// 内容哈希,用于验证完整性
log.setContentHash(DigestUtils.sha256Hex(event.getContent()));
log.setContentLength(event.getContent().length());
repository.save(log);
}
/**
* 生成合规报告(给监管审查用)
*/
public ComplianceReport generateReport(LocalDate startDate, LocalDate endDate) {
List<AIAuditLog> logs = repository.findByTimeRange(startDate, endDate);
long totalRequests = logs.size();
long localRequests = logs.stream()
.filter(l -> "LOCAL".equals(l.getRoutingDecision()))
.count();
long level1Requests = logs.stream()
.filter(l -> l.getSensitivityLevel() == LEVEL_1_MUST_LOCAL)
.count();
// 验证:一级数据是否全部走本地
long level1CloudRequests = logs.stream()
.filter(l -> l.getSensitivityLevel() == LEVEL_1_MUST_LOCAL
&& "CLOUD".equals(l.getRoutingDecision()))
.count();
if (level1CloudRequests > 0) {
// 这是严重违规,需要立即告警
alertService.sendCriticalAlert(
"发现" + level1CloudRequests + "条一级敏感数据被路由到云端!"
);
}
return ComplianceReport.builder()
.period(startDate + " ~ " + endDate)
.totalRequests(totalRequests)
.localRoutePercentage((double) localRequests / totalRequests * 100)
.level1ComplianceRate(
level1Requests == 0 ? 100.0 :
(double)(level1Requests - level1CloudRequests) / level1Requests * 100
)
.build();
}
}网络隔离:从架构层面保证
技术方案再好,如果网络没有隔离,数据还是可能跑出去。需要配合基础设施:
出口代理白名单配置示例(Nginx 正向代理):
server {
listen 3128;
resolver 114.114.114.114;
# 只允许访问已审批的AI服务域名
set $allowed 0;
if ($host ~* "dashscope.aliyuncs.com") { set $allowed 1; }
if ($host ~* "api.deepseek.com") { set $allowed 1; }
if ($allowed = 0) {
return 403 "Access denied by compliance policy";
}
proxy_pass http://$host;
}踩过的坑
坑一:敏感数据检测的漏报。正则规则没有覆盖所有情况,比如"他的工号是001234"——工号不是身份证,但在特定上下文里同样敏感。后来把规则引擎和 NER 模型双保险组合。
坑二:脱敏后模型效果变差。把名字全部打星号后,模型分析结果质量明显下降,因为代词关系混乱了。改用假名化后效果好很多。
坑三:审计日志不完整。最初只记录路由决策,监管检查时要求提供"数据是否出境的完整证明链",我们补充了内容哈希和时间戳签名才过审。
坑四:本地模型性能不足。7B 模型在并发较高时延迟飙升,后来加了异步队列和负载均衡才解决。
小结
数据不出境的架构设计,核心是三件事:
第一,数据分级,不要一刀切。把成本花在值得保护的数据上。
第二,检测要准,不能有漏报。宁可过保护,不能有风险。
第三,审计要完整,能证明你做到了。架构方案再漂亮,没有日志证明等于白做。
下一篇讲混合云 AI 架构,把这篇的本地+云端的思路进一步扩展成更系统的架构方案。
