资讯动态

C-EVAL评测实战:从原理到代码,中文大模型知识水平如何量化

发布时间:2026/10/24 8:55:02 来源:尧图企业网站定制
简介本资源是中文多级多学科评估套件C-EVAL的完整论文PDF面向自然语言处理研究者、大模型评测工程师及对中文语境下模型能力感兴趣的技术人员。C-EVAL覆盖中学、高中、大学、专业四个难度层级横跨人文、社科到科学工程等52个学科并引入C-EVAL HARD高难度子集用于衡量模型的高级推理能力文中还给出GPT-4等主流模型在C-EVAL上的系统性评测结果可帮助快速理解中文大模型的能力边界与短板。压缩包共1个PDF文件大小约4.89MB内容包含论文全文、数据集设计说明、评测流程与实验分析便于离线研读或作为团队内部技术分享材料。当前已有76人学习适合用作大模型中文能力评测的入门文献与基准参考。1. 为什么是 C-EVAL把中文模型的知识水平变成可对比的分数GPT-4 在这套中文评估套件上平均准确率只有 66.4%在更难的子集 C-EVAL HARD 上掉到 53.3%——这是迄今为止唯一能稳定超过 60% 的模型。换个角度看哪怕最强模型面对中文知识题依然有三分之一以上的题目会答错。C-EVAL 并不是又一个人工翻译的英文 benchmark它是专门为中文语境大语言模型设计的评估套件13,948 道四选一真题覆盖 52 个学科横跨初中、高中、大学、专业四级难度。它考的不是日常对话而是模型在中文环境里真正需要具备的知识储备和推理能力。适合用它的人很明确做中文大模型评测或微调的技术人员想量化模型在具体学科上“懂多少”的研究者以及需要在多个模型之间做横向对比的算法工程师。这套资源的核心价值在于它把“中文知识水平”这个模糊概念拆成了可复现、可对比、可按学科追溯的分数。2. 套件设计拆解4 个难度、52 个学科和防数据污染的设计2.1 学科构成与四个难度层级这套题到底考什么C-EVAL 的出题逻辑不是“英文题翻译成中文”而是直接取材于真实中文考试场景。四个难度层级覆盖了不同教育阶段的知识体系难度层级学科数题量代表性学科初中71,409初中数学、初中物理、初中化学、初中生物高中81,594高中数学、高中物理、高中化学、高中历史大学256,249高等数学、概率统计、大学物理、大学化学、计算机组成、计算机网络专业124,696法律职业资格、注册电气工程师、医师资格、导游资格52 个学科按主题又聚成四大类STEM 类 20 科 4,495 题人文类 11 科 2,676 题社会科学类 10 科 2,845 题其他领域 11 科 3,932 题。这里的“其他”恰恰是它区别于英文基准的地方——包含导游资格、艺术、体育这类在传统 NLP 评测里几乎见不到的学科。难度层级之间的区分度是刻意设计的。初中和高中的题重在知识覆盖面大学层级开始加入计算和推导专业层级的题直接照搬职业资格考试的思路题面长、术语多、干扰项接近。以注册电气工程师为例题干里会出现大量中文行业术语和工程计算场景这类题翻译成英文后语义会打折扣但用中文原题考中文模型考察的就是模型在真实中文语料上的知识组织能力。这也是为什么我倾向于用整套 C-EVAL 做评估而不是只挑其中一部分——四个层级的占比不同能同时看到模型对基础知识的掌握和高级推理的上限。所有题目统一成四选一格式每道题只有一个正确答案。这个设计不是图省事而是因为多项选择的评分是确定性的准确率可以直接算、直接比较不需要人工判分也不存在主观题那种“模型答得沾边算不算对”的模糊地带。2.2 防数据污染的设计刻意绕开公开真题数据污染是大模型评测里公认的坑如果测试题出现在预训练语料里分数虚高整个评估失去意义。C-EVAL 在数据采集上做了三件事来降低这个风险。第一尽量不采集公开流传的官方真题。高考题和国家级职业资格考试的真题网络上一搜一大把最容易被爬进预训练语料所以 C-EVAL 刻意避开这类来源改用模拟考试题、地方性小规模考试题比如某些高中自己在网上分享的内部试卷。第二大学层级的题目部分来自高校学生公开分享的期末试题这类内容在通用网页语料里占比很低预训练模型很难系统性覆盖。第三约两千道研究生入学考试模拟题来自有访问门槛的题库网站是获得授权后引入的这些题不会出现在公开互联网的普通爬取范围内。还有一个容易被忽略的细节C-EVAL 的原始数据大部分是 PDF 和 Word 文档不是结构化文本。搜索引擎和爬虫对结构化 HTML 和纯文本的收录率远高于 PDF这就进一步降低了测试题进入预训练语料的可能。代价是解析成本高——PDF 转文本、公式转 LaTeX、人工校验都得做尤其是高等数学这类公式密集的学科需要逐道题把数学符号转成标准 LaTeX工作量很大。2.3 数据切分与统计dev、val、test 各自的用途C-EVAL 每个学科至少收集 200 道题收集完成后按学科随机切分成三份切分题量用途dev260每科 5 道few-shot 示例带人工解析val1,346调试提示词和超参不用于最终报告test12,342正式评估用于横向对比dev 集特意只放 5 道而且每道题都带答案解析。这个设计有两个作用一是做 few-shot 评测时每一科最多取 5 个示例不会出现示例过多导致提示词超长的问题二是 dev 里有解析可以直接支撑 few-shot chain-of-thought 评测让模型先读一段完整的推理过程再作答。val 和 test 按约 1:9 的比例划分。val 是给你调试用的比如试不同的提示词模板、调 temperature但调试过程中如果反复在 val 上试就会产生过拟合所以最终对外报告的分数一律以 test 为准。这个约定在各家模型对比时必须遵守否则两份结果不具备可比性。3. 把评估跑起来数据格式、prompt 模板和答案提取3.1 数据集文件结构从 JSON 里读题目、选项和答案官方数据按学科组织每个学科一个目录里面是 dev.json、val.json、test.json 三个文件。单条样本结构如下{ id: 0, question: 关于计算机网络中 TCP 拥塞控制的说法正确的是____, choices: { A: 慢启动阶段拥塞窗口指数增长, B: 拥塞避免阶段拥塞窗口线性增长, C: 快重传算法在收到三个重复 ACK 时触发, D: 以上说法均正确 }, answer: C, explanation: …… }这里三个细节值得注意。第一choices 是字典不是列表不能按下标直接取第二answer 字段存的是正确答案的字母不是答案文本第三explanation 只出现在 dev 集val 和 test 里没有这是为了防止答案通过字段泄露到模型输入里。读取代码很简单但我建议写成一个通用函数后面 52 个学科循环调用import json from pathlib import Path def load_subject(data_dir: Path, subject: str, split: str): 加载单个学科的指定切分返回题目列表 path data_dir / subject / f{split}.json with open(path, r, encodingutf-8) as f: return json.load(f) # 用法示例 data_dir Path(./ceval_data) dev load_subject(data_dir, computer_network, dev) test load_subject(data_dir, computer_network, test) print(len(dev), len(test)) print(dev[0][answer], dev[0][explanation][:30])逻辑说明函数按学科名拼出子目录路径再按 split 参数拼文件名json.load 读出来就是 list[dict]。注意必须显式传 encodingutf-8否则在 Windows 默认编码下中文题面会直接报 UnicodeDecodeError。加载完建议立刻打印第一条确认 choices 是否四选项完整、answer 字段是否落在 A-D 范围内再跑批量循环。C-EVAL 清洗阶段保证了每道题恰好四个选项但偶尔会遇到从本地题库合并时的脏数据先验证再批量能省掉大量排查时间。参数说明data_dir 是数据根目录subject 传入学科英文名或中文名要与目录结构保持一致split 只接受 dev/val/test 三个值。开发阶段可以先加载一个学科的 test 跑通全流程再扩展到全部 52 科。3.2 prompt 模板零样本、答案 only、CoT 三种设置C-EVAL 的官方评测代码支持多种 prompting 设置实际使用中我维护了三种分散在不同场景里。核心提示词模板如下def build_prompt(subject: str, question: str, choices: dict, mode: str) - str: choice_lines \n.join(f{k}. {v} for k, v in choices.items()) header f以下是中国关于{subject}的单项选择题请选出正确答案。\n\n body f{question}\n{choice_lines}\n if mode zero_shot: return header body 答案 elif mode answer_only: return header body 请只输出选项字母A/B/C/D。\n答案 elif mode cot: return header body 请先给出解题思路最后一行输出选项字母。\n答案 else: raise ValueError(f未知模式: {mode})逻辑说明所有模式共用一段题干和选项区别在尾部指令。zero_shot 模式最接近真实用户直接提问模型自由发挥空间最大answer_only 加了输出格式约束能显著减少无效输出cot 模式要求先推理后作答适合考察模型在复杂题上的推理链路。参数说明subject 传学科中文名要让模型知道自己在面对哪一类考试。我发现把学科名写进提示词对专业类科目影响很大比如“法律职业资格”不写明学科模型会按常识答题写明后能激活对应的知识领域。choices 传原始字典即可函数内部自动拼接成“A. xxx\nB. xxx”的格式。few-shot CoT 是另一个常见设置需要从 dev 集取示例拼到 prompt 头部def build_few_shot_cot(subject: str, test_item: dict, dev_items: list, k: int 5) - str: parts [f以下是中国关于{subject}的单项选择题请根据示例作答。] for item in dev_items[:k]: example f{item[question]}\n example \n.join(f{k}. {v} for k, v in item[choices].items()) example f\n答案{item[answer]}\n解析{item[explanation]} parts.append(example) parts.append( f{test_item[question]}\n \n.join(f{k}. {v} for k, v in test_item[choices].items()) \n答案 ) return \n\n.join(parts)逻辑说明这段代码把 dev 里前 k 条带解析的样本拼在最前面再拼接当前测试题让模型模仿示例的推理格式作答。k 最大取 5因为每科 dev 就 5 条。调试阶段先用 k2 验证格式正确再切到 k5 跑正式实验能节省不少调用量。参数说明dev_items 是开发集列表这里直接用前 k 条。官方 dev 顺序是固定的跨模型对比时必须保持同样的顺序和同样的 k 值否则结果不能直接比较。3.3 推理参数与答案提取模型输出“答案是 A”也要能接住推理端设置是评测复现性最容易出问题的地方。我一般统一用如下参数跑def run_eval(model, subject, test_items, dev_itemsNone, modeanswer_only, temperature0, max_tokens64, seed42): 统一入口跑完一个学科返回准确率和失败样本列表 results [] failures [] for i, item in enumerate(test_items): if mode few_shot_cot: prompt build_few_shot_cot(subject, item, dev_items, k5) else: prompt build_prompt(subject, item[question], item[choices], mode) output model.generate(prompt, temperaturetemperature, max_tokensmax_tokens, seedseed) pred extract_answer(output) if pred is None: failures.append((i, output, prompt)) pred results.append(pred item[answer]) return sum(results) / len(results), failures, results逻辑说明函数封装了“拼提示词→调模型→提取答案→比对”四个环节。temperature 固定为 0 是为了让采样过程尽量确定模型接口支持 seed 参数时同步固定 seed。max_tokens 设在 64 对 answer_only 够用对 CoT 模式偏紧CoT 模式建议放到 256 以上否则推理写到一半被截断最后没有答案字母输出。答案提取的成败直接决定你的分数模型输出的格式五花八门最常见的几种是单独一个字母、带答案前缀的“答案B”、先给解析后夹一个“应选 C”。我的提取函数如下import re def extract_answer(text: str): 从模型输出里提取 A/B/C/D提取不到返回 None text text.strip() # 第一层显式答案标签如“答案B”“答案是A” m re.search(r[答選][案]?\s*[:是]?\s*([A-D]), text) if m: return m.group(1) # 第二层全文正好只有一个孤立的大写字母 letters re.findall(r\b([A-D])\b, text) if len(letters) 1: return letters[0] # 第三层某一行只有一个字母 for line in text.splitlines(): line line.strip() if re.fullmatch(r[A-D], line): return line return None逻辑说明提取策略按优先级逐层下探。第一层先找显式的“答案X”模式命中率最高第二层统计全文孤立大写字母如果恰好只有一个那它就是答案第三层看有没有单独成行的字母。前两层失效时第三层能兜住“解析到最后单独输出字母”这类格式。参数说明正则里的[A-D]只匹配 ABCD 四个字母不匹配答案解析中出现的“选项 A 不对”这类表述。第二层的\b词边界确保只匹配独立字母不会把“BAD”里的 A、B、D 拆出来。如果某次评测失败率高可以把 failures 逐条打印出来看模型输出形态通常能发现模型特有的格式习惯比如喜欢在开头写“这是一个有关XX的题目”这种干扰文本会被第一层正则直接跳过。4. 避坑指南跑 C-EVAL 最容易翻车的四类问题4.1 题面里的 LaTeX 公式和数学符号解析乱码现象跑“高等数学”“大学物理”这类科目时模型输出的答案和人类判断明显对不上回头检查提示词发现题干里全是\frac{a}{b}、\sqrt{2}这类原始 LaTeX 源码有些环境下还带\begin{matrix}块模型根本没识别出这是数学表达式。原因C-EVAL 在数据整理阶段为了让公式完整存进 JSON把数学符号以标准 LaTeX 字符串保存。评测脚本直接把 JSON 读成 Python 字符串拼进 prompt没有处理反斜杠的转义问题模型看到的就是一串混乱的 LaTeX 代码。解决CPU 层面不要做二次 Unicode 转义json.load 读出来的字符串是什么就拼什么。我看到过有人画蛇添足做了一次encode().decode(unicode_escape)反而把 LaTeX 里的反斜杠全部消掉了。模型端不需要渲染公式预训练语料里包含大量 LaTeX 文本给模型原样的\frac{a}{b}通常比渲染成 Unicode 符号更好理解。如果发现某个模型对 LaTeX 敏感度高、输出乱码可以在提示词开头加一句“题干中的数学公式为 LaTeX 格式请按数学语义理解”多数开源模型都能适应。4.2 答案提取失败模型输出了解释但没输出选项字母现象跑 few-shot CoT 模式时模型先写一大段推导过程最后写“所以选 B”但推导过程中出现过“A 明显错误”“C 和 D 都……”这类句子extract_answer 最后提取成了 A 或 C准确率无故掉了十几个点。原因CoT 模式下模型输出文本变长干扰字母变多第二层的“唯一孤立大写字母”策略不再可靠。第三层“单独成行的字母”也经常失效因为模型在最后一行写的是“答案B”而不是孤立字母第一层正则应该命中但如果模型写了“选 B 而非 C”第一层的正则也可能误匹配。解决提取策略需要两级修正。第一在 CoT 提示词尾部加一句硬约束“解析结束后请在新的一行输出答案字母”。第二在提取函数里第一层正则要把“选”和“答案”都纳入匹配范围并且要求答案字母后面必须是行尾或标点避免匹配到“选 B 而非 C”这类句型。改完之后对开源模型基本能回到 95% 以上的提取成功率。剩下的失败样本直接记录到 failures 列表做人工复查比反复调正则更快。4.3 少样本示例的顺序和选择直接用 dev 前 5 条的问题现象用 dev 的前 5 条做 few-shot 示例发现有些学科模型分数比 zero-shot 还低换一批示例学科分数又上去了。原因dev 集每个学科只有 5 道题这 5 道是从该学科全部题目里随机抽的。某些学科的 5 道题恰好偏难或知识点集中作为示例会把模型带偏。而且示例顺序也会影响结果同一个模型交换示例顺序后分数能差出两三个百分点。解决最稳妥的做法是固定使用官方 dev 集的原始顺序这是评测社区统一遵守的约定保证你的结果可以和别人报告的数字直接对比。如果你要研究示例选择对结果的影响那就固定评价维度先按学科分别统计 dev 中 5 道题的答案分布和题面长度再做分层抽样。但日常评测不要频繁换示例换了示例顺序就意味着换了评测条件前后两组结果没有可比性。这个坑很像“玄学”单个示例看似影响不大积少成多之后整体分数偏移超出误差范围。4.4 跨模型对比和数据切分不一致现象在本地复跑某个模型分数和公开榜单上报告的数字差了五到十个百分点反复检查代码没有问题。原因常见原因有三个。第一别人报告的是 test 集分数你的脚本误用了 val 集val 题量小且难度分布和 test 不完全一致分数必然不同第二提示词模板不同比如别人的 answer_only 模式没有加“只输出选项字母”约束模型输出格式不同提取出来的答案也可能不同第三接口服务有缓存或非确定性采样同一段 prompt 重复请求时返回不同结果。解决跨模型对比固定四个条件用 test 集、用官方 dev 前 5 条、temperature0、固定 seed。这四个条件缺一个结果都不能横向比较。另要注意token 级别的差异也会放大误差比如某些模型的接口会对输入自动加 BOS 标记同一 prompt 在不同推理后端上跑出的结果可能不同比较时要记录推理后端和模型版本。公开榜单的分数是在特定版本模型上测出来的模型升级后的分数未必能复现别把榜单分数当成模型能力的“永久标签”。5. C-EVAL HARD 和学科级分析从总分到推理能力的进阶用法5.1 C-EVAL HARD隔离推理能力的设计逻辑C-EVAL HARD 是从 C-EVAL 里单独抽取的困难子集只保留对推理要求极高的学科比如高等数学、大学物理这类需要多步计算的科目。这个子集的平均准确率远低于总集GPT-4 也只剩 53.3%。它存在的意义是区分“知识记忆型模型”和“推理型模型”有些模型在初中、高中层级接近满分一到大学计算题就崩说明它背了大量知识但没有真正形成推理链路。我在实际评测中会分两步走先跑完整 C-EVAL 拿到总体分数再单独跑 C-EVAL HARD 子集。两者一对比立刻能看出模型是被知识覆盖不足拖累还是被推理能力上限卡住。这个信息对模型微调方向很有用——前者该补语料后者该做推理链路增强。5.2 按学科拆解评估结果用一张表定位模型短板总体分数掩盖了大量信息。52 个学科按 STEM、人文、社科、其他分组我把每个学科准确率单独算出来再按大类汇总from collections import defaultdict def report_by_category(predictions: dict, subject_category: dict): cat_stats defaultdict(lambda: [0, 0]) for subject, (correct, total) in predictions.items(): cat subject_category[subject] cat_stats[cat][0] correct cat_stats[cat][1] total return {cat: correct / total for cat, (correct, total) in cat_stats.items()}逻辑说明predictions 是每个学科的 (正确数, 总数) 字典subject_category 是学科到类别的映射表。函数按大类汇总并计算准确率输出结果可以直接用来画柱状图或雷达图。参数说明执行时保留原始学科名作为 dict 键不要提前聚合成大类否则想看具体哪个学科拖后腿还得重新跑一遍评测。我一般会让脚本同时输出两个表按学科细分的准确率和按大类聚类的均值这样既能做纵向对比也能看内部波动。我做完一轮完整评估后习惯把各学科分数从低到高排序先看垫底的十个学科再回看对应学科的题面和模型输出。曾经有一次发现某个模型在“导游资格”上只有 25% 准确率而“计算机组成”接近 90%这两者的差距反映的不是模型聪明与否而是它预训练语料里中文行业文本覆盖不均匀。这类洞察是 C-EVAL 相比单一英文基准最大的价值——把“模型能力”拆成可操作的改进项。从那以后我每次跑完 C-EVAL 都强制走一遍学科拆解流程先分类后排序再逐科看失败样本总分只能用来对外汇报真正找问题还得看这张学科表。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑