现在很多开发者在做 LLM 应用时都会默认给模型接上计算器插件、代码解释器或者干脆相信模型的“心算”。但当我把计算器从 50 个 LLM 手里拿走只让它们用最原始的思维链完成加减乘除时得到的结果比预想中更有意思。这篇文章会完整复盘这次算术评测的测试集设计、评测框架、打分逻辑和避坑方案也会讨论一个工程问题在什么场景下你不应该把算术交给 LLM。1. 背景为什么要把计算器从 LLM 手里拿走1.1 LLM 算术能力为什么是一个工程问题在 RAG、Agent、Function Calling 这些概念满天飞的今天LLM 解决复杂问题的方式已经不再是“单模型硬算”。很多应用会在 LLM 外面套一层工具调用层遇到数学计算就交给计算器插件或代码解释器。这种架构本身没有问题但它掩盖了一个事实模型本身的算术能力仍然脆弱。如果你把一个 LLM 接到一个无法访问外部工具、没有计算器插件的环境里比如纯文本对话机器人只能靠模型自身输出。某些安全环境中不允许执行外部代码。模型服务只有基础的 chat/completion 接口没有 tool calling。Agent 的工具调用失败或超时系统自动降级到普通对话模式。这时模型的“裸算能力”就成了兜底。如果它连基本的两位数乘法都算不对用户的信任度会断崖式下降。1.2 为什么 LLM 会被“看起来很会算”欺骗LLM 本质上是下一个 Token 的预测器它并不像传统计算器那样执行真实的数值运算。它通过学习海量文本学到了很多算术模式。比如1 1 2、7 × 8 56这类高频出现的内容模型可以轻松背出来。但遇到低频率、高位数、需要严格进位的计算时它的表现就会退化成“模式匹配 概率采样”。这也是为什么很多模型在简单数学题上表现极好一旦出现多位数乘法、小数除法、分数比较就会给出一个看起来很有逻辑但数值离谱的结果。1.3 从“盲目信任”到“基准评测”为了避免“觉得模型会算”和“模型真的会算”之间的错觉最直接的办法就是做基准测试。这次评测的做法很明确把计算器从 LLM 身边拿走不提供任何外部工具只给一个题目文本让模型直接输出答案。然后把 50 个主流和常见的 LLM 放在同一批测试集上跑一遍按统一标准打分最后得出一个横向对比结论。这种做法适合任何想评估模型算术能力的开发者参考即使你不打算跑 50 个模型也可以只跑自己负责的那一个看看它在脱离工具后的真实水平。2. 测试集设计如何科学地给 50 个 LLM 出算术题2.1 题目维度拆解算术能力不是一个单一指标我把它拆成以下几个维度类别具体题型示例基础运算整数加减乘除123 456、789 - 321多位数运算三位数以上乘除1234 × 5678混合运算四则混合、括号(12 34) × 5 - 18 ÷ 3小数运算小数的加减乘除0.1 0.2、3.14 × 2.5分数运算分数加减乘除1/3 2/5、7/8 - 3/4比较大小小数/分数/大数比较0.3 和 1/3 哪个大取模与整除余数、整除判断100 % 7、256 能否被 8 整除细分的目的是避免“平均准确率”掩盖单体弱项。一个模型可能整数加减法接近满分但小数运算一塌糊涂。如果没有维度拆分最终分数会掩盖这个问题。2.2 难度梯度设计测试集不能全出简单的题也不能全出偏题。我按难度梯度把题目分成三档基础题COT 不需要太多步骤能直接算出的简单题。中等题需要借位、进位、多步混合运算。困难题多位小数、超大整数、复杂分数通分比较。建议难度比例控制在3:4:3左右这样既能反映基础能力也能区分中高水平模型。2.3 防止记忆污染LLM 的训练数据量非常大一些简单算术题极有可能已经出现在训练语料中。这意味着模型可能不是“算出来”的而是“背出来”的。为了降低这种影响测试题应该尽量使用低热度的数字组合不要全是11、9×9这种高频题。一种有效做法是在测试集中加入动态随机生成的长数字确保网络公开语料里几乎不可能出现完全一样的题目。比如735482 × 1849这种组合模型不太可能直接背过答案。2.4 生成测试集的代码实现下面用 Python 写一个可复现的测试集生成器。通过固定随机种子保证 50 个 LLM 拿到的题目完全一致这样横向对比才有意义。import json import random random.seed(42) def generate_integer_arithmetic(num40): items [] for _ in range(num): a random.randint(10, 9999) b random.randint(10, 999) op random.choice([, -, *, //, %]) if op : answer a b elif op -: answer a - b elif op *: answer a * b elif op //: answer a // b else: answer a % b expression f{a} {op} {b} items.append({ problem_id: fint_{len(items)}, expression: expression, answer: str(answer), category: integer, }) return items def generate_decimal_arithmetic(num30): items [] for _ in range(num): a round(random.uniform(0.1, 999.9), 2) b round(random.uniform(0.1, 99.9), 2) op random.choice([, -, *]) if op : answer round(a b, 2) elif op -: answer round(a - b, 2) else: answer round(a * b, 2) expression f{a} {op} {b} items.append({ problem_id: fdec_{len(items)}, expression: expression, answer: str(answer), category: decimal, }) return items def generate_mixed_expression(num30): items [] for _ in range(num): a random.randint(2, 50) b random.randint(2, 50) c random.randint(2, 50) d random.randint(2, 9) op1 random.choice([, -, *]) op2 random.choice([, -, *]) expression f({a} {op1} {b}) {op2} {c} - {d} # 使用 ast 计算精确结果避免浮点误差干扰 import ast result eval(expression, {__builtins__: {}}, {}) items.append({ problem_id: fmix_{len(items)}, expression: expression, answer: str(result), category: mixed, }) return items if __name__ __main__: dataset [] dataset.extend(generate_integer_arithmetic(40)) dataset.extend(generate_decimal_arithmetic(30)) dataset.extend(generate_mixed_expression(30)) with open(arithmetic_testset.json, w, encodingutf-8) as f: json.dump(dataset, f, ensure_asciiFalse, indent2) print(f生成题目总数: {len(dataset)}) for item in dataset[:5]: print(item)这里说一个注意事项eval在真实工程中并不安全上面的代码主要用于本地生成离线测试集不推荐直接用在线上服务。如果担心表达式求值的可控性可以改用sympy或decimal.Decimal来做精确计算。2.5 测试集文件结构生成后的 JSON 文件结构大致如下[ { problem_id: int_0, expression: 1234 5678, answer: 6912, category: integer }, { problem_id: dec_0, expression: 123.45 67.89, answer: 191.34, category: decimal } ]这种结构方便后续评测脚本直接读取也方便把测试题复用到你自己的评测项目里。3. 评测环境搭建与代码实现3.1 统一模型调用接口要评测 50 个 LLM第一个要解决的问题是“接口不统一”。不同厂商的 API 参数五花八门有的支持messages格式有的支持prompt格式有的有temperature有的没有。为了不让接口差异影响评测结果需要封装一个统一调用层。这里给出一个简化的调用层。它抽象了三种常见模型来源OpenAI 风格接口、Anthropic 风格接口、本地 OpenAI 兼容接口。import json import time import requests from typing import Dict, Any, Optional class LLMClient: 统一的 LLM 评测客户端封装 def __init__(self, model_name: str, api_type: str, base_url: str None, api_key: str None): self.model_name model_name self.api_type api_type self.base_url base_url self.api_key api_key def chat(self, prompt: str, temperature: float 0.0, max_tokens: int 512) - str: if self.api_type openai: return self._chat_openai(prompt, temperature, max_tokens) elif self.api_type anthropic: return self._chat_anthropic(prompt, temperature, max_tokens) elif self.api_type local: return self._chat_local(prompt, temperature, max_tokens) else: raise ValueError(f不支持的 API 类型: {self.api_type}) def _chat_openai(self, prompt: str, temperature: float, max_tokens: int) - str: url f{self.base_url}/chat/completions if self.base_url else https://api.openai.com/v1/chat/completions headers { Authorization: fBearer {self.api_key}, Content-Type: application/json, } payload { model: self.model_name, messages: [ {role: user, content: prompt} ], temperature: temperature, max_tokens: max_tokens, } resp requests.post(url, headersheaders, jsonpayload, timeout60) resp.raise_for_status() return resp.json()[choices][0][message][content] def _chat_anthropic(self, prompt: str, temperature: float, max_tokens: int) - str: url https://api.anthropic.com/v1/messages headers { x-api-key: self.api_key, anthropic-version: 2023-06-01, Content-Type: application/json, } payload { model: self.model_name, messages: [ {role: user, content: prompt} ], temperature: temperature, max_tokens: max_tokens, } resp requests.post(url, headersheaders, jsonpayload, timeout60) resp.raise_for_status() return resp.json()[content][0][text] def _chat_local(self, prompt: str, temperature: float, max_tokens: int) - str: # 假设本地服务是 OpenAI 兼容格式 return self._chat_openai(prompt, temperature, max_tokens)使用示例client LLMClient( model_nameqwen2.5-72b-instruct, api_typelocal, base_urlhttp://localhost:8000/v1, api_keyEMPTY ) resp client.chat(请计算 1234 × 5678 的结果只输出数字。) print(resp)3.2 评测 Prompt 设计“去掉计算器”体现在 prompt 上就是明确要求模型不要使用任何工具、不要编写代码、不要假设外部计算器存在直接用文字推理完成计算。这里有一个很关键的细节是否允许 Chain-of-Thought。标准的 CoT 会让模型把中间的推理步骤写出来这对算术准确率提升很明显。但不同模型在 verbose 输出能力上有差异。为了公平可以设计两套 promptprompt_A直接要求输出答案禁止解释模拟“快速响应”场景。prompt_B允许分步推理但最终必须给一个唯一的数字答案。建议用 prompt_B 作为主评测基准因为它在数学评测中更接近主流实践也更能反映模型真实推理能力。示例 Prompt你是一个数学计算器评测助手。请完成以下算术题。 要求 1. 不要使用任何外部工具、计算器、代码执行环境。 2. 只能依靠你自己的推理能力计算。 3. 你可以在思考过程中写推理步骤但最终一行必须是“最终答案数字”的格式。 4. 不要输出除最终答案外的多余数字。 题目 (12345 6789) * 12 - 456 / 3 请开始。注意题目里不能出现“请写代码”这种引导我们要的就是模型裸算。如果模型尝试输出 Python 代码或描述它可以调用计算器说明它在当前结构下不会乖乖走“纯推理”路径这种情况会被记录为“违规输出”。3.3 评测循环主脚本写一个评测脚本遍历所有测试题和所有模型把结果保存为 JSON。import json import time from datetime import datetime from typing import List, Dict def run_evaluation( models: List[Dict[str, str]], dataset_path: str arithmetic_testset.json, output_path: str eval_results.json, max_retries: int 3, ): with open(dataset_path, r, encodingutf-8) as f: dataset json.load(f) all_results [] for model_config in models: client LLMClient(**model_config) model_result { model: model_config[model_name], api_type: model_config[api_type], start_time: datetime.now().isoformat(), items: [], } for item in dataset: is_correct None raw_output attempts 0 while attempts max_retries: try: prompt build_prompt(item[expression]) raw_output client.chat(prompt, temperature0.0, max_tokens256) is_correct check_answer(raw_output, item[answer]) break except Exception as e: print(f模型 {model_config[model_name]} 题目 {item[problem_id]} 调用失败: {e}) attempts 1 time.sleep(2) model_result[items].append({ problem_id: item[problem_id], category: item[category], expression: item[expression], expected_answer: item[answer], raw_output: raw_output, is_correct: is_correct, attempts: attempts, }) time.sleep(0.5) # 防止请求过快 model_result[end_time] datetime.now().isoformat() all_results.append(model_result) save_results(all_results, output_path) return all_results def build_prompt(expression: str) - str: prompt f请完成以下算术题。 要求 1. 不要使用任何外部工具、计算器、代码执行环境。 2. 只能依靠你自己的推理能力计算。 3. 你可以在思考过程中写推理步骤但最终一行必须是“最终答案数字”的格式。 4. 不要输出除最终答案外的多余数字。 题目 {expression} return prompt def check_answer(raw_output: str, expected_answer: str) - bool: # 优先从“最终答案”提取 if 最终答案 in raw_output: predicted raw_output.split(最终答案)[-1].strip() else: # 兜底取最后一个数字 import re numbers re.findall(r-?\d\.?\d*, raw_output) if not numbers: return False predicted numbers[-1] return normalize_number(predicted) normalize_number(expected_answer) def normalize_number(s: str) - str: s s.strip().replace(,, ).replace(, ) # 去掉多余的 .0 if s.endswith(.0): s s[:-2] return s def save_results(results, path: str): with open(path, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)这个脚本有几点设计值得说明temperature0.0是为了减少采样随机性。虽然很多 API 即使设成 0 也不是完全确定但这是目前最容易保持可复现性的方式。重试机制只处理“调用失败”不处理“答错”。答错是模型能力问题重试不会改善反而会影响评测公平性。time.sleep(0.5)是为了避免并发过高触发限流。3.4 结果统计与打分评测完 50 个模型后需要汇总出每个模型的准确率同时按题型维度做交叉分析。import json from collections import defaultdict def compute_summary(results_path: str): with open(results_path, r, encodingutf-8) as f: results json.load(f) summary [] for model_result in results: items model_result[items] total len(items) correct sum(1 for item in items if item[is_correct]) category_correct defaultdict(int) category_total defaultdict(int) for item in items: cat item[category] category_total[cat] 1 if item[is_correct]: category_correct[cat] 1 model_summary { model: model_result[model], total_accuracy: round(correct / total * 100, 2), total_correct: correct, total_count: total, category_accuracy: { cat: round(category_correct[cat] / category_total[cat] * 100, 2) for cat in category_total } } summary.append(model_summary) summary.sort(keylambda x: x[total_accuracy], reverseTrue) print(f{模型:30} {总分:8} {整数:8} {小数:8} {混合:8}) print(- * 70) for m in summary: cat m[category_accuracy] print(f{m[model]:30} {m[total_accuracy]:8} {cat.get(integer, 0):8} {cat.get(decimal, 0):8} {cat.get(mixed, 0):8}) return summary if __name__ __main__: compute_summary(eval_results.json)汇总输出示例模型 总分 整数 小数 混合 ------------------------------------------------------------ gpt-4o 89.2 95.0 85.0 90.0 claude-3.5-sonnet 87.5 92.5 83.3 86.7 qwen2.5-72b-instruct 84.0 90.0 80.0 83.3 ...这里要注意由于测试集和模型版本随时间变化上面的分数只是展示格式不代表某个模型的固定成绩。实际跑出来的结果可能会有所不同。4. 50 个 LLM 的分级与评测流程4.1 模型池设计“50 个 LLM”不可能全部来自同一家厂商。在实际评测中模型池通常按这几类划分闭源商用 APIOpenAI、Anthropic、Google、阿里云、智谱等。开源可本地部署模型Qwen、Llama、DeepSeek、Mistral、Baichuan、Yi 等。不同尺寸版本同一个开源模型可能有 7B、14B、72B 等多个尺寸应分别评测方便观察参数量对算术能力的影响。非主流或小众模型一些在 Hugging Face 上热度较低的模型也能跑出很有参考性的结果。在写评测列表时可以这样组织models [ # 闭源 API {model_name: gpt-4o, api_type: openai, base_url: None, api_key: YOUR_KEY}, {model_name: claude-3-5-sonnet-latest, api_type: anthropic, base_url: None, api_key: YOUR_KEY}, # 本地部署 {model_name: qwen2.5-7b-instruct, api_type: local, base_url: http://localhost:8000/v1, api_key: EMPTY}, {model_name: qwen2.5-72b-instruct, api_type: local, base_url: http://localhost:8001/v1, api_key: EMPTY}, {model_name: llama-3.1-8b-instruct, api_type: local, base_url: http://localhost:8002/v1, api_key: EMPTY}, ]4.2 评测批次与顺序50 个模型逐个跑会比较慢尤其是每个模型要过 100 道题。建议分批执行每批 5 到 10 个模型避免某个厂商的 API 因为并发过高直接限流。推荐流程先跑 2 个基线模型一个最强商用 API一个本地 7B 小模型确认评测代码没问题。再跑同系列不同尺寸模型观察尺寸变化趋势。然后跑其他厂商/开源模型。最后跑一些特殊的推理强化模型。评测过程中需要持续保存中间结果避免某个模型跑到一半因为网络中断而全部重来。4.3 超时与异常兜底LLM 评测最怕的不是答错而是“挂起”。某些模型在遇到复杂算术题时可能陷入无限生成。因此必须设置请求级超时和输出长度上限。前面代码中的max_tokens256就是一个硬上限。如果模型在思考过程中写太多废话256 个 token 可能不够。这时有两种处理方式增加max_tokens到 512 或 1024允许更长的 CoT。缩短 prompt明确要求“不要写太多推理直接给答案”。建议先用 512 跑通全流程。如果发现大量模型因为 token 截断而答错需要重新设计 prompt 或提高上限不能直接算作错误。5. 评测结果的分析维度5.1 总体准确率排名最直观的分析是总体准确率排名。它能快速给出一个结论当前模型池里谁的算术裸算最强。但这个排名不能完全代表模型好坏因为算术只是 LLM 能力的一小块。比较好的做法是把准确率按区间分层89% 以上算术能力很强可以承担部分无需工具的数值任务。70% - 89%中等水平建议在关键计算场景接工具。50% - 70%较弱不能承担任何精确计算任务。50% 以下基本不具备可靠算术能力。5.2 错误模式分析只看准确率不够还要看模型是怎么错的。我总结了 4 种常见错误模式错误模式表现说明位数错误结果差一个数量级模型可能“看到”了计算过程但最后少数了一位进位错误中间过程正确个位/十位出错反映了 token 级注意力不稳定符号错误减号看成加号或丢掉负号prompt 理解问题幻觉式自信给出一个很精确但完全错误的结果最危险模型会用自信语气掩盖错误在汇总时可以把每道错题的人工筛查结果记录到一个 CSV 里方便后续分析。5.3 模型尺寸与算术准确率的相关性如果把同一个开源模型的 7B、14B、72B 版本放在一起对比通常会看到参数越大算术能力越强。但差异并不是线性的。小模型可能在某些基础题上达到 80 分但到复杂题直接崩盘大模型在复杂题上表现相对稳定但也不是 100% 正确。比较有趣的是有些专门做过数学强化训练的模型即便参数量不大也能在算术评测中超过比自己大几倍的通用模型。所以与其盲目追求大模型不如先确认当前任务是否需要数学推理能力。5.4 输出格式规范性评测时我会额外记录一个指标模型输出是否包含“最终答案”格式。有的模型会在结尾写一句“希望这个答案对你有帮助”导致提取器拿到最后一个数字时取错。这在评测中会被判定为答案错误但其实模型的“计算过程”是对的。这说明输出格式规范化本身也是模型可用性的一个重要维度。工程上可以通过提示词约束和少量示例来改善但模型的固有能力仍然很重要。6. 常见问题与排查思路6.1 模型总是用代码解释器或工具回答问题现象明明提示词要求“不要使用外部工具”但模型还是会输出 “我可以使用 Python 计算代码如下” 之类的文本。可能原因模型的系统提示词里被植入了工具调用偏好。比如某些 API 的默认 system prompt 会告诉模型“你是一个可以调用工具的高级助手”。排查步骤检查调用参数里是否误传了tools字段。只要传了 tools模型就会倾向调用工具。检查 system prompt确保没有“使用代码解释器”的指令。检查模型版本部分模型在指令遵循上较弱容易忽略“不要使用工具”的约束。解决方案在评测时不要传入任何 tools 参数只传原始 user prompt。如果模型仍然输出代码直接判定为“违规输出”记录为错误。6.2 模型输出一段“思考过程”但没有最终答案问题现象模型写了大量 CoT 推理步骤最后没有按“最终答案”格式收尾。可能原因max_tokens不够导致输出被截断。排查步骤查看 raw_output 是否以“最终答案”结尾。检查输出长度是否接近max_tokens限制。解决方案适度提高max_tokens或者在 prompt 中强调“先给出答案再简单解释”。评测类任务不需要追求模型的完整推理过程只需要一个可解析的答案。6.3 小数运算出现浮点误差式错误问题现象0.1 0.2模型输出0.30000000000000004。可能原因模型在预训练时接触了大量编程语言中的浮点数表示方式导致它学到了这种输出。解决方案在打分时对小数结果做容差比较而不是字符串完全匹配。比如将预测值和标准值都转成浮点数允许1e-6的误差。但如果模型输出的是一个明显不对的数比如0.1 0.2 0.5那就要判错。6.4 多个模型 API 并发调用被限流问题现象某个模型跑到一半开始返回 429 或 503。排查步骤检查 API 返回的 HTTP 状态码。看报错信息中是否包含rate limit、quota等字段。解决方案增加请求间隔比如time.sleep(1)。加入指数退避重试机制。将同一个模型的题目分成多个子任务在多个 API key 之间轮询。7. 从算术评测看 LLM 工程落地7.1 什么时候该给 LLM 配计算器经过这次评测我最想强调的一点是工具增强虽然很流行但不是所有场景都适合无脑加计算器。如果任务满足以下条件建议给 LLM 配置工具计算精确度要求高不能容忍小数点后两位的误差。计算过程可以通过代码天然完成不需要模型创造复杂逻辑。用户有足够的等待时间等待工具执行。典型场景包括财务对账、订单金额折算。科学计算、物理模拟。数据分析里的聚合统计。需要生成精确报表的场景。7.2 什么时候不该依赖 LLM 计算如果任务只是对话里顺带一句“那大概多少钱”LLM 的估算能力可能已经够用。用户不会因为你把123.45 * 2计算成246.9还是246.90而产生强烈不满。但在这些场景不建议依赖 LLM 做数值计算涉及真实金钱交易。涉及用户隐私数据不允许把数据发到外部 API 执行代码。需要离线处理无法访问外部工具。延迟要求极高不允许等待代码解释器启动。在这些情况下正确做法是LLM 负责理解用户意图、从文本中抽取关键数字再用传统代码逻辑完成计算最后把结果交给 LLM 组织语言回复。7.3 如何评估一个 LLM 是否适合数值密集型任务如果你正在选型一个 LLM想判断它能不能承担数值类任务可以按以下步骤做一次小规模评测抽取 20 道高频业务算术题不要用模型见过的原题。在“无工具模式”下让模型作答。同时用 Python 算出标准答案。对比准确率和错误模式。如果准确率低于 90%那你需要额外接工具层。如果不想每次手动跑可以把上面的评测脚本保存成一个团队内部的基准脚本。以后每接入一个新模型先跑一遍算术测试再决定是否对它开放工具调用权限。7.4 工程上的其他最佳实践永远不要在业务关键路径上直接信任 LLM 的数值输出。即使模型准确率已经很高也要在代码层做一层校验。对大数运算LLM 容易丢位数。可以用“千位分隔符”的输入格式降低位数识别错误率比如1,234,567。对小数运算尽量要求模型输出固定小数位比如“结果保留两位小数”。对 Agent 架构建议把“计算能力”作为独立工具封装而不是让 LLM 既当裁判又当运动员。评测时保持temperature0.0并且多跑几次取多数答案能降低随机性带来的误判。对开源模型建议在本地起推理服务并统一使用 OpenAI 兼容接口降低评测工具的适配成本。8. 总结把计算器从 LLM 手里拿走相当于扒掉了它的“外挂”直接检查它自身的基本功到底怎么样。这次对 50 个 LLM 的算术评测本质上是一次很朴素但有效的工程摸底在无法调用外部工具的极端前提下模型能不能给出正确答案。从测试集设计、评测框架、模型池分类再到结果分析和错误模式拆解这套流程完全可以复用到你自己的模型选型和 Agent 场景当中。我也建议你不要只跑一次准确率排名而是深入到每个题型、每种错误模式才能真正理解一个模型的数学能力边界在哪里。下一步如果你有兴趣可以做三件事把测试集扩展到包含更复杂的高等数学符号在同样测试集上对比“裸算 vs 带计算器”的准确率差距或者用这道测试题筛选适合做数学教师的微调基座模型。如果你自己跑完评测发现某个模型在算术题上表现得特别“离谱”欢迎在评论区分享你的模型名字和题目类型我们一起看看它到底是哪里翻了车。