BLOG · #Engineering

提示词的工程化:分层、防污染与瘦身

提示词不是一段文本,而是一个有分层结构的构建产物:角色边界、体裁规范、固定质量锚、可替换判断层与输出格式铁律各有变更频率。本文以一次真实事故为样本——重构让蒸馏模板路径跳过质量锚、生成深度塌陷——拆解分层组装、few-shot 污染防护、多轮历史折叠与 overlay 热更四类机制,以及守住它们的离线回归门。

English version: Prompt assembly as engineering: layering, contamination, and trimming。“LLM 应用工程”系列第 2 篇。实证基座与系列各篇相同:一个生产级 AI 写作 agent 的真实源码与测试。

问题:当提示词长到没人敢改

第一个 LLM 功能上线时,提示词通常是一个 f-string:角色设定、格式要求、几个示例、用户输入,拼起来发出去。这个阶段没有问题——问题出现在三个月后:

  1. **改一处,坏别处。**同一段提示词被多个调用路径复用,为 A 场景调的措辞让 B 场景的输出悄悄变差,没有任何测试变红。
  2. **示例内容渗进输出。**为了教模型“怎么写”,你塞进一篇范文;某天用户发现自己报告里出现了范文里的数值和引用。
  3. **多轮越聊越贵。**每轮携带全部历史,历史里又有模型输出的整篇文档,token 消耗随轮数上涨,其中大半是重复内容。
  4. **改措辞要发版。**提示词硬编码在源码里,现场想微调一句话,就得重新打包。

这四类故障有一个共同根因:提示词被当作“一段文本”,而不是构建产物。构建产物意味着三件事:有分层结构(每层变更频率与所有权不同)、会出事故(需要事故记录)、需要回归保护(改动有测试守)。下文用实证基座项目(AI 写作 agent,产出 LaTeX 实验报告)的 prompts.py 及其测试拆解这套做法。

机制一:分层组装,每层各有变更频率

system prompt 是五层的拼接(build_system),不是一段字符串:

内容变更频率可否热换
角色边界身份 + 硬性禁令(禁 AI 腔、禁编造数据与文献)极低overlay 可换
体裁规范报告/笔记两套章节结构overlay 可换
质量锚写作方法论 + 金样例(固定常在的领域资产)overlay 可换
写作判断 skill蒸馏出来的“为什么这么写”可蒸馏、可替换
输出格式铁律LaTeX 模板骨架 + 6 条编译易错点随模板演进overlay 可换

三个设计点:

**知识资产从代码里搬出去。**方法论(约 4KB Markdown)与金样例(2.2KB LaTeX 手法片段)放在 knowledge/ 目录下作为文件资产,代码只负责装配。它们与 bib、素材包一起构成这个 agent 的领域资产,可独立评审、独立替换。

**格式铁律带着报错写。**输出格式层不只说“要怎样”,还列了 6 条编号易错点,每条附违反后的真实编译报错——例如“调 \makeEngPage{} 前必须先 \def 英文元信息……缺任一个都会报 Undefined control sequence”“加粗一律用 \symbf{}\mathbf 会触发 Extended mathchar”。这些是真实翻车后固化进提示词的,等价于把编译器的报错经验前置。

**按调用场景裁剪。**段落级小改(scoped 编辑,见《确定性边界》)用 scoped=True 组装:省掉质量锚——方法论加金样例约 3.2k 字、约 2k token,改一节片段用不上另一实验的整篇范文;角色边界、蒸馏判断层与格式铁律保留,因为片段仍须风格正确、可编译。

flowchart TD A[组装 system] --> B[角色边界] B --> C[体裁规范:报告 或 笔记] C --> D{scoped 段落小改?} D -->|是| E[跳过质量锚,仅留蒸馏判断层] D -->|否,整篇生成| F{有蒸馏 skill?} F -->|有| G[固定质量锚(去重)+ 判断层] F -->|无| H[内置判断层即质量锚] E --> I[输出格式铁律] G --> I H --> I

机制二:一次真实事故——质量锚被跳过

分层不是纸面整洁,是因为不分层真的出过事故。事故记录就写在 build_system 的 docstring 里:

"""⚠️ 早期 L1 重构曾让"蒸馏模板(skill_prompt 非空)"整个跳过 builtin_skill
→ 丢掉金样例+方法论 → 生成深度/风格塌陷("用蒸馏模板时效果差"的根因)。
此处把质量锚抽出固定注入,并对已自带锚的旧内置/旧模板去重。"""

复盘:早期实现把质量锚归在“内置 skill”里;一次重构让“使用蒸馏模板”的路径整个跳过内置 skill——金样例与方法论一并丢失,生成深度与风格塌陷。表象是“用蒸馏模板时效果差”,一度被当成蒸馏本身的质量问题排查,实际是组装缺层。

修复固化为两条结构决策。其一,质量锚与 skill 层解耦:锚是“固定常在的领域资产”,与是否用蒸馏模板无关;蒸馏模板只承载判断层(为什么这么写),不允许顶掉深度/风格锚。其二,对旧模板去重——已自带锚的旧内置模板不重复注入:

if skill:
    anchors = _quality_anchors(kind)
    if anchors and "金样例" not in skill:
        parts.append(anchors)
    parts.append(skill)

教训可迁移:分层组装必须显式声明哪些层无条件在场,否则任何一次“看起来等价”的重构都可能静默抽掉一层——提示词缺层不报错,只降质。

机制三:few-shot 污染防护

金样例是双刃剑。它是一篇关于 YBCO(另一个实验)的范文片段,用来教“怎么写得有深度、规范、可编译”;但 few-shot 的天然风险是内容污染——模型把范文的对象、数值、引用照抄进用户的报告,等于借样例之手编造数据。防护有三道,全部是措辞级的:

  1. 注入时带隔离标注。组装代码把金样例包在一段声明里:“它是一篇关于另一个实验的范文——绝不照抄它的实验对象/题目/关键词/数值/引用/章节安排;你必须严格围绕本会话的素材,写你自己实际做的实验”。
  2. 样例文件自带免疫注释。fewshot_ybco.tex 不是完整报告,是 2.2KB 的“手法片段”:只保留公式段、三线表等写法示范;文件头注释重申“下面的主题只是举例……绝不照抄本片段的题目、数据、引用或章节”,连表格 caption 里都写着“示例——真实报告须填本实验真实数据”。
  3. **方法论层再加铁律。**方法论文件的铁律一节独立重申:“绝不照抄范文/金样例的实验对象、数值或引用——那是别人做的实验。”

三道防线冗余是有意的:措辞级防护是概率性的,单点声明可能被长上下文稀释。最后的兜底不在提示词层——素材溯源体检在服务端用完整素材做确定性核对,属于确定性边界那一侧的事。

机制四:多轮历史折叠与素材瘦身

多轮修订场景里,模型需要的是逐轮反馈的脉络(用户先嫌哪里、后改哪里),而不是每个历史版本的全文。但 assistant 的历史消息里恰恰是整篇 LaTeX 源码。build_messages 的折叠规则:

if role == "assistant" and "\\documentclass" in content:
    content = "[此前已生成的文档版本,此处省略源码]"
elif len(content) > max_chars:
    content = content[:max_chars] + "…(略)"

要点有三:整篇文档替换为一行占位符——当前文档已在本轮 user 内容里单独提供,重复塞每个历史版本既爆 token 又无价值;非文档说明保留——例如自动探索得出的“数据在 xrd/ 目录”这类结论是后续轮次要用的事实;其余超长消息截到 4000 字符,历史最多保留 40 条。这套行为有专门测试钉住(文档折叠、说明保留、条数截断)。

素材侧同理做确定性瘦身(零 LLM、零 token):修订轮超过 12k 字符的素材按与本次反馈的词面重叠过滤;纯数字矩阵行(裸 CSV)一律剔除——图表流程已消化原始数据,正文写作用不上逐行原始数;过滤后留不住两行、又不足 40 字,则回退素材开头,绝不因过滤丢光上下文。chat 路径则把素材做成逐文件配额的稳定摘要(每文件最低 600 字符额度,防止靠后文件被靠前大文件挤没),且只依赖素材本身、跨轮逐字节相同——可作首条消息的稳定前缀,命中 DeepSeek 的自动前缀缓存。

机制五:overlay 热更层与回归门

提示词硬编码的最后一个代价是发版耦合:现场改一句措辞就要重出安装包。解法是 agent-pack overlay:

def _prompt_part(rel: str, default: str) -> str:
    return _overlay_read(rel) or default

角色、体裁规范、格式铁律、方法论、金样例全部支持覆盖:overlay 目录里同名文件命中且非空即用,否则回退内置默认。_overlay_read 每次调用即时读(懒加载)——换 pack 后下一次生成即生效,无需重编、无需重启;读失败或文件为空一律返回空串回退内置,空 pack 绝不把知识丢光。

热更层的危险在于“无覆盖时行为必须一个字节都不变”,这由两组离线测试守着:

  • overlay 行为测试:无 overlay 用随包默认;overlay 命中优先且懒加载(内容从 V1 改成 V2,下次调用即变);空文件回退内置。
  • golden 回归门的 prompt 形状断言:scoped 提示词必须仍含“只重写”与“严禁”禁令(\documentclass\end{document} 必须出现在禁令里);build_user(gaps=None) 与不传 gaps 逐字节一致——护住新增参数没污染既有路径。

两组都是普通 pytest、离线可跑:谁把提示词改回“输出整篇”,或让默认路径悄悄漂移,CI 立刻红。

收益对照

机制不这么做的代价实际效果
分层 + scoped 裁剪每次小改都带全量 systemscoped 编辑省质量锚约 2k token/次;配合作用域编辑,输出 token 降一个数量级
质量锚固定注入依赖每个模板自带修复“蒸馏模板生成塌陷”事故;去重防双份注入
污染标注 × 3裸 few-shot,范文内容渗入三道措辞级防线 + 服务端溯源兜底
历史折叠 + 素材瘦身每轮重发全部历史与素材整篇源码折叠为一行占位符;40 条/4000 字符双截断;稳定前缀命中缓存
overlay 热更改措辞即发版换 pack 下次生成即生效;无覆盖字节不变、golden 门守

适用边界

  1. **原型期不要分层。**只有一个调用路径、提示词还在每天大改时,一个 f-string 就是正确形态;分层与 golden 门是给“提示词已稳定、多调用方复用、有现场热换需求”的阶段准备的。过早抽象付双倍成本。
  2. **形状断言不测语义。**golden 门断言的是关键词在场与字节不变;一次保留关键词的措辞重写可以全绿通过、但质量已变。语义层的漂移要靠在线评测集(见系列第 8 篇《LLM 应用的测试金字塔》)。
  3. **污染防护是概率性的。**隔离标注降低照抄概率,但不归零;对高风险域(数值、引用)必须有提示词之外的确定性校验兜底,防线不能只建在措辞上。
  4. **热更是双刃剑。**overlay 让现场 pack 绕过了随包测试——golden 门守的是内置默认,守不了 pack 里的内容。热更能力必须配套 pack 自身的验收流程,否则等于开了一条无回归保护的变更通道。