微调:让通用大模型变成你的「专属定制ROM」——从AOSP到LoRA的迁移学习

本文是「Android工程师的AI开发实战」系列第3篇。前两篇我们聊了RAG和Agent,这一篇进入微调------AI开发的"深水区"。

从一次失败的Prompt说起

上篇我们给Agent装了工具调用能力。工具会用了,但有个问题一直困扰我:模型的"语气"怎么改都不对劲

我在做一个Code Review辅助工具,想让它用团队的风格给建议。往System Prompt里塞了20条示例,few-shot打满。结果呢?模型确实学了"建议使用xxx"的句式,但语气还是一股说明书味------我们团队那种"兄弟你这写法是认真的吗"的直球风格,它死活学不会。

更离谱的是每次请求都要带一大坨Prompt,token费蹭蹭涨,延迟也跟着上去了。这跟每次启动App都从网络重新拉全量配置一个道理------明显是架构设计有问题。

那一刻我意识到:Prompt工程有天花板,就像只靠Theme/Style改不了系统行为------有些东西,必须改源码

微调是什么:从AOSP到定制ROM

如果你做过Framework层开发,这个类比秒懂:

通用大模型 = AOSP源码。功能完整但没特色。GPT-4就像原生Android------啥都能做,但不会帮你写出符合你们团队规范的代码。

微调 = OEM定制ROM。MIUI、ColorOS、HarmonyOS------都是在AOSP基础上做深度定制,不是从零写系统,而是在通用基础上让它在特定场景下表现更好、更有"个性"。

关键区别:微调≠从零训练(那叫pre-training,动辄百万美元级别)。微调是在训练好的模型上,用少量领域数据"教"它新技能或新风格------投入小、见效快。

Prompt工程的三个硬伤

Prompt的本质是"运行时配置"------在AndroidManifest里改参数、在BuildConfig里换字段。它有三个绕不过去的问题:

1. 上下文窗口有限。128K token听着多,但你不可能把整个代码规范+所有示例都塞进去。等于运行时把整个数据库加载到内存------迟早OOM。

2. token成本线性增长。每次请求都带一大段System Prompt。就像每次网络请求都重新下载缓存------明显该做持久化的事情。

3. 行为改不深。Prompt能影响"说什么",但改不了"怎么想"。你说"请用犀利的风格",它最多加个感叹号------推理模式和内在逻辑纹丝不动。

微调则是"编译期改代码"------直接修改模型权重,让它骨子里就按你要的方式思考和输出。

三种微调路线:full build vs incremental vs instant run

对编译速度深恶痛绝的Android开发者,看这个对比会特别亲切:

方式 Android类比 显存 效果
全量微调 full clean build 4×模型大小 最佳
LoRA incremental build 1.2×模型 接近全量
QLoRA instant run 0.3×模型 微损

全量微调:有钱人的游戏

全量微调更新模型所有参数。7B模型FP16权重14GB,加上梯度+优化器状态,训练时需要约56GB显存。

这就像给大项目做full clean build------结果最完美但每次等40分钟。除非你有A100集群随便用,否则走不通。

LoRA:AI世界的热修复

LoRA(Low-Rank Adaptation)的核心思想,做过热修复的Android工程师会秒懂:

不改原始权重,加一层"差分补丁"

Tinker/Sophix怎么工作的?不重新打包APK,生成一个小的diff patch,运行时合并到原始代码上。LoRA一模一样:

原始权重 W(冻结,不参与训练)

↓ 前向传播时相加

低秩补丁 ΔW = A × B(只训练这个)

输出 = W·x + ΔW·x = (W + A×B)·x

关键点 → A和B的rank远小于W的维度,参数量仅为原模型的0.1%~1%

翻译成Android:W是你的Release APK(几十MB不动),ΔW是热修复patch(几十KB)。patch虽小,行为精准可控。

更妙的是:你可以给同一个base model加载不同的LoRA adapter。今天加"代码Review风格"patch,明天换"需求文档生成"patch------就像同一个APK加载不同的热修复包,一套基座多种人格。

QLoRA:消费级显卡的福音

QLoRA在LoRA基础上加了一招:把冻结的原始权重从FP16量化到4-bit NormalFloat。不仅增量编译,还把"基线代码"压缩了。

实际效果:7B模型QLoRA训练只要6GB显存。一张3090能跑13B,一张4090能怼30B+。个人开发者不用再对着8×A100的配置流口水了。

数据集构建:垃圾进垃圾出

微调最核心的不是算力,是数据。跟写单测一个道理------覆盖率99%但全是happy path的测试集,还不如20个精心设计的边界case有价值。

标准格式:instruction / input / output

微调数据的标准格式是三元组。写过BDD的人秒懂------Given/When/Then:

复制代码
{
  "instruction": "Review这段
    Kotlin代码,指出问题",
  "input": "fun load() {\n
    runBlocking {\n
      api.fetch()\n
    }\n}",
  "output": "兄弟,主线程
    runBlocking是想ANR?
    viewModelScope.launch
    了解一下。"
}

注意output的风格------不是"建议使用协程替代阻塞调用",而是"兄弟你认真的?"这种团队真实语气。这就是微调要学的东西

数据从哪挖?

Android团队最好的数据金矿:

Code Review历史 --- 工蜂/GitLab的MR评论,天然的(代码, review意见)对

IM技术讨论 --- 企微群里"这怎么实现→用xxx方案"的对话

Wiki最佳实践 --- 改写成instruction格式

Bug修复记录 --- issue描述+修复diff,天然的问题→方案对

数据质量清单(参考单测质量标准):• output是否正确?(assert通过吗)• 覆盖边界case了吗?(不只是happy path)• 有互相矛盾的条目吗?(两个test冲突)• 数量:500~2000条起步才有明显效果• 多样性:同一类问题换多种表述方式

实战:LoRA微调代码Review助手

理论够了,上手。基座选DeepSeek-Coder-7B(开源、代码能力强、单卡可跑),用QLoRA方案。

Step 1:环境和依赖

复制代码
# 核心四件套
pip install \
    transformers \
    peft \
    bitsandbytes \
    datasets accelerate

# peft → LoRA/QLoRA官方实现
# bitsandbytes → 4-bit量化
# accelerate → 混合精度+多卡

Step 2:加载4-bit量化模型

复制代码
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)
from peft import (
    LoraConfig,
    get_peft_model,
    prepare_model_for_kbit_training,
)

# 量化配置 --- 类比ProGuard压缩
bnb_cfg = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=(
        torch.float16
    ),
    bnb_4bit_use_double_quant=True,
)

MODEL = (
    "deepseek-ai/"
    "deepseek-coder-7b-"
    "instruct-v1.5"
)

model = (
    AutoModelForCausalLM
    .from_pretrained(
        MODEL,
        quantization_config=bnb_cfg,
        device_map="auto",
    )
)
tokenizer = (
    AutoTokenizer
    .from_pretrained(MODEL)
)

Step 3:配置LoRA参数

复制代码
# 定义LoRA adapter
# = 定义热修复patch的作用范围
lora_cfg = LoraConfig(
    # rank: patch的"厚度"
    # 16对大多数任务够用
    r=16,
    # 缩放因子,一般 = 2×r
    lora_alpha=32,
    # 打patch的目标层
    target_modules=[
        "q_proj",
        "v_proj",
        "k_proj",
        "o_proj",
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

# 冻结原始权重 + 挂载adapter
model = prepare_model_for_kbit_training(
    model
)
model = get_peft_model(
    model, lora_cfg
)
model.print_trainable_parameters()
# → trainable: 13.1M
# → all: 6.9B
# → trainable%: 0.19%

只训练0.19%的参数------7B模型变成13M参数的小活儿。这就是LoRA的魅力。

Step 4:数据预处理 + 训练

复制代码
from datasets import load_dataset
from transformers import (
    TrainingArguments,
    Trainer,
)

def fmt(s):
    return (
        "### Instruction:\n"
        f"{s['instruction']}\n\n"
        "### Input:\n"
        f"{s['input']}\n\n"
        "### Response:\n"
        f"{s['output']}"
    )

ds = load_dataset(
    "json",
    data_files="cr_data.jsonl",
)

def tok(sample):
    enc = tokenizer(
        fmt(sample),
        truncation=True,
        max_length=1024,
        padding="max_length",
    )
    enc["labels"] = (
        enc["input_ids"].copy()
    )
    return enc

ds = ds.map(tok)

args = TrainingArguments(
    output_dir="./cr-lora",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    # lr要小------微调不能太猛
    learning_rate=2e-4,
    warmup_ratio=0.03,
    lr_scheduler_type="cosine",
    fp16=True,
    logging_steps=10,
    save_strategy="epoch",
)

Trainer(
    model=model,
    args=args,
    train_dataset=ds["train"],
).train()

# 保存adapter(只有~30MB)
model.save_pretrained("./cr-lora")

1000条数据、3 epochs、单卡3090:约20分钟跑完。产出的adapter只有30MB------你的"代码Review风格补丁"就做好了。

Step 5:加载adapter做推理

复制代码
from peft import PeftModel

# 加载base + adapter
base = (
    AutoModelForCausalLM
    .from_pretrained(
        MODEL,
        device_map="auto",
        torch_dtype=torch.float16,
    )
)
model = PeftModel.from_pretrained(
    base, "./cr-lora"
)

# 可选:合并adapter到base
# 推理速度和原始模型一样
model = model.merge_and_unload()

merge_and_unload()把adapter永久合并回base权重------合并后推理性能和原模型完全一样,没有额外开销。就像热修复最终还是要合入正式版本发版。

参数调优:和性能优化一样的方法论

微调调参和Android性能调优一个路子------不是瞎试,有方法论:

参数 Android类比 经验值
learning_rate 动画duration 1e-4~3e-4
epochs 重试次数 2~5次
batch_size 线程池大小 受显存限制
rank ® 缓存容量 8~64

我的策略:先用默认值(r=16, lr=2e-4, epochs=3)跑一版baseline,看效果再针对性调整。跟Android一样------先跑通再优化,别在第一版追求完美。

部署:云端 vs 本地 vs 端侧

微调完了,怎么上线?跟Android的架构选型一个逻辑:

模型部署选择?

云端(vLLM/TGI) → 类似后端API服务。无限算力、有网络延迟。适合7B+大模型生产使用

本地(Ollama/llama.cpp) → 类似前台Service。零延迟但吃本地资源。适合开发调试

端侧(MLC-LLM/TFLite) → 类似嵌入SDK。离线可用但需极度压缩(3B以下)。特定场景

我这个CR Review助手选的是云端------vLLM部署在一张A10G上,暴露OpenAI兼容接口,对接工蜂Webhook,每次新MR自动触发review。延迟在2~3秒,完全可接受。

踩坑记录(血泪教训)

1. 数据质量 > 数据数量

第一版用了5000条自动提取的CR评论,效果很差。后来手动筛了800条高质量的,效果直接起飞。100个flaky test不如10个稳定test------微调数据一个道理。

2. 过拟合的信号

模型开始复述训练集里的具体类名、变量名------这就像背题的学生,换个说法就懵。解法:减epochs、加数据多样性、调大dropout。

3. 灾难性遗忘

微调太狠,模型连基本Kotlin语法都答不好了。解法:训练数据混入10~20%的通用编程Q&A,维持基础能力。就像定制ROM改太多底层导致基本功能挂了。

4. 评估要趁早

别等训完再测。每个epoch结束跑test set看变化,跟Android CI一样------每次commit都跑,别攒一堆再排查。

什么时候该微调?

需要改变模型行为?

Prompt/RAG能解决吗?

能 → 就用Prompt/RAG,别上微调

不能 → 有500+高质量数据吗?

有数据 → 上LoRA!投入产出比最高

没数据 → 先用RAG撑着,同时积累数据

说实话,大部分场景Prompt+RAG够用了。微调是"重武器",适合三种情况:需要改变模型的思考模式/语气风格、需要极低推理延迟(省掉长Prompt)、需要在垂直领域达到专家级水平。

下一篇是系列终章:把RAG、Agent和微调三件套组合起来,搭一个完整的"AI Android开发助手"------从架构设计到生产部署,把前三篇的东西全部打通。就像从Fragment/Service/Broadcast Receiver的单独学习,到最终组装一个完整App。

如果你也在尝试微调,评论区聊聊数据集是怎么构建的------这部分其实是整个流程最耗时间的,比训练本身难多了。好数据集价值连城。

相关推荐
Mr数据杨9 小时前
【CanMV K210】传感器实验 DS18B20 温度读取与环境判断
人工智能·硬件开发·canmv k210
wy_hhxx9 小时前
Win11 环境部署 Codex、Claude Code + 国产模型
人工智能
薛定猫AI9 小时前
【深度解析】Antigravity 更新背后的工程化思路:从沙盒权限到长上下文的 AI 编程工具演进
人工智能
qcx239 小时前
【系统学AI】02 token机制全解:LLM如何‘读懂‘人类语言
人工智能·llm·产品经理·token·费用·deepseek
俊哥V9 小时前
每日 AI 研究简报 · 2026-05-24
人工智能·ai
小糖学代码10 小时前
LLM系列:1.python入门:12.异常处理(Exceptions)
前端·人工智能·python·深度学习
BreezeJiang10 小时前
从零开始,手把手带你搭建一个全栈项目
人工智能
小小测试开发10 小时前
KanBots:开源看板工具,每张卡片跑一个并行 AI Agent,Hacker News 147 星炸裂
人工智能
●VON10 小时前
OpenClaw 架构解析:Skill 与 Agent 的设计哲学与实现机制
人工智能·app·agent·skill·豆包·deepseek