把「别重复」做成系统:一次关于平凡输入的诊断
Long conversations degraded into canned mannerisms. The root cause wasn't the model — it was that 34 of our 35 prompt sections were byte-identical every single turn.
长对话聊到 30 轮以后,她开始"样板戏":固定的动作描写、固定的收尾、 同一件事换个说法再说一遍。
我们的诊断写下来只有一句话:35 段 system prompt 里,34 段每一轮完全一样。
问题
症状很好识别,但很难归因:
- 动作描写退化成一套固定模式("(轻笑)" "(摇头)" "(顿了顿)");
- 收尾句式连续复用;
- 同一个话题反复提。
第一反应通常是"模型不够强" ✗ 或者"温度调坏了" ✗。但这两条都解释不了为什么只在长对话后出现。
诊断:不是模型变笨,是输入太平凡
我们数了一遍当时的 system prompt —— 共 35 段,其中 34 段每一轮完全一样 ✓
而 LLM 是一个在重复输入分布里找最短路径的东西 ✗:当每一轮的输入几乎相同, 输出自然收敛到那个"最省力的答案" —— 也就是样板戏 ✓
所以问题不在"她说什么",而在"我们每轮喂给她的东西有没有变化" ✓
这个诊断带来一条设计原则,后来贯穿始终:
每轮都应该有东西在变,而且要放在末尾**(prompt 最底部对下一句影响最大)。**
三个机制
| 优先级 | 机制 | 作用点 | 每轮变化 | 成本 |
|---|---|---|---|---|
| P0 | Author's Note | prompt 最末尾 | ✅ | 低(角色池 + 轮换) |
| P1 | 反样板戏指令 | WRITING_RULES + Note 池 |
部分 | 极低(改一行字) |
| P2 | 示例对话轮换 | 人设里的示例段 | ✅ | 中(需要一条转换管线) |
P0 是主角:每轮从池子里挑一条作者注插在 prompt 最末尾 —— 内容可以是 "这一轮换个收法"、"别用同一个动作"、"把下一句留给博士"。位置是设计的一部分: 放在最前面,它会淹没在几千字的设定里;放在最末尾,它紧贴当前这一轮 ✓
P2 我们没做 ⬜(文档里明确标着"未实施"✓)—— 现在是示例段全量注入, 没有"每轮抽 2–3 条"的轮换。这条诚实地留在技术债里,而不是假装做过 ✓
被砍掉的 P3(以及为什么值得记下来)
我们还设计过 新鲜度种子:每轮注入一点环境变量("窗外下着雨")来打破重复 ✓
砍掉了 ✗ 理由:它会和用户自己建立的场景冲突 ✓ 用户说"今天外面很晴",而系统每轮塞一句"窗外有雨" ⇒ 直接矛盾 ✓ —— 打破重复的办法不能是引入与事实冲突的新信息 ✓
这条留在设计文档里,和"已实施"的部分并列 ✓ 因为半年后最值钱的是"当时为什么没做" ✓
后来的修正(从提示词走到别的层)
那套机制上线后,重复问题并没有消失 ✗ 后来我们做了一次完整的排查(另一篇文章写了全过程), 得到几条与"提示词"无关的结论:
- 重复的成因之一在写入侧:某些自动生成的内容被当成"她说过的话"写进了记忆, 而且同一段写了 4 份 —— 上下文里同一件事出现多次,模型会当成"重要"而反复说它 ✓ ⇒ 修在读取侧:同一句话只让模型看一次 ✓
- 另一部分成因在解码侧:重复是自回归生成的吸引子(输出困惑度趋零)。 别人(比如 SillyTavern 那类前端)把这类问题当采样问题治: repetition penalty / no-repeat n-gram / DRY ✓ 而我们的采样参数从来没开过 ✓
- 判据要分层次:我们一开始只检测"收尾 12 个字是否相同" ✓ 但它抓不到 "话题在中段反复出现" ✓ 于是加了话题级判据:找出最近几轮反复出现的实词, 直接点名"这些事已经聊过了" ✓
- ⚠️ 一次实验推翻了我自己的假设:把她的同一句回复逐字复制 4 份塞进记忆, 她并没有开始复述(两臂都是 0/5 命中 ✓)—— 所以"上下文重复"不是唯一成因, 真正的杠杆在生成侧 ✓
结果
| 之前 | 之后 | |
|---|---|---|
| 每轮变化的内容 | 1 段(人设里的示例,且不轮换) | 末尾作者注池 + 话题级点名 |
| 检测的重复形状 | — | 收尾句式 + 话题共现 |
| 采样层重复惩罚 | 未开 | 已开 |
| 覆盖的对话路径 | 1 条 | 2 条(普通 + 深聊) |
踩坑与教训
- 先怀疑输入,再怀疑模型 ✓ "35 段里 34 段一样"这种事实,比任何调参直觉都有用 ✓;
- 位置是设计的一部分:要影响"下一句",就放在最后 ✓;
- 设计文档要标"未实施" ✓ 否则半年后连自己都以为做过了 ✗;
- 砍掉的方案要留理由(新手会再想一遍"加个环境描写多好" ✗ 而文档里已经写着 "会和用户场景冲突" ✓);
- 规则解决不了的事,去它的上游和下游找 ✓ —— 上游是写入(别重复写), 下游是解码(别被吸引子拖住)✓
这一篇的每个机制都能在仓库里找到实现或"未实施"的标记;每条结论都对应一次可复现的实验。