OpenAI连破数学难题,Fable复现5道:AI解题工程化实战
2026/8/28 9:58:45 网站建设 项目流程

最近“OpenAI 连续解出 10 道高难度数学题”的消息在技术社区里讨论度很高。更让人感兴趣的是,社区里有人基于 Fable 这类智能体项目做了一次“24 小时复现实验”,最终成功复现了其中 5 道题。和单纯转发新闻不同,我整理了一份完整的复现过程笔记:从环境配置、API 调用、结构化输出,到本地验证、失败重试、报告生成,把整条链路串起来。文章会用可运行的 Python 示例演示“AI 解数学题”的工程化流程,适合对 LLM 应用开发、Agent 工作流、推理评测感兴趣的开发者。

1. 背景:为什么“数学难题”是衡量 AI 推理能力的试金石

1.1 所谓“10 道数学难题”是什么

这里所说的“数学难题”,通常来自公开数学推理基准或竞赛级题目,涵盖代数、几何、数论、组合数学等方向。这类题目有几个共同特点:

  • 题干短,推理长:一道题可能只有两三行描述,但完整推理过程需要多步演算。
  • 不能靠“记忆”回答:即使模型在训练数据中见过相似题型,也很难通过直接复述得到正确答案。
  • 答案可以程序化验证:很多题目存在枚举、构造或反推的验证方式,这就为“自动复现”提供了基础。

过去几年,大语言模型在自然语言对话、文本生成上表现很强,但在数学推理上经常出现“看起来很有道理、结果却是错的”的情况。原因在于模型每一步都在做概率预测,多步推理会累积误差。因此,能否稳定解出竞赛级数学题,成为检验模型推理能力的重要指标。

1.2 OpenAI 的“连续破题”为什么值得关注

当模型能连续解出多道高难度题时,说明它不只是“背题”,而是在多个推理步骤之间建立了自洽的逻辑链路。这一进步背后通常有三类技术支撑:

  1. 更强的推理范式:让模型“先想清楚再回答”,而不是直接输出答案。
  2. 代码辅助验证:模型先写代码,再跑程序验证结果,而不是纯靠脑内演算。
  3. 评测链路自动化:把题目下发、模型输出、本地验证、失败重试串成流水线。

第三点尤其重要,因为“能被自动化验证”是工程技术上的前提。没有这一步,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 requests

2.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 分步推理:让模型“想清楚再回答”

直接让大模型输出答案,容易出现“一步错、步步错”。工程上常用办法是拆解推理步骤。在提示词里显式要求模型:

  1. 先复述题目关键条件;
  2. 判断题目类型;
  3. 分步推导;
  4. 最后给出结论。

这样做的好处,是让模型把“推理过程”作为上下文的一部分,而不是直接跳到答案。

先来看一个最简单的提示词模板:

# 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次;
  • 最终返回PASSFAIL状态。

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.json

4.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 配错检查.envOPENAI_BASE_URL,确认网络环境可访问 API 域名
AuthenticationErrorAPI Key 无效或未设置检查.envOPENAI_API_KEY,确认 key 未过期、未泄露
RateLimitError请求频率超限增加time.sleep,降低并发,或提高账号额度
返回内容无法解析为 JSON模型输出被 Markdown 包裹或包含多余文字增加清理逻辑,或让模型只输出 JSON
验证结果为PARSE_ERROR模型输出格式不符合预期在重试时明确提示“必须输出 JSON 格式”
本地验证代码报错数据类型不匹配float(answer)转换后比较,并处理异常

5.2 模型“看似正确但实际错误”怎么办

这是最典型的问题。模型会生成一段看起来很完整的推理,但答案却是错的。

建议按以下顺序排查:

  1. 确认验证器正确:先用穷举或手算验证验证器本身没问题。
  2. 检查答案精度:有些题目答案是浮点数,需要设置误差范围。
  3. 重试时带反馈:把验证错误信息写进提示词,引导模型重新思考。
  4. 降低 temperature:数学推理场景使用temperature=0,减少随机性。
  5. 拆分题目条件:对于复杂题目,可以先把题目拆成子问题,分别推理后再汇总。

5.3 重试仍然不通过怎么办

如果重试 3 次仍然失败,不要盲目增加重试次数。更有效的办法是:

  • 换一个表达方式重新描述题目;
  • 在提示词中加入“请用枚举法验证”等指令;
  • 增加一个“代码生成 + 结果分析”步骤,让模型先生成验证代码,再根据代码输出判断答案;
  • 记录失败的题目,手工分析模型是在哪一步开始出错。

6. 最佳实践与工程建议

6.1 提示词层面

  • 固定系统提示词:不同的求解任务应该使用不同的系统提示词,不要混用。
  • 强调 JSON 输出:在提示词里直接给出 JSON 示例,比只写“返回 JSON”更有效。
  • 要求展示推理摘要:答案是否有用是一回事,推理过程是否合理是另一回事。保留摘要便于事后追溯。
  • 重试时提供上下文:重试不是简单重复提问,而是把上一轮的答案和验证结果一起反馈。

6.2 验证器层面

  • 优先写程序化验证函数:能用枚举、穷举、模拟验证的,就不要靠人工判断。
  • 验证器要覆盖边界条件:比如题目范围是 1 到 100,就必须检查 n=1 和 n=100。
  • 验证器失败信息要具体:不要只返回False,最好返回expected_countactual_count,方便反馈给模型。
  • 统一注册管理:多道题时,用注册表管理验证器,避免流水线代码无限膨胀。

6.3 工程与安全层面

  • API Key 不要硬编码:一律通过环境变量或密钥管理服务读取。
  • 不要公开分享 API Key:任何公网仓库出现 key,都可能导致额度被盗用。
  • 准备预算控制:批量跑题前先估算 token 消耗,设置调用次数上限。
  • 保存完整报告:每道题至少要记录题目、答案、推理摘要、重试次数、最终状态。
  • 失败任务要可重跑:报告里保存足够信息,后续优化提示词后可以只重跑失败题目。

6.4 从“复现”到“评测平台”的扩展方向

如果你不想只跑这一个脚本,可以把它扩展成一个小的评测平台:

  • 支持题目文件批量导入;
  • 支持多种模型对比;
  • 支持并发控制与限流;
  • 支持一键生成 Markdown 评测报告;
  • 支持把失败案例沉淀为回归测试集。

这套架构可以从“复现数学题”延伸到代码生成、逻辑推理、数据标注质检等场景。

7. 总结与下一步建议

这篇文章从“OpenAI 连续解出 10 道数学难题,Fable 24 小时复现其中 5 道”这个热点出发,完整拆解了 AI 数学解题与自动复现的工程链路:环境准备、结构化提示词、统一 API 客户端、本地验证器、失败重试、批量报告。核心思路可以概括为一句话:把模型当作推理引擎,把程序当作裁判,用工程手段保证输出可靠。

如果你接下来想继续深入,建议从这几个方向入手:

  • 给现有流水线增加更多真实数学题和对应验证器;
  • 尝试“代码生成 + 沙箱执行”的路线,让模型自己写验证程序;
  • 对比不同模型、不同 temperature、不同提示词对复现率的影响;
  • 把失败题目整理成小数据集,做回归测试。

环境问题、API 版本、模型名称都可能变化,但“生成候选解、程序化验证、失败反馈”这套方法论是稳定的。希望这份笔记能帮你少踩一些坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询