AI 笔记 · 2026 年 11 月 11 日

为什么你的 Prompt 不稳定:温度、上下文与评测集

同一个 prompt 连跑两次给出两种答案,未必是模型退化。把采样参数、上下文顺序和服务端差异拆开,用格式合法率与多次采样一致率把「稳定」变成两个可测的数字。

早上跑通的 prompt,下午同一个输入却换了说法;转述给同事重写一遍,输出格式又不对。于是开始怀疑模型被悄悄换了版本。

这种怀疑很少能被证实,但「不稳定」本身是可以拆开的。它至少有四个来源:采样参数、上下文组织、服务端实现差异,以及你压根没定义过什么叫「稳定」。

温度只解释一部分#

temperature 控制采样分布的平坦程度,设成 0 也不等于确定性。top_p 同样参与采样,而 seed 只承诺「同一服务端、同一实现、同一批量条件下可复现」,它不是一个跨版本保证。

更隐蔽的是另外三件事:

  • 服务端可能更新权重、推理框架或量化方式,同样的参数下输出会漂移;
  • 批处理调度改变浮点累加顺序,长上下文里足以翻转一个 token;
  • 你的代码把历史对话拼进 messages,两次请求的上下文其实并不相同。

所以「把 temperature 调成 0」只是把问题变小,没有解决。真正要做的,是让不稳定变得可测量。

我遇到过一次典型误判:周三开始答案质量下降,团队花了两天排查模型版本,最后发现是周二上线的一个功能往 messages 里多塞了一条系统提示,把原本的任务指令挤到了中间。

上下文顺序会改变答案#

同一批 few-shot 示例,换个顺序,准确率能差出几个百分点,长 prompt 上尤其明显。模型对开头和结尾更敏感,中间那段容易被稀释。

三个具体动作:

  1. 示例按类别固定排序,不要每次随机打乱;
  2. 指令写在示例之前,待处理数据放在最后,让「最近的内容」就是任务输入;
  3. 不同类别的示例不要混排,宁可拆成多个请求。

还有一个常被忽略的细节:示例里的字段名要和线上真实数据完全一致。示例用 user_id、生产数据用 uid,模型会自己编一套映射,而且每次编得不一样。

输出格式要能被程序校验#

在 prompt 里写「只输出 JSON」是一句建议,不是约束。能落到 schema 的字段就别留给自然语言:字段尽量少、能枚举就别用自由文本、必填项明确写出来。支持结构化输出的接口优先用结构化输出,不支持的至少要在客户端做一次严格解析。

解析失败时不要直接抛错给用户,而是把解析错误回传给模型重试一次,通常能自愈。两次都失败就降级到规则兜底,别让它无限循环。

另一种坏味道是同时要解释和 JSON。解释在前,解析器得先截一段;解释在后,JSON 尾巴多一个句号就炸。要么分两次调用,要么把解释塞进同一个 JSON 的字段里,别让它落在结构外面。

把「稳定」定义成指标#

「看着还行」不是标准。至少要有两个数:格式合法率和多次采样的一致率。前者说明输出能不能被程序用,后者说明同一个问题会不会给出不同答案。

评测时区分两个概念。pass@k 允许 k 次里成功一次,适合探索;pass^k 要求 k 次全部成功,才是产品需要的。一个在 pass@3 上漂亮的 prompt,pass^3 可能只有六成,这个差距就是用户体感里的「时好时坏」。

python
"""stability.py —— 同一 prompt 跑 N 次,测格式合法率与一致率
依赖:pip install openai;需要 OpenAI 兼容端点(OPENAI_BASE_URL / OPENAI_API_KEY)"""
import json, os
from collections import Counter
from openai import OpenAI

client = OpenAI(base_url=os.getenv("OPENAI_BASE_URL"), api_key=os.getenv("OPENAI_API_KEY"))
MODEL = os.getenv("MODEL", "your-model-name")

PROMPT = (
    '把句子里的金额抽成 JSON,格式 {"amount": 数字, "currency": "CNY"},不要输出别的。\n'
    "句子:退款 128.50 元,另外补 20 元运费。"
)

def run_once(temperature: float) -> str:
    resp = client.chat.completions.create(
        model=MODEL,
        temperature=temperature,
        max_tokens=64,
        response_format={"type": "json_object"},  # 能约束就别靠嘴说
        messages=[{"role": "user", "content": PROMPT}],
    )
    return resp.choices[0].message.content

def evaluate(n: int = 10, temperature: float = 0.0) -> None:
    outputs = [run_once(temperature) for _ in range(n)]
    valid, parsed = 0, []
    for raw in outputs:
        try:
            obj = json.loads(raw)
            assert isinstance(obj.get("amount"), (int, float))  # 类型也要断言
            valid += 1
            parsed.append((obj["amount"], obj.get("currency")))
        except Exception:
            continue
    mode, cnt = Counter(parsed).most_common(1)[0] if parsed else (None, 0)
    print(f"schema_ok={valid}/{n}  一致率={cnt}/{n}  众数={mode}")

if __name__ == "__main__":
    evaluate(n=10, temperature=0.0)

跑完你会得到两个数字。格式合法率低于百分之九十五,先去修 schema;一致率低但格式正常,说明任务定义本身有歧义,比如「运费要不要算进金额」你没说清。

不稳定来源 典型症状 处理手段
采样参数 换说法、同义改写 降 temperature,固定 top_p
上下文顺序 换示例顺序就掉点 固定排序,数据放末尾
字段名不一致 编造映射、键名漂移 示例与生产数据结构对齐
服务端更新 某天起整体变差 记录模型版本号,保留回放样本
需求歧义 两个答案都「说得通」 补边界例子进评测集

最后一条最容易被忽略:线上抽百分之一的请求,把输入、输出、模型版本、参数一起落盘。出问题时能回放,比在群里争论「是不是模型变笨了」有用得多。

把 prompt 当成代码管理#

prompt 改动的影响面经常被低估。随手加一句「请简洁一点」,抽取任务的字段完整率可能就掉了两个点,而且没人会把这两件事联系起来。

可行的做法是把 prompt 从业务代码里拿出来,放进带版本号的模板文件,每次请求记录用到的版本。改 prompt 走和改代码一样的流程:改模板、跑评测集、对比格式合法率与一致率、再上线。

灰度也值得做。新版本先切百分之五流量,对着同一批样本比较两版的 pass^k,再配合人工抽检,确认不是指标好看但答案更空。流程听起来重,但它能回答一个很实际的问题:这次改动是真让系统变好了,还是只是让你感觉变好了。

结论#

  • temperature 归零只是减噪,不是确定性保证;先承认漂移存在,再谈控制。
  • 上下文顺序是免费变量,固定示例排序、把数据放末尾就能拿回一部分稳定性。
  • 格式靠 schema 约束,不靠 prompt 恳求;解析失败回传错误重试一次。
  • 用 pass^k 而不是 pass@k 衡量线上体验,用格式合法率衡量可用性。
  • 每次请求记录模型版本与参数,保留可回放的样本。

没有评测集的 prompt 调优,本质上是在对着一团噪声改参数。

完

#Prompt#评测#LLM#工程实践
回到顶部 ↑