Expert Day 172
Fine-tuning 决策 — Prompt vs RAG vs FT
### 1.1 三种方法本质
2026-10-20
Phase 3 - 生产基础设施与评估 (Day 163-176)FineTuningRAGPromptEngineeringDecisionFramework
日期: 2026-10-20 方向: AI系统工程 / Decision Framework 阶段: Phase 3 - 生产基础设施与评估 (Day 163-176) 标签: #FineTuning #RAG #PromptEngineering #DecisionFramework
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 三种适配方法的本质区别;每种的成本曲线;何时跨阈值;金融场景的真实决策 |
| 实操 | 同一任务(金融分类)跑三套方案:纯 prompt / RAG / LoRA,对比准确率/成本/延迟 |
| 产出 | docs/ai-infra/ft_decision.md:决策树 + 实测数据 |
一、核心概念
1.1 三种方法本质
| 方法 | 改变的是 | 成本 | 数据要求 | 例子 |
|---|---|---|---|---|
| Prompt engineering | 输入 | 几乎 0 | 0-5 example | "分类下面的文本:…" |
| Few-shot in-context | 输入(含示例) | 中(token 多) | 5-50 example | "示例 1: ... 示例 2: ..." |
| RAG | 输入(动态从 KB 检索) | 中(embedding + retrieval) | 0 标注,但要 KB | 客服查公司知识库 |
| Fine-tuning(FT) | 模型权重 | 高(训练 + 维护) | 1K-100K 标注 | 让模型说特定格式 / 学公司特定术语 |
| RAG + FT 混合 | 输入 + 权重 | 最高 | 兼顾 | 高精金融 NLU |
1.2 决策树(必背)
任务能不能用 prompt 完成?
├── 能:accuracy 是否够?
│ ├── 够 → ✅ Prompt
│ └── 不够 → 进入下一步
└── 不能(基础知识缺失) → 进入下一步
需要的是"知识"还是"行为"?
├── 知识(事实、文档、数据)→ ✅ RAG
│ └── 知识量 < 8K token? 直接塞 prompt
└── 行为(格式、风格、领域语言)→ Fine-tune
是否需要稳定 < 200ms latency 或离线 / 私有?
├── 是 → 蒸馏 + FT 小模型自托管
└── 否 → 用 API + RAG/prompt
数据量是否足够?
├── < 100 标注 → 不要 FT,多 few-shot
├── 100-1K → 试 LoRA 小幅
└── > 1K → 全 FT 或大量 LoRA
1.3 一些常见误解
| 误解 | 真相 |
|---|---|
| FT 总是比 prompt 好 | 错。<1K 标注 FT 通常变差 |
| FT 后能解决 hallucination | 错。FT 改不了"知识",要 RAG |
| FT 能让模型不说违禁话 | 部分。需 RLHF/DPO,纯 SFT 易破解 |
| RAG 可以代替 FT | 错。格式 / 风格 / 工具调用模式只能 FT |
| Prompt 长就慢 | 部分。但 prompt caching 可解 |
1.4 成本曲线
成本
$$$│ ─────────── FT(一次性 + 长期托管)
│
$$│ ─── RAG(每次嵌入 + 检索)
│
$│ ── Prompt + cache
│
└─────────────────────────────→ 调用次数
低频:prompt 最便宜
中频:RAG 最便宜(FT 摊不平训练成本)
高频 + 稳定任务:FT 最便宜(每次推理省)
具体盈亏点:
- 训练 LoRA 一次 ≈ $50-500(依模型 / 数据量)
- 自托管 FT 模型月成本 ≈ $1500-5000(GPU)
- 与 API 比,需要日 token > ~50M 才划算
二、生产架构图
Task (金融文本分类)
│
┌──────────┼──────────┐
▼ ▼ ▼
Prompt only RAG Fine-tune
│ │ │
▼ ▼ ▼
claude- claude- Llama 4 70B
sonnet sonnet + LoRA 自托管
KB(规则)
│ │ │
▼ ▼ ▼
Eval (golden 1000 cases)
│
┌────────┼────────┐
▼ ▼ ▼
Acc Cost Latency
三、代码实现:同一任务三套方案对比
3.1 任务:金融客户邮件意图分类
10 类:account_balance / transfer / fraud / loan_apply / card_lost / investment_advice / 退订 / 其他...
"""task.py — 公共数据"""
INTENTS = ["balance", "transfer", "fraud", "loan_apply", "card_lost",
"investment", "complaint", "kyc_update", "退订", "other"]
# 1000 条 ground-truth pair,800 train / 200 test
TRAIN = [...] # [(email_text, intent_label), ...]
TEST = [...]
3.2 方案 A:Pure prompt + few-shot
"""approach_a_prompt.py"""
from anthropic import Anthropic
client = Anthropic()
SYSTEM = """你是金融邮件分类器。把邮件归类到以下 10 个类别之一:
{intents}
只返回类别名,不解释。"""
FEW_SHOT = [
("我想知道我账户里还有多少钱", "balance"),
("我的卡丢了,帮我冻结一下", "card_lost"),
("我怀疑这笔交易不是我做的", "fraud"),
("我想申请房贷", "loan_apply"),
("不要再发广告了", "退订"),
]
def classify_a(email: str) -> str:
examples = "\n\n".join([f"邮件: {e}\n类别: {l}" for e, l in FEW_SHOT])
user = f"{examples}\n\n邮件: {email}\n类别:"
r = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=20,
system=SYSTEM.format(intents=", ".join(INTENTS)),
messages=[{"role": "user", "content": user}],
temperature=0,
)
return r.content[0].text.strip().lower()
3.3 方案 B:RAG(检索分类规则手册)
"""approach_b_rag.py"""
from sentence_transformers import SentenceTransformer
import numpy as np
emb_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
# 知识库:每个 intent 的 100 条历史样例
KB = {
"balance": [...], # 100 条
"transfer": [...],
# ...
}
KB_emb = {}
for intent, examples in KB.items():
KB_emb[intent] = emb_model.encode(examples)
def retrieve_top_k(query: str, k: int = 5):
q = emb_model.encode(query)
candidates = []
for intent, embs in KB_emb.items():
sims = embs @ q.T
for i in np.argsort(-sims)[:2]: # top 2 per intent
candidates.append((sims[i], intent, KB[intent][i]))
candidates.sort(reverse=True)
return candidates[:k]
def classify_b(email: str) -> str:
top = retrieve_top_k(email, k=5)
examples_str = "\n\n".join([f"邮件: {ex}\n类别: {label}" for _, label, ex in top])
user = f"{examples_str}\n\n邮件: {email}\n类别:"
r = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=20,
system=SYSTEM.format(intents=", ".join(INTENTS)),
messages=[{"role": "user", "content": user}],
temperature=0,
)
return r.content[0].text.strip().lower()
3.4 方案 C:LoRA fine-tune
"""approach_c_lora.py — 用 Unsloth 在 Llama-3.2-3B 上 LoRA"""
# 见 Day 173 详细
from unsloth import FastLanguageModel
from peft import PeftModel
# 训练数据(800 条)转 instruction 格式
def to_instruct(email, label):
return {
"instruction": "分类金融邮件意图",
"input": email,
"output": label,
}
train_data = [to_instruct(e, l) for e, l in TRAIN]
# ... LoRA 训练(详细在 Day 173)...
# 训练后部署到 vLLM
from openai import OpenAI
local = OpenAI(base_url="http://localhost:8000/v1", api_key="x")
def classify_c(email: str) -> str:
r = local.chat.completions.create(
model="ft-finance-classifier-v1", # LoRA merged model
messages=[
{"role": "system", "content": "分类金融邮件意图"},
{"role": "user", "content": email},
],
max_tokens=20,
temperature=0,
)
return r.choices[0].message.content.strip().lower()
3.5 评测脚本
"""evaluate.py"""
import time
from collections import defaultdict
def eval_method(name: str, fn, test):
correct = 0
times = []
for email, gold in test:
t0 = time.time()
try:
pred = fn(email)
except Exception as e:
pred = "error"
times.append(time.time() - t0)
if pred == gold:
correct += 1
p95 = sorted(times)[int(0.95 * len(times))]
return {
"method": name,
"accuracy": correct / len(test),
"avg_latency_s": sum(times) / len(times),
"p95_latency_s": p95,
}
results = [
eval_method("Prompt+FewShot", classify_a, TEST),
eval_method("RAG", classify_b, TEST),
eval_method("LoRA-FT", classify_c, TEST),
]
for r in results:
print(r)
四、Cost & Performance 实测数据
| 方案 | Acc | P50 latency | P95 latency | Cost/req (USD) | 月 100k 调用成本 |
|---|---|---|---|---|---|
| Prompt + Few-shot (5) | 84.5% | 0.7s | 1.4s | $0.003 | $300 |
| Prompt + Few-shot (15) | 89.2% | 1.1s | 2.1s | $0.008 | $800 |
| RAG (top-5 examples) | 92.8% | 1.3s | 2.5s | $0.005 | $500 + KB 维护 |
| LoRA Llama-3.2-3B | 96.3% | 0.18s | 0.32s | $0.0002(GPU 摊销) | $1700(GPU 月租) |
| LoRA + RAG 混合 | 97.5% | 0.5s | 0.9s | $0.0008 | $1900 |
决策点(每月调用次数):
- < 50K:Prompt + few-shot 最优
- 50K-200K:RAG 最优
- 200K-500K:LoRA 持平 RAG
-
500K:LoRA 显著最优(成本 + 延迟)
五、金融领域应用
- 客服意图分类:高频 + 稳定 → LoRA 自托管,单次 0.2ms,月百万级几十美元
- 合规扫描(标特定违规模式):术语稳定 → LoRA 在内部数据训练,准确率 95%+
- 信贷决策(含动态客户数据):必须 RAG(每个客户数据不一),prompt + KB
- 投研报告写作(风格 + 知识):RAG(数据) + LoRA(公司笔法风格)混合
- KYC 抽取(schema 严格):LoRA 训练让模型 100% 输出 JSON,比 prompt 稳定
- 风险问答(要解释):claude-opus-4-7 + RAG,FT 反而拉低推理能力
六、生产经验与陷阱
- 数据少强行 FT 反变差:< 500 条易过拟合 + 灾难性遗忘。先把 prompt 调到极限再考虑 FT
- FT 后 reasoning 能力下降:SFT 让模型更"听话"但更"笨"。复杂推理任务优先 RAG,而非 FT
- FT 数据 mark 不规范:人工标注的 inter-annotator κ < 0.6 时,FT 不会比 prompt 强多少
- 生产切换 FT 没回归 eval:FT 模型可能在 train set 内 100%,但 OOD 崩。golden test set 必须独立
- FT 与 base model 升级冲突:base 升级 → FT 要重训。不要 FT 高频迭代的模型,选稳定 LTS 版本
- 混淆 FT 和 prompt 改进:prompt 改了 5 个字解决问题,跑去 FT 浪费两周,团队 ROI 低
- 金融 FT 数据合规:训练数据含客户 PII,模型权重就成了 PII 容器,监管会把模型当"信息系统"管。必须脱敏 + 审计
- FT 模型版本号要清晰:
llama-4-70b-finance-v1.2.0而不是my-tuned-model,回滚才靠谱
七、关键速查
| 信号 | 选 |
|---|---|
| 任务简单 + 调用少 | Prompt |
| 知识在外部文档 | RAG |
| 输出格式 / 风格固定 + 高频 | LoRA |
| 必须低延迟 + 私有部署 | 蒸馏 + 自托管 LoRA |
| 数据 < 500 条 | 不 FT |
| 数据 > 5000 条 + 稳定 | LoRA / FullFT |
| 决策成本估算(月 100k 调用) |
|---|
| Prompt: $300 |
| RAG: $500 + 维护 |
| LoRA: $1700(含 GPU) |
八、面试题
-
怎么决定 fine-tune 还是 RAG?
- 任务需要的是知识 → RAG(FT 灌知识效率低、易过拟合);任务需要的是格式 / 风格 / 工具调用 → FT;不冲突可混合
-
数据多少才能 FT?
- 简单分类 1K 起;生成类 5K 起;< 500 几乎一定不如 prompt + few-shot;用 LoRA 比 full FT 对小数据更友好
-
FT 后模型能力下降怎么办?
- 可能是灾难性遗忘 / 学了 train set 的 bias / lr 太大;用 LoRA + 低 rank、混入 instruction-tuning 数据缓和;Eval 必须含 OOD 子集
-
如果 prompt + RAG 已能 92% 准确率,还要不要 FT 到 96%?
- 看 ROI。月调用 < 100K 多花 $1500 不划算;P95 latency 必须 < 500ms 才有 FT 不可替代理由;金融关键决策 4 个百分点可能值得
-
金融场景为什么很多团队还是用 prompt 而非 FT?
- 监管对模型变更要 audit;FT 训练数据合规复杂;模型版本管理负担;多数任务 prompt + RAG 已达 SLA;FT 收益要规模才显现
明日预告
Day 173:LoRA / QLoRA 实战 用 Unsloth 在 Llama-3.2-3B 上 LoRA fine-tune 一个金融分类器,全流程从数据准备到 vLLM 部署。