「模型不懂我们的业务」是今年最常听到的立项理由,紧接着的一句通常是「所以我们准备微调」。
这两个判断之间其实缺了一步:模型不懂的到底是知识,还是行为。知识是「退款几天到账」,行为是「回答时先给结论再给依据、金额必须带币种、不确定就拒答」。前者靠检索喂进去,后者靠微调或 prompt 固化。搞反了,钱和时间都白花。
微调改的是行为,不是事实#
一个被反复验证的结论:微调能让模型学会「怎么说」,很难让它可靠地记住「是什么」。
原因是训练目标。微调优化的是在样本分布上生成下一个 token 的似然,模型学到的是句式、语气、格式和判断倾向。你往训练集里塞两百条产品条款,它可能记住一部分,但没有任何机制保证查询时能准确复述,更不会在条款更新后自动同步。
而 RAG 把事实放在上下文里,改一条文档就改一次行为,不需要重训。代价是每次请求都要带上下文,成本和延迟都更高。
所以判断的第一问不是「数据够不够」,而是「我要改变的是知识还是行为」。答案若是行为,才继续往下走。
决策表#
| 判断维度 | 倾向 RAG | 倾向微调 |
|---|---|---|
| 需求本质 | 补充事实、条款、时效信息 | 固定格式、语气、判断标准 |
| 更新频率 | 内容每周都在变 | 规则半年不变 |
| 可解释性 | 必须给出处、能追溯 | 出处不重要,结果对就行 |
| 数据量 | 有文档,缺标注 | 有几百到几千条高质量输入输出 |
| 迭代速度 | 当天改当天生效 | 能接受重新训练与回归 |
| 输出稳定性 | 格式靠 prompt 约束 | prompt 怎么调都漂移 |
| 成本敏感度 | 能接受长上下文 | 想压短 prompt、降延迟 |
多数真实需求落在中间:知识要 RAG,格式要微调,两件事同时存在。这时候先做 RAG,把知识供给做对,再看剩下多少错误是格式问题。
三个常见误判#
把「答得不好」归因于模型能力。检索没召回、切片切碎、上下文塞太满,都会表现为「模型不懂」。先修检索,往往比微调便宜一个数量级,而且当天见效。
用微调补知识缺口。训练集里写「支持三十天退货」,线上政策改成七天,模型不会知道。它会把旧答案说得更自信,因为微调同时提升了「遵循训练分布」的倾向。
忽略灾难性遗忘。几百条垂直样本训下去,通用能力会掉,尤其是长上下文和指令遵循。上线前必须用一份通用评测集做前后对比,不能只看垂类任务涨了。
微调数据里的隐私和合规问题比想象中严重:客服对话里常有手机号、地址、订单号,直接进训练集就是把它们写进权重。清洗要在构建数据集那一步做,不要指望事后补救。
先跑 prompt 基线,再决定要不要训#
在动手准备数据之前,先花半天做三件事:
- 用 few-shot 加结构化输出,把格式要求写死;
- 用检索把事实补上;
- 在同一批样本上评估。
如果这套组合的可用率已经到八成,剩下的两成大概率不值得一个训练流水线。如果格式仍然漂移、拒答行为不稳定,微调才有明确的目标。
微调的最小可行路径是「小数据 + 严格验收」:两百到五百条高质量样本,LoRA 这类参数高效方法,留出同分布的验证集和一份通用能力评测集,训完两边都跑。
至于数据准备,麻烦的从来不是训练,而是清洗和去重。下面这段代码把 QA 对整理成对话格式,同时做空值、超长和重复检查。
"""build_sft.py —— 把 QA 对整理成对话格式训练集,并做训练前体检(纯标准库)"""
import json
from collections import Counter
RAW = [
{"q": "退款多久到账", "a": "原路退回,通常 1-3 个工作日。", "src": "faq-12"},
{"q": "退款多久到账", "a": "原路退回,通常 1-3 个工作日。", "src": "faq-12"}, # 完全重复
{"q": "退款几天能到账?", "a": "原路退回,通常 1-3 个工作日。", "src": "faq-12"},
{"q": "", "a": "无效样本", "src": "faq-13"}, # 空问题
]
SYSTEM = "你是客服助手,只根据提供的知识回答;没有依据时明确说不知道。"
def norm(s):
return "".join(ch for ch in s.lower() if ch.isalnum())
def to_record(item):
return {"messages": [{"role": "system", "content": SYSTEM},
{"role": "user", "content": item["q"].strip()},
{"role": "assistant", "content": item["a"].strip()}]}
def build(raw, max_chars=1200):
seen, kept, dropped = set(), [], Counter()
for item in raw:
q, a = item.get("q", "").strip(), item.get("a", "").strip()
if not (q and a) or len(q) + len(a) > max_chars: # 空字段与超长一并丢弃
dropped["空或超长"] += 1
continue
key = norm(q + a) # 问题与答案同时重复才算重复
if key in seen:
dropped["重复"] += 1
continue
seen.add(key)
kept.append(to_record(item))
return kept, dropped
if __name__ == "__main__":
records, dropped = build(RAW)
with open("train.jsonl", "w", encoding="utf-8") as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
print(f"保留 {len(records)} 条,丢弃 {dict(dropped)}")
注意 key 用的是问题加答案:同一个答案配不同的问法要保留,这是让模型学会识别同义提问的关键;只有问答都重复才丢掉。
混合方案通常才是答案#
实际项目里最常见的落地形态是混合:检索负责事实,模型或轻量分类器负责路由与表达。
举个例子。客服助手先用小模型判断问题类型(退货、发票、物流),再按类型选择不同的检索集合和回答模板,最后由大模型基于检索结果生成。这里的「判断类型」适合微调,它是一个样本充足、边界固定的分类任务;「产品条款」适合检索,因为它每周都在变。
拆开之后还有一个实际好处:两部分的回归可以分开做。检索错了看召回指标,分类错了看混淆矩阵,不用对着一个整体准确率猜是哪里退步了。
结论#
- 先分清要补的是知识还是行为,知识走 RAG,行为才考虑微调。
- 内容频繁变化、必须给出处的场景,微调不是合适工具。
- 微调前先跑 prompt 加检索的基线,八成可用率就意味着不必上训练。
- 训练集的质量取决于清洗,去重、脱敏、超长过滤要在构建阶段完成。
- 训完必须同时跑垂类评测和通用能力评测,只看前者会漏掉遗忘。
决策顺序比工具重要:先问改变什么,再问数据够不够,最后才是选方法。
完