RAG vs. Fine-Tuning for Domain Adaptation: When to Use Which
TL;DR · AI 摘要
RAG vs. Fine-Tuning for Domain Adaptation: When to Use Which - MachineLearningMastery.com RAG vs. Fine-Tuning for Domain...
核心要点
- 主题聚焦:RAG vs. Fine-Tuning for Domain Adaptation: When
- 来源:Machine Learning Mastery,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
RAG 与微调在领域适应中的对比:何时使用哪种方法 - MachineLearningMastery.com
RAG 与微调在领域适应中的对比:何时使用哪种方法
By
Shittu Olumide
on
2026年9月23日
in
语言模型
0
分享
发布
在本文中,你将学习检索增强生成(RAG)与微调之间的机械差异,了解每种技术适用的场景,以及如何判断你的生产系统实际需要使用哪种方法,或者两者都需要。
我们将涵盖的主题包括:
- 从机械层面解释 RAG 和微调各自的功能及局限性。
- 两个完整且可运行的代码示例——一个用于知识检索场景的 RAG 管道,一个用于结构化输出场景的 LoRA 微调设置。
- 一个具体的六点决策框架,用于在 RAG、微调或两者之间做出选择。
目前,RAG 与微调是应用大语言模型(LLM)工作中最受关注且争议最大的权衡之一,而大多数讨论都停留在错误的抽象层次。这个问题常被简化为非此即彼的单一选择,但2026年的实际情况是,约60%的生产级大语言模型部署同时使用两者,不是因为团队无法决策,而是因为检索增强生成和微调分别解决了两个截然不同的问题,而大多数实际的领域适应项目同时面临这两个问题。
本文将彻底打破这种错误的二元对立:从机械层面解释每种技术的实际作用,提供两个完整、可运行的真实示例(每种方法各一个),然后给出一个具体的决策框架,帮助你判断特定项目实际需要哪种方法,以及何时诚实的答案是两者都需要。
RAG 究竟是什么(以及不是什么)
检索增强生成(RAG)完全不改变模型本身。模型的权重永远不会变化;变化的是模型在回答问题时上下文窗口中看到的内容。检索步骤会在知识库中搜索,提取最相关的文档,并将它们与用户的查询一起传递给模型,使模型在回答问题时面前有参考文档,而不是仅依赖记忆。
这种机制使 RAG 真正擅长解决某一类问题:信息量大、动态变化或两者兼有的场景。但 RAG 无法解决模型的基本行为问题。如果模型的语气不一致,如果它无法可靠地遵循严格的输出格式,或者如果它错误地使用了你所在行业的术语,那么在推理时向模型输入更多文档对这些问题毫无影响,因为问题本身从来就不是信息缺失导致的。
微调究竟是什么(以及不是什么)
微调则恰恰相反:它会改变模型本身,通过在真实输入/输出行为的示例上训练权重,直到这种行为成为模型的默认表现。此时无需在推理时注入任何内容,因为模式已经内化。LoRA 和 QLoRA 是大多数项目的标准方法,通过训练一个小型适配器(通常少于基础模型总参数的1%)来实现,而不是训练整个模型,这使得微调的成本降至几百美元和几小时,而不是完整的重新训练项目。
以下是关键要点:必须明确理解这一点,因为这是整个讨论中最常见的误解,且多个独立来源都使用相同的措辞得出相同结论——微调并不能可靠地增加事实知识。对医学文献堆栈进行微调的模型并不会像真正的检索系统那样“知道”文献中的事实——它只会调整风格、结构和模式识别,但对训练数据的事实记忆不可靠,尤其是对具体事实的回忆。微调是一种行为工具,而非知识工具。请牢记这一区别,因为以下两个示例正是为了直接演示这一区别,而非仅仅断言。
检索增强生成(RAG)
场景:内部工程团队希望使用自然语言向其事件运行手册和事后分析文档提问——这些文档会随着新事件的发生不断添加和修改。这属于典型的RAG场景:知识每周都会更新,且每个答案都必须能追溯到真实来源文档,以便凌晨2点调试时进行核查。
前提条件:
- Python 3.10+
- pip install scikit-learn anthropic
- 一个Anthropic API密钥
首先,文档本身——一套小型但真实的运行手册和事后分析文档:
documents.py DOCUMENTS = [ { "id": "runbook-db-failover-001", "title": "Database Failover Runbook", "text": ( "当主Postgres实例无响应时,首先通过`SELECT now() - pg_last_xact_replay_timestamp()`检查备用实例的复制延迟。 如果延迟低于30秒,使用`pg_ctl promote`提升备用实例。 提升后立即更新配置服务中的连接字符串。 如果复制延迟超过5分钟,不要尝试手动故障转移,而是升级到数据库团队,因为提升过时的备用实例可能导致数据丢失。"), }, { "id": "postmortem-2026-03-outage", "title": "Postmortem: March 2026 Checkout Outage", "text": ( "根本原因是部署后支付服务的连接池耗尽,池大小从100个连接减少到20个连接。 修复方法是恢复池大小并添加最小池大小警报。 待办事项:连接池更改现在需要平台团队的第二位审核者在合并前进行审核。"), }, { "id": "runbook-oncall-escalation-003", "title": "On-Call Escalation Policy", "text": ( "主值班人员有15分钟时间确认告警,否则会升级到次级值班人员。 次级值班人员有10分钟时间确认,否则会升级到团队负责人。 任何影响结账或支付的事件会跳过正常升级流程,直接通知团队负责人,无论确认状态如何。"), }, # 为长度省略了其他文档,完整集合见共享代码文件 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
documents.py
DOCUMENTS
=
[
{
"id"
:
"runbook-db-failover-001"
,
"title"
"Database Failover Runbook"
"text"
(
"When the primary Postgres instance becomes unresponsive, first check "
"replication lag on the standby via SELECT now() - pg_last_xact_replay_timestamp(). "
"If lag is under 30 seconds, promote the standby using pg_ctl promote. "
"Update the connection string in the config service immediately after promotion. "
"Do not attempt manual failover if replication lag exceeds 5 minutes, escalate "
"转交给数据库团队处理,因为提升一个过时的备用节点可能会导致数据丢失。"
)
}
"postmortem-2026-03-outage"
"故障复盘:2026年3月结账服务中断"
"根本原因是支付服务在部署后连接池耗尽,该部署将连接池大小从100个连接减少到20个连接。修复方法是撤销该更改并添加最小连接池大小的告警。待办事项:连接池更改现在需要平台团队的第二位审核人员在合并前进行审核。"
"runbook-oncall-escalation-003"
"值班升级政策"
"主值班人员有15分钟时间确认告警,否则升级至备值班人员。备值班人员有10分钟时间,否则升级至团队负责人。任何影响结账或支付的事件将跳过正常的升级流程,直接通知团队负责人,无论是否已确认。"
此处省略其他文档以控制长度,完整集合见共享代码文件
]
现在是分块和检索索引部分:
retrieval.py import re from dataclasses import dataclass from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity @dataclass class Chunk: doc_id: str title: str text: str def chunk_document(doc: dict, max_sentences: int = 2) -> list[Chunk]: """按句子边界拆分而非固定字符数,确保操作步骤不会在句子中间被截断。""" sentences = re.split(r"(?<=[.!?])\s+", doc["text"]) chunks = [] for i in range(0, len(sentences), max_sentences): chunk_text = " ".join(sentences[i:i + max_sentences]) chunks.append(Chunk(doc_id=doc["id"], title=doc["title"], text=chunk_text)) return chunks class RetrievalIndex: def __init__(self, documents: list[dict]): self.chunks = [chunk for doc in documents for chunk in chunk_document(doc)] self.vectorizer = TfidfVectorizer() self.chunk_vectors = self.vectorizer.fit_transform([c.text for c in self.chunks]) def search(self, query: str, top_k: int = 3) -> list[tuple[Chunk, float]]: query_vector = self.vectorizer.transform([query]) scores = cosine_similarity(query_vector, self.chunk_vectors)[0] ranked = sorted(zip(self.chunks, scores), key=lambda pair: pair[1], reverse=True) return ranked[:top_k]
retrieval.py
import
re
from
dataclasses
dataclass
sklearn
.
feature_extraction
text
TfidfVectorizer
metrics
pairwise
cosine
_
similarity
@
class
Chunk
doc_id
str
title
def
chunk_document
doc
dict
max_sentences
int
->
list
""
"Splits on sentence boundaries rather than a fixed character count,
so a procedure step never gets cut in half mid-sentence."
sentences
split
r
"(?<=[.!?])\s+"
chunks
for
i
range
len
chunk_text
" "
join
+
append
return
RetrievalIndex
__init__
self
vectorizer
chunk_vectors
fit_transform
c
search
query
top_k
tuple
float
query_vector
transform
scores
cosine_similarity
ranked
sorted
zip
key
lambda
pair
reverse
True
这使用TF-IDF加余弦相似度而非神经嵌入模型——这是一种合法的、完全本地的检索方法,无需外部模型下载或API调用即可构建,一些小型生产RAG系统仍直接使用这种方法。
最后是生成步骤,将检索到的片段转化为有引用依据的答案:
generate.py import os import anthropic SYSTEM_PROMPT = """You are an internal engineering assistant. Answer only using \ the provided source excerpts. Cite the source document ID for every claim in \ square brackets, like [runbook-db-failover-001]. If the sources don't contain \ the answer, say so explicitly rather than guessing.""" def answer_question(question: str, index: RetrievalIndex, top_k: int = 3) -> str: results = index.search(question, top_k=top_k) context = "\n\n".join(f"[Source: {c.title} ({c.doc_id})]\n{c.text}" for c, score in results) client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) response = client.messages.create( model="claude-sonnet-4-6", max_tokens=500, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": f"Sources:\n\n{context}\n\nQuestion: {question}"}], ) return "".join(block.text for block in response.content if block.type == "text")
generate.py
os
anthropic
SYSTEM_PROMPT
"You are an internal engineering assistant. Answer only using \
the provided source excerpts. Cite the source document ID for every claim in \
square brackets, like [runbook-db-failover-001]. If the sources don't contain \
the answer, say so explicitly rather than guessing."
answer_question
question
index
results
context
"\n\n"
f
"[Source: {c.title} ({c.doc_id})]\n{c.text}"
score
client
api_key
environ
"ANTHROPIC_API_KEY"
response
messages
create
model
"claude-sonnet-4-6"
max_tokens
500
system
"role"
"user"
"content"
"Sources:\n\n{context}\n\nQuestion: {question}"
block
content
if
type
==
系统提示明确禁止使用除检索到的来源以外的任何信息进行回答,并要求对每个主张都进行引用,这正是使RAG答案可审计的原因——读者可以将“直接联系团队负责人”追溯到runbook-oncall-escalation-003文档并阅读实际政策内容。
整个流程通过端到端的实时API调用进行了验证,确认检索到的上下文确实正确传递给了提示,引用也正确地出现在最终答案中——检索后生成的链式流程完全按照设计运行。
将API密钥导出为ANTHROPIC_API_KEY后,运行python generate.py,或导入answer_question函数并针对文档集提出任何问题。
为一致领域输出进行微调
这个场景(与上述场景在本质上完全不同)是这样的:一家金融服务公司需要将每一条客户投诉按照严格的内部分类体系进行归类(BILLING_DISPUTE、UNAUTHORIZED_TRANSACTION、ACCOUNT_ACCESS、FEE_INQUIRY、CARD_FRAUD_SUSPECTED),这些分类无法与任何公开标准一一对应,且每次都需要生成一致的结构化输出。这正是上述描述的典型场景:模型不需要学习新的世界知识,而是需要可靠地掌握这家公司的特定术语,并满足下游工单系统依赖的严格输出协议。系统提示可以礼貌地提出这种要求,但在高并发和边缘案例场景下,仅靠系统提示无法像实际在数据上训练过的权重那样保持高度一致性。
- pip install peft transformers(实际训练运行还需要bitsandbytes和CUDA GPU进行4位加载)
dataset.py
import json
CATEGORIES = [ "BILLING_DISPUTE", "UNAUTHORIZED_TRANSACTION", "ACCOUNT_ACCESS", "FEE_INQUIRY", "CARD_FRAUD_SUSPECTED", ]
def make_example(complaint_text: str, category: str, severity: int, requires_immediate_action: bool) -> dict: return { "messages": [ { "role": "system", "content": ( "将客户投诉分类到以下类别中的一个:" + ", ".join(CATEGORIES) + ". 返回包含 category、severity(1-5)和 requires_immediate_action(布尔值)的 JSON 对象。" ), }, {"role": "user", "content": complaint_text}, { "role": "assistant", "content": json.dumps( { "category": category, "severity": severity, "requires_immediate_action": requires_immediate_action, } ), }, ] }
TRAINING_EXAMPLES = [ make_example( "我在昨天的账单上看到一条金额为 $340 的未识别交易记录。", "UNAUTHORIZED_TRANSACTION", 4, True, ), make_example( "为什么我被收取了 $35 的透支费用?我以为我有透支保护服务。", "FEE_INQUIRY", 2, False, ), make_example( "今天早上有人在我所在州以外的加油站使用了我的信用卡,这并不是我本人。", "CARD_FRAUD_SUSPECTED", 5, True, ), ]
def validate_examples(examples: list[dict]) -> list[str]: """ 每个标签在训练前都会与真实分类体系进行核对。 在小规模微调数据集中,一个错误标记的示例可能占其整个类别训练信号的五分之一。 """ errors = [] for i, example in enumerate(examples): assistant_msg = next(m for m in example["messages"] if m["role"] == "assistant") parsed = json.loads(assistant_msg["content"]) if parsed.get("category") not in CATEGORIES: errors.append(f"示例 {i}: '{parsed.get('category')}' 不是有效的分类") severity = parsed.get("severity") if not isinstance(severity, int) or not (1 <= severity <= 5): errors.append(f"示例 {i}: severity 必须是 1-5 的整数,得到的是 {severity}") return errors
37
38
39
40
41
42
43
44
45
46
47
dataset.py
json
CATEGORIES
"BILLING_DISPUTE"
"UNAUTHORIZED_TRANSACTION"
"ACCOUNT_ACCESS"
"FEE_INQUIRY"
"CARD_FRAUD_SUSPECTED"
make_example
complaint_text
category
severity
requires_immediate_action
bool
"messages"
"system"
"将客户投诉分类到以下类别中的一个:"
", "
". 返回包含 category、severity(1-5)和 requires_immediate_action(布尔值)的 JSON 对象。"
"assistant"
dumps
"category"
"severity"
"requires_immediate_action"
TRAINING_EXAMPLES
"我在昨天的账单上看到一条金额为 $340 的未识别交易记录。"
"为什么我被收取了 $35 的透支费用?我以为我有透支保护服务。"
False
"今天早上有人在我所在州以外的加油站使用了我的信用卡,这并不是我本人。"
validate_examples
examples
"每个标签在训练前都会与真实分类体系进行核对。在小规模微调数据集中,一个错误标记的示例可能占其整个类别训练信号的五分之一。"
errors
example
enumerate
assistant_msg
next
m
parsed
loads
get
not
"示例 {i}: '{parsed.get('category')}' 不是有效的分类"
isinstance
or
<=
"示例 {i}: severity 必须是 1-5 的整数,得到的是 {severity}"
validate_examples 函数针对一个故意包含三个错误示例的集合进行了测试——一个无效分类、一个超出 1-5 范围的严重性值和一个非布尔标志——并成功检测到所有三个错误。在本使用场景中,这比在大型数据集中更重要:当每个分类仅有少量示例时,一个错误标签会占据该类别所有训练数据中相当大的比例。
from transformers import AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType
model = AutoModelForCausalLM.from_pretrained("your-base-model", load_in_4bit=True, device_map="auto")
lora_config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.1,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
task_type=TaskType.CAUSAL_LM,
)
peft_model = get_peft_model(model, lora_config)
peft_model.print_trainable_parameters()transformers AutoModelForCausalLM peft LoraConfig get_peft_model TaskType from_pretrained "your-base-model" load_in_4bit device_map "auto" lora_config lora_alpha lora_dropout 0.1 target_modules "q_proj" "k_proj" "v_proj" "o_proj" task_type CAUSAL_LM peft_model print_trainable_parameters
load_in_4bit=True 需要实际的 GPU 硬件支持。LoRA 包装机制已通过本地使用相同配置构建的小型模型架构直接验证,确认基础模型会正确冻结,仅适配器层保持可训练状态——在该测试中占总参数的 3.33%,其余模型权重保持不变。这就是此处微调成本低廉且快速的实际机制,而非完整的重新训练项目:您正在训练一个小型、专注的适配器,其基础模型保持冻结状态,而非训练整个模型。
使用场景决策框架
- 您的模型所需信息是否经常变化,或数据量过大无法放入提示中(如产品数据、当前政策、不断增长的文档集)?使用 RAG。
- 是否需要出于合规或审计原因将每个答案追溯到特定来源?使用 RAG,因为 RAG 的每个答案声明都可以追溯到特定检索片段,这在多个可解释 AI 输出的监管框架中被视为比微调模型输出更具实际意义的优势,后者需要单独的评估证据来证明相同特性。
- 您尚未拥有标注训练示例,或需要本周内上线而非下个月?使用 RAG——无论最终添加什么功能,它几乎总是实现首个可用版本的更快路径。
- 模型是否需要保持一致的语气、结构或词汇,而提示方法在大量数据下无法维持?进行微调。
- 如果您的延迟预算足够紧张,每次响应前的额外检索步骤会带来真实成本?进行微调,因为微调模型可以直接回答而无需检索步骤。
- 如果您的查询量足够高,使用较小的微调开源模型相比前沿 API 调用每个查询的成本会显著降低?进行微调——当查询量真正达到高规模时,每个查询的节省成本可能比大多数团队预期的更快抵消数据准备成本。
总结
剥离所有框架语言的真实决策规则:RAG 处理模型需要知道的内容,微调处理模型需要表现的方式,而将其视为非此即彼的选择是浪费数月时间构建错误方案的最可靠方式。从检索开始,因为它几乎总是实现实际成果的更快路径,仅在能明确指向特定、持续的行为问题时添加微调(即使更好的提示和检索方法均未能解决该问题),并预期——从一开始——对于严肃的生产系统,诚实答案很可能是两者都需要。
更多相关内容
- 向量数据库 vs. 图 RAG 在代理记忆中的应用:…
- 领域专用 LLM 的自定义微调
- 领域知识在机器学习中的作用:…
- Ollama 与 LM Studio 与 llama.cpp:哪种本地 AI…
- 结构化输出与函数调用:哪种…
- 机器学习与传统分析:何时使用…