AI ENGINEERING · 方法论2026.09改提示词看手感
删一个空行,掉 76 个点
这就是黑盒搜索
四项研究 · McNemar 配对检验 · judge 校准
📦 5 Parts + Conclusion
👉 滑动
PART 02
建评测集
数据 · 规模 · 标注 · 版本
PART 05
从离线到线上
CI · 灰度 · 活资产
打开任何一篇大模型应用教程,「提示词工程」都是一份技巧清单:加角色设定、给几个示例、让它一步步思考。这份清单的问题不在于技巧本身对不对,而在于你无法预知哪条技巧在你的任务上有效。同一条技巧,换个模型、换批输入,效果可能完全反过来。
到了生产环境,代价被放大。提示词是多数团队唯一能动的「模型参数」,改一个词,影响的是全量用户的每一次请求。而多数团队发布这类改动时,手上没有任何测试兜底,只能靠改完随手试几条。
我的判断是:提示词工程真正的难点在评测,不在措辞。写出来的提示词只是一个待验证的假设,评测集才是说了算的那一个。下面先说清楚肉眼迭代为什么走不通,再讲评测集怎么建,最后用一个真实任务把整条流程跑一遍。
肉眼迭代为什么行不通
MANUAL ITERATION
「改一版,人工看几条,感觉变好了就发」,这套流程有三个地方靠不住。改动的效果事先测不出来;模型的输出本身就不稳定;而你手上那几条观察,在统计上说明不了任何问题。
语义等价的改写,能让性能波动几十个点
提示词对无关细节的敏感程度超出大多数人的想象。Sclar 等人在 FormatSpread 研究(ICLR 2024)里做过系统测量:固定任务和示例内容,只动格式层面的东西,比如大小写、分隔符、示例之间的间距,LLaMA-2-13B 的准确率差距最高能到 76 个百分点。
同一研究里还有一组数据更接近日常体感。他们拿 GPT-3.5 在 53 个任务、320 种格式上跑了一遍,格式之间最大差距 56 个点,中位数 6.4 个点。
中位数和极值要一起看。多数改动的效果就在几个点的量级上浮动,但总有一些改动会落在分布的尾巴上,直接把分数打穿几十个点。问题是,动手之前你不知道自己这次落在哪儿。
示例顺序的杀伤力也一样大。Lu 等人(ACL 2022)发现,只是把 few-shot 示例换个排列,GPT-2 XL 在四样本 SST-2 上就能从 85% 以上掉到接近随机水平(约 50%)。更麻烦的是这种经验搬不走。同一个「好排列」,模型从 GPT-2 XL 换成 GPT-2 Large,准确率从 88.7% 跌到 51.6%。
对抗场景可以看作下界。微软的 PromptRobust 基准测了 9 个 LLM、8 类任务、13 个数据集,共 4,788 条对抗提示,光是词级扰动就够让平均性能掉 39%。
这些数字意味着,你应当把每一次提示词改动都当成可能带来几十个点波动的变更来看待,哪怕你只是删掉了示例之间的一个空行。改提示词和编辑文本是两回事,它更像是在一个起伏剧烈的响应面上做无梯度的黑盒搜索。这类搜索唯一可靠的办法是老老实实测量,直觉在这里帮不上忙。
输出本身不可复现
还有个更隐蔽的原因:就算你什么都不改,同一条输入也不保证拿到同一条输出。
temperature=0 只是把采样换成了贪心解码,每一步取概率最高的 token,它并不等于确定性。Atil 等人 2024 年的论文专门测过这件事:在 temperature=0 这种预期确定的配置下,相同输入照样产出不同输出。成因说起来并不玄乎,分布式推理里浮点归约的顺序受批次调度影响,logits 上那点微小差异到了贪心选择这一步,会被放大成完全不同的结果。
所以「这条 case 我这次跑对了」不算证据,它可能只是采样方差碰巧站在你这边。
小样本观察在统计上不成立
上面两个原因之外,还压着一层:你手工跑那几条 case 得出的「变好了」,在统计上几乎没有信息量。
准确率这类比例指标的抽样误差是有现成公式的。二项分布做正态近似,95% 置信区间的半宽为 1.96·√(p(1−p)/n)。代两组常见数字进去:
100 条样本 · 准确率 80%
半宽 ±7.8 个点。真实值落在 72.2% 到 87.8% 之间,都算正常波动。
50 条样本 · 同样 80%
半宽 ±11.1 个点。
换句话说,你手动跑十条八条去对比新旧提示词,看到的几个点变化完全可能只是抽样噪声;反过来,真实存在的轻微退化也照样会被噪声盖住。
这三件事凑在一起,结果就是:改动结果不可预测,单次结果不可信,手上的样本又太少。
你不仅不知道方向对不对,甚至不确定自己有没有在动。
把评测集当测试套件来建
EVAL SET AS TEST SUITE
原因说完了,接着是该怎么建。软件工程里,改完代码不跑测试直接上线,那叫事故;到了 LLM 应用,改完提示词不看评测就发布,却成了日常默认动作。既然可以把提示词当代码对待,那评测集就是它的测试套件。
数据从真实流量里采,不要拍脑袋
!最常见的错误 🕳
建评测集最常见的错误,是拿随手写的样例或者合成数据凑数。
真正的问题不在数据真假,在于分布。合成数据和线上请求的分布对不上时,离线分数和线上表现之间会出现肉眼可见的落差。你测的是模型在理想输入上的表现,而用户从来不给你理想输入。
该做的是从真实流量里采样,按业务维度分层,比如意图类别、输入长度、来源渠道,每一层按它在线上的实际占比抽。边界样本要单独成层,超长输入、多意图混在一起、错别字和口语碎片,bad case 大多出在这些地方。
冷启动阶段还没流量怎么办?OpenAI 评测指南给了一套数据组合策略,直接拿来用就行:生产数据、历史数据、人工精选、合成数据按需拼,合成数据只负责补长尾,不做主体,上线后陆续换成真实样本。
多大规模,是可以算出来的
评测集要多大,取决于你想测出多大的差异,把置信区间公式倒过来就能算:n = z²·p(1−p)/e²。z=1.96 对应 95% 置信度,p 是预期准确率,e 是你愿意接受的误差半宽。取 p=0.8:
允许 ±5 个点误差
n = 3.8416 × 0.16 / 0.0025 ≈ 246 条。
几十条只够冒烟,几百条才谈得上统计意义,想分辨两三个点的差异得上千条。
Anthropic 的评测文档也是这个路子,宁可单条判分信号粗一点,也要把样本量堆上去:一批能自动判分的大样本,比一小撮人工精标的样本有用。
标注:分歧样本是资产
分类、抽取这类有标准答案的任务,需要人工标注来做金标准。两件事值得注意。
第一件是双人独立标注,算一致性。常用指标是 Cohen’s κ,也就是剔除随机一致之后还剩多少标注者一致性。κ 低于 0.7 通常不是标注员的问题,而是任务定义本身就模糊,先去把标注规范修清楚再往下走。
第二件是把分歧样本当资产。两个人标不一致的样本,恰恰卡在类别边界上,而下一轮提示词要写清楚的「边界定义」,正是指向这些样本。评测集和提示词因此是共同进化的:每修一轮边界,标注规范和提示词一起更新。
存储与版本
存储不用复杂,一条 JSONL 就够,关键是把元数据带全:
{"id": "0042", "text": "昨天下的单选的次日达还没到,我要退掉重新下单", "label": "退款售后", "layer": "短文本-复合意图", "source": "线上抽样-2026-09"}
{"id": "0117", "text": "你们这个保价30天是套路吧,买完第二天就降价", "label": "活动规则", "layer": "短文本-单意图", "source": "线上抽样-2026-09"}
评测集要进 git,和代码享受同等待遇。提示词决定系统当前是什么行为,评测集决定你能发现多大的行为变化,两者都是核心资产,没有高低之分。
跑通一遍完整实例:意图分类的三轮迭代
CASE STUDY · INTENT CLASSIFICATION
接下来把前两节的方法放在一个任务上完整走一遍,输入是什么、每轮改了什么、最后产出什么决策。场景取常见的电商客服。需要说明的是,文中的跑分数字是构造出来的演示数据,用来展示流程和检验方法,代码和方法可以直接搬到真实任务上。
任务与基线
任务是客服会话意图分类,8 个类别:物流查询、退款售后、发票、商品咨询、活动规则、账户安全、投诉、其他。模型用 gpt-4o-mini 这一档,输出约束为「只输出类别名」。
评测集 200 条,按 2.1 的分层方法从线上会话里抽,双人标注,κ=0.87。
v1 就用最常见的写法,一句指令加类别列表:
你是电商客服意图分类器。将用户会话分入以下类别之一,只输出类别名,不要输出其他内容:
物流查询 / 退款售后 / 发票 / 商品咨询 / 活动规则 / 账户安全 / 投诉 / 其他
跑分:84.0%(168/200)。
错误分析:先归因,再动笔
!最容易犯的错 🕳
拿到基线分数之后,最容易犯的错是马上动手「优化提示词」。
正确的下一步是错误分析(error analysis),先别管平均分,去看 bad case。平均分只告诉你差了多少,bad case 才告诉你差在哪儿。
v1 的 32 个错误里,有两类占了主体:
「昨天下的单选的次日达还没到,我要退掉重新下单」,模型判成「物流查询」,期望「退款售后」。用户一句话里同时提了查件和退款,复合意图没有判优规则可用;
「你们这个保价 30 天是套路吧」,模型判成「投诉」,期望「活动规则」。类别本身没给定义,模型只能抓着「套路」这个情绪词猜。
分类归因下来,14 条错在类别定义缺失,11 条错在边界上没有判优规则,剩下 7 条比较零散。错误集中度这么高,说明改进方向是清楚的。
v2:补定义和 few-shot,顺带做一次显著性检验
对着这两个归因,v2 改了三处:每个类别加一行定义,添一段判优规则,再配 4 条 few-shot 示例。其中判优规则是主要增量:
【判优规则】
1. 复合意图按用户最终诉求分类:查进度 + 要求退款 → 退款售后;查进度 + 仅催促 → 物流查询
2. 涉及价格、优惠、保价的疑问,即使语气不满,归活动规则,不归投诉
3. 投诉仅指明确要求投诉登记或升级处理的会话
跑分 91.0%(182/200),涨了 7 个点。这时候该问的不是「还能怎么优化」,而是:这 7 个点是真的吗?
光看两个准确率回答不了这个问题,1.3 节那个置信区间还悬在头顶。该用的工具是配对检验:同一批样本分别跑 v1 和 v2,只盯着结论翻转的那部分看。也就是 McNemar 检验,配对二元数据的显著性检验:
v1 × v2 混淆矩阵(n = 200)
看这张表:只有 v1 对而 v2 错的 10 条(记 b)和 v1 错而 v2 对的 24 条(记 c)携带差异信息。两版都判对的 158 条、都判错的 8 条,对比较没有任何贡献。在「两版无差异」这个零假设下,b 服从 Binomial(b+c, 0.5)。做二项精确检验,P(X≤10 | n=34) ≈ 0.012,双侧 p ≈ 0.024,小于 0.05。
顺便说一句,配对检验为什么比「两版各算一个置信区间再来比」灵敏:同一条样本自身的难度差异,在配对设计里被直接消掉了,检验只对翻转的判断有反应。这是评测集复用带来的直接好处。
平时判断不用每次都去算精确 p 值,正态近似就够:|c−b| > 1.96·√(b+c) 大致就算显著。这一例 |24−10|=14 > 11.4。
v3:被分层报告拦下的一次回归
v3 继续细化边界规则,又加了否定示例,整体 93.0%(186/200),比 v2 高 2 个点。照样先做配对检验:b=4、c=8,|c−b|=4 < 1.96·√12≈6.8,双侧 p≈0.39。整体提升不显著。
接着看分层报告,把指标拆到 2.1 节建的那些层上。「超过 200 字复合咨询」这一层共 20 条,v2 的表现是 12/20(60%),v3 掉到了 8/20(40%);同期另外 180 条从 170 提到了 178。
这就是分层回归的典型样子:
均值上 +2 个点的变化,把这一层 −20 个点的退化盖得严严实实。
只看总分,这时会做出完全错误的合并决策
归因并不难找。v2 那 4 条 few-shot 示例全是 50 字以内的小短句,v3 的规则又加强了「按关键词归判」的倾向,长复合会话的分布偏移始终没被示例覆盖到。
处理方式不是回滚 v3,是修。从这次回归的 bad case 里挑 2 条长复合会话,再补 2 条历史样本,凑成 4 条长文本 few-shot,替掉原来那 2 条短示例,得到 v4。
v4 跑分 95.0%(190/200),各层都不低于 v2。最后一步照旧,拿 v4 对 v2 做配对检验:b=6、c=14,|c−b|=8 < 1.96·√20≈8.8,双侧 p≈0.115。不显著。
这个「不显著」反倒是整个实例里最有价值的结果。同样这 200 条,7 个点的提升算出了显著性,3 个点的提升算不出来。也就是说,200 条的评测集,能可靠分辨的差异在五到七个点以上。比这更小的差异,要么去扩评测集,要么老实承认不确定,把决策权交给线上灰度。
合并与否的裁决规则可以直接固化成三条:
2不显著,但方向为正、各层无退化、改动成本也低:可以先合并,同时扩充评测集后复检。v4 走的就是这条;
3任何一层出现显著退化:一票否决,先修再说,不管整体均值多好看。v3 走的这条。
工程化:让每轮迭代都可复现
撑起这个实例的工程骨架其实很小,评测脚本核心就 20 行:
import json, pathlib
from openai import OpenAI
client = OpenAI()
def run_eval(prompt_path, dataset, out):
prompt = pathlib.Path(prompt_path).read_text(encoding="utf-8")
cases = [json.loads(l) for l in open(dataset, encoding="utf-8")]
results = []
for c in cases:
r = client.chat.completions.create(
model="gpt-4o-mini", temperature=0,
messages=[{"role": "user", "content": prompt + "\n\n会话:" + c["text"]}])
pred = r.choices[0].message.content.strip()
results.append({**c, "pred": pred, "correct": pred == c["label"]})
json.dump(results, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
n_ok = sum(r["correct"] for r in results)
print(f"{prompt_path}: {n_ok}/{len(results)} = {n_ok/len(results):.1%}")
run_eval("prompts/v2.txt", "evals/dataset.jsonl", "results/v2.json")
temperature=0 只能压低方差,不提供确定性(见 1.2 节)。想要更硬的结论,可以每条样本跑 3 次取多数。
目录结构约定:
prompts/ v1.txt v2.txt v3.txt v4.txt # 提示词即代码,改动走 diff 审查
evals/ dataset.jsonl # 评测集,版本化
results/ v1.json v2.json ... # 每轮跑分明细,含逐条对错
results/ 里那份逐条对错明细,同时也是下一轮错误分析的输入,迭代闭环的最后一环在这里合上。到这一步,提示词迭代才算有了和代码迭代同等的待遇:能 diff,能度量,结论追得回去,回滚也有依据。
让 LLM 当评测员:先校准,再放手
LLM AS JUDGE
分类任务有标准答案,判分无非一行 pred == label。摘要质量、回复得体性这类开放生成任务没有唯一正确答案,就得引入 LLM-as-judge,让 LLM 按评分标准给输出打分。但 judge 不是白吃的午餐,它的偏差得先摸清楚。
三种偏差,都已经被量化过
位置偏差
Wang 等人让 GPT-4 评判 Vicuna-13B 和 ChatGPT 的回答,只是把两个回答的呈现顺序换一下,Vicuna 的胜率就从 51.3% 变成 23.8%。GPT-4 当裁判时结论冲突率 46.3%,换成 ChatGPT 当裁判,冲突率高达 82.5%。
冗长偏差
Zheng 等人在 MT-Bench 研究里的定义是,judge 倾向于给更长的回答更高分,哪怕那个回答并不更清晰、更准确。
自我偏好
Wataoka 等人 2024 年的测量显示,LLM 会系统性高估自己风格的输出质量,并且给低困惑度、也就是更像自己生成的文本打高分。judge 对「像自己写的」东西有结构性偏爱。
校准之后可以用
有偏差不等于不能用。同一份 MT-Bench 研究里也给了正面数字:GPT-4 级 judge 和人类评审的一致率超过 80%,已经达到人类评审彼此之间的一致率水平。
前提是得先校准,走四步:
1从评测集里抽 50 到 100 条人工标注,作为 judge 的金标准;
2让 judge 打分,算它和人工的一致率(或者 κ);
3不达标就先改评分标准(rubric)。把「回答质量高」这种话拆成能逐项核查的判分点:事实一致、要点覆盖、无冗余、语气合规。同时要求 judge 先给理由再给分。G-Eval 验证过这条路,先推理再打分、配合 token 概率加权,在 SummEval 摘要任务上和人工评价的 Spearman 相关达到 0.514;
4位置偏差有工程对策:两个顺序各判一次,结论不一致的样本转人工。OpenAI 评测指南也建议用成对比较或通过/不通过判定替代细粒度打分,可靠性更高。
校准完还得摆正它的位置:judge 负责初筛和持续监控,发布决策仍然要人工抽检兜底。
离线评测是必要条件,但不是充分条件。最后说一说从评测体系接到生产体系的这一段。
提示词要进 CI。把评测脚本接进持续集成,提示词文件一有改动就自动跑全量评测,报告直接挂在 diff 上。开源工具 promptfoo 干的就是这件事,官方标语写得很直白:test-driven prompt engineering, not trial-and-error,测试驱动,不是试错。
换模型等于全量回归。换模型、换版本都必须重跑评测。1.1 节 Lu 等人那组数字已经说明,提示词层面的经验没有跨模型可移植的保证。「同一个提示词换个模型不掉点」是个需要验证的假设,不是默认成立的事实。
离线通过了再走灰度。Hamel Husain 把评估体系分成三层,成本逐层递增:单元测试级,每次改动都跑;人工与模型评审级,定期跑;A/B 测试级,重大变更之后跑。他复盘了大量产品,结论是
不成功的 AI 产品几乎都有一个共同根因:没有建立稳健的评估体系。
而产品成败和迭代速度强相关,迭代速度又由评估体系决定。
最后,评测集是活资产。定期从线上 bad case 采样入库,记得先过一遍隐私脱敏。流量分布漂移会以新层的形式冒出来,而每一次「线上出了问题、离线没拦住」,都对应一条该入库的新样本。评测集停止更新的那天,整套体系就开始失效了。
说回标题。技巧清单式的提示词工程有三个修不好的毛病:预测不了改动的后果,分不清改进和噪声,也发现不了局部回归。这三件事,恰好就是工程评测的定义域。
所以提示词工程师的核心能力,不是「会写提示词」,而是一条闭环:
写提示词只是其中「修改」那一环,而且是最不费力的一环。技巧清单会随着模型升级不断贬值,闭环不会。
最小可行版本就一句话:从你的产品流量里导出 50 条真实输入,人工标注,写 20 行评测脚本,把当前线上提示词的基线测出来。往后的每一次迭代,都从这个闭环开始。
Sclar et al., Quantifying Language Models’ Sensitivity to Spurious Features in Prompt Design(ICLR 2024,arXiv:2310.11324):提示格式敏感性,LLaMA-2-13B 最高 76 个准确率百分点差距
Lu et al., Fantastically Ordered Prompts and Where to Find Them: Overcoming Few-Shot Prompt Order Sensitivity(ACL 2022,arXiv:2104.08786):few-shot 示例顺序敏感性
Zhu et al., PromptRobust: Towards Evaluating the Robustness of Large Language Models on Adversarial Prompts(arXiv:2306.04528):对抗提示鲁棒性基准,词级扰动平均性能下降 39%
Atil et al., Non-Determinism of “Deterministic” LLM Settings(arXiv:2408.04667):temperature=0 设置下的输出非确定性
Wang et al., Large Language Models are not Fair Evaluators(arXiv:2305.17926):LLM 裁判的位置偏差与冲突率
Zheng et al., Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena(arXiv:2306.05685):judge 与人类一致率、三类偏差定义
Liu et al., G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment(arXiv:2303.16634):form-filling 评测框架
Wataoka et al., Self-Preference Bias in LLM-as-a-Judge(arXiv:2410.21819):LLM 裁判的自我偏好偏差
OpenAI,Evaluation best practices 开发者文档(developers.openai.ac.cn/api/docs/guides/evaluation-best-practices)
Anthropic,Develop test cases 文档(docs.anthropic.com/en/docs/test-and-evaluate/develop-tests)
Hamel Husain,Your AI Product Needs Evals(hamel.dev/blog/posts/evals/,2024-03):评估体系三层框架与产品复盘
promptfoo(promptfoo.dev):开源提示词评测与回归测试工具
既然看到这里了,如果觉得有用,随手点个赞、在看、转发三连吧。
THANKS FOR READING