现在很多开发者在做 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 的训练数据量非常大,一些简单算术题极有可能已经出现在训练语料中。这意味着模型可能不是“算出来”的,而是“背出来”的。为了降低这种影响,测试题应该尽量使用低热度的数字组合,不要全是1+1、9×9这种高频题。
一种有效做法是在测试集中加入动态随机生成的长数字,确保网络公开语料里几乎不可能出现完全一样的题目。比如735482 × 1849这种组合,模型不太可能直接背过答案。
2.4 生成测试集的代码实现
下面用 Python 写一个可复现的测试集生成器。通过固定随机种子,保证 50 个 LLM 拿到的题目完全一致,这样横向对比才有意义。
import json import random random.seed(42) def generate_integer_arithmetic(num=40): 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": f"int_{len(items)}", "expression": expression, "answer": str(answer), "category": "integer", }) return items def generate_decimal_arithmetic(num=30): 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": f"dec_{len(items)}", "expression": expression, "answer": str(answer), "category": "decimal", }) return items def generate_mixed_expression(num=30): 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": f"mix_{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", encoding="utf-8") as f: json.dump(dataset, f, ensure_ascii=False, indent=2) 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": f"Bearer {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, headers=headers, json=payload, timeout=60) 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, headers=headers, json=payload, timeout=60) 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_name="qwen2.5-72b-instruct", api_type="local", base_url="http://localhost:8000/v1", api_key="EMPTY" ) resp = client.chat("请计算 1234 × 5678 的结果,只输出数字。") print(resp)3.2 评测 Prompt 设计
“去掉计算器”体现在 prompt 上,就是明确要求模型不要使用任何工具、不要编写代码、不要假设外部计算器存在,直接用文字推理完成计算。
这里有一个很关键的细节:是否允许 Chain-of-Thought。标准的 CoT 会让模型把中间的推理步骤写出来,这对算术准确率提升很明显。但不同模型在 verbose 输出能力上有差异。
为了公平,可以设计两套 prompt:
- prompt_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", encoding="utf-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, temperature=0.0, max_tokens=256) 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", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2)这个脚本有几点设计值得说明:
temperature=0.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", encoding="utf-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(key=lambda x: x["total_accuracy"], reverse=True) 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”不可能全部来自同一家厂商。在实际评测中,模型池通常按这几类划分:
- 闭源商用 API:OpenAI、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_tokens=256就是一个硬上限。如果模型在思考过程中写太多废话,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 既当裁判又当运动员。
- 评测时保持
temperature=0.0,并且多跑几次取多数答案,能降低随机性带来的误判。 - 对开源模型,建议在本地起推理服务并统一使用 OpenAI 兼容接口,降低评测工具的适配成本。
8. 总结
把计算器从 LLM 手里拿走,相当于扒掉了它的“外挂”,直接检查它自身的基本功到底怎么样。这次对 50 个 LLM 的算术评测,本质上是一次很朴素但有效的工程摸底:在无法调用外部工具的极端前提下,模型能不能给出正确答案。
从测试集设计、评测框架、模型池分类,再到结果分析和错误模式拆解,这套流程完全可以复用到你自己的模型选型和 Agent 场景当中。我也建议你不要只跑一次准确率排名,而是深入到每个题型、每种错误模式,才能真正理解一个模型的数学能力边界在哪里。
下一步如果你有兴趣,可以做三件事:把测试集扩展到包含更复杂的高等数学符号;在同样测试集上对比“裸算 vs 带计算器”的准确率差距;或者用这道测试题筛选适合做数学教师的微调基座模型。
如果你自己跑完评测,发现某个模型在算术题上表现得特别“离谱”,欢迎在评论区分享你的模型名字和题目类型,我们一起看看它到底是哪里翻了车。