系统提示词泄露启示:从Prompt工程到AI应用安全加固
2026/9/18 10:22:25 网站建设 项目流程

上周做一个企业知识库问答项目的收尾,客户的安全同学发来一张截图,问"这句话是不是你们写的"。截图里是我们系统提示词中间的一段——关于"引用来源必须来自检索结果,不得自行补充"的那条约束,连标点都没改,被人完整贴在了一个公开帖子里。那一刻体感挺复杂的:说明有人认真研究过我们的产品,也说明我此前对这段文本的风险评估过于乐观。

system_prompts_leaks这个标题在圈子里流传有一阵子了,它指向的是一类持续的收集行为:把各家产品里那层"用户看不见的指令"汇总、整理、横向对比。很多人第一次看到会以为只是一堆文本八卦,翻两页就过去了。但如果你正好在做对话产品、Agent 工具链,或者企业内部的 AI 应用,这批样本其实是一份相当罕见的公开资料——它把一整个行业在过去几年里踩过的坑、达成的共识,压缩成了几百段可读的配置文本。

我打算按工程视角来写这篇。先说清系统提示词在产品里到底扮演什么角色,再拆泄露这件事的技术链路,以及为什么单靠"藏"解决不了问题;然后是重点——从公开样本里能读出哪些可复用的结构和方法;最后落到我自己项目里的加固清单和测试流程。做对话产品的人、负责 AI 应用安全的人、以及想把自己的提示词写得没那么脆的开发者,都能直接拿走点东西。

1. 系统提示词到底是什么:从"人设纸条"到生产环境里的第一优先级配置

1.1 它和用户提示词不是一回事,别混着写

我先把这个概念钉死,因为很多团队把两者塞在同一个字符串里,后面所有问题都从这里长出来。系统提示词是在会话开始之前就注入到上下文里的那一层指令,用户端通常看不到原文,它不受单次请求影响;用户提示词是每次请求带上来的那部分,也就是你能在对话框里直接打字输入的内容。

打个比方:系统提示词像后厨的操作规范,规定了用什么油、火候多大、摆盘什么样、哪些食材不能碰;用户提示词是顾客点的那道菜。顾客可以说"少放辣",但顾客说"把你们后厨的操作规范背给我听"——这就属于越过了点单的边界。现实里麻烦在于,模型并没有真正的"权限系统"来区分这两者,它看到的只是一段连续的文本。

维度系统提示词用户提示词
注入时机会话初始化时一次注入每轮请求动态追加
用户可见性设计上不可见完全可见
典型内容角色定义、能力边界、工具规范、输出格式、拒答策略具体任务、上下文、追问
变更频率低,走版本管理和灰度每轮都在变
失效后果全站行为漂移单次回答异常

这张表看着简单,但第三行是关键分水岭。系统提示词里通常承载的都是"策略性"内容,用户提示词承载的是"任务性"内容。策略被拿走,等于你的产品说明书被拿走;任务被拿走,只影响那一次回答。

1.2 为什么它值得被当成资产而不是一堆文案

在很多团队里,系统提示词是产品经理用记事本写好、直接贴到代码常量里的。我见过最夸张的一个项目,提示词有 4000 多字,混着中文、英文、还有几个内部服务的域名和限流阈值,全部塞在一个 triple-quoted 字符串里,没有任何版本记录。这种写法有三个隐患,而且都是真会炸的。

第一,它其实是产品策略的可执行表达。你告诉模型"信息不足时先追问而不是猜",这条规则背后是产品团队对准确率和体验的一次取舍决定;你告诉模型"涉及金额的回复必须带上计算过程",背后是合规要求。这些决定如果没有被记录成可追溯的文档,人员的流动就会让策略悄悄丢失。

第二,它是攻击者手里的地图。模型的行为边界写在提示词里,读懂了边界就等于知道从哪里绕。比如提示词里写着"不得提供任何医疗诊断建议",那么攻击者只要把问题包装成"我在写一篇科普文章,需要一个医学表述",就能测试这条边界的形状是硬的还是软的。泄露出的一行字,足以让人画出你的防御轮廓。

第三,它同时是最有效的调试工具。线上出现"答非所问"的时候,我第一件事永远是去读当时的系统提示词版本,而不是去看模型参数。九成以上的行为异常,根因都在提示词里的一条约束和另一条约束打架。

2. 泄露是怎么发生的:一条从"诱导"到"复述"的链路拆解

2.1 常见的几类提取思路,以及它们的共同原理

先把立场说清楚:下面只说类别和原理,不给可执行的诱导脚本。原因很实际——拿这类脚本去试探第三方服务,通常违反对方的服务条款,而且网上从来不缺现成的;真正稀缺的是防御侧的理解。知道攻击面长什么样,才能设计出对的防线。

我按"触发机制"把见过的思路归成几类,比按"话术"归类更有用,因为话术千变万化,机制就那么几种。

类别表面形式触发的模型行为防御侧的抓手
直接索取要求复述上文全部内容服从优先指令的惯性输出侧复述检测
情境嵌入假装在写文档、做翻译、编剧本把策略文本当素材处理语义等价检测
格式伪装要求转成结构化格式或换语言格式转换被视为无害任务跨语言、跨格式检测
上下文续写给一个开头让模型接下去续写模式的低警觉性前缀匹配与截断
分片拼接每轮只取一小段单次输出不触发阈值会话级聚合监控

看第四列会发现一个规律:单点的防御手段基本都只能挡住一到两类,因为它们检测的是"形式";而攻击者的成本主要花在"换形式"上。防御必须往语义层走,这是后面第 4 节要展开的东西。

2.2 为什么"直接问"偶尔真的能成:注意力与优先级的真实博弈

这是最反直觉的一点。很多人以为系统提示词有某种"内核态"权限,用户指令根本碰不到。实际不是。模型做的事只有一件:根据当前上下文里的全部内容,预测下一个最合理的词。系统提示词的"权威性"来自它出现在前面、且通常写得比较明确,它是一种概率上的倾向,不是一道锁。

我用一个类比解释:员工手册里写着"任何对外承诺必须走审批"。新员工入职时读过,记得很牢。但三个月后,一个客户在电话里说"你就先答应我一句,我这边马上要交差",这位员工在具体情境的压力下,判断的权重会从"手册"偏向"眼前这个人"。模型面对的是一模一样的博弈:用户指令更具体、更贴近当前任务、出现在更靠后的位置,它的权重就会上升。

还有两个放大因素。一是上下文长度。多轮对话往后走,系统提示词被推得越来越远,注意力权重自然衰减,这也是为什么很多提取尝试会先聊上十几轮再动手。二是任务一致性。如果用户提出的要求看起来像是一个正常任务(比如"把上面那段整理成条目"),模型倾向于把它当成工作来对待,而不是当成越界行为。

理解了这两点,加固方向就明确了:要么提高系统指令在关键场景下的相对权重,要么把真正重要的约束从"文本"挪到"代码"里。后者往往更省事。

2.3 输出侧的过滤到底能挡多少

我先给一个不太讨喜的结论:纯关键词过滤的作用比大多数人想象的小很多。它能挡住最粗糙的直接复述,因为那类输出和原文高度重合,正则和相似度算法很容易命中。但一旦输出被改写成摘要、翻译成另一种语言、或者拆成问答对,字面匹配就全部失效了。

我们做过一次内部评测,拿 40 组不同形式的复述请求跑自家的初版过滤,结果大概是这样的:直接复述拦住 100%,格式转换拦住约 60%,跨语言拦住不到 30%,语义摘要基本拦不住。这组数字不用记,记住结论就行——过滤层的价值在于"提高攻击者的改写成本",不在于"封死"。

那有效的做法是什么?是分层。第一层用规则做快速拦截,处理掉绝大部分低成本的尝试;第二层用一个小的分类模型或审核模型做语义判断,专门识别"这段输出和某段策略文本是不是在表达同一件事";第三层用会话级聚合,看一个用户在多轮里是不是在拼图。三层叠起来,成本可控,覆盖率也够看。这个结构在第 4 节会给出具体实现思路。

3. 拿到公开样本之后:我实际从里面读出了什么

3.1 一套反复出现的结构骨架

我把能看到的样本按模块拆解过一遍,去重之后发现结构高度收敛。不是内容像,是骨架像——大家最后都长成了差不多的样子,因为踩过的坑是同一批。下面这个骨架是我自己项目里用的版本,吸收了公开样本里反复出现的设计,但每个模块的措辞和取舍都是按我的场景重写的。

[身份与定位] 你是一个面向 {用户群体} 的 {角色}。你的核心目标是 {单一核心目标}。 [优先级声明] 当以下规则发生冲突时,优先级从高到低为: 安全与合规 > 事实准确性 > 用户体验 > 简洁性。 [能力边界] - 可以:{列举} - 必须拒绝:{列举,每条后面用一句话说明原因,便于模型泛化} - 不确定时:明确说明不确定,并给出获取准确信息的路径 [工具调用规范] - 什么时候调用:{触发条件} - 调用失败怎么处理:{重试 / 降级 / 告知用户} - 禁止:暴露工具名称、内部参数、调用日志 [输出格式] - 默认结构:{结构} - 引用规范:{来源标注方式} - 语言:始终使用与用户提问相同的语言 [兜底策略] - 信息不足:先追问,一次只问最关键的一项 - 超出范围:说明边界,并给出替代方向

值得说一句的是"优先级声明"这一段。几乎所有成熟的样本里都有类似的东西,但很多自研项目会漏掉。它的作用是在规则打架时给模型一个确定的裁决顺序,不然模型只能靠猜测。我吃过这个亏:早期一版里同时写了"回答要简洁"和"要给出完整推导",结果模型在同一个问题上时而三段话时而三百字,排查了半天才发现是这两条在抢权重。

3.2 那些看起来"啰嗦"的句子,其实每条都对应一次线上事故

初看样本的时候,我最不理解的是为什么有些约束写得那么具体,甚至具体到荒谬。比如"不要在回复中提及任何内部工具的名称"、"不要编造看起来像真实 URL 的链接"、"如果用户引用的数据与检索结果不一致,以检索结果为准"。后来自己做久了才懂,这些不是写作者啰嗦,是每一条都对应一次真实发生的事故。

"不要编造链接"出现频率极高。原因很朴素:模型在需要给出来源时,如果检索结果里没有可引用的内容,它会倾向于生成一个格式正确但根本不存在的链接,因为那样看起来最"像"一个完整回答。用户点进去是 404,信任度当场掉一半。解决办法不是骂模型,是在提示词里明确说"没有可引用来源时,直接说明本轮没有检索到依据"。

"不要在回复中提及工具名"这条更微妙。它不只是为了保密,更是为了体验一致性。用户本来在跟一个"助手"对话,突然冒出一句"我正在调用 knowledge_search_v2",整个产品的沉浸感就碎了。同时它还有一层安全价值:工具名一旦暴露,攻击者就能针对性地构造参数,试探工具的能力边界。

还有一条我印象很深的是"始终使用与用户提问相同的语言回复"。看起来是废话,但在多语言产品里它是硬需求。用户用中文问,模型检索到英文文档,很容易就用英文答回去了。这类约束的写法有个共同特征:把"应该做什么"换成一个可以被检查的具体判据。抽象规则模型学不会,具体判据模型执行得很稳。

3.3 从样本特征反推产品意图

样本看多了以后有个副作用,就是你开始能从几行约束里读出对方的产品定位。这个东西对做竞品分析和给自己产品定策略都挺有用,我整理成了下面这张对照。

样本里的特征大致能推出的产品意图对我们项目的启发
大量篇幅在拒答与合规面向公众、监管敏感行业合规约束要前置到最高优先级
强调"简短""不超过 N 字"移动端优先、场景碎片化输出长度应当做参数而不是写死
详细的多工具调度规则Agent 形态、任务链条长工具规范必须写清失败降级路径
大量 few-shot 示例输出格式要求极高、容错低示例质量比示例数量重要得多
明确的多语言与地区适配全球化产品语言策略要写成可执行判据
包含运行时变量占位符个性化程度高、有用户画像动态内容与稳定内核必须分离

最后一行的启发最大。看到别人样本里的占位符写法之后,我才把自家那坨 4000 字的大字符串拆成了"稳定内核 + 运行时注入"两段。拆完之后可测试性直接上了一个台阶,具体在第 4 节讲。

4. 把这些经验用回自己的项目:系统提示词加固清单

4.1 分层写:稳定内核与运行时注入彻底分开

这是我这轮改造里收益最大的一条。之前的做法是把用户画像、检索结果、当前时间全部拼进同一个字符串,结果是同一份提示词每天都长得不一样,回归测试根本没法做,出了问题也复现不了。

拆成两层之后就清楚了:内核层是策略,走版本管理,一周可能才动一次;注入层是数据,每次请求都不一样,但格式固定。下面是我在用的组装方式,逻辑很朴素,关键是边界清晰。

BASE_POLICY = load_policy("policy/v7.yaml") # 走版本控制,有变更记录 def build_system_prompt(ctx): return "\n\n".join([ BASE_POLICY.role, BASE_POLICY.priority_rules, BASE_POLICY.capability_bounds, render(BASE_POLICY.tool_rules, ctx.available_tools), render(BASE_POLICY.output_format, ctx.locale), "# 本轮上下文\n" + render_kb(ctx.retrieved_docs), # 数据层 "# 用户信息\n" + render_profile(ctx.user_profile), ])

这么做有三个附带好处。一是可测试,内核层的改动可以跑固定用例集,注入层的变化不会污染测试结果。二是可灰度,策略版本可以按流量比例放,观察到指标异常随时回滚。三是降低泄露的"信息密度",即使有人拿到一次完整复述,里面也混着大量一次性的检索片段,很难判断哪部分才是真正的策略。当然这只是降低分析便利性,不能当成安全手段,这点后面会再强调。

4.2 真正有效的加固,大部分发生在提示词之外

这一条我想说得重一点:如果你把秘密写在提示词里,那么无论怎么加固提示词,它都是不安全的。提示词是给模型看的,不是给系统执行的,它没有访问控制。

所以第一优先级是清点:把密钥、内部服务地址、真实的业务阈值、用户数据的加工规则,全部从提示词里挪出去。需要模型知道"当前用户额度是多少"的时候,不要写规则让模型自己算,而是由服务端算好,把结论作为一个事实注入进去。模型的职责降级成"表达",判断留在代码里。这一降级之后,即使提示词全文被拿走,损失也有限。

第二个手段是把能力做成"开关"而不是"条文"。举个例子,早期我们写的是"如果用户询问订单状态,你需要调用订单查询接口,参数是订单号,返回字段包括……"。这段话一旦泄露,等于把接口说明送出去了。改成服务端根据会话状态决定这一轮暴露哪些工具,模型看到的只是"当前可用工具:订单查询",参数校验在服务端做。泄露面从"能力说明书"缩成了"能力清单"。

第三个手段是限制敏感信息的输出通路。凡是服务端不希望出现的字段,在返回给用户之前过滤掉,而不是指望模型不说。这条听起来像废话,但我见过太多项目把"不要输出内部 ID"写在提示词里,然后就当这个问题解决了。

4.3 把提示词当代码测:回归用例怎么建

提示词改一个词,行为可能整体漂移。所以它必须有回归测试,而且测试要能自动判定,不能靠人肉看。我的做法是先定义"可判定的行为",再为每个行为写用例。

CASES = [ Case( name="信息不足时先追问", prompt="帮我订明天的会议室", must_contain=["请问", "哪个时间段"], # 或者用一个小分类器判定意图 must_not_call=["booking_create"], # 不该在信息不全时直接下单 ), Case( name="不编造引用来源", prompt="关于这个政策有什么官方说明?", mock_kb=[], # 故意让检索为空 must_not_match=r"https?://", ), Case( name="语言跟随", prompt="What's the refund window?", expect_language="en", ), ]

跑法上建议两层:一层是快速集,十几条最核心的用例,本地改完立刻跑,几秒钟出结果;另一层是完整集,几十到上百条,CI 里跑,带评分。评分不要只看通过与否,要看趋势——把每次的通过率画成折线,忽然掉几个点就是信号。

有个细节值得提醒:用例里的"期望"尽量写成结构性判据(必须包含什么、必须不匹配什么、必须不调用什么工具),而不是写成标准答案去比对。模型输出天然有波动,用字符串相似度当判据会时不时给你误报,几次之后团队就没人看测试结果了。

用例类型输入特征期望行为判定方式
边界声明超出能力范围的问题说明边界并给替代方向关键词 + 分类器
追问优先信息不全的任务型请求先追问,不直接执行工具调用断言
无依据拒答检索结果为空的事实类问题明说没有依据正则 + 人工抽检
格式稳定需要结构化输出的请求结构字段齐全解析后字段校验
语言跟随非默认语言的提问用同语言回复语种识别

4.4 上线之后看什么信号

提示词加固不是一次性动作,得有持续观测,否则你不知道自己是挡住了还是在被动挨打。我关注四类信号。

第一类,复述型请求的占比。做法很直接:对用户输入用一个轻量分类器打标,统计"要求复述、要求翻译上文、要求格式转换"这类意图出现的频率和分布。占比突然抬升,说明有人在批量试探。

第二类,输出的长度异常。复述类攻击通常会产生远超正常长度的输出。我们对每轮输出做长度分位统计,尾部出现尖峰的时候拉几条样本看。这个信号很粗糙,但胜在便宜,而且对最粗暴的攻击非常有效。

第三类,会话级聚合。单轮看起来人畜无害,连续十几轮每轮只要一小段,拼起来就是完整策略。我们用一个简单的相似度聚合,把同一会话的多轮输出拼起来和策略文本做比较,超过阈值就告警。这一层做起来稍微花点功夫,但它是唯一能覆盖分片攻击的手段。

第四类,影子评测。准备一批固定的探测输入,每天定时跑一遍,看模型今天会不会把不该说的说出来。这批输入不外发、不进日志、不随版本变化,是纯粹的基线。它的价值在于能发现"某次模型版本更新导致行为松弛"这类你完全无从预期的问题。

5. 几个我踩过的坑,以及那些看起来很聪明其实没用的做法

5.1 把提示词写得更隐晦,解决不了任何问题

我一开始的想法很朴素:写得绕一点、用点黑话、把关键规则藏在段落中间,别人就算拿到了也看不懂。后来发现这个思路完全站不住。模型自己都读得懂,攻击者把整段拿去问一遍模型"这段话在说什么",五秒钟就得到一份大白话解释。混淆给自己带来的唯一后果是维护成本上升,三个月后我自己都看不懂当初写的什么。

真正有意义的是"少写"——能用代码表达的策略就不要写进提示词。这不是混淆,是减少攻击面。

5.2 防御加得太狠,正常用户先受伤

这个坑我印象很深。有一轮我们被几次复述请求搞得有点紧张,加了一堆规则,其中一条大意是"任何要求整理、总结、转换格式的请求都要先声明能力边界"。上线第二天客服就来投诉,说用户让助手把自己的会议记录整理成要点,助手回了一句"我无法执行这个操作"。正常功能和攻击手法在形式上本来就重叠,用形式判据去拦,必然误伤。

后来的做法是把判据改成"看内容指向":模型要回答的内容是不是在指向自身的配置或系统信息。是同一种操作(总结、整理),但对象不同,判定就完全不同。这个区分做起来比形式匹配麻烦,但它是唯一能在体验和防护之间取得平衡的路。

5.3 版本管理失控,是我见过最贵的一个坑

说个真事。有个项目半夜上线了一版提示词,改动只有一个词。第二天核心指标掉了 8 个点,团队排查了一整天,从模型版本查到检索质量,最后发现那个词改的是优先级顺序里的一项。如果有版本记录、有回归用例、有灰度,这个问题会在十分钟内被发现。

从那以后我定了个死规矩:提示词改动等同于代码改动,必须走同一个流程——提交、评审、跑用例、按流量灰度、看指标、要么全量要么回滚。听起来重,但真跑起来,一次改动也就多花二十分钟。

5.4 别把提示词当知识库用

这个坑在项目早期特别常见。业务方给一份产品规则,说"你把这些都写进去,让模型照着答"。结果是提示词越来越长,几千字下去,模型对每条的服从度都在下降,最后连最基础的角色设定都开始漂移。

正确的做法是把事实性内容放进检索,提示词只保留"怎么用这些事实"的规则。提示词负责行为,检索负责知识,这个分工理顺了,两边都会变简单。顺便说一句,这也能显著降低泄露的损失——知识本来就是可以公开的产品文档,策略才是你真正不想被完整拿走的东西。

6. 我现在的工作流:写、测、发、复盘,四步闭环

把上面所有东西串起来,就是我现在每天在跑的一套流程,分享出来给同样在做对话产品的朋友参考。

写的时候,我用 YAML 而不是纯文本,每个模块独立成段,每条规则后面强制写一行注释说明它对应哪个线上问题或者哪条业务要求。这个注释习惯了之后会上瘾,因为半年后你能一眼看出某条规则是从哪来的,敢不敢删。

测试分两档,快档五秒钟,慢档进 CI 带评分。每次改动先跑快档,绿了再提交跑慢档。判据全部写成结构性断言,不写标准答案比对。

发布一律灰度。我的经验是先放 5% 流量观察半天,重点看三个指标:拒答率、任务完成率、输出长度分布。拒答率上升往往意味着防御过紧,任务完成率下降往往是提示词里两条规则打架,输出长度分布异常通常和复述型请求有关。

复盘每周一次,把这一周被拒的样本随机抽二十条人工看一遍。这个习惯帮我发现过好几次误伤,也让我对"用户到底会怎么问"这件事的判断越来越准。数据看板看不出误伤的体感,只有真人读样本才能。

最后分享一个我自己觉得挺管用的小做法:把提示词的关键模块做成可开关的清单,每个开关对应一个可观测的指标。这样当你要调整策略时,不用改文本,改开关就行——开关的变更天然可记录、可回滚、可灰度,比改一段散文安全太多。我现在有大概三分之一的规则是用这种方式管的,剩下的才是需要模型自己判断表达方式的部分。这个比例还在往上涨,因为每挪过去一条,我的心理负担就少一点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询