最近在 AI 社区的讨论里,“OpenAI 数学推理突破”和“24 小时复现”是两个绕不开的关键词。一边是大模型在数学难题上的表现越来越强,另一边是社区里出现了不少轻量级评测框架,让普通开发者也能快速复现类似的实验。这篇文章不打算追热点,而是把这类“模型评测 + 实验复现”背后的技术栈拆开讲清楚。你会看到:数学推理评测为什么难,复现一个实验需要哪些模块,怎样用 OpenAI 官方 SDK 搭一条最小可运行的“Fable 式复现流水线”,以及项目落地时的常见坑和最佳实践。
如果你是搞 NLP、AI 应用开发,或者正在做大模型评估相关的工程,这篇文章可以给你一套可以照着改的模板。文章里的代码都是独立可运行的片段,版本细节我会特别标注,你拿到之后按自己的环境微调即可。
1. 背景:从 OpenAI 数学突破到复现热潮
1.1 为什么数学难题成为大模型试金石
数学推理是大模型能力评估里非常特殊的一类任务。它不像开放聊天那样可以靠“流畅表达”蒙混过去,也不像选择题那样容易靠概率分布碰运气。数学题的答案是确定的,推理过程是可以被验证的,因此它能直接反映模型的逻辑推理、符号运算和长期规划能力。
最近关于 OpenAI 的讨论点,主要集中在模型在多个数学基准(如 AMC、AIME、MATH 这类比赛级题目)上的表现提升。这类题目难度高,光靠背训练数据很难答对,模型必须真正理解题目条件,拆解步骤,并完成数值计算。所以当模型能连续解决多道高难度数学题时,背后的技术价值就非常高,它说明训练阶段引入的推理增强手段是有效的。
对我们开发者来说,更值得关注的不是“又刷了几个点”,而是这些能力能否被复现、被评估、被集成到自己的业务里。这引出了本文的核心:评测和复现。
1.2 什么是“复现”,以及为什么重要
在机器学习领域,“复现”指的是根据论文、开源代码或官方报告,在本地环境重新得到接近的效果。复现的意义不只是验证一个结果,更重要的是搞清楚达到这个结果需要哪些条件:模型规模、数据格式、提示词、采样参数、评测脚本。
过去复现一个实验门槛很高,因为依赖环境复杂,数据获取麻烦。现在随着各家模型统一提供 API,配合像“Codex Harness”这类开源评测工具,复现成本已经大幅降低。社区里甚至出现了“24 小时复现某某成果”的工程化玩法,本质上就是套用一套流水线:加载题目 -> 调用模型 -> 收集答案 -> 自动判分 -> 出报告。
Fable 就可以看成这类“快速复现流水线”的代称。不同团队实现的框架细节不同,但核心模块高度类似。本文用 Python 手写一套最小可运行的版本,方便你理解内部原理。如果你在 GitHub 上找到了某个 Fable 复现仓库,也可以用本文的思路去阅读和改造它。
2. 复现前需要理解的核心概念
2.1 数学推理评测的基本流程
一个标准的数学推理评测流程包含四个部分:
- 题目集:带标准答案的数学题,通常以 JSONL 或 JSON 格式保存。
- 模型推理:把题目组装成 Prompt 发送给模型,收集模型输出。
- 答案抽取:从模型的自然语言输出中抽取出最终结果。
- 自动判分:把抽取结果与标准答案做比对,计算准确率。
听起来很简单,但实际落地时每一步都有细节。比如答案格式,有的模型会在回复里写大量推导过程,最终结果藏在一段文字里,这时必须设计稳定的抽取规则。再比如判分,数学答案可能是分数、根式、小数,直接字符串比对容易误判,需要统一成 “规范化数值” 再做比较。
评测流程的稳定性直接影响复现结果。同一个模型、同一个题目,如果 Prompt 写法变了,准确率可能差好几个百分点。因此,工程化的复现项目一定会把 Prompt 写进配置文件,保证每次运行使用同一套模板。
2.2 测试时计算与思维链
大模型解决数学题的常用手段是“思维链”(Chain-of-Thought),也就是让模型在输出最终答案之前,先输出一步步的推理过程。这种做法能显著提高复杂题目的正确率,因为模型在逐步推理时,每一小步的难度都低于整体题目,出错概率被分散了。
进一步地,很多前沿模型还会在推理阶段做“测试时计算”(Test-Time Compute)。简单理解就是模型在解码时会对多个推理路径进行搜索或采样,然后通过验证器评估哪条路径最可靠。这种方式让模型可以用更多计算量换取准确率,但代价是响应延迟变高、Token 消耗变大。
在复现评测实验中,我们要注意这一点:如果只请求一次答案,可能无法体现模型的最佳水平。更严谨的评估方式是多次采样,用投票或验证模型选出最终结果。本文后面的实战会给出一个最简单的多次投票实现。
2.3 自动判分与代码执行验证
数学题判分有两种常见路线。第一种是纯文本比对,适合答案规范、形式固定的题目。第二种是代码执行验证,让模型生成一段代码,程序在 Docker 容器中执行并检查输出。第二种方式多用于竞赛编程题或需要复杂计算的题目,也是 OpenAI Codex Harness 这类工具的核心思路。
在“复现”场景里,代码执行验证比文本比对更可靠,因为数学题的中间结果往往不容易用正则抽取。但代码执行也带来安全和工程复杂度问题,稍后我会在最佳实践里具体说明。
3. 环境准备与版本说明
3.1 基础环境
本文示例在以下环境中验证:
- 操作系统:Windows 10/11、Ubuntu 20.04/22.04、macOS 都可以。
- Python 版本:建议 3.10 及以上。
- 包管理工具:pip 或 conda。
-
OpenAI Python SDK:
openai>=1.0。 -
辅助库:
python-dotenv、pandas、tqdm。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。如果你使用的不是 OpenAI 官方服务,而是兼容协议的其他平台,下面代码里的
base_url
参数可以让你轻松切换。
3.2 安装依赖
打开终端,创建一个虚拟环境并安装依赖:
python -m venv .venv
source .venv/bin/activate # Windows 使用 .venv\Scripts\activate
然后安装依赖:
pip install openai python-dotenv pandas tqdm
如果你想把评测结果导出成 Excel 或做更多数据处理,可以额外安装
openpyxl
:
pip install openpyxl
3.3 获取 OpenAI API Key
前往 OpenAI 官方网站创建账号并生成 API Key,然后把 Key 保存到项目根目录下的
.env
文件里:
OPENAI_API_KEY=sk-你的密钥
注意:
.env
文件不要提交到 Git 仓库,建议在
.gitignore
中加入
.env
。运行代码时,
python-dotenv
会自动读取这个文件。
3.4 项目目录结构
建议按下面的结构组织工程:
fable_repro/
├── .env
├── config.json
├── fable_reproduce.py
├── data/
│ └── math_examples.jsonl
└── results/
└── (报告输出目录)
config.json
用来存放模型参数和 Prompt 模板,
data
目录放题,
results
目录放评估报告。把配置和代码分离,是保证实验可复现的第一步。
4. 写一个“最小 Fable 复现流水线”
4.1 定义数据集格式
为了演示,我构造一个非常小的数学题集,包含三道题。实际使用中你可以换成 MATH 数据集、AIME 历年真题或自己的业务题库。
data/math_examples.jsonl
内容如下:
{"question": "解方程:2x + 5 = 15", "answer": "10"}
{"question": "计算:12 * 8", "answer": "96"}
{"question": "计算:3 的 4 次方", "answer": "81"}
每一行是一条题目,字段包括
question
和标准答案
answer
。这种格式易于追加数据,也方便流式读取。
4.2 实现模型调用模块
创建一个
fable_reproduce.py
,先写模型调用函数。这里使用 OpenAI 新版 SDK 的写法,自动从环境变量读取
OPENAI_API_KEY
。
# 文件路径:fable_reproduce.py
import os
import re
import json
import time
import random
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
# 创建客户端,如果你的模型使用兼容接口,可传入 base_url
client = OpenAI()
调用模型时,我建议把 Prompt 模板放到配置里,而不是硬编码在代码中。这样调整措辞不需要改代码,也能记录每次实验使用的 Promp 版本。
def solve_question(
question: str,
model: str = "gpt-4o",
system_prompt: str = "你是数学解题助手。请逐步推理,最后用<<答案>>输出最终结果。",
temperature: float = 0.0,
max_tokens: int = 1024,
timeout: int = 60,
) -> str:
"""调用大模型求解数学题,返回原始输出。"""
try:
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": question},
],
temperature=temperature,
max_tokens=max_tokens,
timeout=timeout,
)
return resp.choices[0].message.content.strip()
except Exception as e:
return f"ERROR: {str(e)}"
这里有几个参数值得说明:
-
system_prompt:系统提示词,决定了模型答题的风格。如果复现某个实验,务必和原实验保持一致。 -
temperature:采样温度。评测场景建议设为 0,减少随机性;如果做多次投票,可以设 0.7。 -
max_tokens:限制输出长度,避免推理过程太长导致成本不可控。 -
timeout:请求超时时间,大模型推理较慢时,超时阈值需要适当提高。
4.3 实现答案解析与评分
模型输出可能是这样的:
思路...
2x = 15 - 5
2x = 10
x = 5
<<5>>
我们需要从输出中提取
<<...>>
中的内容。如果模型没有按格式输出,可以取最后一行或整体去噪后作为兜底。
def extract_answer(text: str) -> str:
"""从模型输出中提取最终答案,优先使用<<>>包裹内容。"""
match = re.search(r"<<(.*?)>>", text, re.S)
if match:
return match.group(1).strip()
# 兜底:取最后一个非空行
lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
if lines:
return lines[-1]
return text.strip()
评分函数需要处理数值形式差异。比如
96.0
和
96
应该算对。简单的方式是先用
float()
转换再比较:
def normalize_answer(answer: str):
"""将答案字符串归一化为可比较的数值。"""
answer = answer.replace(",", "").strip()
try:
return float(answer)
except ValueError:
return answer
def grade_answer(predicted: str, expected: str) -> bool:
"""判断模型答案与标准答案是否一致。"""
pred = normalize_answer(predicted)
exp = normalize_answer(expected)
return pred == exp
注意:这种归一化只适合实数答案。如果题目答案是分数、根式或字符串,需要额外的规则,比如转换为
Fraction
或
sympy
表达式比较。
4.4 主流程与报告输出
现在把加载数据、逐题推理、评分、统计准确率的完整流程写出来,并输出一个 JSON 报告。
def load_dataset(path: str) -> list[dict]:
"""从 JSONL 文件加载题目。"""
items = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
items.append(json.loads(line))
return items
def run_evaluation(
dataset_path: str,
output_path: str,
model: str,
system_prompt: str,
temperature: float,
max_tokens: int,
) -> dict:
"""运行完整评测流程,并保存报告。"""
dataset = load_dataset(dataset_path)
results = []
for idx, item in enumerate(dataset, start=1):
question = item["question"]
expected = item["answer"]
# 简单重试机制:遇到 ERROR 最多重试 2 次
raw_output = ""
for attempt in range(3):
raw_output = solve_question(
question=question,
model=model,
system_prompt=system_prompt,
temperature=temperature,
max_tokens=max_tokens,
)
if not raw_output.startswith("ERROR:"):
break
time.sleep(2)
predicted = extract_answer(raw_output)
correct = grade_answer(predicted, expected)
results.append({
"index": idx,
"question": question,
"expected": expected,
"predicted": predicted,
"raw_output": raw_output,
"correct": correct,
})
total = len(results)
correct_count = sum(1 for r in results if r["correct"])
accuracy = correct_count / total if total else 0.0
report = {
"model": model,
"system_prompt": system_prompt,
"temperature": temperature,
"max_tokens": max_tokens,
"total": total,
"correct": correct_count,
"accuracy": accuracy,
"results": results,
}
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f"评测完成:共 {total} 题,答对 {correct_count} 题,准确率 {accuracy:.2%}")
return report
这里实现了三层关键能力:重试、抽取、评分统计。重试机制能有效避免网络抖动导致的偶发失败,保证评测数据完整性。报告 JSON 里包含了每道题的原始输出,这在复现排错时非常有用。
5. 本地运行与结果说明
5.1 配置文件
把模型参数和 Prompt 模板写进
config.json
:
{
"model": "gpt-4o",
"system_prompt": "你是数学解题助手。请逐步推理,最后用<<答案>>输出最终结果。",
"temperature": 0,
"max_tokens": 1024,
"timeout": 60,
"dataset_path": "data/math_examples.jsonl",
"output_path": "results/report.json"
}
这样就能做到“一次配置,多次复现”。下一次模型升级或 Prompt 调整时,只需修改 JSON 文件。
5.2 在脚本里读取配置
在主模块中加入读取配置和调用运行函数的入口:
if __name__ == "__main__":
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
run_evaluation(
dataset_path=config["dataset_path"],
output_path=config["output_path"],
model=config["model"],
system_prompt=config["system_prompt"],
temperature=config["temperature"],
max_tokens=config["max_tokens"],
)
然后运行:
python fable_reproduce.py
5.3 预期输出示例
因为我用的示例题目非常简单,模型基本都能答对,运行后终端输出类似:
评测完成:共 3 题,答对 3 题,准确率 100.00%
生成的
results/report.json
中包含详细结果:
{
"model": "gpt-4o",
"accuracy": 1.0,
"total": 3,
"result": [
{
"index": 1,
"question": "解方程:2x + 5 = 15",
"predicted": "5",
"correct": true
}
]
}
到这里你就拥有了一套能跑的数学评测复现流水线。虽然功能精简,但数据流是完整的。你可以把题目集换成任意数学题库,就可以评估不同模型在某一组题目上的表现。
6. 扩展:如何复现更难的数学题
6.1 允许多次采样并投票
对于高难度数学题,单次采样往往不够稳定。更科学的做法是对每题采样 N 次,然后用“多数投票”或“独立验证器”决定最终答案。下面是多数投票的简单实现:
def solve_with_voting(question: str, n_samples: int = 5, temperature: float = 0.7) -> str:
answers = []
for _ in range(n_samples):
output = solve_question(question, temperature=temperature)
answers.append(extract_answer(output))
# 简单多数投票
vote_counter = {}
for ans in answers:
key = normalize_answer(ans)
vote_counter[key] = vote_counter.get(key, 0) + 1
best_key = max(vote_counter, key=vote_counter.get)
return best_key
投票机制能让评测更接近模型“真实能力”,也能减少 prompt 随机性带来的波动。代价是 API 调用次数增加,成本上升。
6.2 接入代码执行环境
当题目涉及复杂运算时,文本输出容易出错。更稳的做法是让模型生成 Python 代码,然后在本地或容器内执行,把 stdout 的结果作为答案。伪代码如下:
code = generate_code(question)
result = execute_code_in_sandbox(code) # 需要容器隔离
correct = grade_answer(result, expected)
实际工程中,你可以用 Docker 容器限制代码执行权限,避免模型生成的恶意代码破坏宿主机。OpenAI Codex Harness 正是这类思路的代表。
6.3 使用 Codex Harness 思路做容器化评测
如果你是复现代码生成或竞赛题目,建议参考 OpenAI 开源的 Codex Harness 工程设计。它会把每个题目跑在独立的 Docker 容器里,传入测试用例,然后检查输出是否匹配。这种设计有几个好处:
- 环境隔离,避免污染宿主。
- 可重复执行,测试用例可版本化。
- 并行度高,能显著缩短大规模评测时间。
你不需要完整使用 Harness,只要在自己的流水线里加入 Docker 执行模块即可。
6.4 手工复现实验的注意事项
复现不等同于“调 API 跑完就行”,越难的题目越要注意变量控制。比如模型切换了版本、Temperature 从 0 改成 0.2、System Prompt 加了一句“请用中文回答”,这些都会影响最终准确率。
建议每次复现都记录以下元数据:
- 模型名称与版本。
- Prompt 模板原文。
- 采样参数。
- 题目集版本。
- SDK 版本。
- 评测日期。
- 是否有重试策略。
有了这套元数据,任何一次实验结果都能被追测和比对,这才是“可复现”的核心。
7. 常见问题与排查思路
在实际运行中,最容易出现的问题集中在 API 调用、格式解析和结果不稳定三类。下表汇总了高频场景:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
AuthenticationError
| API Key 未设置或错误 |
检查
.env
文件和是否执行
load_dotenv()
|
RateLimitError
| 请求频率超过账号限制 | 降低并发,加入退避重试,或提高账号配额 |
TimeoutError
| 模型推理时间过长 |
增大
timeout
,减少
max_tokens
|
输出没有
<<>>
| 提示词约束不够 | 在 System Prompt 中增加格式示例,或使用结构化输出 |
| 多次运行结果不一致 |
temperature
不为 0 或未固定随机种子
|
评测场景设置
temperature=0
,需要采样时用投票
|
| 分数类答案判错 | 字符串直接比较 | 使用分数规范化转换后再比较 |
| 本地时区/环境差异 | 依赖版本不一致 |
用
requirements.txt
锁定版本,或使用 Docker
|
| JSON 报告打不开 | 输出目录不存在 |
在写入前用
os.makedirs(..., exist_ok=True)
|
如果遇到
ERROR: ...
被写进结果,首先要检查是不是网络或 API 配额问题,而不是模型不会做题。建议在评测流程里区分“推理错误”和“网络错误”,否则统计准确率会被污染。
8. 最佳实践与工程建议
8.1 密钥与安全
- 不要在前端代码、公开仓库或日志中输出 API Key。
-
使用
.env或密钥管理服务保存密钥。 - 为不同项目创建独立 API Key,泄露时可以单独吊销。
- 不要共享服务账号,遵循最小权限原则。
8.2 成本控制
大模型评测是典型的“烧 Token”场景。建议:
- 先用 10 道题跑通流程,再扩展到全量数据集。
- 对输出长度做上限限制,避免模型“写论文式解题”。
- 对相同的题目结果做本地缓存,避免重复调用 API。
- 大批量评测时,可以把题目分片并行,但要注意速率限制。
8.3 结果可复现性
可复现性要落实到工程文件里:
- 用 Git 管理题目集、Prompt、配置和代码。
- 每次实验生成独立报告文件,命名包含模型和日期。
-
固定
openai包版本,API 升级可能导致行为变化。 - 保留原始模型输出,不只为最终准确率。
8.4 合规与授权
使用外部模型和数据集前,确认自己有权访问和使用这些数据。公司项目中,尤其注意不要将保密数据发送到外部 API。如果数据敏感,可以改用私有化部署的模型或本地模型完成评测。
9. 总结与下一步
本文从“OpenAI 数学突破”和“Fable 24 小时复现”这类话题切入,讲清楚了数学推理评测的基本流程,也给出了一套可直接运行的 Python 复现流水线。你学会了:
- 搭建 OpenAI 数学评测环境;
- 用 JSONL 定义题目集;
- 调用大模型并抽取答案;
- 自动判分并生成报告;
- 通过投票、代码执行等方式扩展复现能力;
- 排查 API 调用和结果不稳定的常见问题。
下一步,你可以把题目集换成公开数学基准,例如 MATH 或 AIME 真题,再对比不同模型在自己题库上的准确率。也可以参考 Codex Harness 的思路,把评测流程容器化,接入并行执行模块,让复现效率提升一个量级。
想在业务中真正用好大模型推理能力,光有 API 调用经验还不够,需要从评测设计、数据版本管理、成本控制和安全合规多个角度持续打磨。希望这篇文章能成为你动手实践的起点。如果你照着跑的过程中遇到问题,欢迎在评论区留下具体的报错信息和你的配置,大家一起研究。
249




被折叠的 条评论
为什么被折叠?



