企业RAG智能体数据安全落地:PII脱敏、权限管控与审计日志工程实践
聊完了RAG系统的幻觉治理——怎么让AI"答得准",接下来聊另一个同样重要的问题:怎么让AI"守规矩"。企业知识库里有客户信息、合同条款、内部数据,RAG系统上线后,这些数据会不会被AI"说漏嘴"?
聊完了RAG系统的幻觉治理——怎么让AI"答得准",接下来聊另一个同样重要的问题:怎么让AI"守规矩"。企业知识库里有客户信息、合同条款、内部数据,RAG系统上线后,这些数据会不会被AI"说漏嘴"?
注:本文讨论的"RAG智能体"指基于检索增强生成(RAG)构建的企业知识问答系统,不涉及自主规划、工具调用等 Agent 机制。
易科势腾是一家专注于AI智能营销的国家级高新技术企业,北京市专精特新中小企业、中关村高新技术企业,拥有21年行业深耕经验。在给企业做 RAG 落地的过程中,数据安全往往是比幻觉治理更靠前的一道坎。
一、RAG系统的数据安全风险模型
先搞清楚RAG系统在数据安全上和传统Web应用有什么不同。
传统Web应用的安全模型是"边界防护"——防火墙拦住外部访问,数据库在内网,API有认证。但RAG系统的数据流是:
原始文档 → 解析 → 切块 → 向量化 → 存入向量库
↓
用户提问 → 检索向量库 → 拼装Prompt → LLM生成 → 返回用户
这条链路上有三个传统安全模型覆盖不到的风险点:
风险一:数据摄入阶段的信息泄露
原始文档在入库前会经过解析和切块。如果文档中包含PII(个人身份信息),这些信息会被原样写入向量库和索引中。后续用户提问时,AI可能直接把PII"背"出来。
举个例子:一份客户合同被切分后,某个Chunk里包含了"甲方:XX公司,联系人:张三,电话:138xxxx"。用户问"我们跟XX公司的合同条款是什么",AI检索到这个Chunk,在回答中可能把联系人姓名和电话都输出。
风险二:检索阶段的越权访问
企业知识库通常对不同角色开放不同内容——销售能看到客户信息,技术看不到;技术能看到架构文档,销售不需要看。但如果向量库没有做权限隔离,任何能访问RAG系统的用户,理论上都能检索到所有Chunk。
传统数据库的权限控制是行级的——WHERE department = ‘sales’。向量库的检索是语义匹配——按相似度排序返回Top-K。你没法在向量检索时加WHERE条件来过滤权限(至少没那么简单)。
风险三:生成阶段的信息外泄
即使检索阶段做对了,LLM在生成回答时也可能"多说"。比如用户问A产品的参数,AI检索到了包含A产品和B产品参数的Chunk,在回答中把两个产品都输出。如果B产品是未公开的内部产品,这就是信息泄露。
这三个风险点对应三道防线:摄入阶段的PII脱敏、检索阶段的权限管控、生成后的审计追溯。
二、PII脱敏:从源头切断信息泄露
PII(Personally Identifiable Information,个人身份信息)脱敏是RAG数据安全的第一道防线。在数据进入向量库之前,就把敏感信息处理掉。
2.1 PII识别:先找到敏感数据在哪里
脱敏的前提是识别。在企业文档中,常见的PII类型包括:
| PII类型 | 正则匹配模式 | 示例 |
|---|---|---|
| 手机号 | 1[3-9]\d{9} | 13812345678 |
| 身份证号 | `[1-9]\d{5}(?:19 | 20)\d{2}(?:0[1-9] |
| 邮箱 | [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} | zhangsan@company.com |
| 银行卡号 | \b\d{16,19}\b(需配合Luhn校验,否则容易误匹配订单号等长数字) | 6222021234567890123 |
| 地址 | NLP实体识别 | 北京市朝阳区XX路XX号 |
| 人名 | NLP实体识别 | 张三、李四 |
正则匹配能覆盖格式固定的PII(手机号、身份证、邮箱),但人名和地址需要NER(Named Entity Recognition,命名实体识别)模型来识别。
2.2 脱敏策略设计
不是所有PII都要一刀切地抹掉。根据业务场景,脱敏有不同的强度级别:
完全脱敏:用占位符替换,不保留任何原始信息。适用于L3/L4级别的数据。
原文:联系人:张三,电话:13812345678
脱敏后:联系人:[人员A],电话:[手机号A]
部分脱敏:保留部分信息,可用于统计和关联,但不可逆推出个人。
原文:张三,13812345678
脱敏后:张**,138****5678
可逆脱敏:用加密存储,有权限的用户可以解密查看原始信息。适用于需要审计追溯的场景。
原文:张三,13812345678
脱敏后:ENC(a2V5MTIz),ENC(bGNkNDU2)
在RAG系统中,推荐使用完全脱敏——因为RAG的核心用途是知识检索和问答,不是客户信息管理。如果用户需要查看客户联系方式,应该走CRM系统的权限体系,而不是通过RAG系统。
2.3 脱敏工程实现
在数据摄入管线中,脱敏应该作为一个独立的处理环节,位于文档解析之后、切块之前:
class PIIDetector:
"""PII检测器:正则 + NER模型双重检测"""
def __init__(self):
# 正则规则集
self.regex_patterns = {
'phone': re.compile(r'1[3-9]\d{9}'),
'id_card': re.compile(r'[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]'),
'email': re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'),
# 注意:16-19位纯数字也可能匹配到订单号、流水号等
# 生产环境应配合Luhn算法校验,降低误匹配率
'bank_card': re.compile(r'\b\d{16,19}\b'),
}
# NER模型用于人名和地址识别
# 可选:HanLP / LAC / 自训练模型
self.ner_model = load_ner_model()
def detect(self, text):
"""检测文本中的所有PII"""
findings = []
# 正则检测
for pii_type, pattern in self.regex_patterns.items():
for match in pattern.finditer(text):
findings.append({
'type': pii_type,
'value': match.group(),
'start': match.start(),
'end': match.end()
})
# NER检测人名和地址
ner_results = self.ner_model.recognize(text)
for entity in ner_results:
if entity['label'] in ['PER', 'LOC']:
findings.append({
'type': 'person' if entity['label'] == 'PER' else 'address',
'value': entity['text'],
'start': entity['start'],
'end': entity['end']
})
return findings
class PIIMasker:
"""PII脱敏器"""
# 接受原始字符串,返回脱敏后的字符串
MASK_MAP = {
'phone': lambda s: s[:3] + '****' + s[-4:],
'id_card': lambda s: s[:6] + '********' + s[-4:],
'email': lambda s: s.split('@')[0][0] + '***@' + s.split('@')[1], # 仅保留本地部分首字符
'bank_card': lambda s: s[:4] + '*' * (len(s) - 8) + s[-4:], # 16-19位变长,动态星号
'person': lambda s: s[0] + '**' if len(s) > 1 else '*',
'address': lambda s: s[:2] + '****',
}
def mask(self, text, findings):
"""对文本中的PII进行脱敏"""
# 从后往前替换,避免位置偏移
for finding in sorted(findings, key=lambda x: x['start'], reverse=True):
original = finding['value']
mask_func = self.MASK_MAP.get(finding['type'])
if mask_func:
masked = mask_func(original)
text = text[:finding['start']] + masked + text[finding['end']:]
return text
在摄入管线中集成的位置:
def ingest_document(doc_path):
"""文档摄入管线"""
# 1. 解析文档
raw_text = tika_parser.extract(doc_path)
# 2. PII检测和脱敏(在切块之前)
findings = pii_detector.detect(raw_text)
if findings:
logger.info(f"检测到{len(findings)}个PII,开始脱敏")
clean_text = pii_masker.mask(raw_text, findings)
# 记录脱敏日志
audit_log.record({
'action': 'pii_masked',
'doc_path': doc_path,
'pii_count': len(findings),
'pii_types': list(set(f['type'] for f in findings)),
})
else:
clean_text = raw_text
# 3. 切块
chunks = structure_based_chunker.split(clean_text)
# 4. 向量化
embeddings = embedding_model.encode(chunks)
# 5. 写入向量库(附带metadata)
vector_store.add(
texts=chunks,
embeddings=embeddings,
metadata=[{
'doc_id': doc_id,
'doc_path': doc_path,
'pii_masked': len(findings) > 0,
'masked_count': len(findings),
} for _ in chunks]
)
2.4 脱敏的注意事项
注意一:脱敏要在切块之前做
如果先切块再脱敏,切分可能把一个手机号切成两半,正则就匹配不到了。必须在完整文档解析后、切块之前做PII检测和脱敏。
注意二:脱敏后的文本质量影响检索
脱敏后的文本语义会有些变化。比如"张三负责的项目"变成"张**负责的项目",Embedding的向量会有偏移。对于高频出现在文档中的核心人名,建议在脱敏的同时保留一个角色标识(如"项目经理A"),而不是简单的星号掩码,这样语义信息保留更好。
注意三:保留脱敏映射表
如果后续需要追溯脱敏前的原始信息(如审计需要),需要保留脱敏映射表。映射表应该加密存储,访问需要审批权限。映射表本身也是敏感数据,需要纳入数据分级管理。
三、权限管控:让用户只能检索到他能看的内容
RAG系统的权限管控比传统应用复杂,因为向量检索不是精确匹配,你没法简单地用WHERE条件过滤。
3.1 权限模型设计
推荐使用RBAC(基于角色的访问控制)+ ABAC(基于属性的访问控制)混合模型:
RBAC层:定义角色和知识库的映射关系。
角色 可访问知识库
─────────────────────────────
销售 产品文档、公开案例、FAQ
技术 产品文档、技术架构、FAQ
管理层 全部知识库
外部合作伙伴 产品文档(指定范围)
ABAC层:在角色基础上,根据用户属性做细粒度过滤。
属性 过滤规则
────────────────────────────────
部门=销售一部 只能看本部门的客户案例
职级>=经理 可以看脱敏后的财务数据
渠道=合作伙伴 只能看公开产品文档,不含内部参数
3.2 向量库的权限隔离方案
在技术实现上,向量库的权限隔离有三种方案:
方案一:分库隔离
为每个角色/部门建独立的向量索引。用户检索时只在自己有权限的索引中搜索。
索引列表:
- kb_public → 所有人可访问
- kb_sales → 销售部可访问
- kb_tech → 技术部可访问
- kb_finance → 财务部可访问
优点:权限隔离干净,实现简单。
缺点:知识库数量多时管理成本高;跨库检索需要合并结果,增加复杂度。适合部门界限明确、知识库数量适中的场景。
方案二:Metadata过滤
所有文档存在同一个向量库中,每个Chunk附带权限metadata。检索时在向量查询中加入过滤条件。
# Elasticsearch 8.x KNN 查询示例
# query_vector 是由 Embedding 模型生成的查询向量(浮点数数组)
{
"query": {
"knn": {
"field": "embedding",
"query_vector": [0.0123, -0.456, 0.789],
"k": 20,
"num_candidates": 100,
"filter": {
"bool": {
"must": [
{"terms": {"allowed_roles": ["sales", "public"]}}
]
}
}
}
}
}
优点:单库管理,维护简单。
缺点:过滤条件太复杂时影响检索性能;Chunk的权限metadata需要维护。适合中小规模知识库。
方案三:检索后过滤
先做全量检索,返回Top-K结果后在应用层按权限过滤。
# 先检索Top-20
raw_results = vector_store.search(query, top_k=20)
# 应用层过滤
filtered_results = [r for r in raw_results if user.has_access(r.metadata['doc_id'])]
# 过滤后如果不足Top-5,需要补检索(按 chunk_id 去重)
if len(filtered_results) < 5:
seen_ids = {r.metadata['chunk_id'] for r in filtered_results}
additional = vector_store.search(query, top_k=50)
for r in additional:
if user.has_access(r.metadata['doc_id']) and r.metadata['chunk_id'] not in seen_ids:
filtered_results.append(r)
seen_ids.add(r.metadata['chunk_id'])
if len(filtered_results) >= 5:
break
优点:不需要修改向量库查询逻辑,对接现有系统改动最小。
缺点:效率低,可能需要多次检索才能凑够结果;检索质量受影响——过滤掉的结果可能是最相关的。适合权限层级简单、大部分文档对大部分用户开放的场景。
选型建议:
- 知识库数量少(<5个)、权限层级简单(2-3级):方案二(Metadata过滤)
- 知识库数量多(>5个)、部门界限明确:方案一(分库隔离)
- 历史系统改造、不想动向量库层:方案三(检索后过滤)作为过渡方案
在易科势腾的实际项目中,方案二用得最多。Elasticsearch 8.x的KNN查询原生支持filter参数,在10万级Chunk的测试环境下,加了filter后的查询延迟增加通常在数十毫秒量级,对用户体验影响有限。实际影响取决于filter的选择性和数据分布,建议根据自己的数据规模做基准测试。
3.3 会话级别的权限透传
权限不能只在检索层做控制。从用户发起提问到AI生成回答的整条链路,权限信息需要全程透传。
用户请求(携带用户Token)
↓
API网关:验证Token → 提取用户角色和属性
↓
RAG服务层:根据角色和属性构建检索过滤条件
↓
检索层:带过滤条件的混合检索
↓
Prompt拼装:在拼装时标注每个Chunk的来源和权限等级
↓
LLM生成:Prompt中包含安全约束(如"不得输出标注为内部的参数")
↓
输出后处理:检查生成内容是否包含越权信息
↓
返回用户(附带来源标注)
关键设计点:Prompt中要包含安全约束。不要指望LLM自己判断哪些信息该说哪些不该说——在系统Prompt中明确写出约束规则:
安全约束:
1. 只能基于以下检索到的内容回答问题
2. 如果内容中标注了[内部],不得在回答中引用该部分信息
3. 不得输出任何个人身份信息(姓名、电话、邮箱等)
4. 如果用户请求的信息超出其权限范围,回答"该信息不在可公开范围内"
四、审计日志:让每一次问答都可追溯
审计日志的价值不只是合规检查。当出现"AI回答了不该说的内容"时,审计日志是你定位问题的唯一手段。
4.1 日志架构
RAG系统的审计日志需要覆盖三个维度:
数据摄入审计:
{
"timestamp": "2026-08-04T10:30:00Z",
"action": "document_ingested",
"doc_id": "doc_20260804_001",
"doc_path": "/data/contracts/customer_a.pdf",
"doc_size": 245678,
"chunk_count": 12,
"pii_detected": true,
"pii_masked_count": 3,
"pii_types": ["phone", "email", "person"],
"classification": "L3",
"allowed_roles": ["sales", "management"],
"operator": "admin@company.com"
}
检索审计:
{
"timestamp": "2026-08-04T10:35:00Z",
"user_id": "user_12345",
"user_role": "sales",
"query": "客户A的合同条款",
"query_hash": "sha256:abc123...",
"retrieved_chunks": [
{"chunk_id": "doc_001_c3", "score": 0.89, "doc_classification": "L3"},
{"chunk_id": "doc_001_c5", "score": 0.85, "doc_classification": "L3"},
{"chunk_id": "doc_002_c1", "score": 0.71, "doc_classification": "L1"}
],
"filtered_out": 2,
"final_context_chunks": 3,
"response_time_ms": 856
}
注意:如果 query 字段可能包含 PII,存储时应做同等脱敏处理。query_hash 用于去重分析,避免原始 query 中的敏感信息长期保留。filtered_out 记录因权限被过滤的 Chunk 数量。
生成审计:
{
"timestamp": "2026-08-04T10:35:01Z",
"user_id": "user_12345",
"model": "glm-4-flash",
"prompt_tokens": 1245,
"completion_tokens": 380,
"response": "根据合同条款...",
"response_hash": "sha256:def456...",
"contains_pii": false,
"safety_check_passed": true
}
注意:response 字段可能包含 PII,建议脱敏后存储;contains_pii 由输出后处理模块检测后标记。
4.2 日志存储和查询
日志存储推荐使用Elasticsearch(如果你的RAG系统已经在用ES,可以复用)。建立专门的审计日志索引:
{
"mappings": {
"properties": {
"timestamp": {"type": "date"},
"action": {"type": "keyword"},
"user_id": {"type": "keyword"},
"user_role": {"type": "keyword"},
"doc_id": {"type": "keyword"},
"query_hash": {"type": "keyword"},
"pii_detected": {"type": "boolean"},
"safety_check_passed": {"type": "boolean"},
"response_time_ms": {"type": "integer"}
}
}
}
日志保留期限参考:
| 日志类型 | 保留期限 | 依据 |
|---|---|---|
| 检索和生成日志 | 至少180天 | 《网络安全法》及等保2.0要求 |
| 数据摄入日志 | 至少1年 | 内部审计需要 |
| 权限变更日志 | 至少1年 | 合规审计需要 |
| 安全事件日志 | 至少2年 | 事后追溯需要 |
4.3 异常检测
审计日志不只用来存着等检查,还应该配置实时告警。以下情况需要触发告警:
- 同一用户短时间大量检索不同分类的文档(可能在试探权限边界)
- 检索结果频繁被权限过滤(说明用户权限设置可能不合理)
- 输出后处理检测到PII(说明脱敏流程有遗漏)
- 非工作时间的异常访问
- API调用频率突增(可能有爬虫行为)
告警可以通过Webhook推送到企业微信或钉钉,由运维和安全团队跟进。
五、私有化部署的安全考量
对于数据安全要求高的企业(金融、医疗、政务),RAG系统通常需要私有化部署。私有化部署不是简单地把系统搬到内网,还需要考虑以下安全架构:
5.1 组件安全清单
| 组件 | 公有云方案 | 私有化替代 | 安全要求 |
|---|---|---|---|
| Embedding模型 | OpenAI/ZhipuAI API | BGE/M3E本地部署 | 模型权重不出网 |
| 向量库 | Pinecone/Weaviate云 | Milvus/ES本地 | 数据不出内网 |
| LLM | GPT-4/Claude API | GLM/Qwen/DeepSeek本地 | 推理不外调 |
| 文档解析 | 云端API | Apache Tika本地 | 文件不外传 |
| 日志分析 | 云日志服务 | ELK本地集群 | 日志不外传 |
5.2 网络隔离
私有化部署的网络架构建议分为三个区域:
┌─────────────────────────────────────────────┐
│ DMZ区 │
│ (负载均衡 + API网关 + WAF) │
│ ↓ 只开放必要端口 │
├─────────────────────────────────────────────┤
│ 应用区 │
│ (RAG服务 + 检索服务 + 向量库) │
│ ↓ 内网通信,无外网出口 │
├─────────────────────────────────────────────┤
│ 数据区 │
│ (原始文档 + 向量库存储 + 审计日志) │
│ ↓ 加密存储,最小权限访问 │
└─────────────────────────────────────────────┘
关键安全规则:
- 应用区的服务不允许直接访问外网(防止数据外泄和LLM API外调)
- 数据区的存储只对应用区的特定服务开放访问
- DMZ区只转发HTTPS请求,拦截所有非标准端口的访问
- 如果使用国产大模型的API(如ZhipuAI),需要通过网络策略限制只允许访问特定的API域名
5.3 模型安全
私有化部署中使用开源模型时,需要注意:
- 模型权重文件从官方渠道下载,下载后做MD5校验
- 不使用来路不明的微调模型(可能被植入后门)
- 模型部署在受控环境中,不对外暴露推理接口
- 定期更新模型版本,但更新前需要做回归测试
六、一套最小可用的安全配置
如果团队资源有限,没法一次性把所有安全措施都做到位,以下是一套最小可用的配置:
必须有(上线前必须完成):
- PII正则检测+脱敏(至少覆盖手机号、身份证、邮箱)
- 基于Metadata的权限过滤(至少区分内部/外部两个层级)
- 检索和生成日志(至少记录用户ID、问题摘要、检索到的文档ID)
- 系统Prompt中的安全约束
建议有(上线后3个月内完成):
- NER模型补充人名和地址的PII检测
- 输出后处理检测(检查生成内容是否包含PII)
- 日志告警机制
- 脱敏映射表的加密存储
最好有(资源允许时推进):
- 全链路权限透传(Token → 角色解析 → 检索过滤 → Prompt约束 → 输出检查)
- 网络隔离的三区架构
- 定期安全审计和渗透测试
这套配置不是一步到位的。在易科势腾的项目经验中,大多数企业RAG系统的安全建设是一个渐进过程——先把"必须有"的做扎实,再根据业务发展需要逐步推进后面的。
七、总结:三道防线,一个闭环
RAG系统的数据安全本质上是在解决一个问题:让AI用该用的信息回答该回答的问题,不多说也不乱说。
PII脱敏解决的是"不该入库的别入库",权限管控解决的是"不该看的别看到",审计日志解决的是"出了问题能查到"。三道防线,从数据摄入到最终输出,形成一个完整的安全闭环。
幻觉治理是"答得准不准"的质量问题,数据安全是"答得该不该"的合规问题。前者影响用户体验,后者影响企业安全。两件事都重要,但在合规要求高的行业,数据安全的优先级应该更高——因为答错了可以改,数据泄露了没法收。
如果你在RAG系统的数据安全落地中踩过坑,或者有更好的实践方式,欢迎分享你的经验。