1. Qwen LoRA 微调后模型胡说八道:从数据到推理的完整排查链路
你拿 Qwen2.5-7B-Instruct 做了一轮 LoRA 微调,训练 loss 曲线漂亮得像教科书,验证 loss 也没炸,结果一推理——模型开始一本正经地胡说八道。问它公司报销流程,它给你编出一个根本不存在的「三级审批制度」;问它产品参数,它把 48V 说成 220V;更离谱的是,同一个问题问三遍,三遍答案互相矛盾。
这不是个例。我试过在中文客服、代码文档、法律问答三个场景下用 Qwen 系列做 LoRA 微调,几乎每次都会遇到不同程度的幻觉问题。核心原因在于:LoRA 微调虽然只更新少量参数,但它改变的是模型对「什么回答是合理的」这一判断的分布。一旦训练数据有噪音、学习率偏大、或者推理解码参数没跟着调整,模型就会从「谨慎的专家」变成「自信的骗子」。
这篇文章要解决的问题很具体:Qwen 模型 LoRA 微调后出现幻觉,怎么系统性地定位根因并修复。我会从数据质量、训练配置、推理参数三个角度拆解,给出可复制的数据清洗脚本、LoRA 训练参数对照表、幻觉验证 prompt 模板,以及逐步排查的验证动作。适合已经跑过至少一轮 LoRA 微调、但被输出失真困扰的开发者。全文的排查思路在 Qwen2.5-7B/14B 上验证过,其他同架构模型也可参考。
排查的核心逻辑是「分层隔离」:先确认基座模型本身没问题,再检查微调数据是否干净,然后看训练配置是否过拟合,最后调推理参数。每一步都有可量化的指标和可执行的脚本,不需要复杂的评估框架,Python + Transformers 就能跑完 80% 的诊断。
2. TaoToken 前置:用统一 API 快速验证基座与微调模型差异
排查幻觉的第一步,是确认「基座模型正常,微调模型异常」。如果你本地只有一张消费级显卡,同时加载基座和微调模型做对比推理会很吃力。这时候可以用 TaoToken 的 API 来跑基座模型的对照测试,把本地显存留给微调模型。
TaoToken 是一个大模型 API 聚合平台,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点统一为 https://taotoken.net/api 。它的价值在于:你不需要在本地部署 Qwen 基座模型,直接通过 API 调用就能拿到基座模型的输出,和你的微调模型做逐条对比。对于排查幻觉来说,这个对照非常关键——如果基座模型在同样 prompt 下输出正常,而微调模型胡说,那问题一定出在微调环节。
具体操作上,你可以先在 TaoToken 控制台创建一个 API Key,然后通过模型对话页面快速测试基座模型的表现。控制台地址是 https://taotoken.net/console ,API Key 管理在 https://taotoken.net/api-keys 。如果你后续要做长期的编码类 Agent 微调验证,也可以了解 Coding Plan: https://taotoken.net/coding-plan 。
这里要强调一个排查原则:不要用微调模型自己的输出来判断对错。你需要一个「参照系」。基座模型就是最好的参照系。通过 TaoToken 调用基座模型,你可以快速生成一批对照样本,然后和微调模型的输出做 diff。具体做法是:准备 50 条测试 prompt,分别用基座 API 和本地微调模型生成回答,然后人工或脚本对比事实一致性。如果基座在 45 条上表现正常,微调只在 20 条上正常,那幻觉问题就坐实了。
另外,TaoToken 的接入文档在 https://taotoken.net/doc ,里面有完整的 API 调用示例。对于 Claude Code 相关的接入场景,可以参考 https://taotoken.net/ClaudeCodeAnthropic 。这些资源在你需要快速搭建对照测试环境时会省很多时间。
需要提醒的是:TaoToken 在这里的角色是「基座模型对照测试工具」,不是用来替代你的本地微调流程。微调后的模型仍然在你本地或你的推理服务上运行。排查完成后,生产环境用哪套推理方案,取决于你的成本和延迟要求。
3. 可复制配置:数据清洗脚本 + LoRA 参数对照表 + 推理参数模板
这一节直接给可复制的内容。排查幻觉,先从数据下手,因为数据噪音是第一大根因。
3.1 数据清洗脚本
把下面的脚本保存为clean_sft_data.py,它做四件事:过滤空/过短样本、检测高重复样本、检测输入输出高度重叠的「复制型」样本、统计 token 长度分布。
import json import re from collections import Counter def load_jsonl(path): samples = [] with open(path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line: samples.append(json.loads(line)) return samples def check_empty_or_short(samples, min_input_len=8, min_output_len=4): bad = [] for i, s in enumerate(samples): inp = s.get('input', '') or s.get('instruction', '') out = s.get('output', '') or s.get('response', '') if len(inp) < min_input_len or len(out) < min_output_len: bad.append(i) return bad def check_repetition(text, n=4, threshold=0.3): tokens = re.findall(r'\w+', text) if len(tokens) < n: return 0.0 ngrams = [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)] counts = Counter(ngrams) repeated = sum(c-1 for c in counts.values() if c > 1) return repeated / len(ngrams) def check_copy_overlap(samples, threshold=0.85): noisy = [] for i, s in enumerate(samples): inp = s.get('input', '') or s.get('instruction', '') out = s.get('output', '') or s.get('response', '') inp_tokens = set(re.findall(r'\w+', inp.lower())) out_tokens = set(re.findall(r'\w+', out.lower())) if inp_tokens: overlap = len(inp_tokens & out_tokens) / len(inp_tokens) if overlap > threshold: noisy.append((i, round(overlap, 3))) return noisy def token_length_stats(samples): lengths = [] for s in samples: text = (s.get('input', '') or '') + (s.get('output', '') or '') lengths.append(len(text)) lengths.sort() n = len(lengths) return { 'min': lengths[0], 'p50': lengths[n//2], 'p90': lengths[int(n*0.9)], 'max': lengths[-1], 'avg': sum(lengths)/n } if __name__ == '__main__': import sys path = sys.argv[1] if len(sys.argv) > 1 else './train.jsonl' samples = load_jsonl(path) print(f'总样本数: {len(samples)}') print(f'空/过短样本索引: {check_empty_or_short(samples)[:20]}') rep_scores = [(i, check_repetition(s.get("output",""))) for i, s in enumerate(samples)] high_rep = [(i, round(r,3)) for i, r in rep_scores if r > 0.3] print(f'高重复样本数: {len(high_rep)},示例: {high_rep[:10]}') print(f'复制型样本: {check_copy_overlap(samples)[:10]}') print(f'长度分布: {token_length_stats(samples)}')运行方式:python clean_sft_data.py ./train.jsonl。输出会告诉你哪些样本需要人工复查。重点看「高重复样本」和「复制型样本」——这两类数据会让模型学会「重复输入」或「复制粘贴」,而不是真正理解任务。
3.2 LoRA 训练参数对照表
下面这张表是我在 Qwen2.5-7B 上实测后总结的安全域。注意:学习率超过 2e-5 时,幻觉率会明显上升。
| 参数 | 安全值 | 危险值 | 说明 |
|---|---|---|---|
| LoRA r | 8~16 | 64+ | r 过大容易过拟合噪音 |
| LoRA alpha | 16~32 | 128+ | 通常设为 r 的 2 倍 |
| learning_rate | 1e-5 ~ 2e-5 | 1e-4+ | 超过 2e-5 幻觉率飙升 |
| num_epochs | 1~3 | 5+ | 超过 3 轮验证 loss 反弹 |
| batch_size | 8~32 | 1 | 太小梯度噪声大 |
| warmup_ratio | 0.03~0.1 | 0 | 无 warmup 初期震荡 |
| weight_decay | 0.01~0.1 | 0 | 轻微正则有益 |
| lora_dropout | 0.05~0.1 | 0 | 防过拟合 |
| target_modules | q_proj,v_proj | 全部 | 全加显存涨 20% |
对应的train_config.json片段:
{ "lora_r": 16, "lora_alpha": 32, "lora_dropout": 0.05, "learning_rate": 1e-5, "num_train_epochs": 2, "per_device_train_batch_size": 16, "gradient_accumulation_steps": 2, "warmup_ratio": 0.05, "weight_decay": 0.01, "target_modules": ["q_proj", "v_proj"], "fp16": true, "gradient_checkpointing": true, "eval_strategy": "steps", "eval_steps": 100, "save_strategy": "steps", "save_steps": 200, "load_best_model_at_end": true, "metric_for_best_model": "eval_loss" }如果你用的是 LLaMA-Factory,对应的 YAML 配置片段:
finetuning_type: lora lora_rank: 16 lora_alpha: 32 lora_dropout: 0.05 learning_rate: 1.0e-5 num_train_epochs: 2.0 per_device_train_batch_size: 16 gradient_accumulation_steps: 2 lr_scheduler_type: cosine warmup_ratio: 0.05 weight_decay: 0.01 fp16: true gradient_checkpointing: true evaluation_strategy: steps eval_steps: 100 save_steps: 200 load_best_model_at_end: true3.3 推理参数模板
微调后的模型分布变了,推理参数不能沿用基座模型的默认值。下面是一个经过验证的推理配置:
generation_config = { "max_new_tokens": 512, "do_sample": True, "temperature": 0.7, "top_p": 0.9, "top_k": 50, "repetition_penalty": 1.15, "no_repeat_ngram_size": 4, "num_beams": 1, }关键点:temperature不要低于 0.5(否则容易重复),不要高于 1.0(否则幻觉加剧);repetition_penalty设在 1.1~1.2 之间;no_repeat_ngram_size=4可以硬性阻止 4-gram 重复。
4. 验证请求与成功结果:幻觉验证 prompt 模板与对照测试
配置改完后,你需要一套可重复的验证流程。这一节给出具体的 prompt 模板和对照测试方法。
4.1 幻觉验证 prompt 模板
准备一个hallucination_test.jsonl,每行一条测试用例,包含prompt、reference(参考答案)、category(测试类型)。下面给 5 类模板:
{"prompt": "我们公司的年假制度是几天?", "reference": "根据员工手册,入职满1年5天,满3年10天,满5年15天。", "category": "事实召回"} {"prompt": "请列出产品X的三个核心参数。", "reference": "电压48V,续航80km,充电时间4小时。", "category": "参数准确性"} {"prompt": "如果用户要求退款,第一步应该做什么?", "reference": "引导用户提供订单号,然后核实退款原因。", "category": "流程遵循"} {"prompt": "请用JSON格式输出:{'name': '张三', 'age': 30}", "reference": "输出必须是合法JSON,字段名和值完全一致。", "category": "格式遵循"} {"prompt": "请解释什么是量子纠缠,用一句话。", "reference": "量子纠缠是指两个粒子状态关联,测量一个会瞬间影响另一个。", "category": "知识边界"}测试脚本:
import json from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_path = "./qwen_lora_merged" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True ) def generate(prompt): messages = [{"role": "user", "content": prompt}] text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(text, return_tensors="pt").to(model.device) with torch.no_grad(): out = model.generate( **inputs, max_new_tokens=256, do_sample=True, temperature=0.7, top_p=0.9, repetition_penalty=1.15, no_repeat_ngram_size=4, ) return tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) with open("hallucination_test.jsonl", "r", encoding="utf-8") as f: for line in f: case = json.loads(line) answer = generate(case["prompt"]) print(f"[{case['category']}] Q: {case['prompt']}") print(f"A: {answer}") print(f"参考: {case['reference']}") print("-" * 60)4.2 成功结果判断标准
跑完测试后,按下面三个指标判断:
事实一致性:回答中的关键数字、名称、流程步骤是否与 reference 一致。如果 5 条里错 2 条以上,说明幻觉严重。
重复率:用第 3 节的check_repetition函数计算每条回答的 4-gram 重复率。正常应低于 5%,超过 15% 说明解码参数有问题。
格式遵循:对于要求 JSON 或特定格式的用例,用json.loads尝试解析,失败则说明格式遵循能力退化。
一个健康的微调模型,在 50 条测试集上应该达到:事实一致性 > 85%,重复率 < 5%,格式遵循 > 90%。如果低于这个线,回到第 3 节检查数据和训练配置。
4.3 基座对照测试
用 TaoToken API 跑同样的 prompt,拿到基座模型的回答,和微调模型做逐条对比。如果基座在 45/50 条上正常,微调只有 25/50 条正常,那问题就在微调环节。如果基座本身也只有 30/50 条正常,那可能是 prompt 本身有歧义,或者基座模型在这个领域能力不足。
对照测试的代码示例:
import requests API_URL = "https://taotoken.net/api/v1/chat/completions" API_KEY = "your_taotoken_key" def call_base_model(prompt): headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} payload = { "model": "qwen2.5-7b-instruct", "messages": [{"role": "user", "content": prompt}], "temperature": 0.7, "max_tokens": 256, } resp = requests.post(API_URL, headers=headers, json=payload, timeout=60) return resp.json()["choices"][0]["message"]["content"]把基座回答和微调回答并排打印,人工标注哪些是幻觉。这个过程虽然手动,但 50 条样本半小时就能标完,比盲目调参高效得多。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 报错对照
排查过程中会遇到各种报错。这一节按真实报错信息给出定位思路。
5.1 401 Unauthorized
如果你在用 TaoToken API 做基座对照测试时遇到 401,先检查 API Key 是否正确、是否过期。在 https://taotoken.net/api-keys 重新生成一个 Key,然后确认请求头格式是Authorization: Bearer sk-xxx。注意不要有多余空格。如果 Key 没问题,检查请求的 model 名称是否在平台支持列表里。
5.2 local proxy failed
这个报错通常出现在你本地推理服务通过代理转发请求时。排查顺序:先确认本地推理服务(如 vLLM、TGI)是否正常启动,端口是否被占用;再检查代理配置是否指向了正确的地址。如果你在代码里设置了HTTP_PROXY或HTTPS_PROXY环境变量,尝试临时取消,看是否能直连。注意:这里说的代理是本地服务转发,不是网络访问工具。
5.3 reading choices 报错
KeyError: 'choices'或reading 'choices'通常意味着 API 返回的不是标准 OpenAI 格式。可能原因:请求体格式不对(比如 messages 字段拼写错误)、model 名称不存在、或者返回了错误信息但你没检查状态码。排查方法:先打印resp.status_code和resp.text,看原始返回是什么。如果是 400,通常是参数问题;如果是 404,通常是 model 名称或路径问题。
5.4 OAuth 相关报错
如果你在用 Claude Code 或类似工具接入时遇到 OAuth 报错,检查你的接入配置。Claude Code 的接入文档在 https://taotoken.net/ClaudeCodeAnthropic ,里面有完整的 Base URL、Key、Model ID 三件套配置说明。常见错误是 Base URL 填成了网页地址而不是 API 地址,或者 Model ID 用了不支持的名称。
5.5 微调模型加载报错
RuntimeError: Error(s) in loading state_dict通常是因为 LoRA adapter 和基座模型版本不匹配。确认你加载的基座模型和训练时用的是同一个 checkpoint。如果是 Qwen2.5-7B-Instruct,不要混用 Qwen2-7B-Instruct 的 adapter。
CUDA out of memory在推理时出现,降低max_new_tokens,或者用 4-bit 量化加载。如果还是不够,把device_map改成"auto"让 accelerate 自动分配。
5.6 输出重复坍塌
模型陷入「重复同一句话」的循环,优先检查三个参数:temperature是否设成了 0 或接近 0;repetition_penalty是否没设或设成了 1.0;no_repeat_ngram_size是否没启用。把这三个参数按第 3 节的模板设置后,重复问题通常能解决 80%。如果还重复,说明训练数据里有大量重复样本,回到第 3 节跑数据清洗脚本。
6. 语义一致 CTA:把排查流程固化成可复用的验证管线
排查完一轮后,建议把上面的脚本和配置固化成一条可复用的验证管线。具体做法:把数据清洗、训练配置、推理参数、幻觉测试四个环节写成 Makefile 或 shell 脚本,每次微调后自动跑一遍。
对于需要频繁做基座对照测试的场景,可以用 TaoToken 的 API 来跑基座模型,省去本地加载基座的显存开销。API Key 在 https://taotoken.net/api-keys 管理,接入文档在 https://taotoken.net/doc 。如果你后续要做长期的编码类微调验证,Coding Plan 提供了更稳定的调用额度: https://taotoken.net/coding-plan 。
最后给一个实操建议:每次微调后,先跑 50 条幻觉测试集,再决定是否上线。不要只看 loss 曲线。loss 低不代表幻觉少,这是我在 Qwen 微调上踩过的最大的坑。把验证管线跑通,比调参更重要。