让 LLM 接管游戏决策的早期版本,system prompt 写得相当”全能”——一份大 prompt,把规则、角色、所有可能遇到的场景全塞进去。开发的时候图省事,反正一份配置走遍全场。
跑了一段时间,发现这哥们儿”全能”得有点像班级里那种啥都会一点、啥都不精的同学。
最后的版本拆成了 23 个 prompt 模板。听起来夸张,但真正动手拆完,回头看那个万能 prompt——只能说”它能跑”已经是项目早期最大的功劳了。
万能 prompt 是怎么烂掉的
最开始那份 prompt 大概长这样:
你是一个游戏决策助手。游戏规则是 XXX。当前回合可能处于以下几种状态: - 出牌阶段:…… - 咨询阶段:…… - 课程选择阶段:…… - 奖励选择阶段:…… - 对话阶段:……
请根据当前状态做出合理决策。
这份 prompt 用了一段时间,问题逐渐暴露:
第一,模型注意力被稀释。 当前明明是”奖励选择”阶段,但 prompt 里同时挂着”出牌策略要点”,模型有时候会把出牌的逻辑误用过来——比如在选奖励的时候开始算”体力剩余”,但奖励选择压根不消耗体力。
第二,无关示例污染输出。 prompt 里给”出牌阶段”配了几个示例,但模型在做”对话选择”的时候会被那些示例的格式带跑,输出一堆莫名其妙的字段。
第三,迭代起来灾难性。 你想改”奖励选择”的策略,得在那一大坨 prompt 里翻定位、改完整体测一遍。改一个地方,怕影响另外八个场景。慢慢就不敢动了——典型的”屎山 prompt”。
拆开之后变成什么
游戏的决策本质上是个状态机。每个状态需要的上下文、可选动作、决策规则差别都很大。我们最后按 phase 把 prompt 全拆了:
1 | |
23 个 Jinja2 模板。每个只管一件事。
第一反应:这么多模板,维护成本不爆炸?
实际跑下来,维护成本反而降了。因为每个 prompt 只服务一个场景,改它的时候心理负担是零——改坏了顶多影响这一个场景,不会牵连别的。
为什么是 phase,而不是别的拆法
拆 prompt 的方式很多——按角色、按任务、按难度。我们最后选按 phase(也就是游戏当前所处的阶段),是因为:
1. phase 切换是离散事件,可以由代码确定
每个 phase 用哪个 prompt,是上层调度代码根据状态决定的,模型自己不参与”该用哪个 prompt”的判断。这一点很关键——让模型选 prompt 等于让模型给自己安排任务,可控性立马崩。
2. 每个 phase 的输出 schema 不一样
出牌阶段输出”选哪张卡”,对话阶段输出”选哪个选项”,课程选择输出”去哪个教室”。schema 不一样,prompt 自然不一样——硬要塞进同一份 prompt 还得用条件判断在 prompt 里翻译,反而把模型搞晕。
3. 每个 phase 的”重要约束”不重合
出牌阶段要算资源、留 buff,得反复强调”不要浪费集中”。 课程选择阶段要看周次、看角色成长曲线,得强调”别在最后周做没收益的事”。
这些约束塞在一起会互相干扰——出牌的时候被”考虑长期成长”带跑,开始打留长线的牌;选课程的时候被”高效用牌”带跑,开始算单次收益。各管各的反而专注。
公共词汇表怎么处理
拆完之后有个新问题——每个 prompt 都得解释一遍”集中是什么”“体力是什么”“buff 是什么”,重复且容易写歪。
解决办法是抽一个 _common_terms.j2,把游戏里的核心概念定义全放进去:
1 | |
每个具体的 system prompt 第一句就 include 它:
1 | |
改”集中”的定义?改一处,23 份 prompt 全跟着变。这种小重构在万能 prompt 的时代是不敢想的。
几个具体收益
调试不再是猜谜
模型决策错了,能立刻定位是哪个 phase 的 prompt 出问题。日志里把当前调用用的 prompt 名字记下来:
1 | |
出问题打开对应的模板看就行。万能 prompt 时代是这样的:模型出 bug 了,你只能盯着一大坨 prompt 猜哪几行影响了它。
不同 phase 可以用不同模型
这个收益是拆完之后才意识到的——既然 prompt 都拆了,调用的时候完全可以按需要换模型。
简单的对话选择,丢给小模型,速度起飞、成本砍半。 复杂的出牌决策,留给大模型,质量优先。 经验复盘那种容错率高的,甚至可以用本地部署的开源模型。
万能 prompt 时代是不可能这么干的——一份 prompt 适配所有 phase,等于所有 phase 都得用最强模型,浪费严重。
多人协作变得可能
拆开之后,团队里不同人可以并行迭代不同 phase 的 prompt,互不干扰。改 system_skill_reward.j2 的人不需要去看 system_exam.j2,review 的时候 diff 也清爽。
万能 prompt 时代经常出现”你刚改完那段我刚改完这段,merge 完两个都不对了”的情况。
拆 prompt 的几条原则(踩出来的)
写下来给后面的人参考:
一份 prompt 解决一个明确的决策。 如果你发现自己在写 “如果是 A 情况则 X,如果是 B 情况则 Y” 这种分支,大概率就该拆了。
phase 切换在代码里做,不在 prompt 里做。 让模型负责”在这个 phase 下怎么决策”就够了,不要让它判断”现在是哪个 phase”。
公共概念抽到共享模板,但只抽真正稳定的部分。 那些”可能这个场景适用、可能那个场景不适用”的边界规则就别抽了,留在各自 prompt 里更安全。
每个 prompt 都有自己的输出 schema,文档化。 这一条非常重要——schema 写清楚,下游解析才稳。
prompt 文件命名要能从名字看出用途。 system_exam.j2 比 prompt1.j2 强一万倍。看似废话,真有人会图省事用后者。
回头看
把万能 prompt 拆成 23 个之前,每次给模型加新场景都像在做”开颅手术”——动哪儿都怕带坏全身。拆完之后变成”加个新文件就行”,新增成本几乎归零。
这事儿和我们写代码非常像——一开始一个文件搞定,到了一定规模就得按职责拆模块。prompt 也是代码,只不过它是写给模型看的代码,但同样适用单一职责、显式依赖、可测试性这些老规矩。
下次再听到”一份 system prompt 就够了”这种话,可以友善地笑一下。