1. 信息抽取任务为什么需要微调 Qwen信息抽取Information ExtractionIE要做的事情很具体从一段非结构化文本里把实体、关系、事件这些结构化字段抠出来。比如一段客服工单里要抽「订单号、故障类型、期望处理方式」一段合同里要抽「甲方、乙方、金额、生效日期」。通用大模型直接 zero-shot 也能抽但一旦字段变多、格式要求严格、领域词偏门输出就开始飘字段名对不上、该给 JSON 却给你一段散文、多个实体漏抽。我拿 Qwen2-7B-Instruct 做过对比在 200 条自建工单测试集上zero-shot 的字段级 F1 大概 0.71主要错在「金额」和「日期」这类需要归一化的字段。而用 LoRA 在 1500 条标注数据上微调后F1 能到 0.89格式合规率从 82% 提到 99%。这就是微调的价值不是让模型变聪明而是让它稳定地按你的 schema 输出。这篇面向的是想快速复现信息抽取微调流程的工程师。你会拿到三样东西一份可直接跑的训练配置QLoRA LoRA、一套信息抽取专用的数据格式模板、一个能算准确率的评测脚本。训练完之后我用 TaoToken 的统一 Key 通道去调用模型做推理验证这样不用在本地反复加载权重验证环节能省不少事。适合谁有 16GB 以上显存的单卡、会一点 Python、手上有几百到几千条标注数据的人。如果你只有 CPU也能跟着走完数据准备和评测部分训练换成小模型 Qwen2-1.5B 即可。先说清楚一个容易踩的坑信息抽取微调和通用对话微调最大的区别在 loss mask。对话任务只对 assistant 回复算损失而信息抽取通常是「输入原文 → 输出 JSON」输入部分必须 mask 掉否则模型会去学怎么复述原文抽取能力反而下降。下面第 3 节的配置里我会专门处理这一点。2. TaoToken 统一通道准备与模型选型在动手训练前先把推理验证的通道搭好。原因是微调过程中你需要频繁对比「微调前 vs 微调后」的抽取效果如果每次都本地加载 7B 权重显存和启动时间都吃不消。用统一的 API 通道调基座模型做 baseline本地只跑微调后的小适配器节奏会顺很多。TaoToken 在这里的角色是一个统一的模型调用入口。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。它的价值在于你用一个 Key 就能切换不同模型做对比不用为每个模型单独配一套鉴权。对信息抽取这种「要试好几个模型看哪个抽得准」的场景省事。具体操作路径第一步进控制台创建 Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面生成一个密钥复制保存。这个 Key 后面既用于调基座模型做 baseline也用于验证微调后的效果。第二步确认你要调的模型 ID。信息抽取建议先用 Qwen2-7B-Instruct 做 baseline它的中文理解和 JSON 输出能力比较稳。模型列表可以在模型对话页面看 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。第三步本地配好环境变量避免 Key 写死在代码里export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api第四步用 curl 做一次最小连通性测试确认通道可用curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: Qwen/Qwen2-7B-Instruct, messages: [ {role: user, content: 从这句话抽取订单号和金额订单 A12345 共 299 元。只输出 JSON。} ], temperature: 0 }返回里如果能看到choices[0].message.content是类似{订单号:A12345,金额:299}的内容通道就通了。注意temperature设 0信息抽取要的是稳定复现不是创意。关于模型选型给你一个决策参考场景推荐基座显存需求说明快速验证流程Qwen2-1.5B-Instruct8GB跑通 pipeline 用效果一般单卡主力Qwen2-7B-Instruct16GBQLoRA性价比最高本文主用字段复杂/长文本Qwen2-14B-Instruct24GBQLoRA抽取准确率更高训练慢如果你后面要做长期的编码或 Agent 类任务可以了解下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。不过信息抽取微调本身用按量调用就够了。3. 可复制的 LoRA 训练配置与数据模板这一节是核心给你能直接抄的配置。先说数据格式再说训练参数。信息抽取的数据我推荐用「指令 原文 目标 JSON」的三段式存成 JSON Lines每行一条。模板长这样{instruction: 从工单文本中抽取订单号、故障类型、期望处理方式输出 JSON。, input: 客户反馈订单 A12345 的空调不制冷希望明天上门维修。, output: {\订单号\: \A12345\, \故障类型\: \不制冷\, \期望处理方式\: \上门维修\}}关键点output必须是合法 JSON 字符串字段名和你的 schema 完全一致不要有多余空格或换行。我见过有人 output 里带 markdown 代码块标记训练完模型也学会输出 json评测时解析全挂。数据准备脚本把原始标注转成上面的格式import json def build_dataset(raw_path, out_path): records [] with open(raw_path, r, encodingutf-8) as f: for line in f: obj json.loads(line) # obj 里应有 text 和 labels 两个字段 target { 订单号: obj[labels].get(order_id, ), 故障类型: obj[labels].get(fault_type, ), 期望处理方式: obj[labels].get(expect, ) } records.append({ instruction: 从工单文本中抽取订单号、故障类型、期望处理方式输出 JSON。, input: obj[text], output: json.dumps(target, ensure_asciiFalse) }) with open(out_path, w, encodingutf-8) as f: for r in records: f.write(json.dumps(r, ensure_asciiFalse) \n) print(f写入 {len(records)} 条) build_dataset(raw_annotated.jsonl, train.jsonl)然后是训练配置。我用 YAML 管理参数方便复现。存成qlora_ie.yamlmodel_name_or_path: Qwen/Qwen2-7B-Instruct data_path: ./train.jsonl output_dir: ./qwen2-ie-lora max_seq_length: 1024 num_train_epochs: 3 per_device_train_batch_size: 2 gradient_accumulation_steps: 8 learning_rate: 2.0e-4 lr_scheduler_type: cosine warmup_ratio: 0.05 bf16: true logging_steps: 10 save_strategy: epoch lora: r: 64 lora_alpha: 128 lora_dropout: 0.05 target_modules: - q_proj - k_proj - v_proj - o_proj - gate_proj - up_proj - down_proj bias: none task_type: CAUSAL_LM quant: load_in_4bit: true bnb_4bit_compute_dtype: bfloat16 bnb_4bit_use_double_quant: true对应的训练脚本train_ie.py重点是 loss mask 的处理——只对 output 部分算损失import json import torch import yaml from datasets import load_dataset from transformers import ( AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, TrainingArguments ) from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training from trl import SFTTrainer, DataCollatorForCompletionOnlyLM cfg yaml.safe_load(open(qlora_ie.yaml, encodingutf-8)) tokenizer AutoTokenizer.from_pretrained(cfg[model_name_or_path], trust_remote_codeTrue) tokenizer.pad_token tokenizer.eos_token bnb BitsAndBytesConfig( load_in_4bitcfg[quant][load_in_4bit], bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_use_double_quantcfg[quant][bnb_4bit_use_double_quant] ) model AutoModelForCausalLM.from_pretrained( cfg[model_name_or_path], quantization_configbnb, device_mapauto, trust_remote_codeTrue ) model prepare_model_for_kbit_training(model) lora_cfg LoraConfig( rcfg[lora][r], lora_alphacfg[lora][lora_alpha], lora_dropoutcfg[lora][lora_dropout], target_modulescfg[lora][target_modules], biascfg[lora][bias], task_typecfg[lora][task_type] ) model get_peft_model(model, lora_cfg) dataset load_dataset(json, data_filescfg[data_path], splittrain) def formatting_func(example): # 用 Qwen2 的 chat 模板拼成单条文本 messages [ {role: system, content: 你是信息抽取助手只输出 JSON不要解释。}, {role: user, content: example[instruction] \n example[input]}, {role: assistant, content: example[output]} ] return tokenizer.apply_chat_template(messages, tokenizeFalse) # 关键只对 assistant 回复算损失 response_template |im_start|assistant\n collator DataCollatorForCompletionOnlyLM(response_template, tokenizertokenizer) trainer SFTTrainer( modelmodel, tokenizertokenizer, train_datasetdataset, formatting_funcformatting_func, data_collatorcollator, max_seq_lengthcfg[max_seq_length], argsTrainingArguments( output_dircfg[output_dir], num_train_epochscfg[num_train_epochs], per_device_train_batch_sizecfg[per_device_train_batch_size], gradient_accumulation_stepscfg[gradient_accumulation_steps], learning_ratecfg[learning_rate], lr_scheduler_typecfg[lr_scheduler_type], warmup_ratiocfg[warmup_ratio], bf16cfg[bf16], logging_stepscfg[logging_steps], save_strategycfg[save_strategy], report_tonone ) ) trainer.train() trainer.save_model(cfg[output_dir]) print(LoRA 适配器已保存到, cfg[output_dir])启动训练pip install transformers4.44.0 peft0.12.0 trl0.9.6 bitsandbytes0.43.3 accelerate0.33.0 datasets2.20.0 pyyaml python train_ie.py在单张 A100 40G 上1500 条数据、3 个 epochQLoRA 大概 40 分钟跑完。如果你用 16GB 卡把per_device_train_batch_size降到 1gradient_accumulation_steps提到 16效果接近。注意response_template必须和 Qwen2 的 chat 模板完全一致写错一个字符 loss mask 就失效模型会连输入一起学。跑之前先打印一条formatting_func的结果确认。4. 推理验证与抽取准确率评测脚本训练完先别急着上生产用评测脚本算清楚 F1。评测分两步本地加载 LoRA 适配器做批量推理或者把适配器合并后走 API。这里我演示本地推理 用 TaoToken 调基座做对照。先写评测脚本eval_ie.py核心是字段级精确匹配import json import re from transformers import AutoTokenizer, AutoModelForCausalLM from peft import PeftModel import torch BASE Qwen/Qwen2-7B-Instruct LORA ./qwen2-ie-lora tokenizer AutoTokenizer.from_pretrained(BASE, trust_remote_codeTrue) base AutoModelForCausalLM.from_pretrained( BASE, torch_dtypetorch.bfloat16, device_mapauto, trust_remote_codeTrue ) model PeftModel.from_pretrained(base, LORA) model.eval() def extract(text): messages [ {role: system, content: 你是信息抽取助手只输出 JSON不要解释。}, {role: user, content: 从工单文本中抽取订单号、故障类型、期望处理方式输出 JSON。\n text} ] prompt tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue) inputs tokenizer(prompt, return_tensorspt).to(model.device) with torch.no_grad(): out model.generate(**inputs, max_new_tokens128, do_sampleFalse) gen tokenizer.decode(out[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue) return gen.strip() def parse_json(s): # 容错去掉可能的代码块标记 s re.sub(r^json|$, , s.strip(), flagsre.M).strip() try: return json.loads(s) except Exception: return {} def field_f1(preds, golds, fields): tp fp fn 0 for p, g in zip(preds, golds): for f in fields: pv str(p.get(f, )).strip() gv str(g.get(f, )).strip() if pv and pv gv: tp 1 elif pv and pv ! gv: fp 1 elif not pv and gv: fn 1 prec tp / (tp fp) if tp fp else 0 rec tp / (tp fn) if tp fn else 0 f1 2 * prec * rec / (prec rec) if prec rec else 0 return prec, rec, f1 fields [订单号, 故障类型, 期望处理方式] test [json.loads(l) for l in open(test.jsonl, encodingutf-8)] preds, golds [], [] for item in test: raw extract(item[input]) preds.append(parse_json(raw)) golds.append(json.loads(item[output])) p, r, f1 field_f1(preds, golds, fields) print(fPrecision{p:.3f} Recall{r:.3f} F1{f1:.3f})跑出来如果 F1 在 0.85 以上基本可用。我实测 1500 条数据能到 0.89 左右。接着用 TaoToken 调基座模型做对照确认微调确实带来了提升。写个baseline.pyimport os, json, re import requests API https://taotoken.net/api/v1/chat/completions KEY os.environ[TAOTOKEN_API_KEY] def call_base(text): payload { model: Qwen/Qwen2-7B-Instruct, messages: [ {role: system, content: 你是信息抽取助手只输出 JSON不要解释。}, {role: user, content: 从工单文本中抽取订单号、故障类型、期望处理方式输出 JSON。\n text} ], temperature: 0 } r requests.post(API, headers{Authorization: fBearer {KEY}}, jsonpayload, timeout60) return r.json()[choices][0][message][content] test [json.loads(l) for l in open(test.jsonl, encodingutf-8)] for item in test[:5]: print(原文:, item[input]) print(基座:, call_base(item[input])) print(微调:, extract(item[input])) print(- * 40)对比几轮你会看到基座模型偶尔会多输出解释文字或者字段名写成英文微调后的输出稳定得多。这就是验证的意义——不是看 loss 曲线而是看真实抽取结果。如果你想把微调后的模型也放到统一通道里调用可以把 LoRA 合并进基座再部署或者用支持适配器的推理框架。合并命令python -c from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base AutoModelForCausalLM.from_pretrained(Qwen/Qwen2-7B-Instruct, torch_dtypeauto, device_mapcpu) model PeftModel.from_pretrained(base, ./qwen2-ie-lora) model model.merge_and_unload() model.save_pretrained(./qwen2-ie-merged) AutoTokenizer.from_pretrained(Qwen/Qwen2-7B-Instruct).save_pretrained(./qwen2-ie-merged) 5. 常见报错排查401、loss 不降、JSON 解析失败微调信息抽取时报错集中在几个地方。我把踩过的坑列出来对照着查。报错一调用 API 返回 401 Unauthorized。这个最常见。先确认环境变量有没有生效echo $TAOTOKEN_API_KEY如果为空说明 export 没在当前 shell 生效。再确认请求头格式是Authorization: Bearer sk-xxx中间有空格Bearer 首字母大写。还有一种情况是 Key 复制时带了换行或空格用echo -n检查长度。如果本地代理配置干扰了请求检查HTTP_PROXY环境变量必要时unset掉。报错二训练 loss 一直不降或者降到 0.1 但抽取效果很差。八成是 loss mask 没生效。检查response_template是否和 tokenizer 实际输出的模板一致。打印一条样本sample formatting_func(dataset[0]) print(repr(sample[-200:]))看 assistant 部分前面到底是|im_start|assistant\n还是别的。如果模板对不上DataCollatorForCompletionOnlyLM找不到分割点就会对全部 token 算损失模型学成「复读机」。另一个可能是学习率太高2e-4 对 7B 有时偏大降到 1e-4 试试。报错三评测时json.loads报Expecting value: line 1 column 1。说明模型输出不是纯 JSON。原因通常是训练数据里 output 带了多余字符或者推理时没加 system prompt 约束。解决训练数据严格用json.dumps生成推理时 system prompt 明确写「只输出 JSON不要解释」评测脚本里加正则清洗代码块标记。如果模型输出{订单号: A12345,}这种尾逗号用json5或先re.sub(r,\s*}, }, s)修一下。报错四CUDA out of memory。QLoRA 已经省显存了还 OOM 一般是max_seq_length太大。信息抽取的输入通常不长1024 够用别设 4096。再不行把per_device_train_batch_size设 1gradient_checkpointing打开。报错五bitsandbytes在 Windows 上装不上。这个库对 Windows 支持不好建议用 WSL2 或 Linux。实在要用 Windows可以放弃 4bit 量化改用 bf16 的 LoRA但显存需求翻倍。报错六reading choices相关错误比如KeyError: choices。说明 API 返回的不是预期结构通常是请求体格式错了或者模型 ID 写错。打印完整响应print(r.json())看error字段。常见的是 model 名写成qwen2-7b而实际要Qwen/Qwen2-7B-Instruct。排查顺序建议先确认通道通curl 测试再确认数据格式对打印一条样本最后确认 loss mask 生效看训练日志的 loss 曲线是否平滑下降。这三步过了基本不会有大问题。6. 把微调流程固化下来从验证到复用跑通一遍之后最重要的是把流程固化下次换领域能直接复用。我的做法是抽三个配置文件数据 schema 配置、训练超参配置、评测字段配置。换任务时只改 schema训练和评测脚本不动。schema 配置schema.yamltask_name: 工单信息抽取 fields: - 订单号 - 故障类型 - 期望处理方式 instruction: 从工单文本中抽取订单号、故障类型、期望处理方式输出 JSON。训练脚本读这个配置生成 instruction评测脚本读 fields 算 F1。这样从电商评论抽取换到医疗病历抽取只改一个文件。另外验证环节用 TaoToken 统一通道有个好处你可以把不同版本的微调模型和基座模型放在同一个评测脚本里跑横向对比。比如同时测 Qwen2-7B 基座、Qwen2-7BLoRA、Qwen2-14B 基座看哪个性价比最高。切换模型只改一个 model 字段不用重新配环境。如果你后面要把这套流程接到自动化 pipeline 里比如每天用新标注数据增量训练可以了解下 Coding Plan 的长期任务支持 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的参数说明。最后给个实用技巧信息抽取的评测集一定要留出「难样本」比如字段缺失、多个实体、口语化表达。我一开始测试集全是规整文本F1 虚高到 0.95上线后遇到真实脏数据掉到 0.7。后来专门挑了 50 条难样本进测试集F1 降到 0.89但这个数字才可信。微调不是终点持续用真实 badcase 迭代数据才是。