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

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

- Canonical (HTML): https://kaguc.com/blog/prompt-assembly-zh/
- Date: 2026-07-29


*English version: [Prompt assembly as engineering: layering, contamination, and trimming](/blog/prompt-assembly/)。“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 编辑，见[《确定性边界》](/blog/deterministic-boundary-zh/)）用 `scoped=True` 组装：省掉质量锚——方法论加金样例约 3.2k 字、约 2k token，改一节片段用不上另一实验的整篇范文；角色边界、蒸馏判断层与格式铁律保留，因为片段仍须风格正确、可编译。

```mermaid
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 里：

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

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

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

```python
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` 的折叠规则：

```python
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：

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

## 适用边界

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

