system_prompts_leaks:系统提示词归档与回归测试

📅 发布时间:2026/9/18 3:24:38
system_prompts_leaks:系统提示词归档与回归测试
1. system_prompts_leaks 到底在解决什么问题第一次看到 system_prompts_leaks 这个仓库名多数人会有两个反应一是这东西能公开吗二是这些 system_prompts 我拿来到底能干什么。我在做 AI 应用落地的这几年里前后整理过三版自己的提示词库也翻过不少同类的归档项目说句实在话这类仓库真正的价值从来不在拿到了别人的原文而在于它把一个平时散落在聊天截图、帖子评论区、产品文档角落里的东西变成了可以检索、可以对比、可以做版本追踪的结构化资料。你如果只是把别人的系统提示词复制粘贴到自己的项目里大概率会翻车但如果你把它当成一份产品行为说明书去读收获会完全不一样。这篇内容我打算从三个层面讲透这类提示词档案库的底层逻辑是什么、一份系统提示词到底由哪些模块构成、以及怎么从零搭一套属于自己的提示词归档与回归测试流程。前者帮你建立判断力中者帮你看懂门道后者才是真正能落到你项目里的东西。不管你是刚接触大模型应用的新手还是已经写过上百条 prompt 的老手只要你手里有任何一个需要长期维护的 AI 产品这套东西都用得上。1.1 系统提示词是什么为什么它值得被单独归档系统提示词system prompt是模型在收到用户第一条消息之前就被注入的那一层指令。它和用户随手打的提问最大的区别在于用户输入是变量系统提示词是常量。它决定了这个 AI 产品的人设是谁、能干什么、不能干什么、用什么样的语气说话、要不要调用工具、输出成什么结构。你可以把它理解成一份员工手册——新员工上岗第一天先读完这本手册之后不管面对什么客户行为基线都在这本手册划定的范围里。那为什么它值得被单独归档因为一个 AI 产品对外表现出的性格绝大部分不是模型本身带来的而是这层文字带来的。同一个底座模型套上两套不同的系统提示词表现差异可以大到像两个完全不同的产品一个话痨一个惜字如金一个动不动就调用搜索工具一个打死也不肯给具体数字。产品经理在评审会上争论我们的 AI 太啰嗦了工程师改的其实就是这层文字里的一句话。归档它的意义相当于给一个黑盒行为做了行为注释——当你想复现某种产品气质的时候你知道该往哪个方向写。另外一个容易被忽略的点系统提示词是会迭代的。同一个产品三个月前和现在的提示词可能改了几十处每处改动背后往往对应一次线上问题、一次用户投诉、一次业务调整。如果你有条件把不同时间点的版本都存下来做一次 diff你能读出的信息量比读十篇产品分析文章都大。这就是为什么成熟的团队会像管代码一样管提示词而不是把它硬编码在某个const string里然后忘了。1.2 一份提示词档案库的四类真实受众很多人以为这类资料只有提示词工程师才看其实不然。我观察下来真正会长期翻这类内容的有四种人各自关注的点完全不同。第一类是提示词工程师和 AI 应用开发者。他们看的是写法比如别人怎么用一句话就把输出格式约束死怎么处理模型不听话的情况怎么把复杂的工具调用协议写得又短又不出错。这类人看图的是结构复用而不是内容照抄。第二类是产品和运营。他们看的是功能边界是怎么被文字表达的。一款产品说我可以帮你规划行程这句话在系统提示词里可能对应着五条限制不推荐具体航司、不给签证建议、不承诺价格、遇到紧急情况要引导联系官方、行程超过 14 天要提示分批。产品看到这些能快速理解对方的边界设计思路。第三类是安全与体验团队。他们关心的是拒答策略、敏感场景的兜底话术、以及模型被诱导时的处理方式。这部分内容对任何要上线 C 端产品的团队都是刚需因为你自己也要写。第四类是独立开发者和做副业的人。他们通常没有资源做大量 A/B 测试最快的路径就是参考成熟产品的结构然后用自己业务的语料把它填满。这类人最容易犯的错是直接照搬原文结果模型表现和自己的业务完全不搭——结构可以借鉴内容必须重写这一点后面我会展开说。1.3 先做预期管理这类资料解决不了什么在动手之前有几件事最好先说清楚免得你花了两周时间整理完才发现方向不对。其一系统提示词和模型版本是强绑定的。一份 2023 年整理的内容放到今天的新模型上很可能出现约束失效——因为新模型对指令的服从度、对长上下文的注意力分布都变了。同一句话在旧模型上是硬约束在新模型上可能只是个温和建议。所以归档的时候一定要记录对应的模型标识和采集时间否则这份资料的半衰期会让你很难受。其二社区整理的版本真假混杂。同一款产品可能流传着五六个版本有的是真采集有的是二手转述有的是别人根据行为反推的猜测。判断可信度需要交叉验证比如看多个来源是否一致、看内容是否与公开产品文档冲突、看里面提到的工具名是否真实存在。我的习惯是给每条记录打一个置信度标记低置信度的单独放一个目录用的时候心里有数。其三别人的提示词是别人的业务产物。它优化的是别人家的场景不是你的。你能学到的最有价值的东西是约束怎么分层、格式怎么锁死、工具协议怎么写不歧义而不是这段话术很优美我要抄下来。带着这个心态去看你才不会陷入无效收藏。2. 提示词档案库的目录设计与版本管理思路2.1 目录分层按来源、按产品、按时间三层切开我试过好几种目录结构最后留下来的是一套三层方案实测下来维护成本最低。核心思路是第一层按来源类型分第二层按产品分第三层按采集日期做快照。这样你既能横向对比不同产品也能纵向追踪同一个产品的演变。prompt-archive/ ├── 00-index/ │ ├── catalog.md │ └── tags.yaml ├── 10-official/ # 官方文档、官方发布中明确写出的指令 │ └── vendor-a/ │ └── chat-product/ │ └── 2024-11-03__surface-web.md ├── 20-community/ # 社区整理版本 ├── 30-inferred/ # 根据行为反推、置信度低单独隔离 └── 90-tools/ # 校验、diff、导出脚本为什么要这么分因为不同来源的可信度差别太大混在一起你很快就不知道哪条能信。10-official是你的地基20-community是可以参考的装修图30-inferred只是猜测用的时候必须打问号。我吃过一次亏早期把三类内容全塞在一个目录里半年后回头看完全分不清哪条是自己验证过的最后只能全部重来一遍。另外提醒一句00-index这个索引目录非常关键。当条目超过 50 条以后你不可能靠记忆找东西必须有一个自动生成的目录和一个标签体系tags.yaml。标签我一般会打这几类场景标签客服、写作、编程、数据分析、结构标签含工具调用、含格式约束、含多角色、风险标签含拒答策略、含事实性约束。2.2 命名规范与元数据字段设计文件名不是随便起个prompt1.md就行的。我用的是一个下划线分隔的四段式命名来源__产品__入口__采集日期.md。入口这一项容易被忽略但其实很重要——同一个产品在网页端、移动端、API 接口上注入的指令经常不一样接口版本有时反而更完整。用双下划线而不是单下划线是为了防止产品名里本身带下划线造成解析歧义。文件头部的元数据我用 YAML front-matter 写字段固定方便脚本批量处理--- title: 示例产品 网页端系统提示词 source_type: official # official | community | inferred captured_at: 2024-11-03 model_id: unspecified surface: web # web | mobile | api | plugin confidence: high # high | medium | low last_verified: 2024-12-01 tags: [客服场景, 含工具调用, 含格式约束] notes: 多处提到内部工具名疑似为 API 版本改写而来 ---这些字段里我最想强调last_verified和notes两个。last_verified是你最后一次确认这条内容还准确的时间超过三个月没验证的用之前要重新核对。notes则是你自己的判断记录——哪句话可疑、哪个工具名对不上、和上一版比改了什么。半年后你回头看这些笔记价值远超原文本身。注意不要在归档文件里记录任何与个人隐私、内部未公开信息相关的内容。归档的对象是公开可见的产品行为说明这条线一定要守住。2.3 把提示词当代码管Git 分支与校验钩子既然系统提示词会迭代、会影响线上效果那就应该用管代码的方式来管它。我最推荐的两个实践是用 Git 管版本用 pre-commit 钩子做结构校验。Git 这边我的分支策略很简单main只放已验证的内容staging放新采集待核实的内容每条重要变更单独开分支合并时用约定式提交信息比如feat(prompt): 新增格式约束段落或者fix(prompt): 修正工具名拼写。这样做的好处是三个月后你想知道为什么当时加了那条拒答约束git log一翻就能找到上下文。校验钩子解决的是另一个痛点条目一多总有几条会忘了写 front-matter、标签打错、日期格式写错。我写了一个几十行的 Python 脚本挂在 pre-commit 上每次提交前自动跑一遍。# 90-tools/validate_meta.py from pathlib import Path import sys, re, yaml from datetime import datetime REQUIRED [title, source_type, captured_at, confidence, tags] VALID_SOURCE {official, community, inferred} VALID_CONF {high, medium, low} DATE_RE re.compile(r^\d{4}-\d{2}-\d{2}$) def parse_front_matter(text: str) - dict: if not text.startswith(---): raise ValueError(缺少 front-matter) _, fm, _ text.split(---, 2) return yaml.safe_load(fm) def check(path: Path) - list[str]: errs [] try: meta parse_front_matter(path.read_text(encodingutf-8)) except Exception as e: return [f{path}: 解析失败 {e}] for key in REQUIRED: if key not in meta or meta[key] in (None, , []): errs.append(f{path}: 缺少字段 {key}) if meta.get(source_type) not in VALID_SOURCE: errs.append(f{path}: source_type 非法) if meta.get(confidence) not in VALID_CONF: errs.append(f{path}: confidence 非法) cap str(meta.get(captured_at, )) if not DATE_RE.match(cap): errs.append(f{path}: captured_at 需为 YYYY-MM-DD) else: try: datetime.strptime(cap, %Y-%m-%d) except ValueError: errs.append(f{path}: captured_at 不是合法日期) return errs if __name__ __main__: problems [] for f in Path(.).rglob(*.md): if 00-index in str(f) or f.name.lower() readme.md: continue problems check(f) if problems: print(\n.join(problems)) sys.exit(1) print(元数据校验通过)这段脚本本身没什么技术含量但它的存在会让你的归档库在半年后依然可用。我见过太多人兴冲冲整理了两百条三个月后自己都不想打开——原因无一例外是格式乱、找不到东西。3. 拆解一份系统提示词的骨架从结构到写法3.1 七段式结构身份、能力、边界、工具、格式、风格、兜底看过的系统提示词多了之后你会发现高质量的版本基本都能拆成七个模块。不是说每条都写全但只要有缺项产品表现上通常就能看出短板。身份定位是最开头那几句回答你是谁、你在为谁服务。这一段的写法讲究具体而非华丽好的写法会明确说清服务对象的特征而不是堆形容词。能力清单回答你能做什么这里的关键是边界清晰宁可写窄一点也不要写一句你可以回答任何问题——那等于没写还会诱导模型胡乱承诺。边界与拒答是所有模块里最难写的它要和能力清单形成互补明确哪些领域不接、遇到时怎么回应。工具与协议在支持工具调用的产品里是核心定义工具名、参数、调用时机、失败处理。输出格式决定用户看到的东西长什么样是有序列表还是一段话要不要标题长度上限多少。语气风格是很多人忽略的一块但它直接决定产品的人感。异常兜底是最后一段处理信息不足超出范围工具失败这几类情况。我举个具体的对比。写得粗糙的版本是这样你是一个专业的客服助手请热情、耐心地回答用户的问题。写得扎实的版本长这样你是某在线订阅服务的一线客服助手服务对象是已付费的个人用户。 你能处理账单与发票、订阅变更、基础功能使用问题。 你不处理涉及账户安全的身份验证、退款审批、法律与税务建议。 当用户请求超出范围说明限制并给出转人工客服的入口说明。 信息不足时先追问一次只追问一个最关键的问题。 回复控制在 150 字以内语气平和直接不使用感叹号。后者明显更长但每句话都在消除一种歧义。这就是我常说的系统提示词的优劣不看文采看的是它堵掉了多少个口子。3.2 约束条款的三种写法与适用场景约束写法大体分三类禁止式、条件式、优先级式。它们在效果上差异很大用错场景会导致模型要么过度保守要么完全无视。禁止式就是不要做某事写法直白但有个副作用模型在长对话里对否定指令的遵循度会衰减尤其是当用户在后续消息中反复追问时。条件式是如果出现 A 情况则执行 B 动作这种写法稳定性明显更高因为它给了模型一个可判断的触发条件而不是一个需要持续记住的禁令。优先级式用于处理多条规则冲突的情况明确告诉模型当两条规则冲突时以哪条为准。写法示例优点风险适用场景禁止式不要给出具体价格简短直接长对话中衰减快简单、单点的约束条件式若用户询问价格说明需以官方页面为准触发明确稳定性高需要穷举场景拒答、转人工、敏感话题优先级式安全约束优先于风格约束解决规则冲突写多了模型负担重复杂业务、多规则并存我的经验是核心的几条安全约束用条件式写清楚风格类约束可以用禁止式比如不使用感叹号然后在整段结尾补一句优先级声明。这三者配合起来实测比纯禁止式的遵循度高出不少。3.3 工具调用说明怎么写才不出歧义带工具调用的系统提示词问题基本都出在三个地方工具名不一致、参数描述含糊、失败路径没定义。工具名一定要和代码里注册的名字完全一致多一个下划线都不行。我见过一个线上事故就是提示词里写的是search_web而实际注册的是web_search模型调用失败后开始自己编造答案用户完全看不出来。参数描述要写清楚类型和取值范围不要只写查询关键词这种模糊描述而要写关键词为 1 到 5 个词不要包含问号或引号。失败路径更是必写项工具返回空结果怎么办、超时怎么办、参数被拒绝怎么办。不写这些模型在异常情况下就会自由发挥。## 可用工具 - search_docs(query: string, top_k: int) - 用途检索产品帮助文档 - query1-5 个关键词不要使用整句自然语言 - top_k默认 5最大 10 - 调用时机用户询问产品功能、操作步骤、限制规则时 - 失败处理若返回为空明确告知未找到相关内容并建议更换关键词或转人工不要自行推测答案这段示例里最有价值的其实是最后一行。绝大多数人写工具说明时只写怎么调不写调不到怎么办而真实线上环境里后者发生的频率高得多。4. 手把手从零搭一套提示词归档与回归测试流程4.1 流程梳理与工具选型整套流程我走过两遍最终固化下来的链路是采集整理 → 元数据校验 → 版本入库 → 结构拆解 → 差异对比 → 回归测试 → 结论沉淀。听起来步骤不少但真正需要人工介入的其实只有第一步和最后一步中间全是脚本。工具选型上我不追求花哨够用就行环节选型选择理由版本管理Git免费、离线、diff 能力强团队协作门槛低元数据校验Python PyYAML依赖少几十行能搞定任何人都能改文本对比difflib / 自写分词 diff中文按字符 diff 噪音大需要先切分回归测试自建评测集 脚本跑批现成平台约束多自建更灵活也更容易复现结果记录Markdown 表格 JSON人能读脚本也能读这里我要多说一句为什么中文文档对比不能直接用difflib的默认字符级对比。中文一个词往往两三个字整体改一句话时字符级 diff 会输出满屏高亮完全看不出改了哪儿。所以我在对比前会先做一次分句和分词按标点和语义单元切开再逐句比对这样输出的差异才是人能读的。4.2 差异对比脚本把改了什么变成可读报告下面这个脚本是我用了最久的一个版本核心逻辑是先按句切分再对句子做归一化最后输出三类结果新增、删除、修改。修改的部分会做字符级二次对齐方便你看清句内改动。# 90-tools/diff_prompt.py import re, sys, difflib from pathlib import Path SENT_SPLIT re.compile(r(?[。\n])) def split_sentences(text: str) - list[str]: lines [ln.strip() for ln in text.splitlines()] body \n.join(ln for ln in lines if ln and not ln.startswith((#, -, , |))) sents [s.strip() for s in SENT_SPLIT.split(body) if s.strip()] return [re.sub(r\s, , s) for s in sents] def char_inline(a: str, b: str) - str: sm difflib.SequenceMatcher(None, a, b) out [] for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag equal: out.append(a[i1:i2]) elif tag replace: out.append(f[-{a[i1:i2]}-][{b[j1:j2]}]) elif tag delete: out.append(f[-{a[i1:i2]}-]) elif tag insert: out.append(f[{b[j1:j2]}]) return .join(out) def main(old_path: str, new_path: str) - None: old split_sentences(Path(old_path).read_text(encodingutf-8)) new split_sentences(Path(new_path).read_text(encodingutf-8)) sm difflib.SequenceMatcher(None, old, new) added, removed, changed [], [], [] for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag equal: continue if tag delete: removed old[i1:i2] elif tag insert: added new[j1:j2] elif tag replace: a_block, b_block old[i1:i2], new[j1:j2] n min(len(a_block), len(b_block)) for k in range(n): changed.append((a_block[k], b_block[k])) removed a_block[n:] added b_block[n:] print(f### 新增 {len(added)} 条) for s in added: print(f {s}) print(f\n### 删除 {len(removed)} 条) for s in removed: print(f- {s}) print(f\n### 修改 {len(changed)} 条) for a, b in changed: print(f* {char_inline(a, b)}) if __name__ __main__: main(sys.argv[1], sys.argv[2])用法很简单python 90-tools/diff_prompt.py \ 20-community/vendor-a/chat-product/2024-08-11__surface-web.md \ 10-official/vendor-a/chat-product/2024-11-03__surface-web.md跑完你会得到一份带-*标记的报告。我一般会把报告贴进归档目录下一个changelog.md里时间久了这份 changelog 本身就是一份非常有价值的产品行为演变史。有意思的是很多改动你单看一条会觉得莫名其妙但连起来看就能看出趋势——比如某产品连续三个版本都在加强不主动推荐第三方服务这类约束。4.3 回归测试集用二十条用例守住你的提示词底线提示词改完最怕的是什么是你改好了 A 问题悄悄弄坏了 B 问题而 B 问题一周后才被用户发现。解决办法只有一个建一套小而稳定的回归测试集。我的做法是维护 20 到 30 条固定用例每条用例包含输入、期望行为、扣分项三部分。这 20 条不用贪多但必须覆盖几个关键维度身份一致性、边界拒答、格式遵循、工具调用正确性、异常兜底、多轮追问下的稳定性。评分不追求精确用 0/1/2 三档就够。用例编号类型输入摘要期望行为评分要点T01身份你叫什么不暴露底座模型名0 分若泄露T02边界请求超出业务范围说明限制并给替代路径0 分若强行作答T03格式要求列三点输出恰好三条1 分若条目数错误T04工具询问需检索的问题正确调用工具0 分若参数错误T05兜底检索无结果明确说明未找到0 分若编造内容T06多轮连续追问三次边界不松动0 分若第三次松口这套表我建议直接用 Markdown 维护人读起来舒服脚本解析也方便。跑批的时候我习惯固定温度参数一般设成 0同一份提示词跑两遍结果基本一致这样对比才有意义。如果温度调高同一用例的输出会随机波动你就分不清是提示词改动的影响还是随机性了。提示每次改动提示词之前先跑一遍基线改完再跑一遍两份结果直接对比。不跑基线的改动等于盲改。5. 常见问题与排查技巧实录5.1 归档环节的六个高频问题整理这类资料时踩的坑其实高度集中我把自己和同事遇到的问题汇总成一张表遇到直接对照查。问题现象常见原因处理方式同一产品多个版本互相矛盾采集入口不同网页端/接口按 surface 拆开归档不要合并内容读起来像翻译腔二手转述未标注归入 inferred标注低置信度工具名与实际接口对不上版本过期或转述失真交叉核对官方文档标注待验证半年后找不到某条记录标签缺失、命名不统一补校验钩子重跑一遍目录索引条目多了检索困难没有索引文件用脚本自动生成 catalog.md改动记录丢失直接覆盖原文件强制走 Git禁止原地改第一行这个坑我印象最深。早期我把同一个产品的网页端和接口版本合在一个文件里结果读的时候总觉得前后矛盾折腾了两天才发现是两个不同入口的指令被混在了一起。后来严格按 surface 拆开问题立刻消失。第三行也值得展开说。工具名对不上的情况非常普遍很多流传的版本里会出现一些现实中根本不存在的工具名这通常说明内容经过了多次转述。判断方法很简单把里面提到的所有工具名列出来逐个和公开的产品能力清单核对对不上的全部打上待验证标记。这个动作花不了十分钟能帮你避免大量无效参考。5.2 提示词改写后效果变差的排查路径改动之后效果变差排查要按顺序走不要一上来就怀疑模型。我固定的排查顺序是先看格式再抠措辞先查硬约束再调语气。第一步确认输出格式约束有没有被破坏。很多时候视觉效果变差只是因为你插入的新段落把格式说明挤到了上下文中间模型对末尾内容的注意力更强中间的规则容易被忽略。解决办法是把格式约束和边界约束放到整段的靠后位置或者在关键约束前加一句显式的强调声明。第二步检查是否出现了规则冲突。你新增的一条约束可能和原有的某条冲突了模型在冲突时往往会随机选一条执行表现出来就是时好时坏。这时候就轮到优先级声明出场了明确写出冲突时的取舍顺序。第三步看约束是不是写得太抽象。我见过太多保持专业语气友好这种描写这类词对模型几乎没有约束力因为无法判断是否满足。改成可验证的描述比如不使用感叹号每段不超过三句不主动询问用户隐私信息效果会立刻稳定下来。第四步检查是不是约束过载了。系统提示词不是越长越好当约束多到一定程度模型会出现整体遵循度下滑尤其是一些次要约束会被成片忽略。我的经验阈值是核心硬约束控制在 10 条以内其余的都归入风格建议。如果你发现某几条约束反复失效优先考虑删掉那些不那么重要的而不是继续加更多强调语句。最后补一个实操小技巧。当你怀疑某条约束是罪魁祸首又不敢直接删的时候可以先把它挪到整段最末尾跑一遍回归测试如果效果恢复说明是位置问题而不是内容问题。这个办法能帮你把删掉和挪位置这两种改动区分开排查效率会高很多。5.3 关于元信息污染的一个提醒还有一类问题比较隐蔽你在整理过程中加进去的注释、来源说明、待办标记如果不小心混进了真正会被送进模型的文本里会显著影响效果。我在项目早期就干过这事把一段TODO核实这里的工具名的注释留在了提示词正文中结果模型开始偶尔回答这个功能待核实用户当场就懵了。所以我在归档实践上定了一条硬规矩正文区只放干净的指令文本所有说明性内容一律放 front-matter 或者单独的notes.md。脚本里也可以加一条简单检查扫描正文中是否出现 TODO、待核实、来源、备注这类词出现就报警。FORBIDDEN_IN_BODY [TODO, 待核实, 来源, 备注, FIXME] def check_body(text: str) - list[str]: _, _, body text.split(---, 2) return [w for w in FORBIDDEN_IN_BODY if w in body]这几行代码看着简陋但它帮我挡掉了好几次会直接上线的低级问题。提示词工程里真正拉开差距的往往不是那些玄乎的技巧而是这类把细节兜住的小机制——把归档做规范、把校验自动化、把回归测试跑起来你的提示词迭代速度会比同行快一个量级而且不容易翻车。