2024 年我带一个小团队做企业客服 AI。立项会上老板的原话是:"让 AI 学会我们公司的产品知识。"我当时几乎没怎么想,脑子里蹦出来的第一个词就是"微调"——把客服系统里积累的几千条历史对话整理成训练数据,拿去微调一个模型,让它"学会"我们的业务。我们真的这么干了:三个人花了快三周清洗对话、对齐格式、跑微调任务。上线那天我挺有成就感,结果一周内就发现两个绕不开的问题。第一,产品文档一更新——比如某个套餐改了价格——微调出来的模型还在用旧知识,想让它知道新价格,只能把数据重新整理一遍、再微调一次。第二,它确实"语气"上像个客服了,客气、有条理,可一旦问到具体的退款政策、具体的价格数字,它还是会答错,而且错得很自信。我们又咬牙微调了两轮,才慢慢回过神来:我们从一开始就走错了路。我们真正要解决的根本不是"模型不会说人话",而是"模型不知道我们最新的、会变的那部分知识"。前者才是微调该干的事,后者应该交给 RAG。把知识检索这块抽出来改成 RAG 之后,产品文档一更新答案立刻就对,还能附上出处链接;微调最后只留下一个很小的用途——把回答的格式和语气固定下来。这篇文章,就是把这套"到底该选 RAG、微调,还是提示工程"的选型方法,连同我们踩过的那些弯路,完整梳理一遍。
问题背景
先把那次项目里我的误判和最后想明白的结论摆清楚,因为后面所有的选型逻辑,都是冲着纠正这个误判展开的。
需求:做一个企业客服 AI,要求它既能用我们公司的产品知识准确回答用户问题,又要语气专业、格式统一。
我当时的错误判断:"让 AI 学会我们的知识"="微调"。把历史对话喂进去训练,模型就"懂"我们业务了。
真相:微调改变的是模型的"行为和风格",不是往模型脑子里"灌知识库"。微调能让模型学会"像我们的客服那样说话",但它并不擅长记住成百上千条精确、易变的事实(价格、政策、库存)。想让模型用上最新的、会变的知识,正确的工具是 RAG——运行时把知识检索出来递给它,而不是把知识烤进模型权重里。
要做对这个选型,得先把三条技术路线和它们各自解决的问题分清楚:
- 提示工程——不改模型、不接外部数据,纯靠把指令、示例和上下文写好,改变模型的输出;
- RAG(检索增强生成)——不改模型,运行时从外部知识库检索相关资料,拼进提示词一起给模型;
- 微调(Fine-tuning)——用你自己的数据继续训练模型,改变模型权重本身。
一句话区分它们:提示工程改的是"你怎么问",RAG 改的是"给模型看什么材料",微调改的是"模型本身"。这三者不是互斥的竞品,绝大多数成熟方案是它们的组合——但组合之前,你得先知道每一个分别擅长什么。
一、三条路线先认清:它们各自在解决什么
选型出错,几乎都源于把这三条路线的能力边界搞混了。最典型的就是我犯的那个错:以为"让模型掌握知识"只能靠微调。所以第一步,先把每条路线能干什么、不能干什么钉清楚。
提示工程是成本最低的一条路:不训练、不部署额外服务,只是把发给模型的那段文字写得更好——更明确的指令、几个示范例子(few-shot)、规定好的输出格式。它的天花板是模型本身已经具备的能力,你只是在"把它已有的能力调用得更准"。
RAG解决的是"知识"问题。模型不知道的事实——公司内部资料、训练截止之后的新闻、冷门细分领域——通过检索从外部知识库取出来,作为材料喂给模型。模型的角色从"凭记忆答题"变成"看着材料做阅读理解"。知识库随时能更新,改一条文档,下一次回答立刻生效。
微调解决的是"行为"问题。当你需要模型稳定地输出某种风格、某种格式,或者掌握一种用提示词很难讲清楚的隐性能力时,用大量样例去继续训练它,把这种行为"固化"进权重。微调不擅长记忆精确事实,擅长的是塑造"模型的默认表现"。
下面这段代码是最朴素的"提示工程"——什么外部数据都不接,纯靠一个写得还算清楚的提示词调用模型。它是后面所有方案的对照基线:
from openai import OpenAI
client = OpenAI()
def prompt_only_answer(question: str) -> str:
"""纯提示工程:不接外部知识、不微调,只把指令写清楚。
这是成本最低的一条路,也是任何选型都该先跑一遍的基线。"""
system = (
"你是一个专业、简洁的客服助手。"
"回答要分点、口语化,不要超过 4 句话。"
"如果你不确定,直接说'这个我需要确认一下'。"
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": question},
],
temperature=0.3,
)
return resp.choices[0].message.content
# 通用问题它答得很好;但一问到"你们家的退款政策",
# 它要么说"需要确认一下",要么就开始编 —— 因为它压根没有这个知识。
print(prompt_only_answer("退款一般几天到账?"))
二、决策的第一刀:你缺的是"知识"还是"行为"
所有选型纠结,都可以先用一个问题切开:模型现在表现不好,是因为它"不知道某个事实",还是因为它"知道、但表现的方式不对"?这一刀切下去,方向基本就定了。
如果是知识问题——模型缺的是事实本身,公司内部数据、最新信息、专业细节,它压根没学过——那答案是 RAG。微调在这里几乎一定是错的:你没法靠训练让模型"记住"几千条会随时变动的精确事实,就算硬塞进去,文档一更新就全部作废。
如果是行为问题——模型其实知道答案,但输出的格式不对、语气不对、风格不稳定——那要看复杂度:简单的格式、语气要求,提示工程就能搞定;只有当这种行为很难用语言描述清楚、或者需要极度稳定时,才轮到微调上场。
还有一个容易被忽略的判断维度是知识的变化频率。同样是"知识",一年都不变的稳定知识(比如基础的产品介绍)可以考虑微调进去;但只要这块知识会频繁变(价格、库存、政策),就只能 RAG,因为微调的迭代周期根本追不上知识的变化速度。
把这套判断逻辑写成代码,就是一个朴素的决策函数。它不复杂,但能逼你在动手前先想清楚问题的性质:
def decide_approach(problem: dict) -> str:
"""根据问题的性质给出选型建议。
problem 里几个关键判断维度:
- gap: 'knowledge'(缺知识) 还是 'behavior'(行为不对)
- volatility: 知识变化频率 'high' / 'low'
- traceable: 是否需要答案可追溯到出处
- behavior_describable: 想要的行为能否用文字讲清楚
"""
if problem["gap"] == "knowledge":
# 缺的是事实 —— 几乎总是 RAG,微调追不上知识更新
if problem["volatility"] == "high" or problem["traceable"]:
return "RAG:知识会变 / 要可追溯,只能运行时检索"
return "RAG 优先;知识极稳定且不大时可考虑微调进模型"
# gap == 'behavior' —— 缺的是表现方式
if problem["behavior_describable"]:
# 行为能用一段话说清楚 —— 提示工程足够,别动微调
return "提示工程:把要求写进 system prompt + few-shot 示例"
# 行为是隐性的、难以言说的(独特文风 / 复杂结构化输出)
return "微调:用大量样例把难以描述的行为固化进权重"
# 我们那次客服项目,真正的问题其实是这个:
print(decide_approach({
"gap": "knowledge", "volatility": "high",
"traceable": True, "behavior_describable": True,
}))
# 输出指向 RAG —— 而我们一开始却选了微调,这就是弯路的起点。
三、RAG 适合什么:知识会变、要可追溯、规模大
把第二节的判断落到 RAG 这条线上,它最擅长的就是三类场景,而这三类恰好都是微调的短板。
第一,知识会频繁变化。价格、政策、库存、最新文档——这类知识今天对、下个月就可能错。RAG 把知识放在外部知识库里,改一条文档就立刻生效,完全不用碰模型。微调想做到这一点,得重新整理数据、重新训练,周期以天甚至周计。
第二,答案需要可追溯。客服、法务、医疗这类场景,用户(和你自己)都需要知道"这个答案的依据是哪份文档"。RAG 天然能做到:检索出来的每个片段都带着来源,回答时把出处一并标出。微调把知识揉进了权重,你根本没法问模型"你这句话是从哪学来的"。
第三,知识库规模大。成千上万份文档的知识,不可能也不该全部塞进模型权重或提示词。RAG 每次只检索出与当前问题最相关的几个片段,用多少取多少,既省 token 又精准。
一轮标准 RAG 的核心是两步:把问题向量化后在向量库里检索相关片段,再把片段拼进提示词。下面是检索那一步的结构化实现——关键是每个片段都带着可追溯的来源:
from typing import List, Dict
def embed(text: str) -> List[float]:
"""把文本转成向量。真实实现调用 embedding 模型。"""
resp = client.embeddings.create(
model="text-embedding-3-small", input=text,
)
return resp.data[0].embedding
def retrieve(question: str, top_k: int = 4) -> List[Dict]:
"""问题向量化,在向量库里检索 top-k 个最相关的文档片段。
每个片段必须带 source —— 这是 RAG '可追溯' 的根。"""
query_vec = embed(question)
# hits = vector_store.search(query_vec, top_k=top_k)
hits = [
{"source": "退款政策v3.md",
"text": "退款审核通过后,款项 3-5 个工作日内原路退回。"},
{"source": "退款政策v3.md",
"text": "已激活的虚拟商品不支持退款。"},
]
return hits[:top_k]
def rag_answer(question: str) -> str:
"""RAG 完整问答:检索材料 -> 拼进提示词 -> 让模型照材料回答。
模型从'凭记忆答题'变成'看着材料做阅读理解'。"""
docs = retrieve(question)
context = "\n".join(f"[{d['source']}] {d['text']}" for d in docs)
system = (
"你是客服助手。只能依据【已知资料】回答,"
"资料里没有就说'这个我需要确认一下'。"
"每条结论后用方括号标注来源文件名。"
)
user = f"【已知资料】\n{context}\n\n【用户问题】{question}"
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
temperature=0.0,
)
return resp.choices[0].message.content
print(rag_answer("退款一般几天到账?"))
# 现在它会照着资料答"3-5 个工作日 [退款政策v3.md]" ——
# 政策文档一改,下次回答立刻跟着变,不用碰模型。
四、微调适合什么:固定风格、固定格式、压缩提示词
说清了 RAG,再回头看微调真正的价值——它不是"灌知识"的工具,而是"塑造行为"的工具。具体落到三个场景上。
第一,固定一种难以描述的风格。有些风格你没法用一段提示词讲清楚,比如"我们品牌特有的那种说话方式"。这种隐性的东西,用几百上千条示范样例去微调,模型能学到提示词永远写不出来的语感。
第二,稳定输出一种复杂格式。当你需要模型每次都吐出严格结构化的结果(特定的 JSON schema、特定的字段顺序),提示工程能做到大概八九成稳定,微调能把它推到接近百分之百。
第三,压缩提示词、降低长期成本。这点常被忽略:如果你的 system prompt 里塞了几百行规则和十几个 few-shot 示例,每次调用都要为这堆 token 付费。把这些规则"微调"进模型后,提示词可以大幅瘦身——前期训练花一笔钱,长期每次调用都省 token。调用量足够大时,这笔账是划算的。
微调的第一步永远是准备训练数据,格式是 JSONL,每行一个完整对话样例。数据质量直接决定微调成败——下面是构造和校验训练数据的代码:
import json
def build_training_example(user_msg: str, ideal_reply: str) -> dict:
"""构造一条微调训练样例。微调学的是'输入长这样时,
理想输出长那样' —— 所以 ideal_reply 必须是你心目中的标准答案。"""
return {
"messages": [
{"role": "system", "content": "你是客服助手。"},
{"role": "user", "content": user_msg},
{"role": "assistant", "content": ideal_reply},
]
}
def dump_jsonl(examples: List[dict], path: str) -> int:
"""把样例写成 JSONL —— 微调 API 要求的格式,每行一个 JSON。"""
valid = 0
with open(path, "w", encoding="utf-8") as f:
for ex in examples:
# 校验:每条至少要有 user 和 assistant,否则学不到映射
roles = {m["role"] for m in ex["messages"]}
if not {"user", "assistant"}.issubset(roles):
continue
f.write(json.dumps(ex, ensure_ascii=False) + "\n")
valid += 1
return valid
examples = [
build_training_example("退款多久到账?",
"您好~退款审核通过后 3-5 个工作日原路退回哦。"),
build_training_example("怎么联系人工?",
"您好~点击对话框右下角'转人工'就可以啦。"),
]
n = dump_jsonl(examples, "train.jsonl")
print(f"有效训练样例 {n} 条") # 真实微调建议至少几百条起步
def submit_finetune(train_path: str) -> str:
"""提交微调任务:上传训练文件,创建 fine-tuning job。
任务是异步的,要轮询状态,完成后会得到一个专属模型 ID。"""
file = client.files.create(
file=open(train_path, "rb"), purpose="fine-tune",
)
job = client.fine_tuning.jobs.create(
training_file=file.id, model="gpt-4o-mini-2024-07-18",
)
return job.id # 用这个 id 轮询 client.fine_tuning.jobs.retrieve(id)
def use_finetuned_model(model_id: str, question: str) -> str:
"""微调完成后,像普通模型一样调用,只是 model 换成专属 ID。
此时风格和格式已固化进权重,提示词可以写得很短。"""
resp = client.chat.completions.create(
model=model_id, # 形如 ft:gpt-4o-mini:org::xxxx
messages=[{"role": "user", "content": question}],
temperature=0.3,
)
return resp.choices[0].message.content
# job_id = submit_finetune("train.jsonl")
# 关键认知:微调让模型学会了'我们的语气',
# 但它依然不知道'最新退款政策' —— 知识那部分仍然得靠 RAG。
五、提示工程能走多远:别低估这条最便宜的路
讲完 RAG 和微调,得专门给提示工程正名:它是三条路里最便宜的,也是最被低估的。我见过太多团队一上来就上微调,最后发现提示工程加 few-shot 就能解决八成需求,白白多花了部署和训练的成本。
提示工程真正的发力点有三个。一是用 few-shot 示例替代微调:想让模型输出某种格式或语气,先别急着训练,在提示词里放 3-5 个示范例子,模型的模仿能力比你想象的强得多。二是用结构化提示词约束输出:明确的角色设定、分点的规则、规定死的输出格式,能把模型的行为约束得相当稳。三是它能和另外两条路自由叠加:RAG 的提示词需要精心设计,微调后的模型也还要靠提示词调用,提示工程是贯穿始终的底层功夫。
选型上有一条朴素的优先级:能用提示工程解决的,就不要上 RAG;能用 RAG 解决的,就不要上微调。每往后一步,系统复杂度和维护成本都显著上一个台阶。下面是用 few-shot 把"教模型一种输出风格"这件事,在不微调的前提下做出来的写法:
def few_shot_answer(question: str) -> str:
"""用 few-shot 示例代替微调:把'理想输出长什么样'
直接以例子的形式放进提示词,模型照着模仿。
这能解决相当一部分本来想靠微调解决的'风格/格式'问题。"""
messages = [
{"role": "system", "content": "你是客服助手,语气亲切、回答简短。"},
# ↓↓↓ 下面这几轮就是 few-shot 示例:示范'问题→理想回答'
{"role": "user", "content": "怎么改绑定手机?"},
{"role": "assistant", "content": "您好~在'设置-账号安全'里就能改绑手机哦。"},
{"role": "user", "content": "发票怎么开?"},
{"role": "assistant", "content": "您好~订单详情页点'申请发票',1 个工作日内开具哦。"},
# ↑↑↑ 示例结束,下面才是真正要回答的问题
{"role": "user", "content": question},
]
resp = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, temperature=0.3,
)
return resp.choices[0].message.content
print(few_shot_answer("怎么注销账号?"))
# 模型会自动模仿示例的语气和长度 —— 没花一分钱训练,
# 就拿到了接近微调的'风格统一'效果。
六、工程坑:成本、数据质量、评测、组合方案
把选型落到生产环境,还有几个绕不开的工程问题。它们大多不是"技术选型"本身,而是选型之后的执行纪律——而恰恰是这些地方,决定了项目是顺利还是返工。
坑 1:只算微调的训练费,不算迭代成本。微调真正贵的不是那一次训练账单,而是迭代成本:每次你的需求变了、数据更新了,就要重新整理数据、重新训练、重新评测、重新上线一整套。RAG 改一条文档就生效,提示工程改几行字就生效。把"未来要改多少次"算进去,微调往往比首次报价贵得多。
坑 2:微调数据质量决定一切,垃圾进垃圾出。微调是"模型照着你的样例学",样例里有多少错误、多少不一致的风格,模型就学走多少。直接拿历史客服对话当训练数据是高危操作——里面混着大量答错的、敷衍的、风格不一的回复。数据必须人工筛选、清洗、对齐,这部分工作量通常被严重低估。
坑 3:没有评测集就别动手。无论选哪条路,你都需要一个固定的评测集——几十到上百个带标准答案的问题。没有它,你根本无法判断"换成 RAG 之后到底是变好还是变坏",所有优化都变成凭感觉。评测集应该在动手做方案之前就建好。
坑 4:真实方案几乎总是组合,不是单选。"RAG 还是微调"这个问题本身就有点伪——成熟方案通常是:RAG 负责知识(可更新、可追溯),轻度微调或 few-shot 负责风格和格式,提示工程贯穿始终把它们粘合起来。我们那个客服项目最终的形态就是如此:RAG 管知识,一点点 few-shot 管语气,谁也没有取代谁。
下面这段代码把"坑 1"和"坑 3"变得可量化——一个粗略的成本对比,和一个用固定评测集打分的评估函数:
def estimate_cost(plan: str, monthly_calls: int,
knowledge_updates_per_month: int) -> dict:
"""粗算三种方案的相对成本,帮助看清'迭代成本'这个隐藏项。
单位是相对值,不是真实货币 —— 重点是看结构,不是看绝对数。"""
# 每次知识更新的代价:微调要重训,RAG/提示工程几乎为 0
rebuild = {"prompt": 1, "rag": 1, "finetune": 800}[plan]
# 每次调用的提示词开销:微调后提示词最短,RAG 因拼接材料最长
per_call = {"prompt": 3, "rag": 5, "finetune": 1}[plan]
setup = {"prompt": 10, "rag": 200, "finetune": 1500}[plan]
update_cost = rebuild * knowledge_updates_per_month
call_cost = per_call * monthly_calls / 1000
return {"plan": plan, "一次性搭建": setup,
"每月调用成本": round(call_cost, 1),
"每月知识更新成本": update_cost,
"每月合计": round(call_cost + update_cost, 1)}
# 知识更新频繁时,微调的'每月知识更新成本'会非常刺眼
for p in ("prompt", "rag", "finetune"):
print(estimate_cost(p, monthly_calls=100_000,
knowledge_updates_per_month=20))
def evaluate(answer_fn, test_set: List[Dict]) -> float:
"""用固定评测集给一个方案打分,返回准确率。
answer_fn 是被测方案(prompt_only_answer / rag_answer / ...),
每次改方案都跑一遍,用这个数字客观比较,而不是凭感觉。"""
hit = 0
for case in test_set:
got = answer_fn(case["question"])
# 简化判定:标准答案的关键词是否都出现在回答里
if all(kw in got for kw in case["must_include"]):
hit += 1
return hit / len(test_set)
test_set = [
{"question": "退款几天到账?", "must_include": ["3-5", "工作日"]},
{"question": "激活的虚拟商品能退吗?", "must_include": ["不支持"]},
]
# baseline = evaluate(prompt_only_answer, test_set)
# with_rag = evaluate(rag_answer, test_set)
# print(f"纯提示 {baseline:.0%} -> 加 RAG {with_rag:.0%}")
# 没有这两个数字,你永远不知道选型到底有没有选对。
选型对照速查
| 维度 | 提示工程 | RAG | 微调 |
|---|---|---|---|
| 解决什么 | 怎么问 | 给模型看什么材料 | 模型本身的行为 |
| 擅长 | 调用已有能力 | 注入会变的知识 | 固定风格 / 格式 |
| 知识可更新 | 不涉及 | 改文档即生效 | 需重新训练 |
| 答案可追溯 | 否 | 是(带出处) | 否 |
| 一次性成本 | 极低 | 中(建知识库) | 高(整理数据+训练) |
| 迭代成本 | 极低 | 低 | 高 |
| 典型场景 | 格式 / 语气微调 | 知识问答 / 客服 | 独特文风 / 复杂结构化输出 |
| 选型优先级 | 先试它 | 缺知识时上 | 前两者都不够才上 |
避坑清单
- "让 AI 学会我们的知识"不等于微调,微调改的是行为和风格,不擅长记忆精确、易变的事实。
- 选型第一刀:先判断模型缺的是"知识"还是"行为",知识问题走 RAG,行为问题走提示工程或微调。
- 知识只要会频繁变化(价格、政策、库存),就只能用 RAG,微调的迭代周期追不上知识更新速度。
- 需要答案可追溯到出处的场景(客服、法务、医疗)必须用 RAG,微调把知识揉进权重后无法溯源。
- 选型优先级:能用提示工程就别上 RAG,能用 RAG 就别上微调,每往后一步复杂度和维护成本都上一个台阶。
- 想固定输出风格或格式,先用 few-shot 示例试,大量需求不微调就能解决,别一上来就训练。
- 微调的真实成本是迭代成本,每次需求或数据变动都要重训重测重上线,把这笔账算进去再决策。
- 微调数据质量决定一切,别直接拿历史客服对话当训练集,里面混着答错的和风格不一的样本,必须人工清洗。
- 无论选哪条路,动手前先建好固定评测集,没有量化分数,所有优化都是凭感觉。
- 真实方案几乎都是组合:RAG 管知识、微调或 few-shot 管风格、提示工程粘合,不要把它们当互斥的单选题。
总结
回头看那个客服项目,我们最贵的一笔学费不是花在重新微调上,而是花在最开始那个想当然的等号上——"让 AI 学会我们的知识 = 微调"。这个等号看起来天经地义,实际上把两件完全不同的事混成了一件。"学会知识"里其实藏着两层:一层是"知道某些事实",另一层是"用某种方式把它说出来"。微调能管第二层,管不了第一层;而我们当时最痛的恰恰是第一层。把这个等号拆开,选型的迷雾就散了一大半。
所以做大模型落地选型,真正的第一步不是去比较 RAG 和微调谁更先进,而是先把自己的问题问清楚:模型现在表现不好,到底是因为它"不知道",还是因为它"知道但说不好"?这两种病因的现象有时很像——都是答得不满意——但药方完全相反。不知道,就把知识检索出来递给它(RAG);知道但说不好,就看这个"说不好"能不能用一段话讲清楚,能讲清楚用提示工程,讲不清楚才用微调。判断清楚病因,比记住任何技术细节都重要。
这三条路线的关系,也不该理解成"三选一的擂台赛"。它们更像工具箱里的三件工具:提示工程是那把最顺手、随时先抄起来试的螺丝刀;RAG 是接外部知识的管线;微调是改造工具本身的机床。一个成熟的方案,通常是三件工具各司其职——RAG 负责把会变的知识喂进来并留下出处,少量 few-shot 或轻度微调负责把语气和格式定住,而提示工程像水泥一样,把这一切粘合成一个能用的系统。问"该选哪个"常常问错了,该问的是"这一块用哪个、那一块用哪个"。
最后想说的是,这套选型思维和我们做传统系统设计其实是一脉相承的。我们不会因为数据库流行就把所有东西都塞进数据库,也不会因为缓存快就给所有数据加缓存——我们会先分析数据的特征:它多大、变多频繁、要不要强一致,再决定放哪。大模型选型也是同一回事:先分析你的"知识"和"行为"各自的特征——变不变、要不要溯源、能不能描述清楚——再决定哪部分交给 RAG、哪部分交给微调、哪部分一句提示词就够了。技术名词会不断更新,但"先理解问题的性质、再匹配工具"这个朴素的工程判断,什么时候都不会过时。下一次再有人对你说"让 AI 学会我们的知识",你就能先停一秒,问一句:你说的"学会",到底是哪一种?
—— 别看了 · 2026