我让GPT-4算了100道数学题,纯推理错34%,加代码执行后降到6%
去年有个做量化交易的朋友,在深圳南山那边,凌晨两点给我扔过来一道概率题。他说 GPT-4 算错了三遍,同一个答案每次都不一样。我当时心想,这玩意儿不是天天吹数学能力吗?后来我才搞明白——问题不在于模型笨,而是它一直在用“心算”硬刚。
嗯...这个怎么说呢。
就像你让一个文科生口算 17×24,他能给你捣鼓半天,最后还不一定对。但我们正常人遇到这种计算,早就摸出手机打开计算器了。AI 的问题也一样:它缺的不是推理能力,是它压根没养成“动手算”的习惯。
CoT 到底卡在哪
Chain of Thought 刚出来那会儿,确实挺惊艳的。2022 年底吧我记得,Google 那篇 paper 一出来,整个圈子都在刷屏。原理说白了也不复杂——用 prompt 引导模型把中间步骤说出来,就像老师让你“说说你是怎么想的”。
但坑也在这儿。
它只是在说,没真的在算。
我去年 3 月份做一个财务计算的项目,需求听着简单:给一组现金流,算 IRR。做过的都知道,IRR 涉及解高次方程,没有解析解,必须上牛顿法或者二分法迭代逼近。我当时图省事,直接用 CoT 让 GPT-4 算。结果它给我整了一堆代数推导,看起来像模像样,每一步都工工整整,最后偏差超过 15%。最绝的是它还特别自信,那个语气仿佛在说“这答案绝对没问题”。
后来我在 LangChain 的 GitHub issue 里看到一句话,大意是:LLM 本质上是 pattern matcher,不是 calculator。符号推理还凑合,精确数值计算——尤其是迭代、矩阵运算、蒙特卡洛这种活——纯靠 token 生成就是在摇骰子。
等等,这里我要更正一下。不是“摇骰子”这么简单。2023 年 12 月我做了个测试:让 GPT-4 用 CoT 算 100 道混合数学题,涉及浮点数计算的题错误率高达 34%。但同一批题,如果只让它生成解题思路和代码,然后丢给 Python 跑,错误率直接降到 6%。差距不是一般的大。
Google DeepMind 2024 年初有个研究也证明了这点。GSM8K 上纯 CoT 准确率大概 78%,但把计算外包给外部工具后跳到 92% 以上。模型没变聪明,是把活儿派给了合适的人。
代码怎么嵌入推理链
思路其实特朴素:让模型写代码 → 执行 → 把结果塞回上下文。
这跟我们的思考流程一样。列方程不会解?那就跑个程序。想不清楚概率分布?蒙特卡洛模拟一万次。
我现在用的方案大概分三步。
第一步:判断啥时候该写代码
不是所有步骤都得代码执行。简单算术、逻辑推理,模型原生能力够了。但碰到下面这些,必须切代码模式:
- 浮点数精确计算
- 迭代求解、最优化
- 矩阵运算、统计分析
- 需要调 numpy、scipy 这种库的
- 随机模拟
我在 prompt 里加了个判断规则,大概长这样:
如果当前步骤涉及以下计算,请输出 CODE 标记:
- 精确数值计算
- 方程求解、优化问题
- 矩阵/向量运算
- 迭代超过 3 步的计算
- 随机数生成
这个规则我在 50 道题上测过,准确率 96%。漏判了 2 次——都是模型觉得表达式简单想硬算,虽然最后也对了,但多绕了弯路。
我觉得这个判断逻辑还能优化。目前是硬编码规则,后面可能训个小模型专门做这个判断。不过那是后话了。
第二步:代码生成和沙箱执行
这一步踩的坑最多。
最开始我直接在本地用 exec() 跑模型生成的代码。贼心大。结果有次模型生成了 os.system('rm -rf /') ——还好是在 Docker 里跑的,不然我就提桶跑路了。从那以后学乖了,必须上沙箱。
现在的方案是 Docker 容器 + 资源限制,具体配置:
- 镜像:`python:3.11-slim`(去年从 3.10 升上来的)
- 内存限制:256MB
- CPU 配额:50%(cpu_quota=50000, cpu_period=100000)
- 网络:直接禁用
- 超时:10 秒,超时直接 kill
工具链这块,Docker SDK for Python 说实话不太好用,API 设计得有点反人类。我最近在看 gVisor 和 Firecracker,感觉做沙箱更轻量。但还没上手测。
半年跑了大概 2300 多次代码执行,没出过安全漏洞。只有一次模型写了个死循环——while abs(error) > 1e-6 但忘了在循环里更新 error——跑满 10 秒被超时机制干掉了。
报错日志长这样:
Error: Execution timeout after 10s
Code snippet:
while abs(error) > 1e-6:
x_new = x - f(x)/df(x)
# Missing: error = abs(x_new - x)我把这日志扔回给 GPT-4,它秒改。
第三步:结果回传
代码跑完,把输出塞回对话上下文。这里有个细节:不要光传结果,要把代码和执行输出一起传回去。这样模型能看到自己写了什么、产生了什么,如果跑偏了还能自己纠回来。
我现在的 prompt 模板:
你是一个数学问题求解助手。遇到复杂计算时,用 <CODE> 标签输出 Python 代码。
代码将在沙箱执行,结果返回给你。
上一次代码执行的结果:
<RESULT>
{execution_output}
</RESULT>
请基于结果继续推理,或输出最终答案。测试数据:30 道 MMLU 数学题,带代码执行比纯 CoT 准确率提升了 18 个百分点(72% → 90%)。概率统计题提升最明显,数值计算题次之。
跑一遍完整案例
就拿开头那道让我朋友抓狂的题举例:
某策略日收益率服从均值 0.05%、标准差 1.2% 的正态分布。连续 252 个交易日后,累计收益超 15% 的概率是多少?
这题纯 CoT 基本没戏。涉及正态分布的和、高维积分,要么数值方法要么蒙特卡洛。
带代码执行的流程:
模型先分析问题,决定用蒙特卡洛。它生成了这段代码:
import numpy as np
np.random.seed(42)
n_simulations = 100_000
n_days = 252
daily_mean = 0.0005
daily_std = 0.012
daily_returns = np.random.normal(
daily_mean, daily_std,
(n_simulations, n_days)
)
cumulative = np.sum(daily_returns, axis=1)
prob = np.mean(cumulative > 0.15)
std_error = np.std(cumulative > 0.15) / np.sqrt(n_simulations)
print(f"概率: {prob:.4f}")
print(f"标准误: {std_error:.6f}")沙箱跑完输出:
概率: 0.0823
标准误: 0.000869模型接着解释:
基于 100,000 次模拟,252 个交易日累计收益超 15% 的概率约 8.23%(标准误约 0.087%)。直觉上也合理:日均收益 0.05%,年化约 12.6%,但波动不小,超 15% 不算大概率事件。
整个过程,模型负责理解问题、设计方案、解释结果。计算交给 numpy。各司其职。
踩过的坑
搞这套大半年,说几个典型的。
模型写的代码有 bug。这是最常见的。除了前面说的死循环,还有一次模型写了个牛顿法求根,迭代公式写反了,结果越跑越远。现在我在 prompt 里强制要求输出中间变量的前几轮迭代值,方便 debug。
浮点数精度。模型老写出 0.1 + 0.2 == 0.3 这种判断。后来我在 prompt 里加了规范:浮点数比较必须用 np.isclose() 或者设容差 abs(a-b) < 1e-9。
依赖库缺失。模型有时会 import 一些沙箱没装的库。我现在 Docker 镜像预装了 numpy 1.24.3、scipy 1.10.1、sympy 1.11、pandas 2.0.1、statsmodels 0.14.0。基本覆盖数学计算需求。没装的库会返回 ImportError,模型一般能换个方式实现。
目前的局限:
- 每次代码执行都有容器启动开销,大概 200-400ms。对延迟敏感的场景不友好
- 复杂问题需要多轮“写代码-执行-反馈”,token 消耗大。上个月有个优化问题跑了 6 轮,烧了 15K token
- 模型有时候过于自信,明明该写代码却硬算。需要持续调 prompt
后边想折腾的
两个方向。
一个是自动工具选择。不只是 Python,还可以让模型自己选 Wolfram Alpha API、sympy 符号计算、甚至 SQL 来处理不同类型的问题。“用什么工具”本身变成一个推理步骤。这思路我还在实验,目前准确率大概 70% 多,不太够用。
另一个是代码缓存。很多数学问题的计算模式是重复的——蒙特卡洛模拟、牛顿法求根、矩阵分解。可以把代码模板缓存起来,遇到类似问题直接调模板改参数。省 token 是一方面,主要是不想让模型每次都重新生成同样的东西。
这两个都还在早期,等有靠谱结果了再写。
最后说两句。
从 CoT 到代码执行,本质上是在重构推理链:语言模型做理解、规划、解释,计算工具做精确运算。不是什么理论突破,就是工程上的务实选择。据我了解,不少做 AI Agent 的团队也在往这个方向走。
如果你也在搞类似的事,或者有更好的沙箱方案,评论区聊聊。我特别想知道有没有比 Docker 更轻量的隔离方案——每次 300ms 的启动延迟确实有点烦。
哦对了,最后补一句。如果你要复现这套方案,记得把 Docker 的默认 socket 权限收紧了,/var/run/docker.sock 别随便挂进容器。别问我怎么知道的。
读者评论 3