大模型评测中的Shortcut Hacking:模型真的会推理吗?

大模型评测中的Shortcut Hacking:模型真的会推理吗? 1. 引言为什么说“答案对了不代表会推理”如果你最近关注过大模型在科学推理评测上的表现可能见过这样一类案例模型在某个“前沿科学基准”上拿到了接近人类专家的分数论文标题写着“达到了博士生水平”“通过了研究生资格考试”但当人们把题目条件稍微改一下比如把化学方程式的产物写反、把图表的横纵坐标调换、把题干里的数值单位从米换成千米模型的正确率立刻跳水。这不是个别现象而是当前 LLM 推理评测里一个被广泛讨论的隐忧模型回答对了但它用的可能根本不是人类以为的那种推理方式。我们把这类现象称为 Shortcut Hacking直译过来就是“捷径黑客”。它指的是模型在评测中借助数据泄漏、格式先验、表面模式统计、答案分布偏移等捷径获得了虚高的正确率但并没有真正学会可迁移的推理能力。更麻烦的是这种问题在“前沿科学基准”上尤其隐蔽因为题目本身难度高读者默认高分就意味着模型会了实际上模型可能只是在做一场精心伪装的模式匹配。这篇文章要重点讨论的就是Shortcut Hacking 到底是什么它会出现在哪些环节为什么越“难”的科学基准越容易被捷径干扰我们在自己的 LLM 测评项目里如何设计更稳健的评测方案实际项目中评测脚本、数据采样、指标解释应该怎么落地。如果你正在做大模型选型、研发自己的 Agent 应用或者准备发布一份评测报告这篇文章会比较适合你。2. 基础概念从“评测得分”到“推理能力”之间发生了什么2.1 推理评测的三种常见误区在展开 Shortcut Hacking 之前我们先澄清一个容易混淆的问题LLM 推理评测到底在测什么通常我们想要测的是模型的“通用推理能力”比如数学推理、科学常识推理、多步逻辑推理。但评测时我们能直接观测到的只有“模型在有限题目上的输出”。这两者之间隔着好几层假设题目代表性假设有限的测试题可以代表真实世界中的推理任务。无泄漏假设模型训练时没有见过这些测试题至少没有见过同款答案。清洁作答假设模型回答问题时不依赖格式、选项位置、提示词语气等表面线索。打分一致性假设自动评测指标能准确判断模型答案的语义正确性。Shortcut Hacking 破坏的主要是第 3、4 条有时也破坏第 2 条。当我们说“模型会推理”时我们期待的是一种可迁移的解题能力。但评测实际捕获到的往往是“模型在给定数据集上取分的策略”。如果模型发现某些数据集中选项 A 出现频率更高、答案总是以某个句式结尾、题目顺序与答案类型存在相关性它会在不真正理解题目内容的情况下利用这些统计规律去猜测。这就像是学生发现考试选择题的正确答案总是 C然后不看题目直接填 C。这种策略在单个数据集上可能有效在真实世界中则完全失效。2.2 Shortcut Hacking 的定义与层次Shortcut Hacking 并不是一个单一动作而是一系列行为的统称。我们可以把它分为三个层次层次名称典型表现后果第一层数据泄漏测试集与训练集重叠、相似度过高得分虚高但真实能力不匹配第二层表面统计捷径选项顺序、答案长度、固定句式、数字分布换题目格式后分数暴跌第三层评测引导模型在 few-shot 示例中学会了“抄作业”每个任务都在投机取巧第一层是数据问题第二层是建模与评测设计问题第三层是评测协议问题。真实的 Shortcut Hacking 现象往往是三层叠加的。2.3 为什么前沿科学基准更容易中招如果说常识推理基准的捷径还容易被人识破为什么“前沿科学基准”反而更容易被 Hacking原因是这类基准有几个共同特点题目数量少科学基准常由专家手工编写数量从几百到几千不等。数据量小意味着分布规律更容易被模型抓取而少量题目也意味着统计检验的置信区间很宽性能差异可能只是噪声。知识门槛高要人工核验模型是否“真的会”需要专家逐题检查。绝大多数团队没有这个人力只能依赖自动指标。格式高度统一科学题目往往排版规整选项数量、句式结构高度一致这让模型更容易找到格式层面的捷径。训练语料容易混入相近文档arXiv、教科书、考试资料广泛存在于预训练语料中即使测试题本身没有故意泄漏数据管道中的去重不彻底也会造成“近似泄漏”。所以一个模型在“前沿科学基准”上取得高分反而要引起我们的警惕分数究竟来自对科学原理的深层理解还是来自训练时见过的相似度极高的文本、以及答题格式的统计倾向答案并不总是让人安心。3. Shortcut Hacking 的典型表现从现象到机制3.1 现象一改选项顺序正确率剧烈波动在很多选择题评测中模型的准确率竟然会随选项顺序变化而大幅波动。这说明模型并不是在比较选项之间的语义而是倾向于选择某个固定位置比如 A 或 C。这种偏好在训练数据中形成很难直接消除。一个典型测试方式是把四个选项随机打乱生成多个版本的同一道题观察模型正确率的方差。如果方差过大就说明模型对格式敏感背后并没有稳定的推理能力。3.2 现象二数字表面相似推理却失效数学和科学题中模型经常会把“单位”或“数量级”当作猜测线索。比如某道物理题里如果所有数据都围绕某个数值设置模型能轻松选出正确答案一旦把系数改成随机、保持计算结构不变模型就完全失去方向。这在神经网络机制上可以理解为“表层特征复用”Transformer 在预训练阶段学习了大量文本统计规律在处理推理任务时它不一定启动深层语义建模而是直接复用这些表层特征。DeepSeekMath 等工作也提到数学推理中既有显式的符号推理也有依赖模式匹配的启发式路径两者并存。评测要区分的正是这条“启发式路径”。3.3 现象三答案泄漏进输入如果把 LLM 当作 Agent 来评测还需要检查工具反馈、知识库检索结果中是否包含最终答案。如果系统提示词里塞入了 few-shot 示例示例和测试题的格式高度一致模型可能直接把示例中的答案模式套用过来而不是真正解题。这类问题在 Agent 评测里比纯文本评测更常见因为 Agent 涉及工具调用、中间步骤输出更容易出现“不正确的中间过程却最终得到正确结果”的情况。3.4 现象四自动打分器被捷径欺骗评测管道里还有另一个容易被忽视的 Shortcut自动打分器LLM-as-a-Judge。如果使用 GPT-4 等模型作为裁判裁判本身也会受到格式和内容的影响。比如裁判倾向于打高分给结构整齐、包含关键术语的答案而不关注逻辑是否正确。我们在做模型对比评测时不能只相信裁判分数需要抽样人工复审。3.5 为什么“正确中间步骤”不能证明推理存在有些人认为只要模型输出了“一步一步思考”的中间推理那结果就是可信的。但研究已经发现推理链条可以被事后编造。模型先猜测答案再生成一段听起来合理的推理过程来凑答案。这在数学题上尤其常见回答过程中的第一步可能是对的第二步就开始出现计算错误或概念曲解但最终答案诡异地正确。因此评测推理能力不能只看最终答案还要看中间过程的逻辑一致性。但“过程一致性”本身又是更难自动评估的课题。4. 如何在项目里落地一套“抗 Shortcut Hacking”的评测方案了解理论之后我们进入实操部分。这里不介绍庞大评测框架而是给出一个可以直接在自己项目里复用的最小可行评测方案。无论你是在选基座模型还是评估一个 Agent大思路都适用。4.1 环境准备与前置条件建议环境Python 3.10使用 OpenAI SDK 或本地 vLLM 部署调用模型准备一个小型评测数据集比如 100 道选择题需要一个可复现的随机种子。如果你的模型接口是自己部署的就用 OpenAI 兼容接口如果直接在本地调用 transformers也可以用同样的评测脚本思路。本文不锁定具体模型名称和依赖版本重点演示评测流程本身。真正的项目里请固定依赖版本并保存到 requirements.txt。4.2 设计最小评测数据集为了避免数据泄漏带来的虚高分数最重要的一步是设计“封板”数据集从原始数据集中抽出一部分题目单独存放确保后续训练、微调、提示词调优阶段都不碰它。对每题生成多个变体打乱选项顺序修改数字系数改写题干措辞保留逻辑结构。记录每个变体的来源 ID方便后续分析。在代码里可以用下面这个函数生成选项乱序变体import random from dataclasses import dataclass, field from typing import List, Optional dataclass class ScienceQuestion: id: str stem: str choices: List[str] answer_idx: int # 原始正确选项索引 explanation: Optional[str] None def shuffled_variant(self, rng: random.Random) - ScienceQuestion: 打乱选项顺序返回新题目。 注意打乱后必须更新 answer_idx。 indices list(range(len(self.choices))) rng.shuffle(indices) new_choices [self.choices[i] for i in indices] new_answer_idx indices.index(self.answer_idx) return ScienceQuestion( idf{self.id}_shuffle, stemself.stem, choicesnew_choices, answer_idxnew_answer_idx, explanationself.explanation, )这段代码把选项顺序打乱并同步更新正确答案索引。后续评测中我们对同一个原始题生成多个变体使模型无法通过“位置偏好”得分。4.3 调用模型进行批量作答如果你使用 OpenAI 兼容接口可以用下面的脚本批量询问模型。这里假设模型支持 chat completion 格式。import json from openai import OpenAI from typing import List client OpenAI(api_keyYOUR_API_KEY, base_urlYOUR_ENDPOINT) def ask_question(client: OpenAI, question: ScienceQuestion, model: str) - str: 将题目包装为选择题返回模型输出的 choices 中某个选项的字母。 choices_text \n.join( [f{chr(ord(A) i)}. {choice} for i, choice in enumerate(question.choices)] ) user_content ( f请回答下面的科学选择题。\n f题目{question.stem}\n f选项\n{choices_text}\n f请直接输出正确选项对应的字母不要输出其他内容。 ) resp client.chat.completions.create( modelmodel, messages[ {role: system, content: 你是一个严谨的科学答题助手。}, {role: user, content: user_content}, ], temperature0.0, max_tokens32, ) return resp.choices[0].message.content.strip() def batch_evaluate(questions: List[ScienceQuestion], model: str) - dict: results [] for q in questions: pred ask_question(client, q, model) correct q.answer_idx ord(pred.upper()) - ord(A) if pred else False results.append({id: q.id, pred: pred, correct: correct}) return results这里的ask_question把选择题转成文本输入要求模型只输出选项字母。实际项目中你可以把输出解析改成更宽容的方式比如允许“A”“答案是 A”“A.”等格式并用正则解析。4.4 核心评测指标准确率方差与选项位置敏感性单看整体准确率远远不够。我们需要三个层面的指标原始准确率模型在原始题集上的正确率。变体准确率模型在所有 shuffle 变体上的正确率。稳定性指标对于同一道题的多份变体模型是否正确性一致。下面的代码展示如何计算“稳定性分数”from collections import defaultdict def compute_stability(results: List[dict]) - dict: 根据每个 question id 的结果计算稳定性指标。 同一题多个变体全部正确记为 stable_correct 全部错误记为 stable_wrong 否则记为 unstable。 q_results defaultdict(list) for r in results: qid r[id].split(_shuffle)[0] q_results[qid].append(r[correct]) stable_correct 0 stable_wrong 0 unstable 0 for qid, corrects in q_results.items(): if all(corrects): stable_correct 1 elif not any(corrects): stable_wrong 1 else: unstable 1 total len(q_results) return { total: total, stable_correct: stable_correct, stable_wrong: stable_wrong, unstable: unstable, stability_rate: stable_correct / total if total else 0, }这个稳定性分数是重点观察对象。如果模型原始准确率 80%但稳定性分数只有 40%说明它很大程度上在依赖选项位置等表层线索。4.5 更细的校验加入“无关变量”为了进一步定位 Shortcut Hacking我们还可以做几个“实验组”对照组 A原始题目不修改任何内容。对照组 B选项顺序打乱题干不变。对照组 C题干里的数字都乘以 2选项同步乘以 2保持正确答案逻辑不变。对照组 D题干改写为相同逻辑的新表述注意对应数值同步改变。针对 D 组一个简单实现思路是手动准备一小批人工改写题目。如果模型在 D 组上准确率明显低于前几组说明它并不是真正理解结构而是记忆了具体文字。D 组的数据改写无法完全自动化但它是评测中最有价值的部分。建议每个数据集至少准备 20 道人工改写题。5. 完整示例一个 100 题的科学推理抗捷径评测下面给出一份完整的可运行示例把前面的代码串起来。实际项目里你可以把这套脚本放到 CI 里用于每次模型迭代后的回归测试。import json import random from dataclasses import dataclass, field from typing import List, Optional from openai import OpenAI random.seed(42) rng random.Random(42) # ---------- 1. 数据准备 ---------- # 示例数据实际请替换为你的测试集 raw_questions [ ScienceQuestion( idq001, stem一个物体以初速度 10 m/s 做匀加速直线运动加速度为 2 m/s²求第 3 秒末的速度。, choices[6 m/s, 14 m/s, 16 m/s, 20 m/s], answer_idx1, explanationv v0 at 10 2*3 16 m/s。, ), # 这里应有更多题目... ] # 生成变体 dataset [] for q in raw_questions: dataset.append(q) dataset.append(q.shuffled_variant(rng)) # ---------- 2. 批量推理 ---------- def ask_question(client, question, model): # 同前文 ask_question 实现 ... def batch_evaluate(client, questions, model): # 同前文 batch_evaluate 实现 ... # ---------- 3. 评测与输出 ---------- client OpenAI(api_keyYOUR_API_KEY, base_urlYOUR_ENDPOINT) results batch_evaluate(client, dataset, modelyour-model-name) original_results [r for r in results if _shuffle not in r[id]] shuffled_results [r for r in results if _shuffle in r[id]] orig_acc sum(r[correct] for r in original_results) / len(original_results) shuf_acc sum(r[correct] for r in shuffled_results) / len(shuffled_results) stability compute_stability(results) print(f原始题准确率: {orig_acc:.2%}) print(fShuffled 准确率: {shuf_acc:.2%}) print(f稳定性分数: {stability[stability_rate]:.2%}) print(f不稳定题目数: {stability[unstable]})运行后你会得到三组关键数字。如果原始题准确率远高于 shuffled 准确率那么基本可以判断模型在依赖选项顺序等表面线索。这个结论可以先在小范围内用人工抽检复核再写成评测报告。6. 运行结果与效果验证6.1 如何判断评测脚本运行成功脚本本身跑通不意味着评测有效。有效评测的标志是每个问题都返回了可解析的答案原始题与变体题数量匹配原始准确率、shuffled 准确率和稳定性三个指标都成功输出随机种子固定重复运行结果一致。如果返回值有大量空字符串或者解析失败优先检查模型输出格式是否被系统提示词约束得足够严格。可以尝试修改ask_question里的提示词把“只输出选项字母”改成“输出 A、B、C 或 D”。6.2 一种可能的实验结果及其解释假设某模型原始准确率 82%shuffled 准确率 61%稳定性分数 45%。这个结果说明模型在原始题集上表现尚可但在消除选项位置线索后显著下降。稳定性分数不到一半意味着模型对同一道题的状态并不稳定存在大量“这道题换个顺序就答错”的情况。从工程视角看这个模型如果用于真实应用题它的推理可靠性会打折扣。虽然不能说模型完全不会推理但至少不能仅凭 82% 的原始准确率宣称其具备科学推理能力。6.3 如果评测失败排查顺序建议按以下顺序排查查看日志中的 API 报错尤其是超时与限流错误检查题目 JSON 格式是否正确特别是中文字符和引号检查answer_idx在 shuffle 后是否同步更新检查模型输出里是否包含了额外文字导致解析失败加入print输出几条原始请求和响应确认提示词没有异常。7. 常见问题与排查方法问题现象可能原因排查方式解决方案模型总是输出同样的选项数据集中存在位置偏差或提示词模型偏好统计每个选项位置被选中的频率随机打乱选项顺序重新评估shuffled 后准确率暴跌模型依赖选项顺序而不是语义比较原始和 shuffled 的准确率报告中单独报告两个指标避免只报原始值同一道题多次推理结果不稳定温度参数过高或模型本身随机性将 temperature 设为 0并使用固定 seed保持温度 0 或使用 vLLM 的确定性采样自动打分结果不准确LLM-as-a-Judge 受格式影响抽取 20% 样本人工复核增加人工复审环节或使用规则化打分测试集与训练集存在重叠没有做去重或数据来源清理对测试题做 embedding 相似度检测构建封板测试集定期更新8. 最佳实践与工程建议8.1 评测报告里要呈现哪些数字在评测报告中建议至少同时呈现三组数据原始准确率变体准确率shuffled、改写题干等稳定性分数。只呈现原始准确率的评测报告是不完整的。如果变体准确率显著低于原始准确率一定要在结论中明示“模型存在对表层格式的依赖”。8.2 评测数据和模型权重都要做版本管理评测不是一次性工作而是伴随模型迭代长期运行的回归任务。建议将评测数据集、评测脚本、模型版本、prompt 版本、随机种子、评测结果全部纳入版本管理方便复现历史结论。8.3 小数据集的置信区间当评测题量只有几十道时两个模型之间的准确率差异可能只是噪声。建议在报告中给出二项分布的置信区间至少说明样本量不足以支撑统计显著结论。8.4 不要忽略 prompt 对评测的影响同一个模型在不同 prompt 下表现差异可能巨大。评测时不要只测一个 prompt而应该测试两到三个不同的 prompt采用“最佳结果”或“平均结果”作为结论。否则很容易把 prompt 的影响误判为模型能力的差异。8.5 安全性数据与权限评测数据如果包含非公开论文、考试真题或付费数据需要注意版权和合规问题。在团队内部使用没有问题但如果公开发布数据集要确认数据来源与授权。评测脚本的 API Key 不要硬编码在代码里建议使用环境变量或密钥管理服务。9. 总结与后续学习方向Shortcut Hacking 提醒我们一件事LLM 评测的本质不是“模型是否能答对题目”而是“模型的回答是否源于我们期望的推理机制”。前者容易测量后者才是我们真正关心的能力。在实际项目中建立一套包含原始题、变体题、稳定性指标的最小评测方案并不复杂。关键是要先把测试集设计好再跑批量推理最后对结果进行分层分析。这样得到的评测结论比只报一个准确率数字要可靠得多。如果你对继续深入有兴趣可以关注几个方向推理过程的可信度评估例如过程奖励模型PRM和一致性校验面向 Agent 的评测引入工具调用轨迹的评估评测数据集的自动生成与人工审核结合模型输出与人类专家推理行为的对齐分析。希望这篇文章能帮你在面对各种基准测试高分时多问一句它到底是真会了还是走了一条我们没察觉的捷径