Qwen3 检索微调 Query 级泄漏,让走 TaoToken 的 Claude Code 改切分逻辑
发布时间:2026/9/19 21:53:11 锦皓数字建站

Qwen3 检索微调 Query 级泄漏让走 TaoToken 的 Claude Code 改切分逻辑Qwen3-Reranker 0.6B 的 Pointwise LoRA 排障里pair 展开后随机切分最容易把验证指标做虚。本文用走 TaoToken 的 Claude Code 改 Query ID 去重切分再转 ms-swift generative_reranker 格式。先打开 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建 Key并把 Claude Code 的 Base URL 填成 https://taotoken.net/api不要带 /v1。这样改脚本和排查配置期间产生的 Token 消耗会记录在 TaoToken 用量中。真正要修的不是一行随机种子而是数据切分协议训练、验证、测试三套必须按 Query ID 互斥不能让同一 Query 的正负例跨集合出现。下面按排障流程走先定位泄漏再给可复制配置和 swift sft 重跑命令最后用 MRR/nDCG 的回归结果判断提升到底来自排序能力还是数据泄漏。原问题Qwen3 检索微调里的 Query 级泄漏在 Reranker 训练里统一候选池通常一行一个 Query带 positives、hard_negatives、medium_negatives、random_negatives。然后为了 ms-swift generative_reranker需要把一行 group 展开成 positive_messages 和 negative_messages。很多脚本图省事先把所有 pair 打散再按 8:1:1 随机切分。这个动作在文本分类里也许只是小问题在检索排序里却是 Query 级泄漏同一个 Query 的候选组被拆到不同集合训练时模型见过这个 Query 的词面、实体和主题验证时当然更容易排对。指标看起来升了实际是验证集嵌入了训练信号。更隐蔽的是泄漏不一定表现为验证 loss 明显下降。Pointwise 训练时每个 pair 是独立 yes/no 判断同一 Query 的正例在训练集、负例在验证集验证集依然可能有一堆容易的负例。MRR10 和 nDCG10 可能只是局部波动但 MAP 或跨数据集结果会露出破绽。排障时要先做三件事检查 train/val/test 的 query_id 交集检查样本行数是 Query 数还是 pair 数检查转换脚本是否在切分后才展开 pair。正确顺序是先在统一候选池按 Query ID 切分三个集合互斥再各自转换成 ms-swift 格式。还要注意 Reranker 的职责边界它处理的是第一阶段已经送进来的候选如果召回阶段漏掉了文档精排模型再强也补不回来。如果为了让每个验证 Query 都有正例强行向候选集插入 positive指标会进一步失真因为召回上限被改写了。本文只修 Query 级泄漏不碰真实候选构造。TaoToken 前置Claude Code 的 settings.json 与 ANTHROPIC_*要让 Claude Code 参与改切分逻辑先把请求入口切到 TaoToken。打开官网创建 Key 后在 Claude Code 的 settings.json 里写 env。项目级可放 .claude/settings.json用户级可放 ~/.claude/settings.json。注意 Base URL 用 https://taotoken.net/api不要写成 https://taotoken.net/api/v1。Claude Code 的 ANTHROPIC_* 配置如下{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: YOUR_API_KEY, ANTHROPIC_MODEL: MODEL_ID, ANTHROPIC_SMALL_FAST_MODEL: MODEL_ID } }MODEL_ID 从 TaoToken 控制台或模型对话页面复制不要手写猜测。改完 settings.json 后重新打开终端或者用 env 临时注入export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEYYOUR_API_KEY export ANTHROPIC_MODELMODEL_ID claude如果你用 TaoToken CLI 管理 Claude Code可以npm i -g taotoken/taotoken taotoken cc -k YOUR_API_KEY -u API -m MODEL_ID这里的 -u API 对应 API 入口不附加 /v1。配置完成后在 Claude Code 里让它读取本地 split_by_query.py、to_swift_reranker.py 和 swift sft 脚本只改数据切分与格式转换不要改模型、loss 和 instruction。对话期间消耗的 Token 可以在 TaoToken 控制台用量中查看便于排障时确认请求确实走了 TaoToken而不是本地旧环境变量。可复制配置按 Query ID 切分并转 ms-swift generative_reranker下面给出两个脚本。第一个只做 Query 级切分输入统一候选池 JSONL输出 train.pool.jsonl、val.pool.jsonl、test.pool.jsonl。每个 Query 只会落在一个集合里。第二个把 train/val 转成 ms-swift generative_reranker 需要的 group 格式并保留 query_id 方便查泄漏。# split_by_query.py import json, random, os def load_pool(path): rows [] with open(path, encodingutf-8) as f: for line in f: line line.strip() if line: rows.append(json.loads(line)) return rows def dedup_by_qid(rows): seen {} for r in rows: qid str(r[query_id]).strip() if qid not in seen: seen[qid] r return seen def split_pool(input_path, out_dir, ratios(0.8, 0.1, 0.1), seed42): rows dedup_by_qid(load_pool(input_path)) qids sorted(rows.keys()) rnd random.Random(seed) rnd.shuffle(qids) n len(qids) n_train int(n * ratios[0]) n_val int(n * ratios[1]) parts { train: qids[:n_train], val: qids[n_train:n_train n_val], test: qids[n_train n_val:], } os.makedirs(out_dir, exist_okTrue) used set() for name, ids in parts.items(): dst os.path.join(out_dir, f{name}.pool.jsonl) with open(dst, w, encodingutf-8) as w: for qid in ids: if qid in used: raise RuntimeError(fduplicate qid across split: {qid}) used.add(qid) w.write(json.dumps(rows[qid], ensure_asciiFalse) \n) print(name, len(ids), dst) if __name__ __main__: split_pool(data/pool/reranker_pool.jsonl, data/pool/split)# to_swift_reranker.py import json, os INSTRUCTION Given a search query, retrieve passages that answer the query def pick_negatives(row, neg_k): hard row.get(hard_negatives, []) medium row.get(medium_negatives, []) random_neg row.get(random_negatives, []) return (hard medium random_neg)[:neg_k] def to_swift_group(row, neg_k7): positives row.get(positives, []) negatives pick_negatives(row, neg_k) if not positives or not negatives: return None return { query_id: row[query_id], messages: [ {role: system, content: row.get(instruction, INSTRUCTION)}, {role: user, content: row[query]} ], positive_messages: [ [{role: assistant, content: p}] for p in positives ], negative_messages: [ [{role: assistant, content: n}] for n in negatives ] } def convert_split(split_dir, out_dir, neg_k7): os.makedirs(out_dir, exist_okTrue) for name in [train, val]: src os.path.join(split_dir, f{name}.pool.jsonl) dst os.path.join(out_dir, f{name}.swift.jsonl) count 0 with open(src, encodingutf-8) as r, open(dst, w, encodingutf-8) as w: for line in r: row json.loads(line) item to_swift_group(row, neg_k) if item: w.write(json.dumps(item, ensure_asciiFalse) \n) count 1 print(name, count, dst) if __name__ __main__: convert_split(data/pool/split, data/swift, neg_k7)检查泄漏的脚本可以单独放在 CI 或训练前执行# check_query_leak.py import json def qids(path): out set() with open(path, encodingutf-8) as f: for line in f: row json.loads(line) if query_id in row: out.add(str(row[query_id])) return out train qids(data/swift/train.swift.jsonl) val qids(data/swift/val.swift.jsonl) print(train/val overlap:, len(train val)) assert not (train val), Query 级泄漏仍未修掉重跑 0.6B Pointwise LoRA 的命令swift sft \ --model Qwen/Qwen3-Reranker-0.6B \ --task_type generative_reranker \ --loss_type pointwise_reranker \ --tuner_type lora \ --dataset data/swift/train.swift.jsonl \ --val_dataset data/swift/val.swift.jsonl \ --torch_dtype bfloat16 \ --use_logits_to_keep true \ --max_length 1024 \ --truncation_strategy right \ --learning_rate 5e-5 \ --lr_scheduler_type cosine \ --warmup_ratio 0.05 \ --weight_decay 0.01 \ --max_grad_norm 1.0 \ --num_train_epochs 1 \ --per_device_train_batch_size 1 \ --per_device_eval_batch_size 1 \ --gradient_accumulation_steps 8 \ --gradient_checkpointing true \ --eval_strategy steps \ --eval_steps 100 \ --save_steps 100 \ --save_total_limit 2 \ --load_best_model_at_end true \ --metric_for_best_model loss \ --greater_is_better false \ --lora_rank 8 \ --lora_alpha 32 \ --lora_dropout 0.05 \ --target_modules all-linear如果 Listwise 对照组内负例数改成 3loss_type 换 listwise_reranker不要拿 Pointwise 的 checkpoint 当起点去比较独立 Listwise。这里只修 Query 级泄漏训练变量尽量少。验证请求重跑 swift sft 后检查 MRR/nDCG 是否来自真实排序重跑前先执行python split_by_query.py python to_swift_reranker.py python check_query_leak.py预期看到 train/val overlap 为 0train.swift.jsonl 和 val.swift.jsonl 的行数等于各自 Query 数而不是 pair 展开后的数量。然后在 Claude Code 会话里让它检查一次 diffsettings.json 中 ANTHROPIC_BASE_URL 是否为 https://taotoken.net/apiswift sft 命令里 --task_type 是否为 generative_reranker--loss_type 是否为 pointwise_reranker--dataset 是否指向新切分的 train.swift.jsonl。确认后启动训练。训练日志里观察几个点数据加载是否报格式错误eval_steps 是否按预期打印验证 losstrainer_state.json 是否保存最佳 checkpoint。LoRA 输出目录应包含 adapter_model.safetensors 和 adapter 配置评测时先加载 Qwen/Qwen3-Reranker-0.6B再挂载 adapter不要把 adapter 目录当完整模型路径。重跑期间 Claude Code 的对话消耗会记录在 TaoToken 用量中如果用量没有变化优先检查终端是否还在旧环境变量下。评测时不要强行给候选集插入正例保留真实 BM25 Top 50 或你的一阶段召回结果。比较修复前后如果修复后 MRR10、nDCG10 明显回落说明之前的高指标有很大一部分来自 Query 记忆和候选组跨集如果修复后仍相对预训练模型稳定提升并且 mMARCO、DuReader、MIRACL 这类不同测试集同向才更可能来自真实排序能力。测试集不能参与选 checkpointcheckpoint 选择仍看验证 loss 或事先固定的验证指标。本篇常见错排查Query 去重、generative_reranker、settings.json 与 swift sft只在 pair 层去重仍按 pair 随机切分。pair 去重不能阻止同一 Query 跨集必须按 query_id 切分。转换时丢掉 query_id。转到 ms-swift 格式后仍保留 query_id 字段否则后续无法自动检查 train/val/test 交集。训练集、验证集、测试集用三套不同随机种子和不同过滤规则。统一在一个 split_pool 函数里先切 Query再分别转换。generative_reranker 字段写错。system 放 instructionuser 放 Queryassistant content 放候选文档正例进 positive_messages负例进 negative_messages。把候选文档塞进 user content。这样框架会把它当 Query 输入的一部分而不是待判断的候选。Pointwise 和 Listwise 的负例数混用。Pointwise 主训练可用 1 正 7 负Listwise 组内联合计算更大常用 1 正 3 负比较时要在实验记录里写清楚。settings.json 的 ANTHROPIC_BASE_URL 带了 /v1。TaoToken API 地址写 https://taotoken.net/api不带 /v1出现 404 先查这里。出现 401 或 403。去 API Keys 页面重新复制 Key确认 ANTHROPIC_API_KEY 没有多余空格且终端已重新加载配置。验证集 MRR 异常高但训练 loss 不降。优先怀疑 Query 级泄漏而不是先调学习率。直接用 BF16 softmax 概率排序。Reranker 评测用 FP32 的 yes_logit - no_logit 更稳避免高置信样本概率饱和成 1.0 后大量并列。截断策略破坏关键信息。instruction、Query、Document 拼接后右截断可能丢长文档尾部答案切换截断方向要做回归。instruction 在训练和评测中漂移。训练用一个检索描述评测换另一个描述指标变化就不能归因到模型。语义一致 CTA排障接入先取 Key再看 Claude Code 文档如果你正在按这篇修 Query 级泄漏下一步不是继续加训练轮数而是先把 Claude Code 的接入和 Key 管理固定下来。到 API Keys 页面创建或复制 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentquery_leak_claude_codeutm_campaignrewrite 然后按接入文档检查 settings.json 与 ANTHROPIC_* 环境变量https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentquery_leak_claude_codeutm_campaignrewrite 。Claude Code 专项配置可看https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentquery_leak_claude_codeutm_campaignrewrite 。想先验证模型对话和 Base URL 是否通可从控制台进入模型对话https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentquery_leak_claude_codeutm_campaignrewrite 。如果后续要把 Claude Code 长期用于检索微调、数据清洗和 Agent 流程再看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentquery_leak_claude_codeutm_campaignrewrite 。先把 Query ID 去重和 swift sft 重跑做扎实再讨论 MRR/nDCG 的提升是否可信。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。