最近一段时间我一直在折腾一个挺典型的落地问题:怎么让大模型在真实业务里稳定输出,而不是靠拍脑袋调提示词碰运气。试了很多套路之后,我越来越确信一件事——想要提示词效果好,必须让测试集说了算;想要成本降下来,必须把大模型的能力“搬”到小模型上。这条链路,我愿称之为“测试集驱动的大小模型协同优化”。今天就把整个思路、操作细节、踩过的坑完整写一遍,希望能帮你少走点弯路。
这篇文章适合谁?如果你正在做 LLM 应用开发,每天都在调 prompt 但总被线上 badcase 打脸;如果你已经把大模型接进了业务,但觉得调用成本太高、延迟太大;又或者你只是听说过“蒸馏”“小模型微调”但不知道从哪下手——那这篇文章就是写给你的。
1. 先理清思路:测试集、提示词、大小模型到底怎么配合
1.1 为什么提示词要靠测试集说话
很多刚接触提示词工程的人有个错觉:prompt 写出来读着通顺、看起来专业,就觉得它“应该能用”。但 prompt 这东西,说白了一个自然语言写的指令,它的质量高低根本不是靠肉眼判断的,而是靠“在足够多的真实样本上表现如何”来判断的。
我见过太多类似的场景:工程师花一下午写了个提示词,自己测了五六个例子觉得挺满意,结果一上线,各种幺蛾子全冒出来了——输出的 JSON 格式不稳定、该抽的实体没抽出来、不该出现的编造内容频繁出现。为什么?因为那几个手测的例子里,你潜意识里已经把 prompt 要什么结果写在脑子里了,你是顺着 prompt 的逻辑去挑例子的,这跟真实线上分布的差距可能非常大。
测试集就是干这个的。它是一组固定不变的输入样本,附带了明确期望的输出标准。提示词好不好,不要问感觉,直接一套评测跑下来看指标。这就好比考驾照不能靠“我觉得我开得挺稳”,得去科目二考场按标线跑一圈,压线就是压线,没过就是没过。所以我在做任何 prompt 工程之前,第一件事永远是先攒测试集。
1.2 大小模型协同的完整链路
很多文章只讲“怎么调 prompt”,也有文章只讲“怎么微调小模型”,但实际工作中最值钱的,其实是把它们串起来的那条完整链路。
我目前最常走的工作流是这样的:先用一个大模型(比如 GPT 系列、Claude,或者是能力比较强的开源模型)配合测试集反复调优提示词,把任务能力做到一个满意的上限。然后,用这套“大模型 + 优化好的提示词”去批量生成高质量的训练数据。最后拿这些数据去微调一个小模型(比如 7B 或更小),把小模型部署上线承担生产流量。
为什么要这么绕一圈?原因很实在:大模型推理贵、延迟高,不适合大规模线上调用;小模型便宜、快、还能私有化部署,但直接拿小模型做任务往往能力不够。这条链路本质上是用提示词工程把大模型变成“数据工厂”,再通过微调把能力转移到小模型里。相当于让老师傅把所有经验写成了操作手册,再让徒弟照着手册练上手,练完之后徒弟独立干活。
1.3 这条路适合哪些场景
不是所有任务都值得走这条路。根据我的经验,凡是“输入输出都是结构化、判断标准相对明确”的任务,非常适合这条链路。典型的就是信息抽取、文本分类、意图识别、情感判断、JSON 字段提取、质检规则判定、客服对话标签化等等。这类任务有两个共同特点:一是可以通过少量样本就能说清楚“什么是对的”,二是大模型做起来很稳,小模型经过微调也能学到八九成。
但如果是开放式创作、长文写稿、需要大量外部世界知识做推理的场景,这条路就不太好走。因为小模型容量有限,你把大模型的推理能力压进去会打很多折扣,用户体感会明显下降。这种场景不如老老实实用大模型 API 或者云端服务,别惦记蒸馏。
2. 测试集:整个优化过程的地基
2.1 测试集样本从哪来
构建测试集是第一步,也是最容易偷懒但其实最不能偷懒的一步。样本来源我一般用三个渠道混合。
第一个渠道是历史标注数据。如果业务之前做过人工标注或者有线上系统的日志,那直接拿过来用。这些数据分布贴近线上真实情况,是最宝贵的“金矿”。没有现成标注的话,可以让业务同学帮忙标几百条,成本不高但价值极高。
第二个渠道是线上日志采样。特别是如果你已经有了一版线上运行的规则或模型,可以把它们处理过的真实数据随机抽样,再人工确认一下标签。这个做法的好处是能暴露真实输入的多样性——你会发现用户的语言远比你想象的更“脏”:有错别字、有口语化表达、有半句话、有中英混排。这些恰恰是测试集里最该有的样本。
第三个渠道是人工构造对抗样本。这是最容易被忽略的。什么叫对抗样本?就是专门挑 prompt 容易翻车的边角料:比如抽取订单号时给一个“订单号疑似为 12345 或 67890 不确定”的句子;分类时给一个“看似 A 类但有明显线索是 B 类”的模糊例子。这类样本数量不用多,每类任务造十来个,就能把 prompt 的鲁棒性逼出来。
2.2 样本怎么分类、难度怎么分层
测试集不是把样本堆在一起就完事的,必须要分层、分类组织,不然优化时无从下手。我的习惯是按照两个维度来切:任务维度和难度维度。
任务维度是指业务里天然有多个子任务,比如“先判断对话属于咨询还是投诉,再抽取订单号,再输出对用户情绪的判定”。这时候我会把每个子任务单独切一个 slice,分别统计指标。好处很明显:总体准确率从 80% 涨到 85%,你根本不知道是哪个子任务贡献的;但如果每个子任务指标单独看,就能精准定位是“情绪判断”这个环节太弱还是“订单号抽取”格式老出错。
难度维度我习惯按 easy / medium / hard 三档分,比例大约 5:3:2。easy 是大多数正常输入,medium 是有点干扰项或表达绕的输入,hard 是各种极端格式、模糊表达、边界情况。这种分层在评估时非常有用——你可以一眼看出优化到底是在“吃 easy 红利”还是真的把 hard 能力也提上去了。
2.3 评估指标怎么设计
测试集有了,评估指标也得定好,否则“好不好”永远是一笔糊涂账。这里我强烈建议至少分为三层:自动规则校验、模型打分、人工抽检。
自动规则校验是底线。如果你的任务有严格结构要求(比如 JSON、XML、固定枚举值),那一定要让程序去检查输出格式是否合法、必填字段是否缺失、枚举值是否在预定范围内。这个指标叫 parse_rate(解析通过率)或 format_pass_rate,不过这一关,后面的语义准确率谈都不用谈。
模型打分是主力。现在比较主流的方式是 LLM-as-a-judge,让一个大模型当裁判,把模型输出和标准答案一起丢给它,让它给出 1-5 分或者直接判对错。这种方式比纯规则灵活,能覆盖很多语义层面的判断。但要注意,裁判模型也会有自己的倾向性,我一般会让裁判输出判断理由,再定期人工抽检裁判的准确率,避免裁判自己“带偏”了评估。
人工抽检是压舱石。每轮评测完成后,我会随机抽 30-50 条让业务同学盲评。为什么必须人工?因为很多业务细节是模型看不到的——比如某些“看起来说错了但其实业务上可以接受”的答案,只有懂业务的人才能判断。人工抽检不追求数量,追求稳定,它存在的意义是防止自动指标和真实业务需求之间产生系统性偏差。
2.4 测试集版本与回归管理
测试集不是一次性建完就固定的,它需要持续维护和演进。我现在每次做回归评测都坚持用同一套“测试集版本”,同时把新增的 badcase 不断沉淀进去。具体做法是:为测试集维护一个 manifest 文件(JSON 或 CSV 都行),每一条样本都带 ID、输入、标注输出、任务类型、难度等级、期望的严格程度、来源(线上日志/人工构造/历史标注)这些字段。
这样做最大的好处是可以做回溯对比。这轮 prompt 改完,我把整个测试集重新跑一遍,不仅能看出总分涨还是跌,还能按任务和难度拆分,看到底是哪里退了。很多时候一个改动会“按下一个葫芦浮起一个瓢”,你要是不做回归,很容易发现不了。
有一件事要特别注意:测试集会过拟合。如果你的 prompt 反复照着同一套测试集调,慢慢就会“背答案”,表面指标越来越高,但换一批新样本立刻打回原形。避免的方法是定期从线上抽样新增样本,并且隔一段时间就把旧样本里已经“太简单”的换掉一部分,始终保持测试集对真实分布的敏感性。
3. 提示词迭代:一次完整的实战过程
3.1 第一步:先做一个可复现的 baseline
拿我自己最近做的一个客服对话抽取任务来举例。业务要求是从一段客服和用户的对话里抽取三个字段:用户订单号、问题类型(枚举值:物流/售后/价格/其他)、用户情绪(正面/中性/负面)。我先把一段最朴素的基础提示词写上,确保它能跑出一个“及格线”的结果,然后才谈优化。
基础提示词大概是这样的(保留核心逻辑,业务细节做了简化):
你是客服对话分析助手。请从下面的对话中抽取信息,并输出 JSON: {"订单号": "这里填订单号或null", "问题类型": "物流/售后/价格/其他", "用户情绪": "正面/中性/负面"} 对话: ...第一次评测的结果我记得很清楚:JSON parse_rate 只有 78%,语义准确率 62%。也就是说,100 条里二十多条是直接输出格式崩了的,剩下的里面还有很多字段抽错。这就是个很典型的 unoptimized baseline,但没关系,有了它,我们就有了一个可以对比一切的“原点坐标”。
3.2 第二步:把错误归类,找到真正的瓶颈
Baseline 跑完,不要急着改 prompt,先做错误分析。很多人一上来就加更多措辞,改得更花哨,这完全是本末倒置。正确姿势是把评测的失败样本全部拉出来,按失败模式归类。我那次分析下来,问题集中在三大类。
第一类格式问题:模型有时候在 JSON 前面夹解释文字,有时候字段名自己改,比如把“订单号”写成“订单ID”。这是小模型或者 prompt 约束不足时特别常见的问题,解决方案是强化输出格式约束,比如让模型先输出一个特定的起始标记,或者给一个完整的 JSON 模板。
第二类漏抽取:用户明明在对话里报了订单号,但模型没抽出来。原因多半是当时上下文比较长,订单号被淹没在大量文本里,基础 prompt 里没有强调“只要对话中出现订单号就必须抽取”。解决方案是要么加规则指示,要么给个 few-shot 示例。
第三类语义理解偏差:特别典型的例子是用户说“你们这破物流怎么回事”,同时又说“行了赶紧把订单给我退了”。模型会把这个判断成“物流”类,但业务上用户真正想解决的是售后退货。这种语义混淆,靠加大模型参数解决不了,必须靠任务拆解和业务规则定义。
3.3 第三步:逐条优化提示词的手段
针对上面三类问题,我分别用了几种不同的优化手段,这里一个个拆开说。
第一个是输出结构约束。与其在 prompt 里说“只输出 JSON”,不如直接给出 JSON Schema 和“不要输出任何解释”。更硬核的做法是要求模型把结构化内容放在特殊标记之间,然后程序只取标记之间的部分去解析。这一步把 parse_rate 从 78% 提到了 93%,收益非常直观。经验是:别指望模型自觉遵守格式,你要在 prompt 里用最短的句子把规则说死。
第二个是 few-shot 示例。不要塞十几个示例,挑两个最有代表性的就够了。我选的一个是“订单号藏在对话中段且夹杂了口语”的例子,另一个是“问题类型容易混淆”的例子。注意 few-shot 不是给模型“参考”,而是给它“边界”——告诉它哪种情况算 A、哪种情况算 B。这比在 prompt 里写一百字抽象描述有用得多。
第三个是任务拆解和 CoT 的结合。我们把一个复杂的抽取任务拆成两步:第一步让模型先判断问题类型并给出理由,第二步再抽取订单号和情绪。这一步实质上是让模型“先想后说”,把推理过程外显。但对于这种短文本抽取任务,CoT 的收益不如对长文本复杂推理那么明显,反而增加了一点延迟和 token 消耗。所以我不建议所有任务都无脑上 CoT,要看你任务里到底有多少“需要多步推理”的成分。
我还加了一条负面约束:“如果对话中没有出现订单号,订单号字段输出 null,不要编造。”这条千万别小看,大模型天生有补全倾向,不明确禁止,它就敢给你编一个看起来像订单号的字符串。这类“不要做什么”的约束,往往比“要做什么”更管用。
经过三轮迭代,评测结果变成了 parse_rate 97%,语义准确率 86%。hard 分层从最初的 40% 提到了 65%。整体可以接受,但还没到敢上线生产的程度。
3.4 回归测试与版本记录
每改一版 prompt,我都会记录这个版本的变更点和评测结果,绝不靠“上一版改了几句话”这种模糊记忆。具体做法是在项目目录里为每一版 prompt 建一个文件,命名带版本号,存放 prompt 全文、变更说明和评测结果。这个习惯帮我避免过两个大坑:一是改了半天发现效果反而倒退,回滚时却找不到上一版原文;二是同一套链路用了不同版本的 prompt 跑数据,导致训练数据质量波动,后面排查起来非常痛苦。
频繁修改 prompt 的人,强烈建议你把 prompt 版本管起来。不需要上特别复杂的工具,就用 Git 仓库管理 markdown 文件就够了,提交信息里写明评测指标的变化。这个习惯的回报周期很短,用不了几天你就会感谢自己。
4. 进阶:把能力从大模型迁移到小模型
4.1 为什么要做这一步
很多人会问:大模型都调得不错了,直接用不行吗?行,但成本可能让你肉疼。我给大家算一笔账:假设你的业务每天要跑 100 万次调用,每次输入输出加起来算 800 token,用主流大模型 API 的话,一天单是推理成本就可能到几千块,一个月下来小十万。就算调到足够便宜的模型,一秒并发几十上百次时延迟和限流也够你喝一壶的。
而换成小模型呢?一个 7B 开源模型用 vLLM 部署在两卡 A 系列显卡上,吞吐可以做到每秒几百甚至上千 token,单次调用边际成本几乎可以忽略,还能内网私有化部署,数据不出域。这种差异带来的不只是省钱,还有做产品时完全不同的自由度——你甚至可以跑一些“高风险”的调用,因为不用担心账单。
所以我的经验是:大模型调 prompt,本质上是付费买方案;小模型微调,本质上是把方案学成自己的本事。两条腿走路,才能既保证效果,又控住成本。
4.2 数据生产:让优化好的大模型当“标注员”
小模型的微调,第一关是数据。数据最关键的一点是:务必用最终版的“大模型 + 提示词”组合来生成,而不是用中间半成品。因为提示词的每一次微小变化,都会影响输出分布,而数据里的“杂音”会直接限制小模型的上限。你在 prompt 上花的功夫,会原封不动地折进训练数据里。
生成的时候我会做几件事。第一,准备一个足够大、分布合理的原始输入池,可以来自历史日志、爬取的公开数据、人工构造输入等,确保覆盖 easy/medium/hard 三类场景。第二,用最终训好的提示词批量跑推理,把输出存下来。第三,做一轮清洗和过滤:格式不合格的丢弃、去重(用 embedding 相似度或者 simhash)、与某些安全规则冲突的排除。
这里有个小技巧:生成数据的时候,可以适当把 temperature 设在 0.3 左右,让输出稳定中带一点多样性,避免训出来的小模型只会一种“标准话术”。还有一个经验:对每一条数据,保留大模型输出的“中间推理”(如果 prompt 里有 CoT 或两步推理的话),这些推理文本可以作为小模型微调时的监督信号,提升它的泛化能力。
但也要清醒:小模型是学不会大模型深层推理的。如果你生成的数据里有太多复杂的多步推理,小模型训练时会很大概率“学歪”——要么输出表层套话,要么干脆崩掉。所以我的建议是数据里尽量保持“输入-输出”的映射关系清晰简单,推理过程只作为辅助信号,不要让小模型承担超出容量的任务。
4.3 微调小模型的方法
数据准备完,后面就是微调阶段了。目前最主流也最容易上手的是用 LoRA / QLoRA 做参数高效微调,基座模型一般选开源 7B 级别的,也有场景适合 1B-3B 的小模型。选择基座时要考虑三件事:一是模型对中文的支持程度,二是模型的指令跟随能力,三是你本地的推理部署资源。
训练数据格式上,要跟基座模型的对话模板保持一致。如果是 Chat 类模型,就把数据组织成 system + user + assistant 的形式,system 里放你要部署后的 prompt 指令,user 里放业务输入,assistant 里放大模型生成的输出。训练时把 system 和 user 部分 mask 掉,只学习 assistant 部分的生成。
我自己常用的工具是 LLaMA-Factory 这一套,或者直接写 HuggingFace Trainer 脚本。数据量不需要特别夸张,一般 5000-20000 条高质量样本就够跑一个很不错的领域小模型了。训练参数上,LoRA rank 我习惯用 16 或 32,学习率 2e-4 到 5e-4,epoch 数 2-3 轮,同时保留一个验证集看全局 loss 防止过拟合。
微调完之后,部署方面用 vLLM 做生产服务非常方便,openai 兼容的接口可以直接复用你之前调大模型 API 的代码。如果只是本机体验,Ollama 也是不错的选择,把微调后的 LoRA 合并导出成 GGUF 格式丢进去就能跑。
4.4 小模型的验证与迭代闭环
小模型训练完,千万不要只看训练 loss 就完事,必须拿它跑同一套测试集。在我那个客服对话任务里,7B 模型微调之后 parse_rate 是 96%,语义准确率 81%,比大模型低几个点,但在可接受范围以内。最让我惊喜的是,hard 分层的准确率提升到了 58%——虽然还是比大模型的 65% 低,但对于一个推理成本几乎为 0 的模型来说,这笔账太划算了。
如果小模型评测分数不达标怎么办?我的建议不是立刻去调整模型训练参数,而是回到数据源头:把测试集里小模型做错的样本找出来,扔回“大模型 + 提示词”的组合里去生成更多类似样本,补进训练集再训一轮。这是一个典型的闭环:测试集发现问题 → 大模型生产数据 → 微调补强小模型 → 再评测。我靠着这个闭环把那个 7B 模型的准确率从 76% 一路提回了 81%。
5. 常见问题与排查实录
5.1 测试集评测过关,但线上效果崩盘
这是最让人崩溃的问题之一。出现这种情况,九成原因是测试集和线上真实分布不一致。比如测试集里数据都是清洗过的整齐文本,但线上来的用户输入夹杂一堆表情、错字、无意义填充词,模型自然就懵了。
排查方法:把线上近期真实数据抽 100 条回来,人工打标后临时加入测试集,跑一版评测,看分数掉多少。如果掉得多,说明是 distribution shift 问题,接下来要做的是往测试集里持续补充线上日志样本,而不是死磕 prompt。
5.2 提示词越写越长,效果反而下降
很多人调 prompt 有个惯性:一看到 badcase 就加一句“千万不要……”,结果 prompt 越来越长,上下文被堆满,模型反而无所适从。我见过有人把提示词写到 3000 字,效果比 500 字的版本差了十万八千里。
一个比较科学的指标是“关键指令密度”——prompt 里有多少句是必不可少的规则,而不是为了安慰自己加的废话。如果你的 prompt 里出现超过十个“不要”,建议停下来重新写一版。尽量用正面指令去表达要求,负面约束只保留最高频、损失最大的那几条,比如“不要编造字段”“不要输出多余解释”。
5.3 小模型和大模型的失败模式不一样
沿用“大模型没问题的场景”,小模型可能直接不按格式来,或者对某些同义表达没有泛化。最典型的情况是:训练数据里“物流”出现得多,换了个说法叫“快递到哪了”,小模型就不认识了,但大模型根本没这个问题。
解决方案有两个方向。一是数据增强:把训练数据里关键表达做同义词替换/句式改写,增加多样性。二是降低任务难度:把复杂任务拆成更简单的子任务,分别训练专门的小模型。后者虽然费一些工程功夫,但往往更彻底。如果条件允许,也可以考虑在 1-3B 的模型上做同样实验,有的任务小模型效果并不差,选一个性价比最高的容量点。
5.4 评估判不准,指标和真实体验互相打架
这种情况经常出现在 LLM-as-a-judge 方案里。裁判模型有时候会偏好格式更好看的输出,哪怕语义有一点偏差;有时候会漏判某些微妙的错误。我的处理方式是给裁判模型设计一个非常严格的评测 prompt,并要求它先引用证据再给分。如果还是不稳,就退回人工抽检一部分样本,用人工结果校准自动指标。
5.5 测试集过拟合,指标虚高
这个前面提过一点。测 zzz 试集过拟合的典型症状是:同一个测试集上指标不断涨,但换一个新季度抽样的样本集,指标掉得厉害。根本原因是测试集被“调教”得太久,模型已经记住了这些样本的“正确答案模式”。对策是给测试集设定“保质期”,每月或每季度主动替换一部分样本,让测试集始终比模型“领先半步”。
6. 实操总结与个人心得
6.1 几个好用的工具和流程建议
整套流程做下来,我沉淀了一个固定动作清单:先建测试集 manifest → 写一个批量评测脚本 → 跑 baseline → 错误分类 → 逐条优化 prompt → 全量回归 → 数据生成与清洗 → 微调小模型 → 部署并持续回流 badcase。每个环节都有对应的工具可以辅助,不一定一步到位,但可以从最简单的脚本开始慢慢搭建。
评测脚本我现在一般用 Python 写,流程是读 manifest、并行调模型、落盘结果、跑规则校验、再调裁判模型打分、最后输出一个结构化的报告。具体实现不复杂,核心代码大概就一两百行,但省下来的时间非常可观。如果你不想从零写,promptfoo 这类开源工具也支持批量回归和对比,关键是让你能快速重复评测而不是靠“手感”。
6.2 微调数据里加一点“坏样本”,效果可能更好
这个经验可能跟很多人的直觉不一样。我第一次微调的时候,只保留了大模型成功输出的数据,结果小模型上线后遇到边缘情况很容易“膨胀”,给一些明显不合理的答案。后来我尝试在训练集里故意保留少量失败样本,把对应的正确输出改成“该情况无法判断,请转人工”的兜底回答,模型的鲁棒性反而提升了。
原因是小模型需要学习“什么时候该承认不会”,而不只是“拿到输入就开始编”。大模型天生有一点这种边界意识,但小模型不教它是学不会的。这类“拒绝回答”的数据不用多,几十条就够,能显著减少线上幻觉问题。
6.3 最后分享一点我的长期体会
如果让我总结这条链路里最核心的一句话,那就是:让测试集成为唯一的裁判。没有测试集,你调提示词是在猜;有了测试集,调提示词才变成了工程。优化好大模型之后,小模型的微调就有了可靠的数据源,整个流程像一条“知识流水线”,每一环都有明确的输入、输出和质量标准。
老实说,这条链路不是一天能搭完的。我第一次完整跑通大概花了两三周,其中有将近一半时间都耗在测试集整理和评测脚本调试上。但搭完之后,后面每次迭代新业务需求,都能复用这套基础设施,效率提升是十倍级别。如果你现在正处于“调 prompt 全靠灵感、上线全靠运气”的阶段,我的建议就是赶紧从第一个测试集建起,别等完美才开始。