尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

RAG安全问答系统实战:数据脱敏、防幻觉与审计追踪

RAG安全问答系统实战:数据脱敏、防幻觉与审计追踪 前两天把一个内部知识库问答系统接到大模型上时团队差点被一个问题劝退模型回答得越来越“像人”但检索出来的用户隐私数据也越来越多地出现在对话里。后来我们回头审视整个 AI 工程链路发现真正要解决的并不是“模型够不够聪明”而是“我们是否正在为了 AI 的效果牺牲掉原本不该牺牲的东西”。最近看到英文标题 “Are We Sacrificing Cemeteries for AI?”直译是“我们是否在为了 AI 牺牲墓地”。这里的“墓地”当然不是字面意思而是一种比喻——我们多年积累的数据资产、历史文档、旧系统以及用户隐私和模型可解释性。在 AI 应用中这些“公墓”正在被悄悄挖开、搬进训练集和上下文窗口。本文从一个 RAG检索增强生成问答系统的完整实战出发讨论大模型应用中的隐私泄露、模型幻觉、内容安全与审计追踪并提供一套可落地的工程实现方案。1. 背景与核心概念1.1 什么是“数字墓地”与 AI 的代价Cemetery 这个词放在 AI 语境下通常指代那些已经“沉淀”下来的数据与知识多年运营产生的大量历史文档、工单、邮件已下线的旧系统里保存的用户行为记录人工维护但早已没人敢动的老数据库个人身份信息、联系方式、健康数据等敏感资产。这些数据不会自己说话但一旦被大模型“挖掘”就可能以两种方式出现一是作为训练语料被卷进模型参数二是作为 RAG 检索的内容被拼进 Prompt跟随用户的提问一起送到模型面前。这两种方式都会带来一个隐患我们为了 AI 的“好用”可能正在牺牲数据所有权、隐私边界和可解释性。用工程的话说就是数据安全与模型效果的trade-off。真正合格的 AI 系统不应该让这种牺牲成为默认选项而应该靠架构设计去控制风险。1.2 大模型应用中的三大风险落地大模型应用时笔者最关注三个技术风险风险表现典型原因隐私泄露模型回答中带出身份证、手机号、邮箱数据未脱敏就进入 Prompt 或训练集模型幻觉模型回答看似合理但事实错误基于参数记忆而非真实上下文内容安全输出违规、越权、不当内容缺少输入输出过滤与安全护栏这三个风险不是独立存在的。大模型没有“边界感”它会忠实地理解用户想要什么然后尽力生成一个看起来合理的回答。如果我们的知识库里有隐私数据模型会把隐私数据缝合进答案如果知识库里没有对应事实模型会自己“脑补”一个事实。这也是为什么今天所有大模型应用都必须把“工程防护”放在模型能力之前。1.3 为什么 RAG 是一把双刃剑RAG 是当前大模型落地的标配方案。它先通过向量检索找到最相关的知识片段再把片段拼进 Prompt让模型基于这些片段回答。它的好处显而易见答案可以被溯源模型不依赖参数记忆更新知识只要改数据库。但 RAG 也把风险放大了检索结果一旦包含隐私数据模型会原样引用检索结果如果互相矛盾模型可能自动“调和”出错误结论Prompt 里的上下文越长模型越容易被干扰输出不可控的片段。所以RAG 不是简单把文档扔进向量库就完事。它需要数据脱敏、权限控制、内容过滤、提示词约束以及完整的审计链路。下面我们就从工程角度一步步构建一套这样的系统。2. 环境准备与版本说明2.1 运行环境与依赖本文示例以 Python 3.10 为主操作系统推荐 Ubuntu 22.04 或 macOSWindows 下同样可运行但需要注意本地模型和 ChromaDB 的路径写法。版本不必完全复制但建议保持相近Python 3.10Ollama 0.5本地模型推理ChromaDB 0.5LangChain 仅用于理解思路本示例尽量不用重量级框架方便看清内部逻辑需要安装的 Python 依赖pip install chromadb ollama其中ollama是 Ollama 的 Python 客户端我们用它调用本地模型和嵌入模型chromadb用于向量存储与检索。2.2 本地模型与向量库选型为了数据不离开本地环境本示例全部使用 Ollama 加载本地模型对话模型qwen2.5:7b负责最终回答生成嵌入模型nomic-embed-text负责把文档和用户问题转成向量。先确保 Ollama 已启动并拉取模型ollama pull qwen2.5:7b ollama pull nomic-embed-text注意模型名称会随版本更新变化请以你本地ollama list的结果为准。2.3 项目结构ai_guard/ ├── data/ │ └── source_docs.txt ├── src/ │ ├── __init__.py │ ├── anonymizer.py │ ├── retriever.py │ ├── safe_llm.py │ ├── audit.py │ └── main.py ├── requirements.txt └── README.mdsrc/anonymizer.py负责隐私数据脱敏src/retriever.py负责文档加载、向量化与检索src/safe_llm.py负责输入输出安全过滤与模型调用src/audit.py负责审计日志src/main.py负责组装整个问答流程。3. 核心设计从数据到答案的完整链路3.1 数据采集与缺失整理很多团队会把原始文档直接喂给 RAG这是最大的错误。原始文档可能包含用户手机号、身份证、邮箱内部会议记录与未公开决策互相矛盾的旧版本说明。所以在进入向量库之前必须做一次数据准入能入库的文档要满足可公开、低敏感、结构清晰。这一步在代码里体现为“脱敏 清洗”但更重要的是制度上明确什么数据可以进 RAG。3.2 数据脱敏与匿名化数据脱敏的常见方式有替换把手机号替换为[手机号]掩码保留前3位后4位中间用****替代删除直接移除敏感字段。在 RAG 链路里最推荐“替换”和“掩码”因为我们需要保留上下文逻辑但又不暴露真实值。比如“李四的电话是 138****1234” 比“李四的电话是 [手机号]” 更自然同时保护了隐私。3.3 文档切分与向量化文档进入向量库前需要切分。切分大小直接影响检索效果切分太短缺少上下文检索结果碎片化切分太长噪声太多相关片段淹没在过长的文本里。一般建议按章节或段落切分每个 chunk 控制在 200 到 500 个 token。示例中为了简化我们按固定字数切分。向量化阶段使用nomic-embed-text把每个 chunk 转成向量。实际项目中可以选择更专业的 embedding 模型但原理一致。3.4 检索增强生成与引用溯源RAG 的标准流程是用户提问向量化用户问题在向量库中检索 top-k 相关 chunk把 chunk 拼进 Prompt大模型基于 Prompt 生成回答。为了控制幻觉Prompt 中必须明确要求模型只基于给定片段回答如果片段不足以支撑答案就回答“暂无相关信息”。同时我们可以让模型返回引用来源序号帮助用户验证。3.5 内容安全过滤内容安全不能只靠模型“自律”。必须设置两个环节输入过滤用户问题中如果包含敏感词或越权内容直接拦截输出过滤模型回答中如果出现敏感词或不合规内容再次拦截并返回兜底文案。这里需要注意关键词过滤只是最基础的防线。生产级系统还需要引入分类模型、语义向量相似度甚至人工审核抽检。3.6 审计日志AI 应用必须可追溯。每次问答都应记录用户标识脱敏后完整问题脱敏后检索到的文档片段 ID模型完整回答时间戳与操作结果。日志本身也是敏感数据建议加密存储并设置访问权限。4. 完整实战案例构建一个带安全护栏的 RAG 问答系统4.1 创建项目与依赖创建项目目录mkdir ai_guard cd ai_guard mkdir -p data src touch src/__init__.py写入requirements.txtchromadb0.5.0 ollama0.3.0安装依赖pip install -r requirements.txt4.2 数据准备在data/source_docs.txt中放入少量测试文档。注意不要放真实敏感数据示例仅为演示员工手册 公司实行弹性工作制核心工作时间为上午10点到下午4点。 员工年度体检可在任何三甲医院完成报销上限为1000元。 客户服务规范 客户投诉应在24小时内响应并在3个工作日内给出解决方案。 联系电话13812345678联系邮箱serviceexample.com。 假期政策 员工每年享有12天带薪年假入职满一年后开始计算。 法定节假日按国家规定执行。可以看到文档里原本有手机号和邮箱我们会用脱敏模块把它们替换掉。4.3 数据脱敏模块文件路径src/anonymizer.pyimport re class Anonymizer: 对文本中的手机号、身份证号、邮箱进行掩码脱敏。 def __init__(self): self.patterns [ # 手机号1开头 10位数字 (re.compile(r(1[3-9]\d)\d{4}(\d{4})), r\1****\2), # 身份证号17位数字 数字或X (re.compile(r(\d{6})\d{8}(\d{3}[\dXx])), r\1********\2), # 邮箱用户名域名 (re.compile(r([\w.-])([\w-]\.[\w.])), r***\2), ] def anonymize(self, text: str) - str: 返回脱敏后的文本。 for pattern, repl in self.patterns: text pattern.sub(repl, text) return text这段代码的思路是先匹配敏感数据再用正则替换为掩码形式。re.sub替换时保留部分结构例如手机号前三位和后四位中间用****代替。这样既保护隐私又不让语义完全丢失。4.4 向量库与检索模块文件路径src/retriever.pyimport os import chromadb import ollama EMBED_MODEL nomic-embed-text COLLECTION_NAME docs CHROMA_DIR ./chroma_db class Retriever: def __init__(self, source_file: str, chunk_size: int 200): self.chunk_size chunk_size self.client chromadb.PersistentClient(pathCHROMA_DIR) self.collection self.client.get_or_create_collection(nameCOLLECTION_NAME) self.source_file source_file def _read_and_chunk(self) - list[str]: 读取文档并按固定长度切分。 with open(self.source_file, r, encodingutf-8) as f: text f.read() chunks [] current for line in text.splitlines(): current line \n if len(current) self.chunk_size: chunks.append(current.strip()) current if current.strip(): chunks.append(current.strip()) return chunks def build_index(self): 将文档切分、向量化并写入 ChromaDB。 chunks self._read_and_chunk() ids [fchunk_{i} for i in range(len(chunks))] embeddings [] for chunk in chunks: resp ollama.embeddings(modelEMBED_MODEL, promptchunk) embeddings.append(resp[embedding]) self.collection.add( idsids, embeddingsembeddings, documentschunks, ) print(f索引完成共 {len(chunks)} 个片段。) def query(self, question: str, top_k: int 3) - list[str]: 查询最相关的 top_k 个文档片段。 resp ollama.embeddings(modelEMBED_MODEL, promptquestion) query_embedding resp[embedding] result self.collection.query( query_embeddings[query_embedding], n_resultstop_k, ) return result[documents][0]这个模块有两个关键点PersistentClient(path)指定向量数据库持久化目录避免每次启动重建索引build_index()会重复写入相同片段若重新执行需先删除已有集合否则会报 ID 冲突。生产环境建议使用文档哈希作为 ID避免重复入库。4.5 安全过滤与模型调用文件路径src/safe_llm.pyimport ollama CHAT_MODEL qwen2.5:7b # 示例敏感词列表请按业务场景替换 SENSITIVE_WORDS [示例敏感词1, 示例敏感词2] class SafeLLM: def __init__(self): self.sensitive_words SENSITIVE_WORDS def check_text(self, text: str) - bool: 输入输出安全过滤含敏感词返回 False。 for word in self.sensitive_words: if word in text: return False return True def generate(self, question: str, context_chunks: list[str]) - str: # 输入过滤 if not self.check_text(question): return 抱歉您的问题包含不当内容无法回答。 # 组装上下文 context \n\n.join( [f[片段{i1}]\n{chunk} for i, chunk in enumerate(context_chunks)] ) system_prompt ( 你是一个严谨的智能助手。请只能基于以下提供的知识片段回答问题。 如果片段中没有提到相关信息请明确回答“根据现有知识无法回答”。 不要编造事实不要泄露任何上下文之外的隐私信息。 回答时可引用片段编号例如“根据片段1”。 ) user_prompt f知识片段\n{context}\n\n用户问题{question} response ollama.chat( modelCHAT_MODEL, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ], ) answer response[message][content] # 输出过滤 if not self.check_text(answer): return 抱歉模型生成内容未通过安全审核。 return answer这里把安全过滤和模型调用放在一起是为了演示“输入检查 - 检索 - Prompt 组装 - 模型调用 - 输出检查”的完整流程。注意真正的生产级过滤不能只有关键词还要有语义模型和人工审核。4.6 审计日志模块文件路径src/audit.pyimport logging from datetime import datetime class AuditLogger: def __init__(self, log_file: str audit.log): self.logger logging.getLogger(audit) self.logger.setLevel(logging.INFO) fh logging.FileHandler(log_file, encodingutf-8) fmt logging.Formatter(%(asctime)s | %(levelname)s | %(message)s) fh.setFormatter(fmt) self.logger.addHandler(fh) def log(self, user_id: str, question: str, answer: str, retrieval_ids: list[str]): record { user_id: user_id, question: question, answer: answer, retrieval_ids: retrieval_ids, time: datetime.now().isoformat(), } self.logger.info(record)在实际系统中user_id应来自统一身份认证且存储前需要做哈希或脱敏日志文件要定期归档并设置文件权限防止未授权读取。4.7 主流程串联文件路径src/main.pyfrom anonymizer import Anonymizer from retriever import Retriever from safe_llm import SafeLLM from audit import AuditLogger def main(): source_file data/source_docs.txt anonymizer Anonymizer() retriever Retriever(source_file) safe_llm SafeLLM() audit AuditLogger() # 首次运行需要构建索引之后可注释掉 retriever.build_index() print(AI 安全问答系统已启动输入 exit 退出。) while True: question input(\n请输入问题) if question.strip().lower() exit: break # 1. 输入脱敏与过滤 question_clean anonymizer.anonymize(question) # 2. 检索 chunks retriever.query(question_clean, top_k3) # 3. 生成 answer safe_llm.generate(question_clean, chunks) # 4. 输出 print(\n--- 回答 ---) print(answer) # 5. 审计 audit.log( user_idanonymous, questionquestion_clean, answeranswer, retrieval_ids[fchunk_{i} for i in range(len(chunks))], ) if __name__ __main__: main()4.8 运行与验证在项目根目录下运行python src/main.py首次运行会构建向量索引控制台会输出索引完成共 6 个片段。 AI 安全问答系统已启动输入 exit 退出。接着输入问题例如请输入问题客户服务热线是什么系统会查询向量库把最相关的几个片段作为上下文交给大模型生成回答。由于我们已经对文档做过脱敏source_docs.txt中的手机号会以138****5678的形式出现而不是完整号码。如果输入问题中包含“示例敏感词1”系统会直接拦截不会调用模型。整个流程的日志会写入audit.log你可以打开文件确认每条问答都有记录。5. 常见问题与排查思路5.1 模型幻觉仍然存在问题现象常见原因解决思路模型回答与检索片段不符提示词约束不够强模型参数量小在系统提示中明确“只能基于片段回答不知道就回答不知道”模型引用了片段之外的背景知识片段上下文不足或检索到无关文档增加top_k或优化文档切分策略还可以对模型回答做“事实一致性校验”用另一轮模型判断回答是否严格基于给定片段。虽然会额外耗时但能显著降低对外发布时的幻觉风险。5.2 敏感数据绕过脱敏问题现象常见原因解决思路输出中出现未脱敏手机号脱敏规则覆盖不全New文本格式变化增加 PII 识别模型对输出做二次脱敏图片或 PDF 中的隐私数据被识别仅处理了纯文本引入 OCR 后同样执行脱敏这里要特别强调脱敏不是一次性操作而是要在每个数据出口都执行一次。包括 Prompt 进入模型前、模型输出返回用户前。5.3 内容安全误杀过多问题现象常见原因解决思路正常问题被拦截敏感词列表过于宽泛使用白名单与上下文判断降低规则误伤拦截日志太多阈值设置过低设置分级过滤高危词直接拦截低危词仅告警生产环境建议把“直接拦截”和“告警后继续”分开并配合人工审核队列。5.4 检索召回效果差问题现象常见原因解决思路检索不到相关内容文档切分不合理embedding 模型不匹配调整 chunk_size尝试更专业的 embedding 模型检索结果冗余top_k 过大适当降低 top_k并增加相似度阈值过滤可以通过召回率、命中率等指标评估检索效果再针对性地调参。5.5 本地模型性能不足问题现象常见原因解决思路回答延迟高模型参数量大、无 GPU更换更小的量化模型或使用 API 服务内存溢出向量库过大使用分片索引或迁移到专业向量数据库本地模型的好处是数据不出内网适合隐私要求高的场景但如果性能不足也可以采用私有化部署 API 网关的方式在网关层做同样的安全控制和审计。6. 最佳实践与工程建议6.1 数据治理与最小化原则不要把所有数据都接入 AI 应用。遵循最小化原则只放当前业务需要的、适合公开给模型的数据敏感性高的数据放到另一套权限控制更强的系统中不直接接入 RAG文档入库前由负责人审批而不是让开发人员自己决定。可以用一张清单做准入检查[ ] 是否包含可公开信息 [ ] 是否包含个人敏感字段 [ ] 是否已经过脱敏处理 [ ] 是否明确数据归属与更新责任人6.2 提示词工程与安全护栏提示词不是随便写写。关键要写明模型只能使用给定片段不知道时主动承认禁止输出 Prompt 本身禁止尝试绕过系统限制。同时输入输出过滤要独立于提示词不能只依赖模型“自觉”。可借鉴 AWS 的 “guardrails” 思路把内容策略定义从业务代码中剥离出来便于统一管理。6.3 可解释性与来源引用对外提供服务时用户需要知道答案的根据。建议在回答中附带文档编号或来源链接根据员工手册片段2核心工作时间为上午10点到下午4点。如果模型生成内容没有引用来源宁可返回“无法回答”也不要直接展示。这也是控制幻觉的有效手段。6.4 权限控制与审计AI 应用必须做用户级权限控制。不同的用户应该看到不同的知识范围。实现方式有两种检索前过滤根据用户角色过滤向量库中文档的可见性生成后过滤对模型输出再次做权限裁剪。第一种更安全因为不安全的上下文根本不会进入 Prompt。审计日志则要包含用户 ID、问题、回答、检索片段、时间戳和审核状态并且日志本身要防篡改。6.5 模型迭代与灰度发布大模型的版本升级可能带来不可预见的输出变化。上线前要做红队测试用一批“危险问题”检验模型的越狱能力、幻觉率和安全过滤召回率。建议每次切换模型前先跑一遍自动化测试集采用 A/B 或灰度发布让少量用户先体验保留旧模型回滚通道出现问题时快速切换。7. 下一步学习路线如果你刚开始接触 AI 工程建议按这个顺序深入掌握 RAG 原理理解向量检索、文档切分、Prompt 拼装学习提示词工程学会设计 system prompt降低幻觉实践数据脱敏从正则到 PII 识别模型逐步完善了解向量数据库从 Chroma 到 Milvus、Qdrant 等生产级方案了解内容安全语义过滤、多模态审核、人工审核流程体系化评估建立离线测试集和线上监控指标持续优化。真正成熟的 AI 应用一定是在模型效果和数据安全之间找到了平衡。不要把“提高准确率”作为唯一目标还要多想想——你正在用什么样的数据去喂养模型那些数据背后的“数字墓地”是否真的允许被这样打开。
返回列表