你有没有发现一个很有意思的现象:ChatGPT 能帮你写论文、改代码、做翻译,但如果你问它“178294 × 34987 等于多少”,它可能会犹豫很久,然后给你一个看起来合理但完全错误的答案。更离谱的是,有些大模型甚至连“三位数减法”都会算错,却能解出需要多步逻辑推理的数学应用题。
这不是偶然。LLM 的数学能力存在明显的“偏科”现象:有些数学问题它非常擅长,有些却一碰就错。这篇文章我想从一个更系统的视角来拆解这件事:LLM 到底擅长什么样的数学?不擅长什么样的数学?背后的机制原因是什么?以及我们在实际开发中,应该如何围绕这些边界来设计 Prompt、搭建应用、避免踩坑。
无论你是刚接触大模型的新手,还是在做 LLM 应用开发的工程师,这篇文章都会给你一个清晰的认知框架。我们会从原理讲起,再用可运行的 Python 代码测试一组典型数学问题,最后给出工程落地时的建议。
1. 背景:为什么 LLM 的数学能力让人捉摸不透
1.1 一个看似矛盾的现象
我们先来看两类截然不同的表现。
第一类:大模型可以非常轻松地完成“鸡兔同笼”这类经典应用题。你给它一段自然语言描述,它能读懂题意,设出未知数,列出方程,然后得出正确答案。这类问题考察的是“将自然语言转成数学表达式”的能力,LLM 做得相当好。
第二类:如果你直接让它计算一个稍微大一点的数字乘法,比如“238947 × 192873”,它可能直接给出一个错误结果,而且错得“理直气壮”。你反复追问,它甚至可能在一轮回答中给出两个完全不同的结果。
表面上看很矛盾:既然能解应用题,为什么算不对大数乘法?既然能做多步推理,为什么简单的算术反而翻车?
1.2 核心原因:LLM 不是计算器
要理解这个现象,关键要记住一件事:LLM 本质上是一个“下一个词预测器”。它生成每一个 token(可以理解为词元或字符块)时,都是在根据前面的上下文,计算下一个 token 的概率分布。它没有真正执行“进位”“借位”“乘法表”这样的离散运算过程,而是在做“基于模式的高概率预测”。
数字在 LLM 内部并不是像人类那样被当作连续的数值来理解的。它看到的是 token 序列——可能是“238947”被拆成“23”“894”“7”这样的块,也可能被拆成多个数字字符。它没有“数值大小”的直觉,只有“文本序列”的统计规律。
所以,理解 LLM 数学能力的核心,不是去问“它懂不懂数学”,而是去问“它的训练数据里,这种数学模式出现过多少次,模式是否稳定”。
1.3 常见应用场景与读者定位
这个话题在以下场景中非常有用:
- 教育类 AI 应用:你想做一个数学解题助手,需要知道哪些题可以交给 LLM,哪些题必须挂载计算器或符号计算引擎。
- 数据分析与金融场景:LLM 做汇总、提取、解释没问题,但如果让它做精确计算,就必须设计校验机制。
- Agent 工具编排:你要决定什么时候让 LLM 直接回答,什么时候让它调用代码解释器、计算器或外部 API。
- 模型评测与选型:你要对比不同模型的能力,需要理解 GSM8K、MATH、MMLU 这些评测基准到底在测什么。
下面我们先从环境准备开始,然后通过代码逐项测试 LLM 的数学能力边界。
2. 环境准备与工具链
2.1 运行环境说明
本文中的代码示例以 Python 3.9+ 为例,核心依赖如下:
openai:用于调用 OpenAI 兼容接口的大模型 API。transformers:用于本地加载开源模型,方便离线测试。sympy:Python 符号计算库,用来作为“外部工具”对比。numpy:数值计算库,用来做精度对比。
具体版本不需要完全一致,以你的项目实际情况为准,这里重点演示的是思路和流程。
2.2 安装依赖
pip install openai transformers sympy numpy2.3 准备测试模型
本文的代码示例支持两种方式:
方式一:调用远程 API
设置环境变量:
export OPENAI_API_KEY=你的API_KEY export OPENAI_BASE_URL=https://api.openai.com/v1然后通过ChatCompletion接口测试。
方式二:加载本地开源模型
如果你的机器支持,可以通过transformers加载模型:
from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "Qwen/Qwen2.5-7B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")版本和模型名需要根据你的实际网络环境调整。这里不展开推理优化细节,重点放在数学能力测试逻辑上。
3. 核心原理:LLM 数学能力的机制拆解
3.1 Token 化:数字不是数字
这是理解一切的基础。当 LLM 看到一个数字时,它先要把文本切分成 token。不同模型用的 tokenizer 不一样,切分规则也不一样。
例如,对于字符串 “1234567”,有的 tokenizer 会切成下面几种形式之一:
"123" "456" "7" "12" "345" "67" "1" "2" "3" "4" "5" "6" "7"不管怎么切,模型看到的都是离散 token,而不是一个连续的数值。模型可以记住这些 token 的共现模式,但它没有真正执行“数值运算”的组件。
这解释了一个现象:LLM 对小数字的运算准确率远高于大数字。因为小数字在训练语料中出现频率高,模型有足够多的模式可以依赖;而大数字的组合是稀疏的,模型只能靠“猜”。
3.2 训练目标:下一位 token 预测 vs 数学演绎
传统的计算器执行的是精确的演绎计算。每一步都是确定的:
238947 × 192873 = 238947 × (192000 + 873) = ...每一步都基于算术规则,结果是确定且可验证的。
LLM 的训练目标是最大化下一个 token 的似然概率。对于数学题,它确实能学到一些“模式化推理”,比如:
- 看到“鸡兔同笼”的关键词,就激活相关的解题模板;
- 看到“设未知数 x”,就按方程的常见步骤输出;
- 看到“因此答案是”,就接着输出一个大概率正确的数字。
这种模式匹配在训练数据中出现频率高的题型上表现良好,但一旦遇到训练数据中少见的数值组合、特殊符号、复杂结构,就容易退化。
3.3 模式记忆 vs 逻辑推理:两种能力的博弈
为了让这个区别更直观,我们可以把数学问题分成三类:
第一类:模式记忆型
这类问题在训练语料中出现频率很高,本质上就是“背诵”。
典型例子:
- 1 + 1 = 2
- 9 × 8 = 72
- 勾股定理:3² + 4² = 5²
- 常见方程的解法步骤
对于这类问题,LLM 不需要真正计算,它只需要回忆训练数据中的高频答案即可。准确率非常高。
第二类:模式组合型
这类问题需要多步推导,但每一步都比较常见。例如:
- 一元一次方程:2x + 3 = 11
- 鸡兔同笼、行程问题、工程问题
- 简单概率问题:抛一枚硬币两次,至少一次正面的概率
对于这类问题,LLM 能利用训练中学到的“推理骨架”一步步推进。虽然不是真正的逻辑演绎,但效果接近。这也是为什么有些大模型在 GSM8K(小学数学应用题数据集)上能拿到很不错的准确率。
第三类:精确计算型
这类问题需要精确的算术操作,尤其是大数运算、高精度小数、浮点数比较等。
典型例子:
- 238947 × 192873
- 0.1 + 0.2 到底等于几
- 9988776655 除以 133
- 多位小数的四舍五入
对于这类问题,LLM 的 token 预测机制几乎不占优势,它没有一个逐步执行进位、借位、乘法的“内部计算器”,因此经常产生“接近但不精确”的结果。
4. 实战:测一测 LLM 的数学能力边界
下面我们通过五个典型问题,来实际测试 LLM 的数学表现。为了保证可复现,我写了一个简易的测试脚本,你可以替换成自己的 API Key 或本地模型。
4.1 创建测试脚本
文件路径:math_test.py
import os import json from openai import OpenAI client = OpenAI( api_key=os.environ.get("OPENAI_API_KEY"), base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), ) def ask_math(question: str, model: str = "gpt-4o-mini") -> str: resp = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": "你是一个数学助手,请直接输出答案和简要步骤。"}, {"role": "user", "content": question}, ], temperature=0, ) return resp.choices[0].message.content if __name__ == "__main__": questions = [ "1. 计算 238947 × 192873 = ?", "2. 解方程:2x + 5 = 17,x = ?", "3. 已知一个圆的半径为 5,求圆的面积(保留两位小数)。", "4. 一个袋子里有 3 个红球和 2 个蓝球,不放回地连续摸出两个球,求两个球都是红球的概率。", "5. 请比较 0.1 + 0.2 和 0.3 是否相等,并说明理由。", ] for q in questions: print("问题:", q) print("回答:", ask_math(q)) print("-" * 60)运行:
python math_test.py4.2 示例输出与表现分析
下面是一组典型的模型输出(不同模型、版本结果会有差异,重点看能力倾向):
问题 1:大数乘法
238947 × 192873 = 46095572731实际真值:
238947 * 192873 # 46095572731如果模型输出了这个结果,说明它在训练中见过或者推理正确了。不过在实际测试中,很多模型在这个问题上会出现数位错误,比如算成“46095572831”或“46095573731”。模型对于大数精确乘法的稳定性并不高。
问题 2:一元一次方程
2x + 5 = 17 2x = 12 x = 6这类问题多数现代模型都能答对,因为步骤简单,训练样本丰富。
问题 3:圆的面积
圆的面积 S = πr² = 3.14 × 5² = 3.14 × 25 = 78.50(保留两位小数)这里有一个隐蔽的坑:如果模型直接用 3.14 代替 π,结果就是 78.50;如果用更精确的 π ≈ 3.14159,结果就是 78.54。不同模型可能给出不同结果,这并不一定是“算错”,而是近似精度选择不同。
问题 4:概率题
第一次摸红球概率:3/5 第二次摸红球概率:2/4 两个都是红球概率 = (3/5) × (2/4) = 3/10 = 0.3这种多步概率题,只要模型能正确理解“不放回”这个条件,通常都能答对。
问题 5:浮点数比较
0.1 + 0.2 在计算机中并不严格等于 0.3, 因为二进制浮点数的表示精度有限。 严格来说,0.1 + 0.2 = 0.30000000000000004。这个大模型通常能给出准确回答,因为这类问题在技术社区讨论中非常常见,属于“背诵型知识”。
4.3 结果汇总
| 问题类型 | 难度来源 | 模型通常表现 | 失败模式 |
|---|---|---|---|
| 大数乘法 | 精确逐位运算 | 不稳定,取决于具体数字 | 数位错、中间进位错 |
| 一元一次方程 | 多步但模式固定 | 较好 | 符号处理失误 |
| 几何公式计算 | 公式记忆 + 小数处理 | 中等 | π 精度导致结果不一致 |
| 不放回概率 | 条件推理 + 乘法 | 较好 | 忽略“不放回” |
| 浮点数精度 | 领域常识 | 好 | 很少出错 |
这组测试告诉我们一个规律:LLM 擅长“能用自然语言描述、训练数据中出现过解题套路”的数学问题;不擅长“需要精确逐位运算、数据组合稀疏”的数学问题。
5. 工程上如何用好 LLM 的数学能力
了解边界后,我们需要在应用中针对不同场景采取不同策略。
5.1 场景一:只需解题思路
如果用户提问“这道题怎么做”,你可以让 LLM 输出解题步骤和最终答案,不要求极高精度。此时 LLM 的“模式组合”能力已经很够用。
def solve_step_by_step(question: str) -> str: prompt = f"""请解出下面的数学题,要求: 1. 先分析题意; 2. 列出关键公式; 3. 逐步推导; 4. 给出最终答案。 题目:{question}""" return ask_math(prompt)5.2 场景二:必须精确计算
如果应用场景是金融计算、物理实验数据处理、工程测量,那么永远不要让 LLM 直接输出精确数值。正确做法是:让 LLM 编写代码,然后执行代码得到数值。
这里给出一个工具调用示例:
import re import subprocess import tempfile import os def run_code_for_math(question: str) -> str: # 让模型生成 Python 代码 prompt = f"""请把下面的数学问题转化为一段 Python 代码,要求: - 使用 math 或 numpy 完成计算; - 只输出代码,不输出解释; - 代码中要有一个变量 result 保存最终数值。 问题:{question}""" code = ask_math(prompt) # 提取代码块 match = re.search(r"```python\n(.*?)\n```", code, re.S) if match: code = match.group(1) # 执行代码 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) tmp_path = f.name try: result = subprocess.run( ["python", tmp_path], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: return f"代码执行失败:{result.stderr}" return result.stdout.strip() finally: os.unlink(tmp_path)使用示例:
question = "计算 238947 × 192873" print(run_code_for_math(question)) # 预期输出:46095572731这个方案把“计算”外包给了 Python,LLM 只负责“生成代码”。精确度、可解释性、可验证性都得到了保障。
5.3 场景三:符号代数运算
如果是方程化简、求导、积分、矩阵运算等符号计算任务,建议直接集成sympy,让 LLM 负责把自然语言翻译成 sympy 调用,而不是直接让 LLM 输出结果。
from sympy import symbols, solve, Eq, diff, integrate x = symbols("x") # 解方程 expr = Eq(2*x + 5, 17) solution = solve(expr, x) print(solution) # [6] # 求导 print(diff(x**3, x)) # 3*x**2 # 积分 print(integrate(x**2, x)) # x**3/3这种模式下 LLM 的“不精确”问题完全被绕开,符号计算由专门引擎负责。
5.4 场景四:思维链增强
如果你不想引入外部代码执行器,也可以先用思维链(Chain-of-Thought, CoT)提升模型的多步推理能力。简单来说,就是让模型把思考过程写出来,而不是直接给答案。
def ask_with_cot(question: str) -> str: prompt = f"""请一步一步地思考并解决下面的问题。每一步都要写清楚计算过程,最后给出答案。 问题:{question}""" return ask_math(prompt)举个例子,对于下面的问题:
小明有 5 个苹果,他给了小红 2 个,又从妈妈那里得到了 3 个,最后他有多少个苹果?如果不加 CoT,模型可能直接输出“6”。加入 CoT 后,模型会输出:
小明原有 5 个苹果。 给了小红 2 个后:5 - 2 = 3。 又从妈妈那里得到 3 个:3 + 3 = 6。 所以最后有 6 个苹果。CoT 的价值在于:它把隐式推理变成了显式推理,模型在生成每一步时都有上下文可以依赖,降低了遗漏步骤的概率。
5.5 场景五:外部数学工具集成
在 Agent 应用中,更推荐的方式是把数学能力做成独立工具。模型只负责“规划”和“调用”,计算结果完全交给工具。
工具列表可以设计如下:
| 工具名称 | 功能 | 适用场景 |
|---|---|---|
python_executor | 执行任意 Python 数学代码 | 大数计算、数值计算 |
sympy_solver | 解方程、求导、积分 | 符号代数 |
calculator | 四则运算 | 简单精确算术 |
wolfram_alpha | 外部专业计算引擎 | 复杂数学与公式推导 |
实现时,每个工具对应一个函数,LLM 根据用户问题选择调用哪个工具。
6. 常见问题与排查思路
在实际测试和开发中,常见的现象和解决办法如下:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 大数乘法结果错误 | token 化后数字序列模式稀疏 | 改用代码执行器计算 |
| 答案“接近但不对” | 模型在近似,而非精确计算 | 将温度设为 0,减少随机性 |
| 同一问题换一种问法答案不同 | 问题表述模式影响推理路径 | 用固定 Prompt 模板,或提示“逐步计算” |
| 小数位数不一致 | π 或浮点近似选择不同 | 明确指定精度和 π 取值规则 |
| 题目太长导致中间步骤出错 | 上下文窗口内注意力分散 | 拆分子问题,分步求解 |
| 概率题漏条件 | 模型忽略题干关键信息 | 提取关键条件后重新组织 Prompt |
| 代码执行器返回异常 | LLM 生成的代码有 Bug | 增加代码修复循环或使用更稳定的模板 |
如果你在开发中遇到“模型偶尔答对、偶尔答错”的情况,优先从两个方向排查:
- 数据确定性:是否设置了
temperature=0?随机采样会直接影响数学输出稳定性。 - Prompt 确定性:是否强制模型逐步展示计算过程?没有推理链时,模型容易跳步。
7. 最佳实践与工程建议
7.1 明确能力边界,设计兜底策略
在系统设计阶段就要想清楚:哪些路径允许 LLM 直接输出数学答案,哪些路径必须走工具。我建议的默认策略是:
- 涉及精确数值的最终输出,一律经过代码执行器或符号计算引擎验证。
- LLM 的输出应定位为“解题思路 + 代码生成”,而不是“最终数值”。
- 如果必须直接输出,加入“数字合理性校验”,例如结果是否超出了现实的量级。
7.2 用评测集量化模型能力
不要凭感觉判断某个模型“数学好不好”。建议构建一个覆盖不同类型数学题的小型评测集,例如:
- 20 道基础算术题(含大数乘法、除法、取余)
- 20 道应用题(行程、工程、鸡兔同笼)
- 10 道几何题(面积、体积、三角)
- 10 道概率统计题
- 10 道符号代数题(方程、求导、积分)
然后分别统计准确率,找出模型的薄弱项。这样在选型、调 Prompt、是否引入工具等问题上,你会有数据支撑。
7.3 设计 Prompt 模板时固定计算规则
如果让 LLM 直接输出数学结果,建议在 system prompt 中显式写入规则,例如:
计算规则: 1. 所有计算必须逐步展开,不能直接跳到最后结果。 2. 如果题目包含多位小数的计算,保留至少 6 位有效数字。 3. 如果题目涉及 π,保留两位小数,使用 3.14。 4. 最后单独一行输出“最终答案:...”。这种方式能显著提高输出的一致性和可解析性。
7.4 评估时区分“理解能力”和“计算能力”
一个模型可能“理解题意”很准确,但“计算结果”不可靠。在评测时,建议把两个维度分开打分:
- 理解分:模型的解题思路、公式选择、步骤逻辑是否正确。
- 计算分:最终数值是否精确。
很多情况下,理解正确但计算错误,可以通过引入工具解决;理解错误则需要换模型或优化 Prompt。这个区分能帮你更精准地定位问题。
7.5 注意安全与成本边界
无论采用哪种方案,都要注意:
- 代码执行器一定运行在沙箱或隔离环境中,避免任意代码执行风险。
- 对 LLM 生成的代码设置超时时间与输出大小限制。
- 调用外部 API 时管理好配额和密钥,不要在前端暴露。
- 涉及生产环境变更、资金计算、医疗数据等场景,必须经过严格的人工复核和合规审查。
8. 总结与下一步学习方向
通过这篇文章,我们理清了 LLM 数学能力的基本边界:它在模式识别、应用题理解、多步推理方面有很强表现,但在精确计算、大数运算、稀疏组合数字运算上并不可靠。背后的核心原因是 LLM 的 token 预测机制本身就不是为演绎计算设计的。
对于开发者来说,最有价值的收获应该是:不要试图让 LLM 成为一个万能计算器,而是把它当作一个“能理解数学语言的编排器”,把精确运算交给代码执行器、符号计算引擎等专业工具。
如果你想继续深入,建议按以下方向学习:
- 评测方法:掌握 GSM8K、MATH、MMLU 等基准的概念,了解模型在不同数学任务上的量化表现。
- Prompt 工程:深入理解思维链(CoT)、自洽性(Self-Consistency)等技巧,提升模型推理稳定性。
- Agent 工具编排:学习如何让 LLM 调用 Python、sympy、Wolfram Alpha 等外部工具,构建更可靠的数学问答系统。
- 微调与对齐:了解通过数学推理数据微调模型,是否会真正提升逻辑能力,还是只提升了模式记忆。
数学能力是检验 LLM 推理能力的重要试金石,但它并不等于 LLM 的全部能力。理解边界、用好工具,才能真正发挥大模型在数学场景中的价值。