
如果只看各家大模型的宣传页数学好像早就不算问题了能写代码、能解题、能当 Agent 调度工具。但最近 Show HN 上有一个项目反着来——它把计算器从 50 个大语言模型手里收走不让调用代码解释器、不让接外部工具逼着模型纯靠“脑子”做算术然后统一打分。这个测试的核心问题是大模型的数学能力到底是真会算还是只是会接话这个项目值得所有做 LLM 应用、做 Agent、做模型选型的人看一眼。原因很直接现在很多应用默认给模型套上了计算器裸算短板被藏住了但一旦遇到工具调用失败、Function Calling 没触发、或者模型需要做多步推导的场景错误就会开始累积。拿掉计算器再考本质上是测模型的“内功”。这篇文章我会拆解这个评测思路说明如何自己设计一套“LLM 裸算算术”测试集写一个批量评测脚本跑多个开源或 API 模型并解决输出解析、批量任务、重试、性能观测和结果稳定性这些问题。文章不给具体模型排名因为不同版本、不同 API 日期、不同温度都会影响结果但我可以帮你把测评系统搭好让你自己测。1. 核心能力速览能力项说明项目类型LLM 算术能力评测裸算不允许外部工具评测对象50 个大语言模型来自开源与闭源 API测试内容整数加减、乘法、除法、小数、分数、百分数、复合运算、文字应用题核心约束不允许调用计算器、代码解释器、外部工具只能直接输出数字评分方式按题目正确率打分可另统计输出格式合法率复现依赖Python 3.9、OpenAI 兼容 API 或本地推理框架Ollama / vLLM 皆可是否支持批处理支持50 个模型 x 多道题目可设计成批量评测队列是否支持 API是所有模型都通过 API 或本地 HTTP 服务访问适合读者LLM 应用开发者、Agent 开发者、模型选型、数学推理研究者这里要强调一下这不是一个需要 GPU 部署的模型项目而是一个评测脚本项目。你既可以用官方 API 测闭源模型也可以用 Ollama 或 vLLM 测本地开源模型。如果只有一张 6G 显存的显卡量化的 7B/8B 模型也能跑只是推理速度会慢一些如果走 API则基本没有硬件门槛。2. 这个评测为什么值得做先说结论LLM 在接上工具之后算术成绩会“看起来”很好但那不是模型的能力而是计算器的能力。如果你在做 Agent 应用应该关注的是模型在没有任何外部帮助时的原始能力。这个评测要回答三个问题模型能不能稳定计算多位数乘法和除法模型算错时是计算错误还是格式错误不同模型之间的裸算差距有多大值不值得为此选择更贵的模型从标题来看作者设计的是“50 个模型 算术题 统一打分”。这是一个很好的最小实验设计。很多团队在选型时只看 MMLU、MATH、GSM8K 这类基准分但这些基准通常允许模型使用 CoT思维链甚至允许通过代码解释器解题。真正在生产环境里模型被要求“直接给答案”的场景并不少见比如数字表单提取、API 参数计算、金额校验、库存计算。这些场景如果模型裸算不可靠就必须加一层工具兜底如果裸算可靠反而可以省掉一次工具调用降低延迟和成本。另外这个测试还有一层意思模型可能在训练时见过大量题目如果题目是常见题库正确率高不一定是“会算”而是“见过”。所以好的裸算评测题目必须自己生成避免数据污染。3. 裸算算术评测的题目集设计题目设计是整个评测的核心。如果你随便找几道题测试结果没有区分度如果题目太难或太偏所有模型都做不对也没意义。参考这个项目的思路题目应该分层设计。3.1 分层题型可以分成 6 组整数四则运算一位数、两位数加减例如a b、a - b。多位数乘除法两位数乘两位数、三位数除以一位数。小数与分母例如0.3 0.45、1/3 1/6。百分数与比例例如15% of 200、80 is what percent of 50。复合运算包含括号、多步例如(23 47) × 2 - 18 ÷ 3。文字应用题把数学关系包装成自然语言例如“苹果单价 3.5 元买 4 个付 20 元应找回多少”。每种题型 10 到 20 题总共 50 到 100 题比较合理。题目数量太少随机性和偶然性都大数量太多API 成本增加。3.2 模板生成与避免数据污染不要直接抄 GSM8K 或 SimpleQA 题库因为很多模型训练时见过这些题目。建议用代码模板动态生成题目保证测试集是新的。下面给一个 Python 生成器示例。import random def gen_integer(level1): if level 1: a random.randint(10, 99) b random.randint(10, 99) op random.choice([, -]) return f{a} {op} {b} elif level 2: a random.randint(12, 99) b random.randint(12, 99) op random.choice([*, /]) return f{a} {op} {b} else: a random.randint(20, 99) b random.randint(2, 9) c random.randint(10, 50) return f({a} {b}) * {c} - {a} def gen_fraction(): d1 random.randint(2, 12) d2 random.randint(2, 12) n1 random.randint(1, d1 - 1) n2 random.randint(1, d2 - 1) return f{n1}/{d1} {n2}/{d2}, n1 / d1 n2 / d2 def build_questions(): questions [] for _ in range(20): expr gen_integer(1) questions.append((expr, eval(expr))) for _ in range(20): expr, ans gen_fraction() questions.append((expr, ans)) return questions注意这里eval只用于本地生成时的正确答案不要把它用于解析模型输出。题目生成后应该保存成 JSON 文件方便后续复现。3.3 正确结果的计算与保存生成题目时就必须同时生成正确答案。正确答案在本地由程序算好评测时只和模型输出比对。你不需要用另一个 LLM 来判分否则结果不公正。{ id: 1, question: 23 47 * 2 ?, answer: 117, type: complex }4. 复现评测的环境准备这个评测项目不挑硬件主要看你的模型访问方式。4.1 API 路线如果你要测 OpenAI、Claude、Gemini、DeepSeek、Qwen 等在线模型只需要Python 3.9 以上openai、requests、pandas等库各平台 API Key安装依赖pip install openai requests pandas建议把 API Key 写入环境变量不要写死在代码里。export LLM_API_KEYyour-api-key export LLM_BASE_URLhttps://api.example.com/v14.2 本地模型路线如果你想测 Llama、Qwen、DeepSeek 等开源模型推荐使用 Ollama 或 vLLM。Ollama 的安装和启动最简单# 拉取模型 ollama pull qwen2.5:7b # 启动服务 ollama serve用本地模型推理时显存占用和量化格式强相关。7B/8B 模型用 Q4 量化通常需要 6G 左右显存FP16 需要 14G 以上14B 模型量化后大约需要 9G 到 12G70B 模型即使量化也建议 48G 显存以上。这些数字会随模型版本和上下文长度变化实际以本机观察为准。4.3 统一访问接口不管用 API 还是本地模型都建议把模型调用封装成一个统一函数方便批量评测。大多数在线服务都兼容 OpenAI 的/v1/chat/completions接口Ollama 也提供类似接口。你只需要切换base_url就能切换模型来源。5. 核心评测脚本让模型“裸算”评测脚本的核心是给模型一条算术题要求它只能输出一个数字不能写解释不能写代码然后拿输出和本地正确答案比对。下面是一个使用 OpenAI 兼容接口的示例。import os import time from openai import OpenAI client OpenAI( api_keyos.getenv(LLM_API_KEY, ollama), base_urlos.getenv(LLM_BASE_URL, http://localhost:11434/v1), ) def ask_model(model: str, question: str) - str: 请求模型只返回答案文本。 resp client.chat.completions.create( modelmodel, messages[ { role: system, content: You are a calculator. Output only the numeric answer. Do not explain. Do not write code., }, { role: user, content: fCalculate: {question}, }, ], temperature0, max_tokens32, timeout30, ) return resp.choices[0].message.content.strip()这里关键的设置是temperature0尽可能让输出确定性更强。max_tokens32是防止模型写一大段解释。但要注意即使你这样要求仍然会有模型输出“答案是 3.5”或者“239”之类的多余内容所以后续的答案解析很重要。5.1 答案解析与容错模型输出不一定是一个纯数字常见问题有输出“117。”输出“117.0”输出“答案是 117”输出“$117”输出“117/100”所以需要写一个宽容的解析器。import re def extract_number(text: str): 从模型输出中提取第一个数字。 if not text: return None # 去掉千分位逗号 text text.replace(,, ) # 匹配整数或小数 match re.search(r-?\d\.?\d*, text) if match: return float(match.group()) # 如果匹配不到尝试匹配分数 match re.search(r(\d)\s*/\s*(\d), text) if match: a, b int(match.group(1)), int(match.group(2)) if b ! 0: return a / b return None def judge(pred_text: str, answer: float): pred extract_number(pred_text) if pred is None: return False, parse_error if abs(pred - answer) 1e-6: return True, correct return False, wrong解析错误也算错但要单独标记为parse_error。统计正确率时可以分别算“答案正确率”和“格式合法率”。有些模型可能答案对了但格式不合格这在真实场景里同样不可用。5.2 单模型评测流程评测一个模型的完整流程是def evaluate_model(model: str, questions: list): results [] for q in questions: question_text q[question] answer q[answer] try: start time.time() pred_text ask_model(model, question_text) elapsed time.time() - start correct, status judge(pred_text, answer) results.append({ model: model, question: question_text, answer: answer, pred_text: pred_text, pred_numeric: extract_number(pred_text), correct: correct, status: status, latency: elapsed, }) except Exception as e: results.append({ model: model, question: question_text, answer: answer, pred_text: , correct: False, status: ferror: {e}, latency: 0, }) return results这个循环是串行的代码简单但 50 道题 x 50 个模型就是 2500 次请求串行会比较慢。后面会给出批量并发方案。6. 批量评测 50 个模型的队列设计评测 50 个模型不是一次性写完就能跑的需要把它当成一个批处理任务来设计。6.1 模型配置先把模型列表集中管理可以使用 JSON 配置文件。{ models: [ {name: gpt-4o-mini, base_url: https://api.openai.com/v1}, {name: claude-3-5-haiku, base_url: https://api.anthropic.com/v1}, {name: qwen2.5:7b, base_url: http://localhost:11434/v1} ] }每个模型可以有自己的base_url和api_key评测脚本在读取时覆盖默认配置。6.2 批量调度的基本结构对于这样的评测并发很重要。简单方案是用ThreadPoolExecutor每个模型一个线程模型内部题目串行或小并发。这样能控制每个模型的请求速率避免触发 API 限流。from concurrent.futures import ThreadPoolExecutor, as_completed def run_batch(model_config, questions): model model_config[name] results [] for q in questions: # 这里可以加超时和重试 try: r evaluate_one(model_config, q) results.append(r) except Exception as e: results.append({ model: model, question: q[question], correct: False, status: ferror: {e}, }) return results configs load_model_configs() all_results [] with ThreadPoolExecutor(max_workersmin(8, len(configs))) as executor: future_map {executor.submit(run_batch, cfg, questions): cfg for cfg in configs} for future in as_completed(future_map): cfg future_map[future] try: all_results.extend(future.result()) except Exception as e: print(fmodel {cfg[name]} failed: {e})max_workers不要开得太大。如果你用的是同一个 API Key并发过高可能触发限流如果评测本地模型并发过高会直接爆显存。稳妥的做法是从 4 到 8 并发开始观察延迟和错误率。6.3 失败重试与结果记录在批量任务里API 偶尔超时是正常的需要设计重试机制。def call_with_retry(func, retries3, delay2): for i in range(retries): try: return func() except Exception as e: if i retries - 1: raise time.sleep(delay * (i 1))每次请求的结果都应该及时落盘不要等全部跑完再保存。建议每完成一个模型就把结果追加写入 CSV 或 JSONL。import csv def save_append(results, pathresults.csv): write_header not os.path.exists(path) with open(path, a, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesresults[0].keys()) if write_header: writer.writeheader() writer.writerows(results)这样即使中途某个模型挂了之前的评测结果也不会丢。7. 可能的表现规律与结果解读我没有拿到作者最终的评分表所以这里不写具体分数。但根据常识和过去多轮评测来看这类测试通常会呈现几个规律你跑完可以对照观察。7.1 简单题几乎全对复杂题开始分层一位数加法、两位数减法大多数模型都做得好毕竟这几乎是训练集里的高频内容。但到了两位数乘三位数、多步复合运算模型之间的差距会拉大。表现好的模型仍然能算对表现差的会从某一步开始计算错误并且无法自查。7.2 小模型更容易“一本正经地错”参数量较小的开源模型经常出现“答案接近但不对”的情况比如23 * 47得出1080而不是1081。它知道乘法的过程但进位处理做不到完全稳定。这个问题在小模型上更明显大模型相对更稳但不代表没有。7.3 输出格式错误比你想象的更常见评测脚本里我把“格式不合法”单独统计因为不少模型会写成“答案是 117 元”或加一句“让我算一下”。在实际 Agent 场景这种多余输出反而更难处理。一个只做算术的模型如果连“只输出一个数字”的指令都执行不好那它做更复杂的结构化输出时也更可能出问题。7.4 温度的影响温度设为 0 时同一个题目多次测试也会因为采样和随机性出现偶发错误。评测时建议每道题至少跑 3 次取多数票判定减少偶然误差。标题里提到的项目是否做了多次重测我不确定但一个严谨的评测应该这样做。7.5 题目越“新”正确率越低你自己生成的题目对模型来说大概率是没见过的因此成绩会比常见基准评测低一些。这不是模型变差了而是你剥掉了“记忆分”。从产品角度这个分数反而更有参考价值。8. 为什么 LLM 裸算容易出错这里把原因说透。理解原因之后你才知道该在什么层面解决问题。8.1 自回归模型不会“算”只会“猜下一个 token”大模型的训练目标是预测下一个 token。推理时模型看到23 * 47 它会从训练数据中学到的统计规律去生成“最可能的下一个 token”而不是像计算器那样做乘法运算。对常见的题目它记住的规律很强所以表现不错对不常见的组合它只能靠模式拼接容易出错。8.2 数字的 tokenization 会影响计算不同模型对数字的切分方式不一样。有的模型把 12345 切成[123, 45]有的切成[12, 345]。这种切分方式直接影响模型对每一位数字的感知能力。当运算需要进位或逐位对齐时切分不合理的模型更容易产生进位错误。8.3 训练数据中“算术过程”的表示不一致模型见过的数学题格式五花八门有横式、竖式、文字、带符号、带单位。它很难抽象出一个统一的“数值运算规则”更多是在模仿多种格式下的输出风格。训练数据里如果这种格式的数学题出现频率不够高算错就很正常。8.4 浮点精度与量化不是主因网上经常讨论 FP16、FP32、BF16 对模型精度的影响。推理阶段权重和激活值的数值精度确实会影响输出尤其在量化到 INT8 或更低时模型质量会有波动。但 LLM 算术出错首要原因还是架构和训练目标不是浮点精度。本地部署时用 FP16 还是 INT8会影响模型性能上限但不会让一个算术很差的模型变成计算器。9. 功能测试从裸算结果延伸到提示词对比既然题目和评测脚本都跑通了可以顺手做几个功能对比实验。这些实验对这个项目本身很有价值。9.1 对比“直接输出”和“CoT 引导”用一个变量控制提示词对比模型在“直接给答案”和“请先分步计算再给答案”两种情况下的正确率。TEMPLATE_DIRECT Calculate: {question}\nOutput only the number. TEMPLATE_COT ( Calculate step by step, then output the final number.\n Question: {question}\n Final answer: )一般来说CoT 提示会提高正确率但会明显增加 token 消耗和延迟。评测时记录耗时这样可以量化“多花多少成本换多少正确率”。9.2 对比“裸算”和“代码解释器”如果评测对象是支持代码解释器的模型可以在第二组测试里允许模型调用 Python计算eval(expression)再看正确率。结果大概率会接近 100%。这个对照组很有说服力它证明模型能力不足的部分可以靠工具 100% 弥补。9.3 对比不同量化的本地模型如果你有本地模型用 Ollama 分别跑 Q4_K_M、Q8_0、FP16 三个版本比较同一批题目的正确率。这能帮你判断是否需要为了纯度而选择更大的量化。10. 接口 API 与批量任务的工程细节这个项目本质上是一个“评测 API”批处理任务。除了前面提到的并发调度还有几个工程细节值得注意。10.1 超时控制模型调用不能无限等待。Chat Completions 接口建议设置 30 到 120 秒超时。如果某个模型超时按题目错误记录不要中断整个评测。resp client.chat.completions.create( modelmodel, messagesmessages, temperature0, max_tokens32, timeout30, )10.2 限流与并发控制如果你跑 50 个模型且都是同一个云服务商建议限制全局并发。比如ThreadPoolExecutor(max_workers5)并在每轮请求之间加一个小 sleep避免 429 限流。10.3 结果汇总统计评测结束后可以用 pandas 做聚合。import pandas as pd df pd.read_csv(results.csv) summary df.groupby(model).agg( total(correct, count), correct(correct, sum), acc(correct, mean), ) print(summary)同时建议统计每个题目的通过率找出“所有模型都错”的题。如果某道题所有模型都错先检查答案是不是算错了再考虑是否保留。11. 资源占用与性能观察跑评测和跑模型推理一样要关注资源占用。11.1 API 路线CPU 和 GPU 都不用关心但要注意 latency 和 token 消耗。记录每道题的平均耗时。CoT 提示词会让输出 token 变大导致耗时和费用明显上升。11.2 本地模型路线运行 Ollama 或 vLLM 时可以用nvidia-smi观察显存占用。watch -n 1 nvidia-smi启动qwen2.5:7b后Q4 量化的显存占用通常在 5G 到 7G 左右FP16 大约 14G 以上。同时注意 CPU 和内存占用因为模型加载时需要把权重读入内存。如果并发评测多个模型建议实验前先确认显存不会被打满。11.3 降低资源占用的办法本地模型优先选量化版本。控制max_tokens算术题不需要很长的输出。并发线程数控制在 2 到 4。评测前先清掉其他占用显存的任务。12. 常见问题与排查方法问题现象可能原因排查方式解决方案模型输出空白或超时API 限流、网络超时查看日志直接 curl 测试 API加长超时时间增加重试与并发退避输出带解释文字解析失败提示词约束不足查看pred_text字段更新 system prompt增加 few-shot 示例评测结果不稳定temperature 未设为 0检查评测配置设置 temperature0多次重测取多数票本地模型显存溢出并发过大或量化低运行 nvidia-smi 观察降低并发改用量化模型某个模型整体分数低模型本身算术弱对比单题输出确认是否属于正常差异必要时换更大的模型所有模型都做错同一道题题目答案生成错误重新用 Python 验算修正 JSON 中的正确答案API 返回 401API Key 错误检查环境变量重新设置密钥API 返回 429请求频率超限检查限流错误日志降低并发增加重试间隔CoT 提示词后延迟暴增输出 token 变长记录 latency 与 token权衡正确率与成本13. 最佳实践与使用建议如果你打算复刻这个评测项目或把它改造成自己的模型评测基线下面几条建议可以直接用。13.1 固定一切能固定的变量temperature0固定随机种子使用同一套 prompt 模板使用同一批题目记录 API 版本和模型版本这样评测结果才可复现。13.2 题目、输出、日志分目录保存建议目录结构如下eval_arithmetic/ ├── questions.json ├── results/ │ ├── raw/ │ └── summary/ ├── scripts/ │ ├── generate_questions.py │ ├── run_eval.py │ └── analyze.py └── logs/批量任务一定要有日志。每个模型跑完后至少记录开始时间、结束时间、成功题数、失败题数。13.3 评审结果不要只看正确率正确率是最直观的指标但还要看格式合法率多少输出能被程序解析。平均延迟模型是否适合对延迟敏感的产品。token 消耗CoT 和工具调用会显著增加成本。13.4 合规与使用边界这是一个评测脚本项目本身很安全但要注意几个边界调用 API 时不要把敏感数据放进题目里。题目是公开的算术题不存在隐私问题但如果你扩展到私有数据集必须做脱敏和授权确认。使用开源模型时注意模型许可证和部署合规性。发布评测结果时记录模型版本和评测日期避免误导读者。算术评测可以帮你发现模型短板但不要据此对某个模型做绝对化评价。13.5 先小范围试跑第一次测试不要直接跑 50 个模型。先用 3 个模型、10 道题跑通整个流程确认脚本、解析、保存、统计都正常再扩大范围。这样可以节约 API 费用也更容易排查问题。14. 总结与下一步“把计算器从 50 个模型手里拿走”这个评测项目的思路很有意思。它没有引入新技术但提出了一个很实际的问题在工具链断裂的时候模型本身的算术能力会不会成为瓶颈。如果你正在做 LLM 应用或 Agent这个测试可以帮你回答几个问题你的模型在裸算场景下是不是靠得住需不需要在算术环节强制走代码解释器或计算器工具多轮推理中的错误会不会因为算术失误被不断放大建议先从 3 个模型 20 道题开始试把评测脚本跑通再扩大到更多模型和更复杂的题目。最容易踩的坑就是模型输出的格式五花八门解析逻辑不宽容的话真实正确率会被低估。下一步可以扩展的方向是把同样的评测思路迁移到数学应用题、单位换算、日期计算、表格数值提取这些生产场景。测完“会不会算数”再测“会不会正确调用工具”实际上是评估 Agent 稳定性的一条可复用路径。如果你也想验证自己选的模型到底会不会算数最好的方式不是听宣传而是把这套题跑一遍。