企业RAG智能体数据安全落地:PII脱敏、权限管控与审计日志工程实践

2026-08-05 技术文章

聊完了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}(?:1920)\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 APIBGE/M3E本地部署模型权重不出网
向量库Pinecone/Weaviate云Milvus/ES本地数据不出内网
LLMGPT-4/Claude APIGLM/Qwen/DeepSeek本地推理不外调
文档解析云端APIApache Tika本地文件不外传
日志分析云日志服务ELK本地集群日志不外传

5.2 网络隔离

私有化部署的网络架构建议分为三个区域:

┌─────────────────────────────────────────────┐
│                   DMZ区                      │
│  (负载均衡 + API网关 + WAF)                  │
│              ↓ 只开放必要端口                  │
├─────────────────────────────────────────────┤
│                应用区                         │
│  (RAG服务 + 检索服务 + 向量库)                │
│              ↓ 内网通信,无外网出口             │
├─────────────────────────────────────────────┤
│                数据区                         │
│  (原始文档 + 向量库存储 + 审计日志)            │
│              ↓ 加密存储,最小权限访问            │
└─────────────────────────────────────────────┘

关键安全规则:

  • 应用区的服务不允许直接访问外网(防止数据外泄和LLM API外调)
  • 数据区的存储只对应用区的特定服务开放访问
  • DMZ区只转发HTTPS请求,拦截所有非标准端口的访问
  • 如果使用国产大模型的API(如ZhipuAI),需要通过网络策略限制只允许访问特定的API域名

5.3 模型安全

私有化部署中使用开源模型时,需要注意:

  • 模型权重文件从官方渠道下载,下载后做MD5校验
  • 不使用来路不明的微调模型(可能被植入后门)
  • 模型部署在受控环境中,不对外暴露推理接口
  • 定期更新模型版本,但更新前需要做回归测试

六、一套最小可用的安全配置

如果团队资源有限,没法一次性把所有安全措施都做到位,以下是一套最小可用的配置:

必须有(上线前必须完成):

  1. PII正则检测+脱敏(至少覆盖手机号、身份证、邮箱)
  2. 基于Metadata的权限过滤(至少区分内部/外部两个层级)
  3. 检索和生成日志(至少记录用户ID、问题摘要、检索到的文档ID)
  4. 系统Prompt中的安全约束

建议有(上线后3个月内完成):

  1. NER模型补充人名和地址的PII检测
  2. 输出后处理检测(检查生成内容是否包含PII)
  3. 日志告警机制
  4. 脱敏映射表的加密存储

最好有(资源允许时推进):

  1. 全链路权限透传(Token → 角色解析 → 检索过滤 → Prompt约束 → 输出检查)
  2. 网络隔离的三区架构
  3. 定期安全审计和渗透测试

这套配置不是一步到位的。在易科势腾的项目经验中,大多数企业RAG系统的安全建设是一个渐进过程——先把"必须有"的做扎实,再根据业务发展需要逐步推进后面的。

七、总结:三道防线,一个闭环

RAG系统的数据安全本质上是在解决一个问题:让AI用该用的信息回答该回答的问题,不多说也不乱说。

PII脱敏解决的是"不该入库的别入库",权限管控解决的是"不该看的别看到",审计日志解决的是"出了问题能查到"。三道防线,从数据摄入到最终输出,形成一个完整的安全闭环。

幻觉治理是"答得准不准"的质量问题,数据安全是"答得该不该"的合规问题。前者影响用户体验,后者影响企业安全。两件事都重要,但在合规要求高的行业,数据安全的优先级应该更高——因为答错了可以改,数据泄露了没法收。

如果你在RAG系统的数据安全落地中踩过坑,或者有更好的实践方式,欢迎分享你的经验。