为什么同一个模型,输出忽好忽坏

很多人有过这样的体验:同一段需求,昨天模型答得很好,今天换了个说法就答偏了;同事用同一句提示词效果好,你复制过来却差强人意。这不是模型“心情不好”,而是大模型的输出本身就是概率采样——提示词里任何一处措辞变化,都会改变下一个 Token 的条件概率分布,进而让整条生成路径分叉。

所以要接受一个前提:提示词不是许愿,而是给模型划定搜索空间。你写得越模糊,模型能“合理”续写的方向就越多,输出自然越飘。工程化的做法是把模糊需求拆成可执行、可验证的约束,再让模型在这个约束集合内发挥。

提示词工程的目标不是找到一句神奇的咒语,而是把人类的需求翻译成模型可执行、可复现、可评测的约束集合。

下面这套方法在多个生产项目里验证过,核心只有一句话:先固定结构,再逐项调优,最后用样例守住底线。

结构化提示词的四个必备要素

一条稳定的提示词通常包含四个部分,缺一不可。

结构化提示词的四个要素
  • 角色告诉模型“你是谁、面向谁说话”,用于收窄语气与知识范围。
  • 任务一句话说清目标产物,避免同时要求多件互相冲突的事。
  • 约束长度、格式、禁止项、必须包含项,越可检验越好。
  • 示例给一到三条输入输出样例,这是最有效的稳定性手段。
Prompt 调试示意:层叠的半透明对话气泡由虚线渐变为实线,末端连接发光的调谐旋钮与滑块控件
提示词调校的直观模型:从模糊的气泡到明确的约束,逐步把输出收敛到想要的形状。

把这四段写进提示词时,建议用清晰的分隔标记(如小标题、编号、特定标签)把它们的边界划出来。模型对结构非常敏感:同样的文字,用编号列表写和用一段话平铺写,遵循率可以差出十几个百分点。

角色设定与约束:引导概率分布

角色设定的作用经常被高估,也经常被误解。它不是“念咒”——不会因为写了“你是世界顶级专家”就真的获得专家能力。它真正的价值是引导模型的语料分布:当你指定“你是面向非技术读者的科普作者”时,模型会倾向于选择更通俗的词汇、更多的类比和更短的句子。

因此角色要写得具体、可观察,而不是堆砌赞美词。

  • 模糊:你是最厉害的运营专家。具体:你是电商详情页文案,面向 25-35 岁用户,语气克制、不用感叹号、每段不超过 40 字。
  • 模糊:请专业一点。具体:使用行业标准术语;术语首次出现时用括号给出不超过 15 字的解释。

约束最好写成“可检验”的形式。凡是“好一点”“简短些”这类词,模型只能猜;换成“不超过 3 条”“每条不超过 20 字”“必须包含数字”,就变成了可自动校验的规则。

def build_prompt(role, task, constraints, examples=None):
    """把四要素拼成结构化提示词:结构清晰,模型遵循率更高。"""
    parts = [f"# 角色\n{role}", f"# 任务\n{task}"]
    parts.append("# 约束\n" + "\n".join(f"{i+1}. {c}" for i, c in enumerate(constraints)))
    if examples:
        blocks = [f"输入:{e['in']}\n输出:{e['out']}" for e in examples]
        parts.append("# 示例\n" + "\n\n".join(blocks))
    parts.append("# 输出\n直接给出结果,不要解释思考过程。")
    return "\n\n".join(parts)

prompt = build_prompt(
    role="你是面向开发者的技术文档编辑,语气平实,不用营销词。",
    task="把一段口语化的功能描述改写为发布说明。",
    constraints=["不超过 120 字", "必须包含 1 个具体的用户收益", "不使用感叹号"],
    examples=[{"in": "现在导出快多了",
               "out": "导出性能提升:大型表格导出耗时平均减少 40%,等待时间明显缩短。"}],
)
print(prompt)

把提示词拼装过程写成函数的好处是:它天然支持复用与测试,改一处约束就能全局生效,而不是在十几个字符串里手工同步。

少样本示例:最有效的稳定手段

如果只能保留一种技巧,我会选少样本示例(Few-shot)。原因很直接:模型是在做“续写”,你给出两三条高质量的“输入 → 输出”对,它就会模仿示例的格式、粒度、语气甚至排版习惯。

示例要抓住三个要点:

  1. 覆盖边界情况。普通例子谁都会写,真正决定质量的是“输入不完整”“信息自相矛盾”“超出范围”时该怎么答。
  2. 保持格式一致。示例的标点、字段名、缩进都要一样,否则模型会随机挑一种。
  3. 数量控制在 1 到 5 条。再往上收益递减,还吃掉了宝贵的上下文长度。

示例也是放“反例”的好位置:在示例里明确写出“当信息不足时应回答无法判断”,模型在面对模糊输入时就不再硬编一个答案。

思维链与分步推理

思维链(Chain of Thought)的思路是让模型先分步推理再给出结论。对数学、多步逻辑、需要综合多个条件的任务,它能显著提升正确率;对简单的分类与抽取任务,反而可能引入噪声。

工程上有两种用法。一种是“显式思考”:要求模型输出推理过程,你再从最后一段取答案——可读性好,但费 Token、结果需要解析。另一种是“隐式思考”:让模型在内部推理,只输出最终答案——省 Token,但出错时难以定位。

折中做法是要求“简要列出 2 到 3 条判断依据,然后给结论”。既保留可追溯性,又不会让思考过程淹没答案。注意在系统提示里明确禁止无关的自我怀疑、重复表述和“作为 AI 我认为”这类填充句,它们会明显拉低有效信息密度。

输出格式控制:让结果可被程序消费

要把模型接入程序,输出必须是机器可解析的。最简单可靠的做法是要求 JSON,然后在代码里做校验与重试。

import json, re

SCHEMA_PROMPT = """从下面的招聘信息中抽取字段,只输出 JSON,不要任何解释和 Markdown 代码块。
字段:{"city": string, "salary_min": number|null, "salary_max": number|null, "skills": string[]}
信息:%s"""

def extract(job_text, call_model, retries=2):
    for attempt in range(retries + 1):
        raw = call_model(SCHEMA_PROMPT % job_text)
        text = re.sub(r"^```(json)?|```$", "", raw.strip(), flags=re.I).strip()
        try:
            data = json.loads(text)
        except json.JSONDecodeError as e:
            if attempt == retries:
                raise ValueError(f"模型输出无法解析: {e}; 原文={raw[:120]}")
            continue
        if not isinstance(data.get("skills"), list):
            continue          # 字段级校验不通过,重试
        return data
    raise RuntimeError("解析失败")

# call_model 由你替换为真实 API 封装:
# print(extract("佛山 · 15-25K · Python、SQL、Docker", call_model))

三个实用细节:第一,明确要求“不要 Markdown 代码块”,否则要额外写正则剥离;第二,即使要求了 JSON 也要容错,把第一次解析失败当成正常路径处理;第三,给字段设默认值和类型校验,别让脏数据流进下游。