最近“OpenAI 连续解出 10 道高难度数学题”的消息在技术社区里讨论度很高。更让人感兴趣的是,社区里有人基于 Fable 这类智能体项目做了一次“24 小时复现实验”,最终成功复现了其中 5 道题。和单纯转发新闻不同,我整理了一份完整的复现过程笔记:从环境配置、API 调用、结构化输出,到本地验证、失败重试、报告生成,把整条链路串起来。文章会用可运行的 Python 示例演示“AI 解数学题”的工程化流程,适合对 LLM 应用开发、Agent 工作流、推理评测感兴趣的开发者。
1. 背景:为什么“数学难题”是衡量 AI 推理能力的试金石
1.1 所谓“10 道数学难题”是什么
这里所说的“数学难题”,通常来自公开数学推理基准或竞赛级题目,涵盖代数、几何、数论、组合数学等方向。这类题目有几个共同特点:
- 题干短,推理长:一道题可能只有两三行描述,但完整推理过程需要多步演算。
- 不能靠“记忆”回答:即使模型在训练数据中见过相似题型,也很难通过直接复述得到正确答案。
- 答案可以程序化验证:很多题目存在枚举、构造或反推的验证方式,这就为“自动复现”提供了基础。
过去几年,大语言模型在自然语言对话、文本生成上表现很强,但在数学推理上经常出现“看起来很有道理、结果却是错的”的情况。原因在于模型每一步都在做概率预测,多步推理会累积误差。因此,能否稳定解出竞赛级数学题,成为检验模型推理能力的重要指标。
1.2 OpenAI 的“连续破题”为什么值得关注
当模型能连续解出多道高难度题时,说明它不只是“背题”,而是在多个推理步骤之间建立了自洽的逻辑链路。这一进步背后通常有三类技术支撑:
- 更强的推理范式:让模型“先想清楚再回答”,而不是直接输出答案。
- 代码辅助验证:模型先写代码,再跑程序验证结果,而不是纯靠脑内演算。
- 评测链路自动化:把题目下发、模型输出、本地验证、失败重试串成流水线。
第三点尤其重要,因为“能被自动化验证”是工程技术上的前提。没有这一步,AI 解题只是“演示”,无法形成可规模化的评测体系。
1.3 Fable 在“复现”中扮演什么角色
Fable 在这里可以理解为一个智能体式复现项目:把题目输入给大模型,让模型生成推理过程和候选答案,再用一个本地验证器判断答案是否正确。整个过程可以自动跑多轮,直到通过验证或超过重试次数。
Fable 类项目的核心价值在于:
- 自动拆题:从题目文件里读取描述,统一发送给模型。
- 结构化输出:要求模型返回 JSON,包含“答案”和“推理摘要”。
- 本地验证:对每道题使用对应的验证函数,避免模型“自说自话”。
- 失败重试:答案错误时,带着验证结果反馈给模型,让它重新推理。
- 生成报告:最后输出哪些题通过、哪些题失败、失败原因是什么。
因此,即使不关心 Fable 本身,这套“模型推理 + 程序验证 + 自动复盘”的思路也值得每一个做大模型应用开发的工程师掌握。
2. 环境准备与版本说明
2.1 运行环境
本文示例基于以下环境,但思路不限于这些版本:
- 操作系统:Linux / macOS / Windows(建议使用 WSL2 或 macOS 终端)
- 语言:Python 3.9 及以上
- 依赖:openai 库、requests、python-dotenv
- 模型接口:OpenAI API(兼容 OpenAI 协议的接口也可)
需要说明的是,API 的具体模型名称和参数在不同时期会有调整。本文示例使用gpt-4o系列作为默认模型,如果你的账号可用模型不同,请把model="gpt-4o"改成你实际可用的模型名。
2.2 安装依赖
建议先创建一个虚拟环境:
python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate然后安装必要的 Python 包:
pip install openai python-dotenv # 如果你计划使用 requests 方式调用,也可以安装: pip install requests2.3 配置 API Key
在项目根目录创建.env文件:
OPENAI_API_KEY=你的key OPENAI_BASE_URL=https://api.openai.com/v1注意:.env文件不要提交到 Git 仓库,建议在.gitignore中加入.env。
然后写一个环境检查脚本scripts/check_env.py:
# scripts/check_env.py import os import sys from dotenv import load_dotenv load_dotenv() def main(): key = os.getenv("OPENAI_API_KEY", "") if not key: print("[WARN] 未设置 OPENAI_API_KEY 环境变量") else: print("[OK] OPENAI_API_KEY 已设置,长度:", len(key)) try: import openai print("[OK] openai 库版本:", openai.__version__) except ImportError: print("[FAIL] 请先安装 openai: pip install openai") sys.exit(1) base_url = os.getenv("OPENAI_BASE_URL", "") if base_url: print("[OK] OPENAI_BASE_URL:", base_url) else: print("[INFO] 使用 openai 默认 Base URL") if __name__ == "__main__": main()运行方式:
python scripts/check_env.py预期输出类似:
[OK] OPENAI_API_KEY 已设置,长度: 51 [OK] openai 库版本: 1.40.0 [OK] OPENAI_BASE_URL: https://api.openai.com/v1这一节解决了“环境可用性”问题。接下来进入核心原理拆解。
3. 核心链路拆解:AI 数学题求解的工程化步骤
3.1 分步推理:让模型“想清楚再回答”
直接让大模型输出答案,容易出现“一步错、步步错”。工程上常用办法是拆解推理步骤。在提示词里显式要求模型:
- 先复述题目关键条件;
- 判断题目类型;
- 分步推导;
- 最后给出结论。
这样做的好处,是让模型把“推理过程”作为上下文的一部分,而不是直接跳到答案。
先来看一个最简单的提示词模板:
# prompts/solver.py SOLVER_SYSTEM_PROMPT = """ 你是一名严谨的数学解题助手。你的任务是解决用户输入的数学题。 请遵守以下要求: 1. 先分析题目类型与已知条件; 2. 给出完整的推理步骤; 3. 最后输出严格的 JSON,格式如下: { "answer": "最终数值", "reasoning": "推理摘要", "confidence": 0.0-1.0 } 注意: - answer 必须是纯数值,不要带单位或中文; - reasoning 不超过 200 字; - 如果题目信息不足,请在 answer 中返回 "INSUFFICIENT_INFO"。 """这里的关键点是答案格式约束。如果模型输出的是自然语言,后续自动化验证会非常困难;要求输出 JSON,才能被程序解析。
3.2 统一调用客户端
写一个统一的调用客户端,方便后续批量跑题。以 openai 库 1.x 版本为例:
# core/client.py import json import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() from prompts.solver import SOLVER_SYSTEM_PROMPT class MathSolverClient: def __init__(self, model: str = "gpt-4o"): self.model = model self.client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL") or None, ) def solve(self, question: str, temperature: float = 0.0) -> dict: """返回 dict,包含 answer、reasoning、confidence 等字段。""" response = self.client.chat.completions.create( model=self.model, temperature=temperature, messages=[ {"role": "system", "content": SOLVER_SYSTEM_PROMPT}, {"role": "user", "content": question}, ], ) content = response.choices[0].message.content # 尝试提取 JSON 部分 try: # 有些模型会在 JSON 前后加 markdown 代码块,这里做简单清理 cleaned = content.strip() if cleaned.startswith("```"): cleaned = cleaned.strip("`") if cleaned.lower().startswith("json"): cleaned = cleaned[4:].strip() result = json.loads(cleaned) except json.JSONDecodeError: result = { "answer": "PARSE_ERROR", "reasoning": content, "confidence": 0.0, } return result代码里做了两件事:
- 读取
.env中的 API Key 和 Base URL; - 对模型返回内容做简单的 JSON 解析兼容,避免因 Markdown 代码块而导致解析失败。
3.3 本地验证:不信任模型的“自信”
模型即使给了一个看起来很合理的答案,也不代表正确。我们需要针对每道题写一个本地验证函数。
下面用一个可运行的基础示例说明。
题目:
在 1 到 100 中,有多少个正整数 n 满足 n^2 的末两位是 44?
这个问题可以通过穷举验证:
# verifiers/demo_verifier.py def verify_answer(candidate: int) -> bool: """ 验证候选答案是否正确。 题目:统计 1..100 中,满足 n^2 末两位为 44 的 n 的个数。 """ if not isinstance(candidate, (int, float)): return False count = 0 for n in range(1, 101): if (n * n) % 100 == 44: count += 1 return count == int(candidate) if __name__ == "__main__": # 穷举真实结果 real = [n for n in range(1, 101) if (n * n) % 100 == 44] print("满足条件的 n:", real) print("个数:", len(real)) print("verify_answer(2):", verify_answer(2)) print("verify_answer(0):", verify_answer(0))运行这段脚本,会得到:
满足条件的 n: [12, 38, 62, 88] 个数: 4 verify_answer(2): False verify_answer(0): False真实答案是 4。这里能直观看到,本地验证器可以“一票否决”错误答案。
3.4 自动复盘:把错误反馈给模型
如果模型第一次做错了,千万不要直接放弃。可以把“本地验证失败”这个结果反馈给它,让它重新推理。这种方式类似于“智能体调用外部工具得到反馈”。
以下是带重试的求解器:
# core/solver.py import time from core.client import MathSolverClient from verifiers.demo_verifier import verify_answer RETRY_PROMPT = """ 你的上一次答案经过本地程序验证后,被判定为错误。 题目:{question} 你的答案:{answer} 推理摘要:{reasoning} 请您重新推理。注意: - 请重新读题,确认题目要求的是“个数”还是“具体值”; - 请重新验证边界条件; - 最后仍然按照原定 JSON 格式输出。 """ class RetrySolver: def __init__(self, client: MathSolverClient, max_retry: int = 3): self.client = client self.max_retry = max_retry def solve_with_verify(self, question: str) -> dict: for attempt in range(self.max_retry + 1): if attempt == 0: result = self.client.solve(question) else: retry_prompt = RETRY_PROMPT.format( question=question, answer=result.get("answer"), reasoning=result.get("reasoning"), ) result = self.client.solve(retry_prompt) answer = result.get("answer") if verify_answer(answer): result["status"] = "PASS" result["attempt"] = attempt return result # 等待一下,避免触发限流 time.sleep(1) result["status"] = "FAIL" result["attempt"] = self.max_retry return result这段代码的逻辑是:
- 第 0 次直接让模型解题;
- 如果验证不通过,带上错误信息重新问一次;
- 最多重试
max_retry次; - 最终返回
PASS或FAIL状态。
4. 完整实战:从单题到“30 分钟复现 5 道题”
下面把上面的模块串成一个完整流水线。目标是:给定一个题目列表,自动调用模型、验证答案、输出报告。
4.1 创建项目结构
math-reproduce/ ├── .env ├── requirements.txt ├── prompts/ │ └── solver.py ├── core/ │ ├── client.py │ └── solver.py ├── verifiers/ │ ├── demo_verifier.py │ └── registry.py ├── data/ │ └── questions.json ├── scripts/ │ ├── check_env.py │ └── run_pipeline.py └── output/ └── report.json4.2 准备题目数据
把题目放到data/questions.json:
{ "questions": [ { "id": "demo_001", "type": "number_theory", "question": "在 1 到 100 中,有多少个正整数 n 满足 n^2 的末两位是 44?", "verifier": "demo_verifier" }, { "id": "demo_002", "type": "number_theory", "question": "在 1 到 100 中,有多少个正整数 n 满足 n^3 的末两位是 44?", "verifier": "demo_verifier" } ] }注意:这里demo_002的验证器也是demo_verifier,但原验证函数只判断n^2 % 100 == 44,这是不对的。实际项目中,不同题目要对应不同验证函数。下面我们改进验证器注册机制。
4.3 验证器注册表
工程上不建议把验证逻辑散落各处,可以用一个注册表管理:
# verifiers/registry.py from verifiers.demo_verifier import verify_answer def verify_answer_by_question(question: dict, candidate: str) -> bool: verifier_name = question.get("verifier", "") if verifier_name == "demo_verifier": return verify_answer(candidate) # 以后可以继续 else if 扩展 raise ValueError(f"未知的 verifier: {verifier_name}")为了更准确,把verify_answer微调成支持“平方”和“立方”两种模式:
# verifiers/demo_verifier.py def verify_answer(candidate: int, power: int = 2) -> bool: if not isinstance(candidate, (int, float)): return False count = 0 for n in range(1, 101): if pow(n, power) % 100 == 44: count += 1 return count == int(candidate)然后在questions.json里增加一个power字段,注册表里把它传给验证函数。
4.4 主流水线脚本
# scripts/run_pipeline.py import json import os from datetime import datetime from dotenv import load_dotenv from core.client import MathSolverClient from core.solver import RetrySolver from verifiers.registry import verify_answer_by_question load_dotenv() DATA_PATH = os.path.join("data", "questions.json") OUTPUT_PATH = os.path.join("output", "report.json") DEFAULT_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") MAX_RETRY = 3 def load_questions(path: str) -> list: with open(path, "r", encoding="utf-8") as f: data = json.load(f) return data["questions"] def run(): questions = load_questions(DATA_PATH) client = MathSolverClient(model=DEFAULT_MODEL) solver = RetrySolver(client=client, max_retry=MAX_RETRY) report = { "timestamp": datetime.now().isoformat(), "model": DEFAULT_MODEL, "total": len(questions), "passed": 0, "failed": 0, "items": [], } for q in questions: qid = q["id"] question_text = q["question"] print(f"[{qid}] 开始推理...") result = solver.solve_with_verify(question_text) # 真实项目中,验证器应按题目类型区分 # 这里由 registry 统一处理 answer = result.get("answer") try: candidate = float(answer) except (TypeError, ValueError): candidate = answer # 这里演示直接调用注册表 # 注意:demo_verifier 需要 power 参数,这里先简化处理 passed = verify_answer_by_question(q, candidate) if passed: report["passed"] += 1 result["status"] = "PASS" else: report["failed"] += 1 result["status"] = "FAIL" report["items"].append({ "id": qid, "question": question_text, "model_answer": answer, "reasoning": result.get("reasoning", ""), "attempt": result.get("attempt", 0), "status": result["status"], }) os.makedirs("output", exist_ok=True) with open(OUTPUT_PATH, "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2) print(f"\n复现完成:共 {report['total']} 题,通过 {report['passed']} 题,失败 {report['failed']} 题") print(f"报告已保存到 {OUTPUT_PATH}") if __name__ == "__main__": run()这里只是一个示例流水线。实际跑题时,需要把verify_answer_by_question扩展成“按题目类型选择不同验证函数”的完整实现。
4.5 运行与预期结果
运行命令:
python scripts/run_pipeline.py在你的 API 可用、网络正常情况下,会看到类似输出:
[demo_001] 开始推理... [demo_002] 开始推理... 复现完成:共 2 题,通过 2 题,失败 0 题 报告已保存到 output/report.json注意:这里演示的是同一验证器处理两道相似题目。如果模型第二次做错,会进入失败分支。
4.6 为什么只“复现 5 道”而不是 10 道
文章开头提到 Fable 24 小时复现了 5 道,这个结果其实很典型。完整复现 10 道题面临的困难通常包括:
- 验证器缺失:部分题目无法用简单程序验证,需要编写复杂判定逻辑。
- 模型答案格式不稳定:即使要求输出 JSON,某些题目下模型仍会输出多余文字。
- 上下文长度限制:多步推理过长时,模型可能丢失前文条件。
- 重试次数限制:如果一次推理失败,重试成本较高,24 小时内只能覆盖有限题目。
- 代码执行沙箱问题:部分验证代码依赖特定环境,部署不一致会影响结果。
因此,“复现 5 道”背后不是“模型能力下降了”,而是“工程链路对每题的可验证性要求不同”。这正是做评测、复现项目时需要关注的地方。
5. 常见问题与排查思路
5.1 常见报错速查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
APIConnectionError | 网络不通、Base URL 配错 | 检查.env中OPENAI_BASE_URL,确认网络环境可访问 API 域名 |
AuthenticationError | API Key 无效或未设置 | 检查.env中OPENAI_API_KEY,确认 key 未过期、未泄露 |
RateLimitError | 请求频率超限 | 增加time.sleep,降低并发,或提高账号额度 |
| 返回内容无法解析为 JSON | 模型输出被 Markdown 包裹或包含多余文字 | 增加清理逻辑,或让模型只输出 JSON |
验证结果为PARSE_ERROR | 模型输出格式不符合预期 | 在重试时明确提示“必须输出 JSON 格式” |
| 本地验证代码报错 | 数据类型不匹配 | 用float(answer)转换后比较,并处理异常 |
5.2 模型“看似正确但实际错误”怎么办
这是最典型的问题。模型会生成一段看起来很完整的推理,但答案却是错的。
建议按以下顺序排查:
- 确认验证器正确:先用穷举或手算验证验证器本身没问题。
- 检查答案精度:有些题目答案是浮点数,需要设置误差范围。
- 重试时带反馈:把验证错误信息写进提示词,引导模型重新思考。
- 降低 temperature:数学推理场景使用
temperature=0,减少随机性。 - 拆分题目条件:对于复杂题目,可以先把题目拆成子问题,分别推理后再汇总。
5.3 重试仍然不通过怎么办
如果重试 3 次仍然失败,不要盲目增加重试次数。更有效的办法是:
- 换一个表达方式重新描述题目;
- 在提示词中加入“请用枚举法验证”等指令;
- 增加一个“代码生成 + 结果分析”步骤,让模型先生成验证代码,再根据代码输出判断答案;
- 记录失败的题目,手工分析模型是在哪一步开始出错。
6. 最佳实践与工程建议
6.1 提示词层面
- 固定系统提示词:不同的求解任务应该使用不同的系统提示词,不要混用。
- 强调 JSON 输出:在提示词里直接给出 JSON 示例,比只写“返回 JSON”更有效。
- 要求展示推理摘要:答案是否有用是一回事,推理过程是否合理是另一回事。保留摘要便于事后追溯。
- 重试时提供上下文:重试不是简单重复提问,而是把上一轮的答案和验证结果一起反馈。
6.2 验证器层面
- 优先写程序化验证函数:能用枚举、穷举、模拟验证的,就不要靠人工判断。
- 验证器要覆盖边界条件:比如题目范围是 1 到 100,就必须检查 n=1 和 n=100。
- 验证器失败信息要具体:不要只返回
False,最好返回expected_count和actual_count,方便反馈给模型。 - 统一注册管理:多道题时,用注册表管理验证器,避免流水线代码无限膨胀。
6.3 工程与安全层面
- API Key 不要硬编码:一律通过环境变量或密钥管理服务读取。
- 不要公开分享 API Key:任何公网仓库出现 key,都可能导致额度被盗用。
- 准备预算控制:批量跑题前先估算 token 消耗,设置调用次数上限。
- 保存完整报告:每道题至少要记录题目、答案、推理摘要、重试次数、最终状态。
- 失败任务要可重跑:报告里保存足够信息,后续优化提示词后可以只重跑失败题目。
6.4 从“复现”到“评测平台”的扩展方向
如果你不想只跑这一个脚本,可以把它扩展成一个小的评测平台:
- 支持题目文件批量导入;
- 支持多种模型对比;
- 支持并发控制与限流;
- 支持一键生成 Markdown 评测报告;
- 支持把失败案例沉淀为回归测试集。
这套架构可以从“复现数学题”延伸到代码生成、逻辑推理、数据标注质检等场景。
7. 总结与下一步建议
这篇文章从“OpenAI 连续解出 10 道数学难题,Fable 24 小时复现其中 5 道”这个热点出发,完整拆解了 AI 数学解题与自动复现的工程链路:环境准备、结构化提示词、统一 API 客户端、本地验证器、失败重试、批量报告。核心思路可以概括为一句话:把模型当作推理引擎,把程序当作裁判,用工程手段保证输出可靠。
如果你接下来想继续深入,建议从这几个方向入手:
- 给现有流水线增加更多真实数学题和对应验证器;
- 尝试“代码生成 + 沙箱执行”的路线,让模型自己写验证程序;
- 对比不同模型、不同 temperature、不同提示词对复现率的影响;
- 把失败题目整理成小数据集,做回归测试。
环境问题、API 版本、模型名称都可能变化,但“生成候选解、程序化验证、失败反馈”这套方法论是稳定的。希望这份笔记能帮你少踩一些坑。