很多团队接入大模型时,关注点都集中在生成质量、响应速度、上下文长度和成本上,唯独容易忽略输入侧的问题:你发给 LLM 的每一条 Prompt,到底在哪些环节被保存、被转发、被记录?Prompt 在 LLM 应用里既是指令,也经常夹带业务数据——客户手机号、合同编号、内部文档原文、代码片段。一旦 Prompt 被 API 平台日志落盘,被第三方插件转发,或者被 RAG 向量库长期保留,这条信息的暴露范围就完全失控了。
这次不聊新模型,聊一个所有 LLM 应用开发者在接入大模型时都会碰到、又很少系统处理的问题:Prompt 隐私。本文会把提示词隐私当成一项工程能力来处理,先明确风险通路,再给出检测方法,然后是脱敏实现、API 调用规范和批量任务里的隐私控制点。无论你是自己搭本地 LLM 服务,还是调用云端 API,这套流程都能直接对照使用。下面先看一张 Prompt 隐私核心风险速览表,把最容易出问题的位置和对应的防护方法列出来。
1. Prompt 隐私核心风险速览
| 风险点 | 触发场景 | 风险等级 | 受影响群体 | 防护手段 |
|---|---|---|---|---|
| API 日志落盘 | 通过云端 API 发送提示词,服务端记录请求内容 | 高 | 企业协同、客服机器人、代码助手 | 关闭日志记录、网关过滤、数据最小化 |
| 模型供应商训练数据 | 使用默认配置调用公共模型,提示词被用作训练语料 | 高 | 未单独配置隐私选项的开发者 | 在控制台关闭训练选项,或改用本地模型 |
| 浏览器缓存与本地日志 | 使用 Web 版对话,浏览器开发者工具可看到完整请求 | 中 | 企业内部 Web 工具使用者 | 禁止 Web 页面录入明文敏感信息 |
| 第三方插件转发 | 安装翻译、总结、同步插件,Prompt 从原平台转发到另一服务 | 中高 | 浏览器插件用户 | 最小权限插件、抓包审查请求域名 |
| RAG 知识库存储 | 将内部文档切片后建立向量索引,原始文本留存在向量库 | 中 | 企业内部知识库问答系统 | 对库内文档做脱敏、权限分级、定期清理 |
这张表不是用来制造焦虑,而是为后面的检测脚本和防护流程提供优先级:先查日志,再查训练开关,然后查 RAG 库,最后检查插件和浏览器端。
2. 提示词为什么会成为新的泄露对象
2.1 提示词本身就不是普通文本
在传统软件开发里,接口调用传的是结构化参数,敏感信息存在于数据库字段和配置文件里,可以用字段级权限、加密存储、审计日志来管理。但到了 LLM 应用里,这些东西全被压缩进了一串自然语言 Prompt 中。用户说“帮我把这个客户合同里的付款条款总结一下”,这条 Prompt 里可能就带着合同原文。
换句话说,Prompt 是一个没有边界的传输通道。它把业务系统中原来分散存储的数据,集中到一条请求里发给模型。谁控制这个请求,谁就控制了对数据的访问能力。这是 Prompt 隐私问题比其他数据泄露更难处理的原因——敏感信息不是单独传输,而是藏在自然语言里。
2.2 泄露通路一:API 日志与审计
云端 LLM API 出于安全审计和问题排查的考虑,基本都会在服务端保留请求和响应的日志。很多开发者在本地打通接口后不会关注服务端的日志保留策略,直到审计或者安全事件爆发,才发现大量用户输入的明文 Prompt 已经在平台上留了几个月甚至更久。
有些平台允许在控制台关闭“训练数据收集”,但关闭训练开关不代表关闭日志。不同平台的日志保留周期、数据存储区域、售后技术支持能否查看请求内容,这些信息在供应商文档里都有,但真正去读的人很少。如果你所在企业有数据合规要求,调用第三方 LLM API 之前,至少要确认四件事:训练开关是否默认开启,日志保留策略是什么,数据是否出境存储,供应商内部是否有权限直接查看请求内容。
2.3 泄露通路二:模型供应商训练数据
这条通路在免费账号和默认配置下尤其明显。当你使用某个对话产品输入 Prompt 时,如果产品条款写明了“对话内容可能被用于模型训练”,那么你输入的财务数据、源码片段、客户信息,都可能进入后续训练语料。虽然模型训练不等于直接公开泄露,但这些数据一旦进入训练集,就无法精确删除。后续可能以近似形式出现在其他用户的生成内容里,也可能存在于模型的记忆评估中。个人使用影响有限,企业场景就是严重事故。
2.4 泄露通路三:RAG 知识库与向量检索
RAG 架构下,企业内部文档先切片、向量化,存入向量数据库。业务系统查询时,把用户问题转成向量,检索相关片段,再拼进 Prompt 发给 LLM。这个链路里有三个存放点:原始文档、向量数据库、发给 LLM 的 Prompt。向量数据库一旦被内部人员导出,或者索引权限配置错误,被索引的文档就全部暴露。因为向量化之后不便于按原文搜索,很多团队甚至意识不到向量库里已经明文保存了原始文本。
这一点实操时最容易被忽略。提示词做了脱敏,但向量库里存的是完整切片原文,等于前面的工作白做。RAG 场景的 Prompt 隐私,实际上是三个连续问题:数据入库前是否脱敏,向量库权限是否隔离,检索结果是否二次脱敏。
2.5 泄露通路四:第三方插件与浏览器缓存
技术团队容易忽略浏览器端泄露。使用 Web 版对话产品时,浏览器保存会话记录,插件可以读取页面输入框。如果习惯把客户隐私直接粘贴到对话页面,那么任何有权限读取页面的扩展程序都能拿到明文。之前有的团队排查过类似事故:敏感信息不是从服务端泄露,而是从浏览器插件把用户输入转发到了不明服务端。
3. 使用场景与合规边界
3.1 谁适合重点看这篇文章
第一类是正在把 LLM 接入业务系统的开发者,需要确认 API 调用链路中没有明文敏感数据。第二类是负责企业内部知识库、客服系统、代码助手的运维和平台管理员,需要制定提示词保留和脱敏策略。第三类是想在个人项目里使用大模型,又不想把个人数据落到第三方平台的独立开发者。
3.2 需要重点保护的场景
- 调用云端 LLM API,且业务数据包含手机号、身份证号、银行卡、病历、合同、员工薪酬等敏感字段。
- 企业内部 RAG 系统,索引了内部文档、客户资料、代码仓库。
- 批量推理任务,一次处理大量用户文本,比如批量客服工单、批量简历筛选、批量合同审核。
- 通过第三方集成工具调用大模型,比如在低代码平台里接入 LLM 节点。
3.3 合规与授权边界
涉及个人信息、医疗健康、金融、未成年人信息等场景时,不能只靠技术手段规避风险。首先要确认是否有合法授权:数据来源是否允许用于大模型处理,用户是否知情,处理行为是否符合平台规则和法律法规。技术上能脱敏、能加密、能不上传,都不等于业务上合规。工程上的建议是:在业务设计阶段就把数据分级和授权流转放进需求,不要等上线后出问题再补。
4. 环境准备与前置条件
4.1 本地部署场景
本地部署是解决 Prompt 隐私的终极手段之一,前提是能在可控环境里运行模型。准备清单如下:
- 一台具备虚拟化能力的 Linux 或 Windows 主机,有 NVIDIA 或 AMD 显卡;模型较小时,CPU 推理也可以,只是速度更慢。
- Python 3.10 或以上版本。
- 本地推理框架,例如 Ollama、llama.cpp、vLLM,或者带 WebUI 的整合包。
- 足够的磁盘空间存放模型权重。量化后的 7B 级模型大约需要 4 到 8GB 空间,70B 级模型需要 40GB 以上,实际以模型文件大小为准。
启动本地服务可以使用 Ollama 等框架,下面是一个最简示例,拉到本地后,不需要把 Prompt 发送到外部云端:
# 拉取一个本地可运行的 7B 级别开源模型,具体模型名以你选择的为准 ollama pull qwen2.5:7b # 启动本地推理服务 ollama serve# 验证服务是否就绪,返回模型列表说明服务已正常运行 curl http://127.0.0.1:11434/api/tags执行验证时需要注意,如果11434已经被其他进程占用,要换端口启动。本地推理不会把 Prompt 发送到外部云端,但本机进程日志、终端历史、Python 控制台仍然可能把 Prompt 记录下来。本地部署不等于不查日志,这个点不能想当然。
4.2 云端 API 场景
业务必须使用云端 API 时,部署前建议做一轮前置检查:
- 阅读 API 提供方的数据使用条款,确认是否默认将输入用于训练。
- 在控制台关闭“训练数据收集”或类似选项。
- 确认日志保留策略,是否支持较短保留周期或自动删除。
- 明确数据存储区域,是否满足公司数据合规要求。
- 在 API 网关层增加请求内容脱敏组件。
下面是一个 Python 调用云端 LLM 的示例,重点在于不打印完整请求体:
from openai import OpenAI client = OpenAI( api_key="你的密钥", base_url="实际可用的接口地址" ) try: response = client.chat.completions.create( model="你的模型名", messages=[ {"role": "user", "content": "这是需要处理的文本"} ], temperature=0.7 ) print(response.choices[0].message.content) except Exception as exc: # 不要打印 exc 或 response,避免 Prompt 回显到日志 print(f"调用失败:{type(exc).__name__}")4.3 数据分级清单
在部署前做一份简单的数据分级,后续的检测规则都以它为依据:
| 数据级别 | 示例 | 允许进入 Prompt 吗 |
|---|---|---|
| L1 公开数据 | 公开新闻、公开文档 | 可以 |
| L2 内部数据 | 内部技术文档、会议纪要 | 脱敏后可以 |
| L3 敏感数据 | 手机号、身份证、合同金额 | 尽量不进入,必须处理时使用替代字段 |
| L4 极敏感数据 | 密码、密钥、医疗信息 | 不进入,使用本地模型或最小化方案 |
5. Prompt 隐私检测与验证方法
5.1 从自己发出去的请求查起
在真正的泄露发生之前,先检查自己能否从日志、网络请求、进程输出中看到 Prompt 明文。如果开发者自己都能看到,那么拥有更高权限的人也能看到。检测分三步:
- 在本地开发环境启动一个日志采集点。
- 调用 LLM 服务,发送一条带测试敏感信息的 Prompt。
- 检查日志、流量监控、模型平台控制台的审计记录,看这条 Prompt 出现在哪里。
如果出现在本地进程日志,说明代码里有敏感输出日志。如果出现在网络抓包里,说明明文经过网络链路。如果出现在 API 平台控制台里,说明服务端有日志记录。
5.2 用 Python 正则扫描 Prompt 中的敏感信息
下面是一个简单的敏感信息扫描器,可以放在本地服务入口处,对即将发送的 Prompt 做一次出境前检查:
import re SENSITIVE_PATTERNS = { "手机号": r"(?<!\d)1[3-9]\d{9}(?!\d)", "邮箱": r"[\w.+-]+@[\w-]+\.[\w.]+", "身份证号": r"(?<!\d)\d{17}[\dXx](?!\d)", "IP 地址": r"(?<!\d)(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}(?!\d)", } def scan_prompt(text: str) -> list: findings = [] for name, pattern in SENSITIVE_PATTERNS.items(): for match in re.finditer(pattern, text): findings.append({ "type": name, "value": match.group(), "position": match.start() }) return findings if __name__ == "__main__": test_text = "客户手机号 13812345678,邮箱 test@example.com,请生成回复" for hit in scan_prompt(test_text): print(hit)输出结果会显示每条敏感信息的类型、位置和原始值。这个扫描器只是示例,生产环境建议使用更完整的脱敏组件,并把扫描结果接入报警监控。
5.3 日志逃逸检查
很多团队在代码里没有直接打印 Prompt,但异常堆栈、HTTP 请求日志、SDK 内部的 debug 日志仍可能泄露输入。检查方法是在测试环境把日志级别调到 DEBUG,发一条测试请求,然后搜索日志文件:
grep -r "13812345678" logs/ grep -r "test@example.com" logs/只要能搜到,说明日志链路里还有组件把完整 Prompt 写入了,需要继续找。这是最直观也最容易被忽略的验证方式。
5.4 抓包验证
如果服务是 HTTP 明文调用,或者通过第三方网关转发,用抓包工具看一下请求体。生产环境不要随意抓包,先在测试环境复现。检查清单如下:
- 请求 URL 和目标域名是否符合预期。
- 请求头里是否携带了额外凭证。
- 请求体里是否存在完整 Prompt 明文。
- 响应体里是否回显了用户输入内容。
- 是否有请求被重定向到未注册的第三方地址。
6. 提示词脱敏与防护实现
6.1 模板与业务数据分离
这是最容易做到、也最有效的方式。不要等业务数据拼进模板后再做检查,而是先在业务侧拆出数据字段,只把必要字段传给 LLM。
不推荐的做法:
prompt = f"请根据以下信息写一封商务邮件:客户姓名:{name},手机号:{phone},订单号:{order_id}"推荐的做法:
record = { "customer_id": "C1001", "order_status": "shipped", "estimated_delivery": "2025-06-10" } prompt = "请根据订单信息写一封商务邮件:" + "客户编号:{customer_id},订单状态:{order_status},预计送达:{estimated_delivery}".format(**record)核心原则是不传手机号、身份证号等不需要的字段,而不是仅仅打码。数据最小化本身就是最强的隐私保护。
6.2 敏感字段脱敏
如果某个字段必须参与生成,可以先脱敏再发送。例如手机号中间四位替换为星号,邮箱保留域名后缀:
import re def mask_phone(text: str) -> str: return re.sub(r"(?<!\d)(1[3-9]\d)\d{4}(\d{4})(?!\d)", r"\1****\2", text) def mask_email(text: str) -> str: return re.sub(r"([\w.+-]+)@([\w-]+\.[\w.]+)", lambda m: "****@" + m.group(2), text) def redact_prompt(text: str) -> str: text = mask_phone(text) text = mask_email(text) return text sample = "客户手机号 13812345678,邮箱 test@example.com" print(redact_prompt(sample)) # 输出:客户手机号 138****5678,邮箱 ****@example.com脱敏之后,生成任务需要的信息仍然存在,但完整敏感字段已经不在明文里了。注意,正则规则要根据实际业务调整,最好把规则集中放在配置文件中:
{ "redact_rules": { "phone": {"pattern": "(?<!\\d)1[3-9]\\d{9}(?!\\d)", "mask": "keep_tail"}, "email": {"pattern": "[\\w.+-]+@[\\w-]+\\.[\\w.]+", "mask": "keep_domain"} }, "block_if_contains": ["身份证号", "银行卡"] }这样当规则变化时,不需要重新改代码。
6.3 内容最小化:长文档改成摘要
如果模型任务是总结长文档,优先在本地做一次文档抽取,把关键段落提取出来,再拼接成 Prompt,不要把整份 PDF 或 Word 原文发送给云端 API。这一步既能降低 token 成本,也能减少敏感信息暴露量。可以先用 pypdf、pdfplumber 之类的工具在本地解析,再通过关键词或规则抽取关键段落。不要图省事,直接把整个文件喂进上下文。
6.4 本地部署兜底
当数据级别达到 L4 时,最稳妥的方案是使用本地模型。本地模型不向第三方发送数据,但需要团队自行管理模型版本、推理资源、安全补丁和日志删除。更重要的是,本地模型同样会记录日志,不要因为“本地”就放松日志审计。前文提到的扫描和日志检查流程,在本地部署环境下同样适用。
7. 接口 API 调用中的隐私控制
7.1 调用层面的隐私配置
调用云端 API 时,优先确认配置项中是否有关闭训练开关、关闭日志保存、设置数据保留周期的能力。不同平台配置方式不一样,以实际控制台页面为准。常见做法是创建 API Key 时关联一个独立的项目空间,并在项目设置里把所有隐私相关选项调到最严格。如果平台不支持关闭训练,就要评估是否改用本地模型。
7.2 异常处理与日志脱敏
LLM API 出错时,错误信息里有时会包含请求参数,或者 SDK 直接把请求体打进堆栈。代码里要统一封装异常处理,确保任何异常输出都不会带出 Prompt 明文:
import logging logger = logging.getLogger("llm_client") def safe_llm_call(client, messages): try: response = client.chat.completions.create( model="你的模型名", messages=messages ) return response.choices[0].message.content except Exception as exc: # 关键点:只记录异常类型,不记录完整错误消息 logger.error("LLM call failed: %s", type(exc).__name__) raise这里最容易犯的错是直接logger.exception(exc)或者把response.text打出来,一旦 SDK 的响应里回显了输入,日志就等于复制了一份 Prompt 明文。
7.3 批量任务中的隐私风险
批量任务比单条请求更容易造成大规模泄露,因为批量处理通常会把输入放在文件里,而日志很容易把整个批次文件路径打印出来。建议在批量任务中加入以下流程:
- 输入文件加密存储,任务完成后按策略删除。
- 每一条输入在进入队列前先做脱敏扫描。
- 任务日志只记录批次 ID 和任务状态,不记录输入内容。
- 失败重试时,从脱敏后的副本重试,不重新发送原始 Prompt。
- 输出文件中如果包含原始输入片段,同样做脱敏。
下面是批量任务脱敏封装的简单示例:
import csv def process_batch(input_file, output_file, redact_func, llm_func): with open(input_file, "r", encoding="utf-8") as fin, \ open(output_file, "w", encoding="utf-8") as fout: reader = csv.DictReader(fin) fieldnames = reader.fieldnames + ["result"] writer = csv.DictWriter(fout, fieldnames=fieldnames) writer.writeheader() for row in reader: safe_text = redact_func(row["content"]) row["result"] = llm_func(safe_text) writer.writerow(row)这只是一个架构示意,实际项目要加入队列、重试、审计和删除策略。
7.4 网关拦截设计
如果团队有多个服务接入