第2113篇:微调还是RAG?——LLM知识注入方案的系统性选择指南
大约 10 分钟
第2113篇:微调还是RAG?——LLM知识注入方案的系统性选择指南
适读人群:需要给LLM注入领域知识的工程师 | 阅读时长:约19分钟 | 核心价值:明确微调和RAG各自的适用边界,避免选型错误导致的返工
"这个问题,是用微调还是RAG?"
这是AI工程师群里最常被问到的问题之一,也是最容易给错建议的问题。我见过太多团队花了三个月训练了一个微调模型,最后发现RAG就能解决;也见过另一些团队把RAG做得很复杂,但业务其实需要的是改变模型的回答风格——这是RAG做不了的。
这篇文章帮你系统性地理解两个方案的本质区别,给出明确的选择框架。
本质区别:知识存在哪里
/**
* 微调 vs RAG 的本质区别
*
* ===== 微调(Fine-tuning)=====
*
* 知识存储位置:模型权重(参数)
*
* 类比:重新训练一个大脑
* 知识变成了模型的"本能"
* 不需要任何外部查询,模型直接从"记忆"中调取
*
* 适合:
* ✓ 改变回答风格/格式(这是RAG做不到的)
* ✓ 注入相对稳定的专业知识(法律术语、医疗规范)
* ✓ 教会模型特定的任务范式(输出固定的JSON格式)
* ✓ 高频调用场景(每次调用节省prompt token成本)
*
* 不适合:
* ✗ 知识需要频繁更新(重新训练成本高)
* ✗ 数据量少(< 几百条高质量样本)
* ✗ 需要精确引用来源(模型不能说"根据文档第X页")
* ✗ 预算有限(需要GPU算力)
*
* ===== RAG(检索增强生成)=====
*
* 知识存储位置:外部知识库(数据库)
*
* 类比:给AI一个图书馆和搜索能力
* 模型本身不变,每次回答时从外部检索相关内容
*
* 适合:
* ✓ 知识需要频繁更新(新文档加入即生效)
* ✓ 数据量大(全量文档都能搜索)
* ✓ 需要引用来源(可以告诉用户"参考了哪份文档")
* ✓ 快速上线(不需要训练周期)
*
* 不适合:
* ✗ 改变模型行为风格(RAG只影响内容,不影响风格)
* ✗ 知识高度碎片化(难以有效检索)
* ✗ 对延迟极敏感(每次都要检索)
*/选择决策树
/**
* 选择框架:5个关键问题
*
* Q1:你是要改变模型的"行为方式",还是"知道什么"?
*
* 改变行为方式(始终用日语回答、输出特定JSON格式、
* 扮演特定角色)→ 必须用微调(RAG做不到)
*
* 改变知道什么(知道我们公司的产品、知道行业法规)
* → 继续往下看
*
* Q2:知识多久更新一次?
*
* 每周/每天更新 → RAG(微调跟不上更新速度)
* 每季度/每年更新 → 两者都可以
* 基本不变 → 微调有优势
*
* Q3:你有多少训练数据?
*
* < 100条 → RAG(数据太少微调效果差)
* 100-1000条 → 看数据质量,高质量可以微调
* > 1000条高质量数据 → 微调可以考虑
*
* Q4:用户需要知道答案来自哪里吗?
*
* 需要引用来源 → RAG(微调没有溯源能力)
* 不需要 → 两者都可以
*
* Q5:调用频率和成本敏感度?
*
* 极高频调用且对延迟敏感 → 微调
* (避免每次RAG检索的延迟和token成本)
*
* 正常频率 → 两者都可以
*/微调数据准备
/**
* 微调数据集构建
*
* 微调的效果90%取决于数据质量,不是数据量
*
* 数据格式:问题-回答对
* 最重要的是:回答的风格和质量要完全符合预期
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class FineTuningDatasetBuilder {
private final ChatLanguageModel baseModel;
/**
* 从现有文档构建微调数据集
*
* 思路:让LLM帮我们生成高质量的QA对
* 再人工筛选和修正
*/
public List<FineTuningExample> buildFromDocuments(
List<String> documents, DatasetConfig config) {
List<FineTuningExample> examples = new ArrayList<>();
for (String document : documents) {
List<FineTuningExample> docExamples = generateQaPairs(document, config);
examples.addAll(docExamples);
}
log.info("生成微调样本: total={}", examples.size());
return examples;
}
/**
* 从文档生成QA对
*/
private List<FineTuningExample> generateQaPairs(String document, DatasetConfig config) {
String prompt = """
请从以下文档中生成%d个问答对,用于训练AI模型。
文档内容:
%s
要求:
1. 问题要自然,像真实用户会问的
2. 回答要%s
3. 回答要准确,严格基于文档内容
4. 避免生成过于简单的问题("文档说了什么"这类)
返回JSON:
{
"pairs": [
{
"question": "问题",
"answer": "回答(按照要求的风格)"
}
]
}
只返回JSON。
""".formatted(
config.getPairsPerDocument(),
document,
config.getResponseStyle()
);
try {
String response = baseModel.generate(prompt);
return parseQaPairs(response, config);
} catch (Exception e) {
log.warn("QA对生成失败: {}", e.getMessage());
return List.of();
}
}
private List<FineTuningExample> parseQaPairs(String response, DatasetConfig config) {
try {
String json = extractJson(response);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
List<FineTuningExample> examples = new ArrayList<>();
for (JsonNode pair : root.path("pairs")) {
String question = pair.path("question").asText("").trim();
String answer = pair.path("answer").asText("").trim();
if (question.isEmpty() || answer.isEmpty()) continue;
examples.add(FineTuningExample.builder()
.systemPrompt(config.getSystemPrompt())
.userMessage(question)
.assistantResponse(answer)
.build());
}
return examples;
} catch (Exception e) {
log.warn("QA对解析失败: {}", e.getMessage());
return List.of();
}
}
/**
* 数据质量过滤
*
* 过滤掉明显低质量的样本
*/
public List<FineTuningExample> filterQuality(List<FineTuningExample> examples) {
return examples.stream()
.filter(e -> e.getUserMessage().length() >= 10) // 问题太短
.filter(e -> e.getAssistantResponse().length() >= 30) // 回答太短
.filter(e -> e.getAssistantResponse().length() <= 2000) // 回答太长
.filter(e -> !e.getUserMessage().equals(e.getAssistantResponse()))
.filter(e -> !containsHallucination(e))
.toList();
}
private boolean containsHallucination(FineTuningExample example) {
// 简单检测:如果回答里有"根据文档第X页"这类引用,但文档编号是假的,过滤掉
// 实际实现更复杂
return example.getAssistantResponse().contains("我不确定");
}
/**
* 导出为OpenAI微调格式(JSONL)
*/
public String exportToOpenAIFormat(List<FineTuningExample> examples) {
StringBuilder sb = new StringBuilder();
ObjectMapper mapper = new ObjectMapper();
for (FineTuningExample example : examples) {
Map<String, Object> trainingItem = new LinkedHashMap<>();
List<Map<String, String>> messages = new ArrayList<>();
if (example.getSystemPrompt() != null) {
messages.add(Map.of("role", "system", "content", example.getSystemPrompt()));
}
messages.add(Map.of("role", "user", "content", example.getUserMessage()));
messages.add(Map.of("role", "assistant", "content", example.getAssistantResponse()));
trainingItem.put("messages", messages);
try {
sb.append(mapper.writeValueAsString(trainingItem)).append("\n");
} catch (Exception e) {
log.warn("序列化失败: {}", e.getMessage());
}
}
return sb.toString();
}
private String extractJson(String s) {
int start = s.indexOf('{');
int end = s.lastIndexOf('}');
return (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
}
@Data
@Builder
public static class DatasetConfig {
@Builder.Default
private int pairsPerDocument = 5;
@Builder.Default
private String responseStyle = "简洁专业,不超过200字";
private String systemPrompt;
}
@Data
@Builder
public static class FineTuningExample {
private String systemPrompt;
private String userMessage;
private String assistantResponse;
}
}微调效果评估
/**
* 微调模型与基础模型的对比评估
*
* 微调上线前必须做的评估:
* 1. 在目标领域的准确性(核心指标)
* 2. 通用能力有没有退化(副作用检测)
* 3. 和RAG方案的对比(确认选型正确)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class FineTuningEvaluationService {
private final ChatLanguageModel baseModel;
private final ChatLanguageModel fineTunedModel;
private final OutputQualityEvaluator evaluator;
/**
* 域内性能对比(目标领域的问答质量)
*/
public DomainEvaluationResult evaluateDomainPerformance(
List<EvaluationDataset.EvaluationCase> domainTestCases) {
log.info("开始域内性能评估: cases={}", domainTestCases.size());
List<Double> baseScores = new ArrayList<>();
List<Double> ftScores = new ArrayList<>();
for (EvaluationDataset.EvaluationCase testCase : domainTestCases) {
// 基础模型回答
String baseOutput = baseModel.generate(testCase.getInput());
double baseScore = evaluator.evaluate(testCase, baseOutput).getScore();
baseScores.add(baseScore);
// 微调模型回答
String ftOutput = fineTunedModel.generate(testCase.getInput());
double ftScore = evaluator.evaluate(testCase, ftOutput).getScore();
ftScores.add(ftScore);
}
double baseAvg = baseScores.stream().mapToDouble(d -> d).average().orElse(0);
double ftAvg = ftScores.stream().mapToDouble(d -> d).average().orElse(0);
double improvement = ftAvg - baseAvg;
log.info("域内性能: base={:.3f}, fine-tuned={:.3f}, improvement={:+.3f}",
baseAvg, ftAvg, improvement);
return new DomainEvaluationResult(baseAvg, ftAvg, improvement,
improvement > 0.05 ? "IMPROVEMENT" :
improvement > 0 ? "SLIGHT_IMPROVEMENT" : "NO_IMPROVEMENT");
}
/**
* 通用能力退化检测
*
* 微调有时会导致"灾难性遗忘"——在目标领域提升,
* 但在其他领域性能下降。这里用通用QA数据集检测
*/
public RegressionResult checkGeneralCapabilityRegression(
List<EvaluationDataset.EvaluationCase> generalTestCases) {
// 在通用测试集上对比两个模型
List<Double> baseScores = new ArrayList<>();
List<Double> ftScores = new ArrayList<>();
for (EvaluationDataset.EvaluationCase testCase : generalTestCases) {
double baseScore = evaluator.evaluate(testCase,
baseModel.generate(testCase.getInput())).getScore();
double ftScore = evaluator.evaluate(testCase,
fineTunedModel.generate(testCase.getInput())).getScore();
baseScores.add(baseScore);
ftScores.add(ftScore);
}
double baseAvg = baseScores.stream().mapToDouble(d -> d).average().orElse(0);
double ftAvg = ftScores.stream().mapToDouble(d -> d).average().orElse(0);
double regression = baseAvg - ftAvg;
boolean hasSignificantRegression = regression > 0.1;
log.info("通用能力: base={:.3f}, fine-tuned={:.3f}, regression={:.3f}",
baseAvg, ftAvg, regression);
if (hasSignificantRegression) {
log.warn("检测到明显的通用能力退化!可能需要添加通用QA数据到训练集(数据混合)");
}
return new RegressionResult(baseAvg, ftAvg, regression, hasSignificantRegression);
}
record DomainEvaluationResult(double baseScore, double fineTunedScore,
double improvement, String verdict) {}
record RegressionResult(double baseScore, double fineTunedScore,
double regression, boolean hasSignificantRegression) {}
}RAG vs 微调混合方案
/**
* 在某些场景下,RAG和微调可以结合使用
*
* 典型混合方案:
*
* 方案A:微调负责风格,RAG负责内容
* 先微调模型,让它学会特定的回答风格和格式
* 运行时用RAG注入最新的知识内容
* 适合:需要特定专业风格 + 频繁更新知识的场景
*
* 方案B:微调负责分类路由,RAG负责回答
* 微调一个小分类模型,判断问题类型
* 根据类型选择对应的RAG知识库
* 适合:多领域知识库 + 需要精准路由的场景
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class HybridKnowledgeService {
private final ChatLanguageModel styledModel; // 微调了特定风格的模型
private final VectorStore knowledgeBase; // RAG知识库
private final EmbeddingModel embeddingModel;
/**
* 方案A:风格微调 + RAG内容注入
*
* 微调教会了模型怎么说话(格式、语气、专业术语使用方式)
* RAG提供了说什么(实时知识内容)
*/
public String answer(String userQuestion) {
// 1. 从RAG检索相关知识
float[] queryVector = embeddingModel.embed(userQuestion).content().vector();
List<VectorStore.SearchResult> relevantDocs = knowledgeBase.search(
queryVector, 5, null);
// 2. 把知识注入到Prompt
String knowledgeContext = relevantDocs.stream()
.map(VectorStore.SearchResult::getContent)
.collect(Collectors.joining("\n\n"));
// 3. 用风格微调模型生成回答
// 模型已经通过微调学会了特定的回答风格
// 这里只提供知识内容,风格由模型的参数决定
String prompt = """
请根据以下参考信息回答用户的问题。
参考信息:
%s
用户问题:%s
""".formatted(knowledgeContext, userQuestion);
return styledModel.generate(prompt);
}
}成本对比分析
/**
* RAG vs 微调的成本对比
*
* 帮助做投资决策
*/
public class CostAnalysis {
/**
* 计算微调的总成本
*/
public static FineTuningCost calculateFineTuningCost(FineTuningScenario scenario) {
// 训练成本(一次性)
// GPT-4o-mini微调:$25/1M tokens
double trainingCost = scenario.getTrainingTokens() / 1_000_000.0 * 25;
// 微调模型推理成本(比基础模型贵)
// GPT-4o-mini-ft:$0.3/1M tokens(输入)
double inferencePerCall = scenario.getAvgInputTokens() / 1_000_000.0 * 0.3;
double monthlyInferenceCost = inferencePerCall * scenario.getMonthlyCallCount();
// 数据准备人工成本
double dataPrepCost = scenario.getDataPrepHours() * scenario.getHourlyRate();
// 重训练周期成本(知识更新时需要重训)
double annualRetrainingCost = scenario.getRetrainingFrequencyPerYear() * trainingCost;
return new FineTuningCost(
trainingCost,
dataPrepCost,
monthlyInferenceCost,
annualRetrainingCost,
dataPrepCost + trainingCost + monthlyInferenceCost * 12 + annualRetrainingCost
);
}
/**
* 计算RAG的总成本
*/
public static RagCost calculateRagCost(RagScenario scenario) {
// 向量化成本(一次性 + 增量更新)
// text-embedding-3-small: $0.02/1M tokens
double embeddingCost = scenario.getTotalDocTokens() / 1_000_000.0 * 0.02;
// 向量数据库运营成本(每月)
double monthlyVectorDbCost = scenario.getVectorDbMonthlyCost();
// 每次调用增加的token成本(检索内容作为context)
double extraTokensPerCall = scenario.getAvgRetrievedTokens();
// GPT-4o-mini: $0.15/1M tokens
double monthlyExtraInferenceCost =
extraTokensPerCall / 1_000_000.0 * 0.15 * scenario.getMonthlyCallCount();
// 实现和维护工程成本
double devCost = scenario.getDevHours() * scenario.getHourlyRate();
return new RagCost(
embeddingCost,
devCost,
monthlyVectorDbCost + monthlyExtraInferenceCost,
devCost + embeddingCost + (monthlyVectorDbCost + monthlyExtraInferenceCost) * 12
);
}
/**
* 对比并给出建议
*/
public static String compareAndRecommend(
FineTuningCost ftCost, RagCost ragCost) {
double ftAnnual = ftCost.annualTotalCost();
double ragAnnual = ragCost.annualTotalCost();
double diff = Math.abs(ftAnnual - ragAnnual);
double diffPercent = diff / Math.min(ftAnnual, ragAnnual) * 100;
if (diffPercent < 20) {
return String.format("成本相近(差异%.1f%%),建议根据技术因素选择。" +
"RAG更灵活,微调延迟更低。", diffPercent);
}
if (ftAnnual < ragAnnual) {
return String.format("微调年成本($%.0f)低于RAG($%.0f)," +
"在技术条件满足的前提下微调更经济。", ftAnnual, ragAnnual);
} else {
return String.format("RAG年成本($%.0f)低于微调($%.0f)," +
"优先考虑RAG方案。", ragAnnual, ftAnnual);
}
}
// 场景参数类
record FineTuningScenario(
long trainingTokens, long avgInputTokens, long monthlyCallCount,
double dataPrepHours, double hourlyRate, int retrainingFrequencyPerYear
) {}
record RagScenario(
long totalDocTokens, long avgRetrievedTokens, long monthlyCallCount,
double vectorDbMonthlyCost, double devHours, double hourlyRate
) {}
record FineTuningCost(
double trainingCost, double dataPrepCost, double monthlyInferenceCost,
double annualRetrainingCost, double annualTotalCost
) {}
record RagCost(
double embeddingCost, double devCost, double monthlyRunCost,
double annualTotalCost
) {}
}实践建议
90%的场景,先试RAG
微调的前期投入很高(数据收集、训练、评估、部署),而RAG可以在1-2周内上线并验证效果。我的建议是:除非有明确的理由必须用微调(改变行为风格、极高频调用的成本压力),否则先用RAG,验证价值后再考虑是否需要微调。很多场景里,一个好的RAG系统就已经足够。
微调数据质量是决定因素
我见过有团队用几千条自动生成的低质量数据训练,结果模型表现比基础模型还差。微调最好的数据是:领域专家人工撰写的回答,或者人工审查过的LLM生成内容。宁可数据集小但精,不要大而糙。100条高质量样本往往比1000条低质量样本效果好。
先做小实验,再做全量
如果决定微调,先用10-20%的数据训练一个小版本,在测试集上验证效果,确认方向对了再投入全量资源。微调的整个周期可能要1-2周,如果发现数据方向错了才返工代价很高。
