跳到主内容
快讯直播
AI智模界
教程

上手文心 5.5 Flash Base:从开放权重到中文指令模型

这篇能做出什么

读完并跟着做完,你会得到一个能直接对话的中文模型:给它一句“帮我把这段合同条款改写成客户能看懂的说明”,它会输出成段的答复,而不是接着你的话往下续写英文或复读问句。整个过程走完后,你手上会有三样东西:

1. 一份已经校验过的 Base 权重和配套 tokenizer;

2. 一份补齐的对话模板(chat template),Base 权重靠它才能“听懂”多轮格式;

3. 一个用你自己的领域数据(售后工单、病历摘要、内部制度问答,随便哪种)做小样本 SFT 出来的 LoRA 适配器,以及一份“同尺寸基准复测”的结果表,能量化说明微调后答得好在哪、退步在哪。

关键认知先说清楚:Base 权重不是聊天模型。它是“下一个词预测器”,你把问题丢进去,它最自然的反应是接着问题往下编,而不是回答你。这不是模型坏了,而是缺少两样东西——对话格式(模板)和指令跟随行为(SFT)。下面的流程就是按这两步补上。

前置条件清单

  • 硬件:一张能装下 Base 权重的 GPU。具体显存需求按权重文件大小、精度(bf16 / fp16)和是否量化来估,乘以 1.2~1.5 的余量;LoRA 微调比全参省得多,单卡通常够用。具体数值以官方模型卡当前版本为准。
  • 软件:Python 3.10+、PyTorch、transformers、datasets、peft、trl、accelerate。版本号以官方文档当前版本为准,不要死磕某个旧版本,trl 的 API 跨版本变动较大。
  • 数据:300~2000 条高质量领域指令数据起步,jsonl 一行一条。
  • 磁盘:权重 + 训练输出 + 合并后权重,预留三倍空间。
  • 一个评测题集:50~200 条你自己写的题,不要和训练数据重叠。这是后面复测的基准,先写好再训练。

分步骤

第 1 步:建环境、定目录

先把目录固定下来,后面所有命令都基于这个结构,照抄即可。

```bash

mkdir -p ernie-lab/{ckpt,data,out,scripts}

cd ernie-lab

python -m venv .venv

source .venv/bin/activate

pip install -U torch transformers datasets peft trl accelerate

pip install -U "huggingface_hub[cli]"

```

如果国内网络拉取权重慢,用 ModelScope 的下载工具,或者给 huggingface-cli 设置镜像端点,具体端点地址以官方文档为准。

第 2 步:下载权重并做一次“裸跑”

仓库 ID 以官方发布页为准,这里用占位符:

```bash

huggingface-cli download <组织>/<模型名>-Base \

--local-dir ./ckpt/base \

--exclude "*.gguf" "*.onnx"

```

下完先做完整性检查,同时看清 tokenizer 里已经有哪些特殊 token——这决定你第 4 步能不能“沿用”而不是“自造”:

```python

from transformers import AutoAI 词典:Token">Tokenizer, AutoModelForCausalLM

import torch

path = "./ckpt/base"

tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)

print("模板:", tok.chat_template)

print("特殊token:", tok.special_tokens_map)

print("词表大小:", len(tok))

model = AutoModelForCausalLM.from_pretrained(

path, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True

)

prompt = "请用三句话解释什么是复利。"

ids = tok(prompt, return_tensors="pt").to(model.device)

out = model.generate(**ids, max_new_tokens=120, do_sample=False)

print(tok.decode(out[0], skip_special_tokens=True))

```

如果输出是“请用三句话解释什么是复利。请用三句话解释什么是复利。……”或者把问题翻译成英文再展开,恭喜,你确认了 Base 的本来面目。把这段输出截图存下来,它是复测时的“改造前”基线。

第 3 步:确认模型有没有自带模板

先看第 2 步打印的 tok.chat_template。若为 None,说明权重是纯续写模型,模板必须自己补。若已有模板,直接沿用,不要改,否则你等于在原始分布之外重造格式,小样本下模型很难适应。

无论哪种情况,都要先做个“模板自检”:

```python

msgs = [

{"role": "system", "content": "你是一名严谨的技术文档编辑。"},

{"role": "user", "content": "把这句话改短:由于当前系统正在进行升级维护,因此暂时无法提供相关服务。"},

]

text = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)

print(repr(text))

```

repr 用上,空格和换行才是坑的来源。

第 4 步:补全对话模板

模板要复用 tokenizer 已有的特殊 token,别自造 <|user|> 这种没进词表的字符串——它会被切成三四个普通 token,训练时极不稳定。正确做法是查 special_tokens_map,看有没有 <start_of_turn>、<|im_start|>、[CLS] 之类的现成标记,用它们拼。

同时必须用 {% generation %} 块把 assistant 内容包起来,trl 才会只对回答算 loss:

```jinja

{%- for message in messages -%}

{%- if message['role'] == 'system' -%}

<|system|>{{ message['content'] }}\n

{%- elif message['role'] == 'user' -%}

<|user|>{{ message['content'] }}\n

{%- elif message['role'] == 'assistant' -%}

<|assistant|>{% generation %}{{ message['content'] }}{% endgeneration %}\n

{%- endif -%}

{%- endfor -%}

{%- if add_generation_prompt -%}<|assistant|>{%- endif -%}

```

把这段存成 scripts/chat_template.jinja,加载时显式传入:

```python

tok.chat_template = open("scripts/chat_template.jinja").read()

```

上面 <|system|> 这类标记请替换成你模型 tokenizer 里真实存在的特殊 token。改完后重跑第 3 步的自检,肉眼核对:生成提示片段必须精确等于训练数据里 assistant 开头的那一段,一个空格都不能差。

第 5 步:把领域数据整理成 jsonl

一行一条,messages 格式:

```json

{"messages":[{"role":"system","content":"你是某品牌净水器的售后客服,只依据保修条款作答。"},{"role":"user","content":"滤芯用了两年坏了能免费换吗?"},{"role":"assistant","content":"滤芯属于耗材,保修期为 6 个月。您使用两年已超出保修范围,需自费更换。若您购买过延保服务,请提供订单号,我帮您核对。"}]}

```

清洗要点,逐条过一遍:

  • 答案要短而准,200~600 字之间为主,混杂少量长答案。全是长答案,模型会学会啰嗦。
  • 问题要有真实口吻,别全写成“请介绍……”。加错别字、口语、半句话,模型抗噪能力才上得来。
  • 拒绝样本要占 5%~10%,比如问超出业务范围的问题时,回答“这个我查不到,建议联系人工”。不然模型会开始编。
  • 去重:sort train.jsonl | uniq -d | head,重复样本会让模型背诵。
  • 防泄漏:用评测题集里的关键词 grep 一遍训练文件,撞车的一条不留。

第 6 步:跑 LoRA 微调

```python

from datasets import load_dataset

from transformers import AutoTokenizer

from trl import SFTTrainer, SFTConfig

path = "./ckpt/base"

tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)

tok.chat_template = open("scripts/chat_template.jinja").read()

if tok.pad_token is None:

tok.pad_token = tok.eos_token

ds = load_dataset("json", data_files="data/train.jsonl", split="train")

cfg = SFTConfig(

output_dir="./out/lora",

num_train_epochs=3,

per_device_train_batch_size=1,

gradient_accumulation_steps=8,

learning_rate=1e-4,

lr_scheduler_type="cosine",

warmup_ratio=0.03,

bf16=True,

max_length=2048,

gradient_checkpointing=True,

logging_steps=10,

save_strategy="epoch",

report_to="none",

)

trainer = SFTTrainer(

model=path,

args=cfg,

train_dataset=ds,

processing_class=tok,

)

trainer.train()

trainer.save_model("./out/lora")

```

几个必须盯住的点:

  • 学习率:LoRA 用 1e-4 量级,全参微调降到 1e-5 量级。小数据上学习率开大,模型会迅速忘掉通用能力。
  • epoch 数:1000 条以内 2~3 轮足够。第 4 轮开始,验证集 loss 往往回升。
  • max_length:设得比数据实际长度略大即可,过长浪费显存。
  • loss 曲线:从 1.x 稳步下降是正常的;如果第 20 步就掉到 0.1 以下,多半是数据重复或模板把答案部分重复计入了输入。

第 7 步:合并并用对话接口验证

```python

from peft import PeftModel

from transformers import AutoModelForCausalLM

base = AutoModelForCausalLM.from_pretrained(path, torch_dtype="bfloat16", device_map="auto")

model = PeftModel.from_pretrained(base, "./out/lora").merge_and_unload()

model.save_pretrained("./out/merged")

tok.save_pretrained("./out/merged")

```

然后换回对话模板推理:

```python

msgs = [{"role": "user", "content": "滤芯用了两年坏了能免费换吗?"}]

text = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)

out = model.generate(**tok(text, return_tensors="pt").to(model.device),

max_new_tokens=256, do_sample=False)

print(tok.decode(out[0][tok(text).input_ids.shape[-1]:], skip_special_tokens=True))

```

注意只解码“新生成的部分”,别把输入再打一遍,否则你分不清是续写还是回答。

第 8 步:同尺寸基准复测

固定三样东西再比:同一套评测题、同样的解码参数、同样的打分口径。

```bash

python scripts/eval.py --model ./ckpt/base --out base.jsonl

python scripts/eval.py --model ./out/merged --out sft.jsonl

```

eval.py 里对每题同时算两类分:

  • 规则分:是否包含必须出现的关键事实、是否超过长度上限、是否出现“我无法回答”这类拒答、是否重复句子。这类分能自动跑。
  • 人工分:1~5 分打“是否符合业务口径”,抽 50 条足够。

再单独跑一组通用能力回归题(算术、改写、常识),10~20 条就行。这张表的重点不是“平均分涨了多少”,而是看有没有掉点——领域题涨了 30%,但通用改写题掉了 20%,说明模型被你的数据带偏好,需要减 epoch 或掺入通用数据。

常见坑与排错

模板不一致,微调白做。 现象:训练 loss 一路降,推理时模型自问自答或输出格式标记。原因是训练用 apply_chat_template、推理手拼字符串,中间差了个换行。解决办法:全流程只走 apply_chat_template 一个入口。

模型不收尾。 输出停不下来,一直生成。检查 eos_token 是否在训练数据末尾出现、pad_token 是否被误设成 eos 导致注意力掩码出问题。补齐后重新训练,比在推理端加 stop 字符串靠谱。

loss 算到了问题部分。 忘记加 {% generation %},模型会学“复述用户提问”。判断方法:验证集 loss 异常低,但实际对话里模型爱先重复你的话。

padding 方向搞反。 训练时 padding_side 应当在左,推理时在左,解码批量时更要一致。搞反后同一条 prompt 的单条推理和批量推理结果不同,非常难查。

量化后精度掉了,误判为微调失败。 先拿 bf16 原权重复测一遍再下结论。

评测集混进训练集。 这是最隐蔽的坑,分数漂亮但线上无效。训练前做一次关键词交叉检索。

显存不够就降 max_length 和 batch,别急着换 4bit。 先降长度、再开梯度检查点、最后才量化,量化会改变数值分布。

下一步建议

模板跑通、SFT 出效果之后,往上走有四条路:一是补 DPO 或偏好数据,把“答得对但答得不客气”修掉;二是扩充数据到万条量级,同时同步扩充评测集,否则评测就失去意义;三是上量化推理服务,用 vLLM 之类引擎起 OpenAI 兼容接口,把 /v1/chat/completions 接到你现有的应用里;四是把每轮复测的 jsonl 存档,做成版本对比,后面每换一版数据或超参,都能一句话说清“这一版比上一版好在哪”。

最后提醒一句:所有涉及仓库地址、镜像 tag、显存门槛、上下文长度的具体数字,都以官方模型卡和文档的当前版本为准,本文里出现的只是流程骨架,照着换个占位符就能用。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。