
1. 评测分数虚高的元凶测试集被模型见过怎么办大模型评测里最容易被忽略的一件事就是数据污染检测。你辛辛苦苦跑完一轮评测发现某个模型在测试集上准确率突然涨了十几个点第一反应可能是这模型真强但更常见的情况是测试集里的样本模型在训练阶段已经见过了。这就是所谓的数据污染也叫测试集泄漏。数据污染检测要解决的问题很具体在正式评测之前确认测试集有没有被模型提前看过。它适合谁适合所有需要拿分数说话的人——做模型选型的算法工程师、写评测报告的团队、给客户交付 benchmark 结果的服务方。因为一旦测试集不可信后面所有的分数对比、能力结论、选型决策都建立在沙子上。污染不只是完全重复。完全重复最好查规范化文本后做哈希就能命中。真正麻烦的是近似重复和答案泄漏题干换了几个词、选项顺序调了一下、训练语料里混进了带解析过程的问答对这些都会把分数抬上去但用简单的字符串匹配根本查不出来。我试过在一个代码评测集上直接跑分结果某模型通过率高得离谱后来做了一轮近似重复检测发现将近 3% 的样本能在参考语料里找到高度相似的题面。剔掉这批样本重跑分数回落了 8 个点。这件事说明评测前不做污染检测后面的分数对比就缺少可信基础。这篇要交付的是一套可跟做的流程用 TaoToken 统一 Key 接入检测用的模型接口跑精确匹配、近似重复、答案泄漏三层检测最后对同一测试集做污染比对确认命中率和日志输出。整套流程你可以在本地脚本里复现不需要改评测框架本身。核心检索词先明确数据污染检测是什么——它是评测前对测试集做泄漏排查的步骤能做什么——发现完全重复、近似重复、答案泄漏适合谁——需要确认测试集没被模型见过的评测团队。下面从接入准备开始一步步把流程跑通。2. TaoToken 统一 Key 接入检测脚本的模型调用前置做污染检测尤其是近似重复和答案泄漏这两层需要调用 embedding 模型或对话模型来判断语义相似度。如果每个检测脚本都单独配一套 Key、单独处理不同厂商的 Base URL维护成本会很高。TaoToken 在这里的作用是提供一个统一的 API 入口让你用同一个 Key 调用不同模型检测脚本里只改 Model ID 就能切换。先说清楚它不是什么TaoToken 不是编辑器替代品也不是让你绕过评测框架的东西。它就是一个统一的模型调用入口官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。你把它理解成一个 Key 打通多个模型就行。接入前你需要准备三件套这三件套在任何检测脚本里都要写全Base URLhttps://taotoken.net/apiAPI Key在控制台创建地址 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentModel ID比如做 embedding 相似度用 embedding 类模型做答案泄漏判断用对话类模型具体可用模型在文档里查文档地址 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content如果你用的是 Claude Code 这类编码工具来写检测脚本接入配置也走同一套。Claude Code 的接入文档在 https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面写了 Base URL 和 Key 怎么填。Coding Plan 适合长期跑检测任务的团队地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。为什么检测脚本要用统一 Key因为污染检测往往要交叉验证MinHash 算一遍、embedding 算一遍、再用对话模型对高风险样本做人工复核辅助。如果每换一个模型就要改一套鉴权逻辑脚本会变得很脆。统一 Key 之后你只需要在配置里换 Model ID。这里给一个环境变量的约定后面所有脚本都读这套变量export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_EMBED_MODEL你的embedding模型ID export TAOTOKEN_CHAT_MODEL你的对话模型ID把 Key 放环境变量而不是硬编码进脚本是为了避免提交到仓库时泄漏。这一点在团队协作里尤其重要检测脚本经常要共享硬编码的 Key 一旦进了 git 历史就很难彻底清掉。接入验证很简单先用一个最小请求确认 Key 和 Base URL 通import os from openai import OpenAI client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) resp client.embeddings.create( modelos.environ[TAOTOKEN_EMBED_MODEL], input[测试集污染检测接入验证], ) print(len(resp.data[0].embedding))能打印出向量维度说明接入通了。如果这一步就报错先别往下写检测逻辑回到第 5 节排查。接入通了之后检测脚本的模型调用部分就固定下来了后面只关心检测算法本身。3. 可复制配置三层污染检测脚本与 settings 片段这一节给可直接复制的配置和脚本。检测分三层精确哈希、近似重复、答案泄漏。三层不是替代关系是递进的——精确重复都没清掉复杂检测就没意义。先给一个统一的配置文件用 JSON 写路径放在项目根目录的contamination_config.json{ dataset: eval_v3, corpus_path: ./data/reference_corpus.jsonl, testset_path: ./data/testset.jsonl, exact_hash: true, minhash: { enabled: true, num_perm: 128, jaccard_threshold: 0.85 }, embedding: { enabled: true, model_env: TAOTOKEN_EMBED_MODEL, cosine_threshold: 0.92, batch_size: 64 }, answer_leakage: { enabled: true, model_env: TAOTOKEN_CHAT_MODEL, top_k: 200 }, report: { output_path: ./reports/contamination_report.json, keep_removed_samples: true } }阈值不要照搬。代码题、数学题、问答题的文本相似度分布完全不同统一阈值会带来误杀或漏检。jaccard_threshold和cosine_threshold建议先在小样本上跑一遍看分布再定。第一层精确匹配规范化文本后做 SHA256import hashlib import re def normalize(text: str) - str: text text.lower() text re.sub(r\s, , text) return text.strip() def fingerprint(text: str) - str: return hashlib.sha256(normalize(text).encode()).hexdigest() def exact_match_check(testset, corpus): corpus_hashes {fingerprint(s[text]) for s in corpus} hits [] for sample in testset: if fingerprint(sample[text]) in corpus_hashes: hits.append(sample[id]) return hits第二层近似重复用 MinHash 算 Jaccard 相似度from datasketch import MinHash, MinHashLSH def build_minhash(text: str, num_perm: int 128) - MinHash: m MinHash(num_permnum_perm) for token in normalize(text).split(): m.update(token.encode(utf8)) return m def near_duplicate_check(testset, corpus, threshold0.85, num_perm128): lsh MinHashLSH(thresholdthreshold, num_permnum_perm) for i, sample in enumerate(corpus): lsh.insert(fcorpus_{i}, build_minhash(sample[text], num_perm)) hits [] for sample in testset: m build_minhash(sample[text], num_perm) result lsh.query(m) if result: hits.append({id: sample[id], matched: result}) return hits第三层用 embedding 做语义相似度走 TaoToken 统一 Keyimport os import numpy as np from openai import OpenAI client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) def embed_batch(texts, model, batch_size64): vectors [] for i in range(0, len(texts), batch_size): batch texts[i:i batch_size] resp client.embeddings.create(modelmodel, inputbatch) vectors.extend([d.embedding for d in resp.data]) return np.array(vectors) def cosine_sim(a, b): a a / np.linalg.norm(a, axis1, keepdimsTrue) b b / np.linalg.norm(b, axis1, keepdimsTrue) return a b.T答案泄漏检测用对话模型辅助判断对高风险样本问一句这段解析是否直接给出了答案def answer_leakage_probe(question, reference, model): prompt ( 判断下面的参考文本是否直接泄漏了问题的答案。 只回答 是 或 否。\n\n f问题{question}\n参考文本{reference} ) resp client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], temperature0, ) return resp.choices[0].message.content.strip()如果你用 Cline 或带 MCP 的工具来组织这套脚本配置里同样要写全三件套Base URL 填https://taotoken.net/apiKey 填控制台创建的 KeyModel ID 填你选的模型。三件套缺一个都会在请求阶段报错别只填 Key 就以为通了。4. 验证请求对同一测试集跑污染比对看命中率配置写完接下来是验证动作对同一测试集跑一遍完整检测确认命中率和日志输出符合预期。这一步的目的是证明流程真的能跑通而不是停留在配置看起来对。先准备数据。测试集和参考语料都用 JSONL每行一个样本至少包含id和text字段{id: q001, text: 请解释什么是梯度消失问题} {id: q002, text: 写一个快速排序的Python实现}跑检测的主脚本import json from contamination import ( exact_match_check, near_duplicate_check, embed_batch, cosine_sim, ) with open(contamination_config.json) as f: cfg json.load(f) testset [json.loads(l) for l in open(cfg[testset_path])] corpus [json.loads(l) for l in open(cfg[corpus_path])] report { dataset: cfg[dataset], total: len(testset), exact_match: 0, near_duplicate: 0, answer_leakage_suspected: 0, manual_confirmed: 0, } if cfg[exact_hash]: exact_hits exact_match_check(testset, corpus) report[exact_match] len(exact_hits) print(f[exact] hits{len(exact_hits)}) if cfg[minhash][enabled]: near_hits near_duplicate_check( testset, corpus, thresholdcfg[minhash][jaccard_threshold], num_permcfg[minhash][num_perm], ) report[near_duplicate] len(near_hits) print(f[minhash] hits{len(near_hits)}) if cfg[embedding][enabled]: import os model os.environ[cfg[embedding][model_env]] test_vecs embed_batch([s[text] for s in testset], model) corpus_vecs embed_batch([s[text] for s in corpus], model) sim cosine_sim(test_vecs, corpus_vecs) threshold cfg[embedding][cosine_threshold] emb_hits int((sim.max(axis1) threshold).sum()) report[answer_leakage_suspected] emb_hits print(f[embedding] hits{emb_hits}) with open(cfg[report][output_path], w) as f: json.dump(report, f, ensure_asciiFalse, indent2) print(json.dumps(report, ensure_asciiFalse, indent2))跑完之后你会看到类似这样的日志[exact] hits87 [minhash] hits214 [embedding] hits39 { dataset: eval_v3, total: 12000, exact_match: 87, near_duplicate: 214, answer_leakage_suspected: 39, manual_confirmed: 0 }命中率怎么解读exact_match是硬命中直接剔除。near_duplicate是近似重复需要看匹配到的语料样本判断是否真的算污染。answer_leakage_suspected是疑似答案泄漏数量通常不多但影响大建议全部进人工复核队列。报告里要区分污染等级不要只给一个比例。manual_confirmed字段留给人工复核后回填。评测报告里应明确说明是否剔除了污染样本以及剔除前后分数变化否则读者无法判断模型提升来自能力还是数据问题。验证成功的标志有三个日志三层都有输出、报告 JSON 字段完整、被剔除样本清单落盘。keep_removed_samples设为 true 时脚本要把命中样本的 id 和判定依据写进单独文件方便后续复用。5. 常见报错排查401、local proxy failed 与 reading choices检测脚本跑不起来八成是接入层的问题。这一节对照真实报错给排查路径。401 UnauthorizedKey 没读到或填错。先确认环境变量真的导出了echo $TAOTOKEN_API_KEY如果为空说明当前 shell 没加载。注意脚本里读的是os.environ[TAOTOKEN_API_KEY]如果你在 IDE 里跑IDE 可能没继承终端的环境变量需要在运行配置里单独设。Key 本身也要确认是在控制台创建的、没有多余空格。local proxy failed / connection error这类报错通常是 Base URL 写错或网络层配置问题。检查TAOTOKEN_BASE_URL是不是https://taotoken.net/api注意结尾不要多加/v1或斜杠。如果你本地有网络层工具在跑先确认它没有拦截这个域名。这类问题不要靠猜直接用 curl 打一发curl -s -o /dev/null -w %{http_code}\n \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ https://taotoken.net/api/models返回 200 说明链路通返回 401 是 Key 问题返回其他码再看文档。reading choices / KeyError: choices脚本在解析响应时拿不到choices字段。常见原因是请求打到了 embedding 接口却按对话响应解析或者 Model ID 填成了不存在的模型。检查你调用的方法client.embeddings.create返回的是dataclient.chat.completions.create返回的才是choices。两者别混。OAuth / 鉴权失败如果你用 Claude Code 或类似工具接入报 OAuth 相关错误说明工具走的是它自己的登录流程而不是 API Key。这时候要按 Claude Code 接入文档改配置把鉴权方式切到 API KeyBase URL 和 Model ID 一起填全。文档在 https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。embedding 维度对不上cosine_sim里两个矩阵维度不一致通常是测试集和语料用了不同 embedding 模型。确认两边都读同一个TAOTOKEN_EMBED_MODEL。MinHash 命中数为 0先确认num_perm和threshold是否合理。threshold设太高比如 0.95会几乎查不出东西设太低比如 0.5会大量误报。建议从 0.85 起步看分布再调。排查顺序建议固定先 curl 验证链路再验证 Key再验证 Model ID最后才看脚本逻辑。大部分报错在前三步就能定位不用一上来就怀疑检测算法。6. 把检测流程固化下来从一次性脚本到可复用资产污染检测跑通一次不难难的是让它成为团队可复用的资产。实际报告里应保留被剔除样本清单和判定依据这样后续更换模型或扩展数据集时团队可以复用污染判断而不是每次从零开始做主观筛选。具体做法是把三层检测的命中结果落盘成结构化文件字段至少包含样本 id、污染类型、匹配到的语料 id、相似度分数、判定依据。下次换模型评测时先拿这份清单过滤测试集再跑分。这样分数对比才是在同一套干净数据上做的。阈值也要版本化。把contamination_config.json一起提交到仓库每次调整阈值都留记录。不同任务的阈值不同代码题和问答题分开维护配置文件别用一套阈值打天下。如果你要长期跑检测任务Coding Plan 比按次调用更省心地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。需要临时验证某个模型对某批样本的判断用模型对话页面直接试地址 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Key 管理和文档分别在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后提醒一个容易踩的坑别把污染检测当成一次性动作。测试集会更新参考语料会扩充模型会换代每次评测前都该重跑一遍。把脚本和配置固化下来跑一次的成本很低但省下的是一次错误结论带来的返工。评测分数只有在测试集可信时才有意义先确认模型没有见过考题再讨论它是否真的会做题。