
1. 为什么 Qwen 微调后必须做领域问答准确率评测你花了两天时间用 LoRA 把 Qwen2.5-7B 在医疗问答数据上微调了一轮loss 曲线漂亮得像教科书。然后你随手问了几个问题回答看起来也挺像那么回事。于是你准备上线。先别急。我见过太多团队在这个节点翻车微调后的模型在训练集上表现惊艳一到真实业务问题就开始一本正经地胡说八道。更麻烦的是你根本不知道它错在哪里、错了多少、比微调前好了还是差了。这就是领域问答准确率评测要解决的问题。它不是跑一个通用 benchmark 打个分就完事而是要回答三个具体问题微调后的 Qwen 在你关心的领域里答案对不对对到什么程度哪些类型的问题仍然会错领域问答评测和通用评测有本质区别。通用评测比如 MMLU、C-Eval考的是模型的知识广度题目有标准答案对错分明。但领域问答往往是开放式的用户问“这个患者的降压方案该怎么调整”参考答案可能是一段结构化的临床推理不是一个选项。这时候你用精确匹配去打分基本全是零分。所以你需要一套分层的评测体系。第一层是规则类指标处理那些有明确标准答案的问题比如药品剂量、法条编号、参数数值。第二层是语义类指标处理表述不同但意思相同的情况比如 BERTScore 或者基于嵌入的相似度。第三层是 LLM-as-a-Judge用一个更强的模型或者同系列更大的模型来评判答案质量配合评分准则给出细粒度打分。这套体系的核心价值在于可复现。你今天测出来准确率 78%下周模型迭代后测出来 82%这个提升是真实的还是评测噪声只有当你固定了评测数据集、固定了评分脚本、固定了随机种子这个对比才有意义。适合读这篇文章的人正在做 Qwen 系列模型微调、需要量化微调效果的算法工程师需要向业务方证明模型质量的产品经理以及任何想建立一套靠谱评测流程的开发者。接下来的内容会从环境准备开始一步步带你跑通完整的评测管道包括数据集划分、评测脚本、指标计算和常见报错排查。2. 用 TaoToken 接入 Qwen 评测模型的前置准备做 LLM-as-a-Judge 评测你需要一个评判模型。最省事的方案是用 Qwen2.5-72B-Instruct 作为 judge因为它的判断能力和 Qwen2.5-7B 有足够差距能给出有区分度的评分。但 72B 模型本地部署对显存要求太高单卡 A100 80G 跑 4-bit 量化才勉强够用。更实际的方案是通过 API 调用。TaoToken 提供了统一的模型接入层你可以在一个控制台里管理多个模型的 API Key包括 Qwen 系列。这样评测脚本里只需要改一个 model 参数就能切换 judge 模型不用重新部署。先注册并拿到 API Key。访问官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 完成注册后进入控制台的 API Keys 页面创建一个新的 Key。建议给评测任务单独建一个 Key方便后续做用量统计和权限控制。创建 Key 的直达链接是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。进去之后点“创建新密钥”复制生成的 Key 字符串存到环境变量里。接下来确认你要用的模型 ID。Qwen 系列常用的几个Qwen2.5-7B-Instruct 适合做候选模型被评测的对象Qwen2.5-72B-Instruct 适合做 judge 模型。你可以在模型对话页面先手动测试一下模型是否可用链接是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API 的基础地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数直接用于代码里的 base_url 配置。完整的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各个语言 SDK 的调用示例。环境变量配置建议这样写export TAOTOKEN_API_KEYsk-你的密钥 export TAOTOKEN_BASE_URLhttps://taotoken.net/api export JUDGE_MODELQwen2.5-72B-Instruct export CANDIDATE_MODELQwen2.5-7B-Instruct如果你打算长期做模型评测和迭代可以考虑 Coding Plan它提供了更稳定的调用配额和更低的单次成本适合需要频繁跑评测的场景。入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Python 环境方面你需要安装 openai SDKTaoToken 兼容 OpenAI 接口格式、datasets、pandas、scikit-learn 和 tqdm。建议用虚拟环境隔离python -m venv eval-env source eval-env/bin/activate pip install openai datasets pandas scikit-learn tqdm到这里前置准备就完成了。你有了 API Key、知道了 base_url、确定了 judge 模型和候选模型、装好了依赖。下一节开始写可复制的评测配置和脚本。3. 可复制的评测配置与数据集划分脚本评测的第一步是准备数据。你需要一个 JSONL 格式的评测集每行包含 question、reference_answer、category 三个字段。category 用来做分层统计比如按“药物剂量”“诊断标准”“手术方案”分类。数据集划分的原则是评测集必须和训练集完全隔离。如果你用训练数据里的问题来评测那测出来的准确率没有意义。建议从原始数据里随机抽 10% 作为评测集如果数据量少于 500 条至少留 50 条做评测。下面是一个数据集划分脚本它会读取原始 JSONL按比例切分并保证每个 category 都有样本import json import random from collections import defaultdict def split_dataset(input_path, eval_ratio0.1, seed42): random.seed(seed) with open(input_path, r, encodingutf-8) as f: data [json.loads(line) for line in f if line.strip()] by_category defaultdict(list) for item in data: by_category[item.get(category, default)].append(item) eval_set, train_set [], [] for cat, items in by_category.items(): random.shuffle(items) n_eval max(1, int(len(items) * eval_ratio)) eval_set.extend(items[:n_eval]) train_set.extend(items[n_eval:]) random.shuffle(eval_set) random.shuffle(train_set) with open(eval_set.jsonl, w, encodingutf-8) as f: for item in eval_set: f.write(json.dumps(item, ensure_asciiFalse) \n) with open(train_set.jsonl, w, encodingutf-8) as f: for item in train_set: f.write(json.dumps(item, ensure_asciiFalse) \n) print(f评测集: {len(eval_set)} 条, 训练集: {len(train_set)} 条) return eval_set, train_set if __name__ __main__: split_dataset(raw_qa.jsonl, eval_ratio0.1)接下来是评测配置文件。用 JSON 格式定义评测参数包括 judge 模型、评分准则、指标列表和并发数{ judge_model: Qwen2.5-72B-Instruct, candidate_model: Qwen2.5-7B-Instruct, base_url: https://taotoken.net/api, metrics: [exact_match, f1, llm_judge], rubric: 请判断候选答案与参考答案是否语义一致。如果候选答案包含参考答案的核心信息且无事实错误给1分如果部分正确但有遗漏或轻微错误给0.5分如果答案错误或包含幻觉信息给0分。只输出分数不要解释。, max_concurrency: 8, temperature: 0.0, seed: 42 }这个配置里的 rubric 是关键。评分准则写得好不好直接决定 LLM-Judge 和人工判断的一致性。好的 rubric 要包含三个要素任务描述判断什么、评分阶梯什么情况给什么分、边界说明什么算错误。建议先用 20 条样本做小规模测试人工核对 judge 的评分是否合理再全量跑。如果你用 Claude Code 做开发可以把评测脚本集成到项目里通过 settings.json 配置环境变量。在项目根目录创建 .claude/settings.json{ env: { TAOTOKEN_API_KEY: sk-你的密钥, TAOTOKEN_BASE_URL: https://taotoken.net/api, JUDGE_MODEL: Qwen2.5-72B-Instruct } }这样在 Claude Code 里执行评测脚本时环境变量会自动加载。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有完整的配置说明。数据集和配置准备好之后下一节写核心评测脚本包括调用候选模型生成答案、调用 judge 模型评分、计算各项指标。4. 评测脚本实现与准确率召回率验证核心评测脚本分三步加载评测集、对每条问题调用候选模型生成答案、用 judge 模型对答案打分。最后聚合所有分数计算准确率、召回率和 F1。先写一个通用的 API 调用函数封装重试和超时逻辑import os import time import json from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ.get(TAOTOKEN_BASE_URL, https://taotoken.net/api) ) def call_model(model, messages, temperature0.0, max_retries3): for attempt in range(max_retries): try: resp client.chat.completions.create( modelmodel, messagesmessages, temperaturetemperature, max_tokens512 ) return resp.choices[0].message.content.strip() except Exception as e: if attempt max_retries - 1: raise time.sleep(2 ** attempt) return 然后是 judge 评分函数。把问题、参考答案、候选答案拼成 prompt让 judge 模型输出 0、0.5 或 1def judge_score(question, reference, candidate, rubric): prompt f{rubric} 问题{question} 参考答案{reference} 候选答案{candidate} 分数 result call_model( os.environ.get(JUDGE_MODEL, Qwen2.5-72B-Instruct), [{role: user, content: prompt}] ) try: score float(result.strip().split()[0]) return min(max(score, 0.0), 1.0) except (ValueError, IndexError): return 0.0接下来是主评测循环。对评测集里每条数据先让候选模型生成答案再让 judge 打分同时计算精确匹配和 token F1import re from collections import Counter def normalize(text): text text.lower().strip() text re.sub(r[^\w\s], , text) return .join(text.split()) def exact_match(ref, cand): return 1.0 if normalize(ref) normalize(cand) else 0.0 def token_f1(ref, cand): ref_tokens normalize(ref).split() cand_tokens normalize(cand).split() if not ref_tokens or not cand_tokens: return 0.0 common Counter(ref_tokens) Counter(cand_tokens) overlap sum(common.values()) if overlap 0: return 0.0 precision overlap / len(cand_tokens) recall overlap / len(ref_tokens) return 2 * precision * recall / (precision recall) def run_eval(eval_path, config): with open(eval_path, r, encodingutf-8) as f: dataset [json.loads(line) for line in f if line.strip()] results [] for i, item in enumerate(dataset): question item[question] reference item[reference_answer] candidate call_model( config[candidate_model], [{role: user, content: question}], temperatureconfig.get(temperature, 0.0) ) em exact_match(reference, candidate) f1 token_f1(reference, candidate) judge judge_score(question, reference, candidate, config[rubric]) results.append({ id: item.get(id, i), category: item.get(category, default), question: question, reference: reference, candidate: candidate, exact_match: em, token_f1: f1, llm_judge: judge }) if (i 1) % 10 0: print(f已完成 {i1}/{len(dataset)} 条) return results跑完评测后聚合指标并输出报告import pandas as pd def summarize(results): df pd.DataFrame(results) summary { 样本数: len(df), 精确匹配准确率: df[exact_match].mean(), Token F1: df[token_f1].mean(), LLM Judge 准确率: df[llm_judge].mean(), LLM Judge 召回率: (df[llm_judge] 0.5).mean() } print( 整体指标 ) for k, v in summary.items(): print(f{k}: {v:.4f} if isinstance(v, float) else f{k}: {v}) print(\n 分类别指标 ) cat_stats df.groupby(category).agg( 样本数(id, count), 精确匹配(exact_match, mean), TokenF1(token_f1, mean), Judge准确率(llm_judge, mean) ) print(cat_stats.to_string()) return summary, cat_stats这里的“LLM Judge 召回率”定义是在所有 judge 评分大于等于 0.5 的样本中实际有多少比例是真正正确的。更严谨的做法是引入人工标注的金标准计算 judge 和人工的一致性。如果暂时没有人工标注可以用 judge 评分大于等于 0.5 作为“模型认为正确”的代理指标。运行完整评测if __name__ __main__: with open(eval_config.json, r, encodingutf-8) as f: config json.load(f) results run_eval(eval_set.jsonl, config) summary, cat_stats summarize(results) with open(eval_results.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2) print(\n详细结果已保存到 eval_results.json)跑完之后你会得到一份完整的评测报告包括整体准确率和分类别表现。如果某个类别的准确率明显偏低比如“药物剂量”只有 45%那就说明模型在这个细分领域还需要补充训练数据。5. 评测过程中的常见报错与排查方法评测脚本跑起来之后最容易遇到的报错集中在 API 调用和评分解析两个环节。下面按报错类型逐一排查。401 认证失败。报错信息通常是Error code: 401 - Unauthorized。原因一般是 API Key 没设置对或者环境变量没加载。检查步骤先确认echo $TAOTOKEN_API_KEY能输出正确的 Key再确认代码里base_url是https://taotoken.net/api不要多加路径最后确认 Key 没有过期或被禁用。如果用的是 Claude Code 的 settings.json 配置检查 JSON 格式是否正确环境变量名是否和代码里读取的一致。local proxy failed 连接失败。这个报错说明请求根本没发出去通常是网络层的问题。检查你的运行环境是否能正常访问外网如果是公司内网确认有没有配置 HTTP 代理。注意不要在代码里硬编码代理地址用环境变量HTTP_PROXY和HTTPS_PROXY控制。如果是在 Docker 容器里跑确认容器的网络模式是 bridge 还是 hostbridge 模式下需要额外配置 DNS。reading choices 解析错误。报错信息类似KeyError: choices或AttributeError: NoneType object has no attribute choices。这说明 API 返回的响应结构不符合预期。最常见的原因是模型名称写错了比如把Qwen2.5-72B-Instruct写成了qwen2.5-72b导致 API 返回错误信息而不是正常的 completion 结构。排查方法在调用后先打印resp的原始内容确认返回的 JSON 结构。另外如果max_tokens设置得太小模型可能返回空内容也会导致解析失败。OAuth 相关报错。如果你用的是 Claude Code 或者某些需要 OAuth 授权的客户端可能会遇到OAuth token expired或invalid_grant。这类问题通常需要重新走一遍授权流程。在 TaoToken 的控制台里重新生成 API Key 是最快的解决方式。如果用的是 Coding Plan确认订阅状态是否正常。judge 评分解析失败。LLM-Judge 返回的内容可能不是纯数字比如返回“分数是1”或者“1分”。这时候float(result.strip().split()[0])会抛异常。改进方案是用正则提取第一个数字import re def extract_score(text): match re.search(r(\d\.?\d*), text) if match: return min(max(float(match.group(1)), 0.0), 1.0) return 0.0另外如果 judge 模型返回了 0 和 1 之外的分数比如 3 分满分 5 分需要在 rubric 里明确说明评分范围是 0 到 1。评分准则写得越明确解析失败的概率越低。评测速度太慢。如果评测集有几百条串行调用会非常慢。解决方案是用并发。把评测循环改成concurrent.futures.ThreadPoolExecutor并发数控制在 8 到 16 之间太高容易触发 API 限流from concurrent.futures import ThreadPoolExecutor, as_completed def run_eval_parallel(eval_path, config, max_workers8): with open(eval_path, r, encodingutf-8) as f: dataset [json.loads(line) for line in f if line.strip()] def process_item(item): question item[question] reference item[reference_answer] candidate call_model( config[candidate_model], [{role: user, content: question}], temperatureconfig.get(temperature, 0.0) ) return { id: item.get(id), category: item.get(category, default), question: question, reference: reference, candidate: candidate, exact_match: exact_match(reference, candidate), token_f1: token_f1(reference, candidate), llm_judge: judge_score(question, reference, candidate, config[rubric]) } results [] with ThreadPoolExecutor(max_workersmax_workers) as executor: futures {executor.submit(process_item, item): item for item in dataset} for future in as_completed(futures): results.append(future.result()) return results并发跑的时候注意judge 模型的调用频率会比候选模型高因为每条数据都要调一次 judge。如果遇到 429 限流把max_workers降到 4或者在调用之间加一个小的 sleep。6. 建立可复现的评测流程与持续迭代跑通一次评测只是开始。真正有价值的是把评测变成模型迭代流程里的固定环节每次微调后自动跑一遍用数据决定是否上线。具体做法是把评测脚本集成到 CI 里。每次训练完成产出新的模型 checkpoint 后触发评测任务对比基线指标。如果 LLM Judge 准确率下降超过 2 个百分点就阻断上线流程。这个阈值可以根据业务容忍度调整医疗、法律这类高风险领域建议设成 1 个百分点。评测集本身也需要维护。领域知识会更新新的问题类型会出现旧的评测集可能逐渐失去区分度。建议每季度做一次评测集审查补充新出现的边界 case淘汰已经全部答对的简单样本。评测集的版本要和模型版本一起管理每次评测记录用的是哪个版本的评测集。关于 judge 模型的选择实测下来 Qwen2.5-72B-Instruct 在中文领域问答上的判断一致性已经足够好和人工标注的 Kappa 系数能到 0.8 左右。如果你的预算有限Qwen2.5-14B-Instruct 也能用但需要更精细的 rubric 设计来弥补判断力的差距。最后提醒一点评测指标是手段不是目的。准确率从 78% 提升到 82% 固然好但更重要的是搞清楚那 18% 的错误集中在哪些类型的问题上。分类别统计能帮你定位到具体的薄弱环节比如“药物相互作用”类问题准确率只有 60%那就针对性地补充这类训练数据比盲目扩大数据量有效得多。评测脚本和配置都可以在项目里复用把 eval_config.json 和 run_eval.py 放到独立的 eval 目录下和训练代码解耦。这样换模型、换数据集的时候只需要改配置不用动代码。