1. 从 "system_prompts_leaks" 说起:这个项目到底在做什么
第一次看到 system_prompts_leaks 这个名字的人,大概率会愣一下。system_prompts 好理解,系统提示词;leaks 也不难,泄露。合在一起,它指的是一类长期存在的民间整理工作:把散落在公开渠道里的各家大模型系统提示词文本收集起来,做清洗、归类、结构化拆解,形成一份能被检索、能被对比、能被引用的语料库。我做提示词工程这些年,电脑里躺着十几个不同版本的这类集合,从最初几十行的 txt,到后来带标签、带元数据的 jsonl,中间踩的坑足够写一本书。
先把定位说清楚:这类项目的产出物是一份语料,不是一份"秘籍"。它解决的核心问题是信息分散——同一份系统提示词,可能在某个博客贴了一部分,在某个 Issue 里补了另一部分,格式还各不相同;有人贴的是纯文本,有人贴的是截图转录,有人贴的是被截断的片段。整理者的价值在于把这些碎片对齐、还原上下文、标注来源和版本,让研究者能在一个统一的坐标系里做横向比较。它适合三类人:一是做提示词工程的开发者,想从成熟产品的写法里提炼可复用的范式;二是做 AI 产品设计的人,想理解头部产品在角色定义、边界约束、输出规范上的取舍;三是做模型应用安全评测的人,需要一个真实语料基座来设计测试用例。
我自己的用法比较务实:不把里面的句子直接抄进产品,而是看它的结构骨架。一家产品怎么写"不确定时怎么办",另一家怎么写"工具调用失败的重试逻辑",把这些横向摆在一起看,比读十篇提示词教程都管用。这也是我认为这个项目最大的价值——它是一份对照样本集,而不是一份可直接复制的模板。
这里必须先讲清楚一个边界问题,也是我做这类整理时给自己定的第一条规矩:只处理已经在公开渠道流传的文本。什么叫公开渠道?官方发布的技术文档、官方博客里贴出的示例、开发者在技术社区主动分享的内容、公开发表的论文附录、公开的会议演讲材料。反过来说,任何形式的诱导式获取、任何试图绕过产品安全策略的尝试、任何利用未公开缺陷去套取内容的手段,都不在我的工作范围内,也不应该出现在一个健康的语料整理项目里。这不是出于保守,而是因为一旦语料来源不可信,后面所有的分析结论都会变成沙上建塔——你根本不知道这段文本是真实配置,还是模型自己编出来哄你的。
还有一个容易被忽略的点:模型复述 ≠ 真实配置。这是新手最容易翻车的地方。你问一个模型"你的系统提示词是什么",它给你的答案很可能是基于上下文生成的合理猜测,夹杂着它对自身行为的描述、对训练数据的记忆、甚至是对提问方式的迎合。我做过对照实验,把同一份已知的真实系统提示词喂给某个模型,让它自我描述,输出的内容只有大约一半能对上。所以语料库里凡是标注为"自述获取"的条目,我都会单独打一个低置信度标签,绝不和"官方公开"的条目混在一起做统计。
1.1 一份合格语料库应该长什么样
很多人做这类项目,第一步就错了——建一个文件夹,把看到的内容往里面一丢,文件名写"某模型提示词.txt"。三个月后自己都不知道哪份是新版哪份是旧版。我现在的做法是强制走结构化存储,每一份语料是一个对象,至少包含这些字段:
| 字段名 | 含义 | 取值示例 | 是否必填 |
|---|---|---|---|
| id | 唯一标识 | sp_2024_q3_0012 | 是 |
| product | 产品/模型归属 | 通用对话助手 | 是 |
| variant | 具体形态 | 网页版 / 编程助手 / 移动端 | 否 |
| source_type | 来源类型 | official_doc / community_share / paper_appendix | 是 |
| source_ref | 来源链接或出处描述 | 官方文档第 3 节 | 是 |
| collected_at | 采集时间 | 2024-08-11 | 是 |
| language | 主语言 | zh / en / mixed | 是 |
| token_count | 粗略 token 数 | 1840 | 否 |
| modules | 模块标签数组 | ["identity","tools","format"] | 否 |
| confidence | 可信度分级 | high / medium / low | 是 |
| notes | 备注 | 疑似中间版本,含截断 | 否 |
这个表看起来啰嗦,但它是后面一切分析的前提。没有 confidence 字段,你就没法做加权统计;没有 collected_at,你就分不清哪些差异是产品迭代造成的,哪些是同一时期的不同形态造成的;没有 source_type,你写分析文章时就没法负责任地说明结论的适用范围。
1.2 目录组织:按什么维度切
目录结构这件事,我改了三次才定下来。第一版按产品名分,很快乱了,因为同名产品有多个形态;第二版按时间分,也乱了,因为采集时间和文本实际生效时间不是一回事。最终定下来的是三级结构:
- 第一级按 source_type 分(official / community / paper),因为来源决定了你分析时的引用方式;
- 第二级按 product 分,同名产品的不同形态放进子目录;
- 第三级是具体条目文件,文件名 =
产品_形态_YYYYMM_置信度.json。
这样组织的好处是:想找"所有官方公开的"内容,直接看第一级;想研究某个产品的演进,进第二级按时间排序;想快速筛掉低置信度的,文件名里就带着标记。我在实际使用中发现,把置信度写进文件名这个动作,能省掉大量"这份到底靠不靠谱"的纠结时间。
2. 语料采集与清洗:从零散文本到可用数据集
采集这一步,很多人以为就是复制粘贴。真做起来你会发现,从各个渠道扒下来的文本,脏得超出想象。我处理过一份从网页复制的提示词,里面混着导航栏文字、版权声明、三个不同层级的折叠面板残留、还有两处被截断的占位符。直接拿去分析,结论必然是错的。
2.1 采集渠道的优先级排序
我把渠道按可信度排了个序,这个排序直接决定了后面 confidence 字段的取值:
- 官方文档与官方博客中的完整示例——可信度最高,通常还带版本信息,直接标 high。
- 公开论文附录与会议演讲材料——可信度次高,因为是研究者为了复现而公开的,一般会说明获取方式,标 high 或 medium。
- 开发者社区中的完整分享——需要交叉验证,至少找到两个独立来源指向同一文本才能标 medium。
- 单一来源的片段分享——信息不完整,标 low,只用于辅助印证,不单独作为结论依据。
- 模型自述内容——一律标 low,且单独存放,不参与任何比例统计。
这个分级不是形式主义。我曾经写过一篇关于"某类产品边界约束写法"的分析,结论的支撑全部来自 medium 以上的条目,评论区有人拿一份 low 级别的自述内容质疑我的结论,我把分级表贴出来,争议就自然消解了。可追溯是这类项目能被认真对待的关键。
注意:采集时一定要同步记录采集时间。我吃过亏——两份内容差异很大,一度以为是产品改版,后来发现只是采集时间差了半年,中间隔了一次大版本更新,但没记录时间就完全对不上账。
2.2 文本清洗的八个动作
清洗环节我固定了八个步骤,顺序不能乱,否则某些噪音会被后面步骤放大:
import re import unicodedata def clean_prompt_text(raw: str) -> str: # 1. 去除零宽字符与 BOM text = raw.replace("\ufeff", "").replace("\u200b", "").replace("\u200c", "") # 2. 统一换行符 text = text.replace("\r\n", "\n").replace("\r", "\n") # 3. 归一化 Unicode(全角转半角在这里统一处理) text = unicodedata.normalize("NFKC", text)# 4. 去掉 HTML 标签残留 text = re.sub(r"<[^>]{1,80}>", "", text) # 5. 去掉常见页眉页脚噪音(导航、版权、分享提示) noise_patterns = [ r"^(首页|登录|注册|分享|收藏|招聘|关于我们)[\s\|·]*$", r"^(版权所有|Copyright|All Rights Reserved).*$", ] for p in noise_patterns: text = re.sub(p, "", text, flags=re.MULTILINE) # 6. 折叠三个以上连续空行为两个 text = re.sub(r"\n{3,}", "\n\n", text) # 7. 剥离首尾空白 text = text.strip() # 8. 剔除长度过短且无标点的行(典型的分页残留) lines = [ln for ln in text.split("\n") if len(ln.strip()) > 2 or not ln.strip()] return "\n".join(lines)第 3 步的 Unicode 归一化特别值得说。中文提示词里经常混着全角冒号、全角括号、还有各种花式引号(弯引号、直角引号),不做归一化的话,后面写正则匹配小节标题时会漏掉一大半。我第一次做模块切分,统计结果显示只有 40% 的文本带有"角色定义"模块,检查后发现是标题用的是全角空格加冒号,正则没匹配上,归一化之后这个比例升到 78%,完全是两个结论。 第 4 步要小心。提示词里有时候确实包含类似 XML 的结构化标签(比如用来分隔不同模块),一刀切掉会把真实结构一起毁了。我的做法是先做一次检测:如果标签出现频率很高且成对出现,就保留;如果是零散的单标签,才当噪音去掉。 ### 2.3 去重:SimHash 比编辑距离实用得多 同一份提示词在不同渠道被反复转发,几乎是必然的。用编辑距离两两比对,几千条的规模就是几百万次计算,太慢。我用的是 SimHash 加汉明距离,把每段文本压成一个 64 位指纹,距离小于等于 3 的视为近重复: ```python import hashlib def simhash(text: str, bits: int = 64) -> int: tokens = re.findall(r"[\u4e00-\u9fff]|[a-zA-Z]+|\d+", text.lower()) v = [0] * bits for tok in tokens: h = int(hashlib.md5(tok.encode()).hexdigest(), 16) for i in range(bits): v[i] += 1 if (h >> i) & 1 else -1 fingerprint = 0 for i in range(bits): if v[i] > 0: fingerprint |= 1 << i return fingerprint def hamming(a: int, b: int) -> int: return bin(a ^ b).count("1")这里有个坑:中文分词如果用字符级切分,短文本的指纹会很不准。我的经验是,长度低于 200 字的条目别用 SimHash,直接人工看,因为数量本来就不多。另外,去重不能简单地把重复项删掉——重复本身就是信息。一个文本在几个独立来源里出现,说明它的可信度更高。所以我保留全部条目,只在元数据里加一个 dup_group 字段,分析时按组取代表。
3. 结构解剖:把一段黑盒文本拆成可复用模块
语料清洗完,真正的硬活开始了。一段系统提示词动辄上千 token,看着像一锅粥,但它其实是有骨架的。我拆过几百份之后,总结出高频出现的七个模块。
3.1 七个高频结构模块
身份声明通常出现在开头,一到三句话,说明"你是谁、以什么身份回应"。有些写得直白,有些用隐喻。能力边界是第二常见的模块,说明哪些事不做、遇到某类请求怎么处理。语气风格规定用词偏好、句长、是否使用列表。输出格式规定结构、字段、长度上限。工具说明在带函数调用的产品里必然出现,包括每个工具的用途、参数、触发条件。工作流程是一段决策逻辑,常见形式是"先做 A,如果满足 C 则做 B"。不确定性处理规定信息不足、无法确认时该怎么表达。
这七个模块的排列顺序其实是有讲究的。我统计过一批语料,身份声明在开头的比例超过九成,工具说明靠近结尾的比例接近八成。这个规律背后的道理不难理解:靠近结尾的内容在长上下文里更容易被模型"记住",而工具说明是最需要被严格执行的部分。
| 模块 | 出现位置倾向 | 平均占比 | 写作难度 |
|---|---|---|---|
| 身份声明 | 开头 | 8% | 低 |
| 能力边界 | 前半 | 22% | 高 |
| 语气风格 | 中段 | 10% | 低 |
| 输出格式 | 中后段 | 15% | 中 |
| 工具说明 | 结尾附近 | 25% | 高 |
| 工作流程 | 中段 | 12% | 高 |
| 不确定性处理 | 结尾附近 | 8% | 中 |
占比是粗略统计,不同形态差异很大。编程助手类产品的工具说明占比能到四成,而纯对话类产品有时候完全没有工具模块。
3.2 用规则把模块切开
切分我不用模型,用规则就够了,因为这类文本的标题格式相当规整。先归一小节标题,再按标题切块:
SECTION_ALIASES = { "identity": ["角色", "身份", "你是谁", "role", "identity", "persona"], "boundary": ["边界", "限制", "禁止", "不得", "policy", "restriction", "refuse"], "tone": ["语气", "风格", "tone", "style", "voice"], "format": ["输出格式", "格式要求", "format", "output", "structure"], "tools": ["工具", "函数", "tool", "function", "api"], "workflow": ["流程", "步骤", "workflow", "process", "step"], "uncertainty": ["不确定", "未知", "无法确认", "uncertain", "unknown"], } def split_modules(text: str): blocks = {} current = "preamble" blocks[current] = [] for line in text.split("\n"): stripped = line.strip().strip("#*:: ") matched = None for key, aliases in SECTION_ALIASES.items(): for a in aliases: if stripped.lower().startswith(a) and len(stripped) < 40: matched = key break if matched: break if matched: current = matched blocks.setdefault(current, []) else: blocks[current].append(line) return {k: "\n".join(v).strip() for k, v in blocks.items()}实测下来,这套规则在结构规整的语料上能切对八成以上。剩下的两成主要是两种情况:一是整段没有任何标题,全靠自然段分隔;二是标题用了不常见的说法,比如把"边界"写成"我们希望你注意的事项"。前者只能人工标,后者靠维护一份不断增补的同义词表来解决。我现在的同义词表已经积累到两百多条,这大概是做这类项目最不好看但最有用的资产。
提示:切分结果一定要抽检。我的习惯是每处理 50 条随机抽 5 条,人工比对切分边界是否合理。错过一次就会带着系统性偏差往下走,后面所有统计都白做。
3.3 用聚类找"家族相似性"
切完模块,可以做一个有意思的分析:把"能力边界"模块单独抽出来,做向量化后聚类。我用句子向量加 HDBSCAN,因为不需要预先指定簇数量,还能识别出离群点。结果挺有意思——几百条边界描述会自动聚成几大族:一类是围绕"不提供具体建议"展开,一类围绕"涉及隐私的处理",一类围绕"时效性信息的处理"。同一个族里的表述虽然措辞不同,但逻辑结构高度相似,这说明这类文本存在明显的行业惯用范式,大家在写的时候多少都参考过相似的思路。
聚类还有个副产品:离群点往往是写法最独特的那几份,值得单独精读。我从离群点里学到好几个写法技巧,比如把约束条件写成"当 X 出现时,先 Y 再 Z"的条件式表达,比单纯罗列禁止项更容易被模型稳定执行。
4. 从语料里提炼可复用的写作范式
看了这么多份之后,真正能带走的东西其实不多,就那么几条。我挑几条最实在的说说。
4.1 角色定义不要写成形容词堆砌
新手写角色定义,喜欢堆形容词:"你是一个专业的、严谨的、有耐心的、经验丰富的助手"。这种写法信息量极低,模型对形容词的敏感度远不如对具体行为描述。语料里写得好的角色定义,通常是"身份 + 行为倾向 + 典型场景"三段式。比如先说明身份定位,再用一句话说明在模糊情况下倾向于怎么做,最后举一个典型场景说明期望的回应形态。
你是面向初学者的代码讲解助手。 当用户给出的代码存在多种理解方式时,优先按最常见的那种理解作答, 并简短说明你采用的假设。 面向的问题通常是:这段代码为什么这样写、这样写会有什么副作用。这段只有几十个字,但比一长串形容词管用得多。原因在于它给了模型可执行的判断依据,而不只是一个模糊的风格倾向。
4.2 约束要写"遇到什么做什么",而不是"不要做什么"
这是我在语料对比里发现的最大差异点。早期写法大量使用"不要""禁止"开头,而近两年的写法明显转向条件式:先说触发条件,再说期望动作。为什么?我的理解是,纯禁止式表述只告诉模型边界在哪,没告诉它边界外该往哪走,模型很容易在边缘地带给出一个含糊的回应。条件式表述把"替代路径"也写清楚了,行为更可控。
举个对照:
| 写法 | 示例 | 实测表现 |
|---|---|---|
| 纯禁止 | 不要给出医疗诊断 | 容易变成敷衍的一句"建议咨询医生" |
| 条件式 | 涉及诊断类问题时,先说明无法提供诊断,再给出就医建议,并提示可以帮忙整理症状描述 | 回应更有信息量,用户满意度更高 |
第二种写法我抄进自己的项目之后,同类问题的用户追问率下降明显。这不是玄学,是因为条件式表述给了模型一个可填补的行动框架。
4.3 输出格式控制:给结构,也给判断依据
格式控制这块,语料里的写法分三档。最低档是"请用列表输出",基本没用。中间档是给出字段名和顺序。最高档是不仅给结构,还给出什么时候用哪种结构的判断依据。比如说明简单问题用段落、多步骤问题用编号列表、对比类问题用表格,并给出判定标准——涉及三个以上并列项时用列表,涉及两个对象的多维比较时用表格。
这套判断依据的价值在于,它把"选格式"这件事从模型的主观偏好变成了可推导的决策。我做过简单的 A/B 测试,加了判断依据的版本,输出格式的一致性提升相当明显,尤其是处理混合类型的多轮对话时。
4.4 工具描述:参数约束要写全
带工具调用的语料里,写得细的那些会明确列出:每个参数的取值范围、必填还是可选、缺省行为、以及调用失败时的处理建议。写得粗的只给一句用途说明,结果就是模型经常传错参数类型或者在不该调用的时候调用。
我总结的最小可用模板是四段:用途一句话、参数表(名称/类型/必填/说明)、触发条件(什么情况下应该调用)、失败处理(调用返回错误时怎么做)。第三段和第四段是区分度最高的部分,也是最多人漏写的部分。
5. 反向思考:怎么保护自己的系统提示词
做这类整理久了,必然会想到另一面:如果我自己在做一个产品,该怎么处理这个问题。先说结论——指望系统提示词不外泄是不现实的,把它当成产品机密来保护,投入产出比很低。真正值得保护的是业务逻辑、数据、以及后端的权限控制,而不是那几千字的文本。
5.1 为什么"藏"这个思路本身就有问题
原因有三层。第一,只要文本被送进模型上下文,在任何具备一定输出能力的系统里都存在被间接暴露的可能,你能做的是拉高成本,不是彻底封死。第二,即使文本外泄,如果它本身不含敏感业务信息,损失也就是别人知道了你的写法,而这些写法往往可以从产品行为里反推出来。第三,把大量精力放在"藏"上,容易忽视真正该做的事情——把权限校验放在服务端而不是靠提示词里的一句话来约束。
我见过最典型的反模式,是有人把"未经授权不得访问某数据"写进系统提示词,然后以为这就安全了。这等于把门锁画在墙上。正确的做法是这条规则同时也必须在服务端强制执行,提示词里那句只是为了让模型的回应更自然。
5.2 分层防护的四层做法
指令层:系统提示词里不写具体的业务规则细节、不写内部代号、不写任何凭据类信息。业务规则放在服务端判断,模型只负责表达。
架构层:动态拼接,按当前场景只下发必要片段,不要把一份几千字的完整配置全量塞进去。用户对话历史与系统配置严格分离,避免模型在复述上下文时把两者混在一起输出。
检测层:对输出做模式检测。常见信号包括:输出里出现大段与你配置高度相似的文本、输出结构突然从自然语言变成条目化的"规则列表"、用户连续多轮以不同方式询问配置细节。我把这几种模式做成简单的检测规则,命中后触发一次温和的重定向回应。
法务层:服务条款里明确说明配置内容属于产品组成部分,禁止用于训练竞品或其他商业用途。这一层不解决技术问题,但它是后续所有动作的基础。
5.3 版本管理:别用一份文件打天下
这一点跟语料整理是相通的。我建议系统提示词走版本管理,每次改动留记录,写明改了什么、为什么改、预期影响是什么。原因很实在——当你发现某个行为异常时,你需要快速判断是不是最近一次改动引入的。没有版本记录,排查只能靠回忆,效率极低。
我自己的做法是用一个简单的 yaml 保存配置,加注释说明每段的意图,改动走代码审查流程。这听起来小题大做,但当你面对一个跑了半年、改了二十几版的产品时,会感谢当初这么做。
6. 常见问题与排查实录
6.1 清洗与切分环节的五个典型坑
坑一:把用户输入误当成系统配置。很多公开帖子里,提问内容和回应混在一起贴出来,中间没有明确分隔。我最初靠缩进和引号判断,准确率一般。后来改成看内容特征:系统配置通常用陈述句、第二人称、包含规则性词汇;用户提问通常是疑问句、第一人称、包含具体场景。用这两个特征做交叉判断,准确率提升很多。
坑二:把工具 schema 当成提示词正文。带函数调用的产品,其工具定义一般是结构化 JSON,和自然语言的提示词是两回事。混在一起统计会严重拉高"工具模块"的占比。我的处理是把 JSON 块单独抽出来存到另一个字段。
坑三:多语言混杂导致聚类失真。同一份配置有多语言版本时,向量会被语言差异主导,聚类结果全是按语言分而不是按内容分。解决办法是聚类前统一按语言分组,组内再聚类。
坑四:截断没标记。有些片段明显在句子中间断掉,如果直接入库,后面做长度统计时会出现异常的短条目。我的做法是检测末尾是否缺少结束标点,命中就打上 truncated 标记。
坑五:同一条目重复入库但版本不同。这个最隐蔽。两份文本 95% 相同,只有一两句话不同,很容易被判为重复而合并,但那一两句话可能正是关键差异。SimHash 距离阈值我调到 3 而不是 6,宁可多留一些近似项,也不冒漏掉版本差异的风险。
6.2 分析结论失真的三种情况
第一种,样本选择偏差。整理者往往更容易收集到结构规整、格式漂亮的文本,而那些写得零散的可能压根没人愿意转录。如果你直接统计"有多少比例的配置包含某某模块",结论会系统性偏高。我的应对是明确说明样本构成,并尽量补充低质量样本。
第二种,时间混淆。跨年度的语料混在一起统计,得出的规律可能是三个不同阶段的平均,掩盖了演进趋势。按时间分桶之后再看,趋势往往比静态结论有价值得多。
第三种,把个例当范式。某份配置里有一个很妙的写法,就写成"业界通用做法"。我给自己定的门槛是:一个模式要至少在三家不同来源里出现,才用"较为常见"这个措辞。
6.3 常见问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 模块切分比例异常低 | 标题格式未归一化 | 检查全角半角、冒号类型 |
| 聚类结果按语言分开 | 多语言混在一起 | 按语言分组后再聚类 |
| 同一产品多条差异巨大 | 形态不同或版本不同 | 核对 variant 与 collected_at |
| 统计占比明显偏高 | 样本选择偏差 | 检查低质量样本是否缺失 |
| 去重后条目骤减 | SimHash 阈值过松 | 阈值从 6 降到 3 重跑 |
| 文本末尾缺标点 | 原帖截断 | 打 truncated 标记,不参与长度统计 |
还有一个实操心得值得单独说:保留原始文本快照。清洗后的文本便于分析,但一旦清洗规则有误,你没法回头验证。我的目录里每个条目都存两份,一份 raw 一份 clean,raw 只读不改。这个习惯帮我纠正过两次正则错误,每次都救回了几十条被误删内容的条目。
最后说个我自己的体会。做这类语料整理,最大的收获不是某一句话怎么写,而是慢慢建立起一种"看结构"的习惯。拿到一段提示词,第一反应不再是它说了什么,而是它由哪几块拼成、每块承担什么职责、先后顺序为什么这么排。这个视角一旦建立,自己写配置的效率会有明显变化——从凭感觉堆砌,变成按模块组装。我现在的做法是维护一个自己的模块库,每类模块存三到五个写法版本,写新配置时按需拼装,再根据具体场景微调。这套方法没什么技术含量,但比我试过的任何自动化生成工具都管用。