提示词不是写完就不变的配置。为了让模型更简洁而改了一句话,可能导致 JSON 外多出解释;为了补充一个边界条件而增加规则,也可能让原本稳定的分类结果发生变化。如果每次修改都只靠人工试问几个例子,问题往往会在上线后才暴露。本篇只聚焦一个核心知识点:把提示词当作有版本的程序输入,并用小型回归测试保护它

为什么提示词需要版本

提示词通常同时包含角色、任务、输入格式、约束和输出格式。它虽然不是 Python 代码,却会直接改变模型行为,因此也需要类似代码的变更纪律。

最基本的做法是给每个提示词一个明确的版本标识,例如 ticket_summary_v1。一次修改不要直接覆盖旧文本,而是创建 v2,并记录变更目的。这样可以回答三个实际问题:当前线上使用的是哪一版?某次结果异常时应该复现哪一版?新版本是否真的改善了目标问题?

版本号本身不是质量保证。关键是让提示词选择变成显式参数,而不是散落在业务代码中的字符串。调用方传入版本,测试也针对版本运行,切换才可追踪。

最小的版本化实现

下面的示例不调用真实模型,而是先把版本管理和测试逻辑独立出来。这样运行测试不需要密钥,也不会产生 API 费用。实际项目中,render_prompt() 的结果再传给模型客户端即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from dataclasses import dataclass


@dataclass(frozen=True)
class Prompt:
version: str
template: str


PROMPTS = {
"ticket_summary_v1": Prompt(
"ticket_summary_v1",
"请用一句中文总结工单。只输出 summary 一行。\n工单:{ticket}",
),
"ticket_summary_v2": Prompt(
"ticket_summary_v2",
"请用一句中文总结工单。只输出 summary 一行,不要添加建议。\n工单:{ticket}",
),
}


def render_prompt(version: str, ticket: str) -> str:
try:
prompt = PROMPTS[version]
except KeyError as exc:
raise ValueError(f"未知提示词版本:{version}") from exc
return prompt.template.format(ticket=ticket)


if __name__ == "__main__":
print(render_prompt("ticket_summary_v2", "用户无法登录,重置密码后仍提示失败。"))

Prompt 使用 frozen=True,避免程序运行期间意外修改模板。模板中的 {ticket} 是唯一动态输入,业务代码不需要拼接多段字符串。未知版本立即报错,比悄悄回退到某个默认提示词更容易发现配置问题。

示例中的 v2 只增加一个约束,并没有删除 v1。若线上仍需要 v1,可以按请求、用户或实验分组选择版本;确认 v2 稳定后,再决定是否淘汰旧版本。生产环境还应把版本标识写入日志或调用追踪信息,但不要记录未经脱敏的用户原文。

测试提示词的正确边界

提示词测试不应该断言模型必须逐字返回某句话。模型输出具有随机性,措辞变化不等于功能回归。更稳妥的测试分三层:

  1. 渲染测试:确认版本存在、变量被填充、关键约束仍在。
  2. 契约测试:确认模型结果满足可验证的格式或业务条件,例如只有一行、包含必需字段。
  3. 样例回归测试:准备少量代表性输入,检查结果是否仍满足任务目标。

第一层完全不需要网络。可以使用标准库 unittest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import unittest

from prompt_demo import render_prompt


class PromptTest(unittest.TestCase):
def test_v2_renders_input_and_constraints(self):
text = render_prompt("ticket_summary_v2", "支付失败")
self.assertIn("支付失败", text)
self.assertIn("只输出 summary 一行", text)
self.assertIn("不要添加建议", text)

def test_unknown_version_fails_loudly(self):
with self.assertRaises(ValueError):
render_prompt("ticket_summary_v9", "支付失败")


if __name__ == "__main__":
unittest.main()

把第一个代码块保存为 prompt_demo.py,第二个保存为 test_prompt_demo.py,在同一目录运行:

1
python -m unittest -v test_prompt_demo.py

这两个测试验证的是程序确定能控制的事实,而不是模型的主观文风。运行结果中的 OK 只说明渲染契约通过,并不代表模型一定会正确总结;模型行为仍需单独做样例测试。

给模型调用加一层可替换接口

为了测试业务流程,可以把模型调用抽象成函数,并在测试时注入假的实现。下面的假实现只用于演示,不要把它当成模型质量评估:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def summarize(ticket: str, version: str, call_model) -> str:
prompt = render_prompt(version, ticket)
result = call_model(prompt)
if not result.strip():
raise ValueError("模型返回为空")
return result.strip()


def fake_model(prompt: str) -> str:
ticket = prompt.split("工单:", 1)[1]
return f"summary:{ticket}"


print(summarize("支付失败", "ticket_summary_v2", fake_model))

call_model 可以在生产环境中封装真实 SDK,在测试中替换为 fake_model。这样可以验证版本选择、空结果处理和下游逻辑,而不必每次运行测试都发起真实请求。真实模型测试则应控制频率、成本和数据脱敏,并保存输入、提示词版本、模型名和可比较的结构化指标。

如何设计一组有用的样例

测试集不必一开始就很大,但要覆盖任务边界。以工单摘要为例,至少准备正常输入、很短的输入、包含换行的输入、包含敏感信息的输入,以及信息不足的输入。每条样例都应写清期望检查项,例如“输出非空”“不超过两句”“不出现建议段落”,而不是只保存一段期望原文。

如果任务有结构化输出,就优先解析后测试字段和类型;如果只有自然语言,可以测试长度、关键词、禁用内容和人工标注的类别。测试规则要避免过度依赖某个措辞,否则提示词只是换了同义词就会产生大量误报。

一次修改应先运行旧版本测试,再运行新版本测试,并记录哪些样例改变。通过不代表可以盲目上线:如果新版本改善了一个边界样例,却损害了核心样例,就需要继续修改或保留旧版本。提示词评估的目标是可解释地做取舍,而不是追求一个神奇的总分。

常见问题

能不能只把提示词放在数据库里? 可以,但必须有版本号、发布记录和可回滚能力。数据库只是存储位置,不会自动提供可追踪性。

每次改一个标点都要升版本吗? 如果它可能影响模型输入,建议升版本。版本号的成本远低于无法复现线上问题的成本。

测试时固定 temperature 就够了吗? 不够。固定参数只能减少随机性,不能保证模型、服务端策略或提示词变化后行为不变。仍应检查任务契约。

失败样例应该删除吗? 不应删除。修复问题后把它保留为回归样例,否则同一个缺陷很容易再次出现。

小结

  • 提示词是影响模型行为的输入,应像代码一样有明确版本和变更记录。
  • 不要覆盖旧提示词;用显式版本选择、日志标识和可回滚机制保持可追踪。
  • 先测试确定性的渲染与输出契约,再用代表性样例检查模型行为。
  • 用可注入的模型调用隔离网络和费用,让大部分测试可以稳定、快速地运行。
  • 测试通过不等于质量绝对正确;下一步可以进入长文本处理,学习如何把大输入组织成模型能够稳定处理的上下文。