大模型+符号计算:构建数学求解智能体的工程实践
2026/9/1 18:28:48 网站建设 项目流程

当标题里出现OpenAI Astra 内部版攻克 10 大数学难题时,真正值得做的不是追问这个内部版本是否真实存在,而是把它拆成一组可复现的工程问题:大模型如何理解题意,如何调用数学工具完成推导,又如何在结果不确定时判断对错。对做 AI 应用的开发者来说,这类说法唯一有价值的部分,是它指向了一条成熟的技术路线——把语言模型当作规划器,把 SymPy 这类符号计算引擎当作执行器,再用独立验证器兜底。下面围绕 OpenAI API、Python 和 SymPy 搭建一个名为 Astra Math Solver 的数学求解智能体,并用覆盖 10 类高难度数学题目的测试集评估它的能力边界。阅读这篇文章的读者最好已经能调用 OpenAI 的聊天补全接口,并具备基本的 Python 和数学符号运算概念。

1. 先把说法放一边:这类标题背后真正值得讨论的工程问题

1.1 不要把一个无法复现的成绩当成方法论

“OpenAI Astra 内部版攻克 10 大数学难题”之所以会在技术圈引起讨论,是因为它很容易让人产生一种错觉:只要有一个足够强的模型,数学问题就能直接被“问”出来。但从工程角度看,这类表述缺少几个关键信息:模型版本是什么、数学难题的具体集合是什么、验证答案的标准是什么、是否允许调用外部工具。没有这些信息,任何“攻克”都无法被复现,也就无法成为可进入项目的方法论。

对开发者而言,更实际的问题是:如果我要做一个数学解题系统,应该怎么设计?这个问题的答案并不依赖某个未公开的内部模型。即使是普通的公开 API,只要把“模型生成”和“程序校验”分开,系统的稳定性和可解释性都会明显上升。这也是本文选择“智能体 + 符号计算 + 验证器”作为主线的原因。

1.2 把标题拆成三个可以动手实现的能力

“攻克 10 大数学难题”可以拆成三个独立能力:

  • 数学理解与规划能力:由大模型提供,负责读题、识别题型、选择解题方向、拆解推导步骤。
  • 数学计算与工具能力:由 SymPy、mpmath、NumPy 等程序库提供,负责积分、解方程、化简、矩阵运算这类确定计算。
  • 验证与纠错能力:由程序化验证器提供,负责判断模型的符号表达式是否成立,不通过就让模型重新推导。

“内部版”在工程上可以理解为一套我们自己维护的专用系统:包含固定的题目数据、专用的提示词、固定的工具调用链和独立的验证规则。这套系统不一定比通用模型更强,但它具备可观测、可回滚、可评估的特点,这恰恰是解决复杂问题最需要的基础设施。

1.3 本文要构造的系统边界

Astra Math Solver 的目标不是复刻任何未公开系统,而是完成一个最小可运行的数学求解智能体:

  • 输入:自然语言数学题,例如“求不定积分 x*sin(x) dx”。
  • 输出:结构化 JSON,包含推理摘要、最终答案、工具调用记录、验证结果。
  • 工具:OpenAI Chat Completions + Function Calling,SymPy 做符号计算。
  • 验证:模型给出的答案至少要被一个独立工具重新计算确认。
  • 评估:在 10 类数学题目上统计通过率,并记录失败样本供后续优化。

这个边界足够小,适合学习;也足够完整,适合作为生产原型的起点。

2. 为什么大模型直接回答数学题会失败:先理解推理与验证分离的必要性

2.1 语言模型的本质是“生成下一个 token”,不是“计算正确答案”

大语言模型的工作方式是根据已有的上下文,预测下一个最有可能出现的 token。这个过程在自然语言任务里表现很好,因为自然语言的判断标准是“通顺”“合理”“符合常见模式”。但数学题不一样,数学要求的是精确符号操作。(a+b)^2展开成a^2 + 2ab + b^2需要确定性的规则,而不是概率性的猜测。

当模型生成答案时,它生成的内容会尽量像“一个正确的数学答案”,但“像正确答案”和“是正确答案”之间没有必然联系。尤其在多步推导中,只要某一步符号写错,后续步骤即使看起来流畅,结果也大概率是错的。这是所有直接用模型做数学计算都会遇到的问题。

2.2 数学题里三种典型的失败模式

在实际测试中,模型直接输出答案的失败模式通常可以归为三类:

符号操作错误。模型在展开括号、合并同类项、换元积分时,容易丢掉系数或符号。例如把-x*cos(x) + sin(x)写成x*cos(x) + sin(x),这种错误在文本上很难被发现,因为整段推导依然是通顺的。

计算精度错误。涉及大整数、浮点数、阶乘、组合数时,模型很容易算错。比如问“从 52 张牌中取 5 张的组合数”,模型可能给出接近 2598960 但差一位的数字。

逻辑跳步错误。在证明类和逻辑推理类题目中,模型可能会假设一个并不成立的条件,或者在推导中偷换概念。这类错误用“代码跑一遍”很难发现,必须依赖结构化验证规则。

正是因为存在这些失败模式,把一个系统设计成“问一句就出答案”是危险的。正确的做法是让模型只负责它擅长的事:理解题目、拆解思路、选择工具调用。

2.3 解决思路:LLM 负责规划,工具负责计算,验证器负责兜底

可以引入三个机制解决这个问题:

第一,思维链。让模型在给出答案之前先输出推理步骤,把“直接猜答案”改成“逐步推导”。这一步的作用不是让模型严格保证正确,而是给后续验证提供可检查的中间状态。

第二,Function Calling。让模型在需要计算时调用明确的函数,例如sympy_integratesympy_simplify。积分和化简都交给 SymPy,而不是让模型自己口算。这样可以消除很大一部分符号操作错误。

第三,独立验证器。模型给出最终答案后,验证器用工具重新计算一次,比较两个结果是否等价。这里的关键是“重新计算”,而不是让模型检查自己的答案。模型的自检通常会沿袭同样的错误,独立程序不会。

这样,系统的正确性就从“模型预测”迁移到了“工具执行 + 规则校验”。这也是后面所有实现的设计基础。

3. 环境准备:API 接入、依赖版本与项目结构

3.1 OpenAI API 使用前的合规与账号准备

使用 OpenAI API 前,需要先确认几件事:账号是否开通了 API 权限、当前账号可用的模型有哪些、项目是否有数据合规要求。如果开发环境在公司或学校网络内,还要先确认外部 API 调用符合所在组织的安全规范。

API Key 应该通过 OpenAI 官方平台获取,并且只能保存在本机环境变量或密钥管理服务中。不要把 Key 写在代码里,不要提交到 Git 仓库,也不要使用他人分享的 Key。学习阶段建议先用量小、价格低的模型跑通链路,再根据效果决定是否切换更强的模型。

注意:API Key 的权限直接对应你的账号额度。任何泄漏都可能造成额度被盗用,建议在控制台开启用量限制,并设置为定期轮换。

3.2 Python 依赖与环境创建

建议使用虚拟环境隔离项目依赖。在 Python 3.10 及以上版本中,按下面步骤创建环境:

python -m venv .venv source .venv/bin/activate # Windows 下使用 .venv\Scripts\activate pip install -U openai sympy pydantic python-dotenv tenacity pytest

各依赖的作用如下表:

依赖用途
openai调用 OpenAI API,需要 1.0 以上版本
sympy符号计算,包括积分、方程求解、表达式化简
pydantic定义题目、答案、工具调用的结构化模型
python-dotenv加载.env文件中的环境变量
tenacity对 API 请求做重试,规避瞬时限流
pytest编写自动化验证脚本

如果原始项目没有指定版本,建议先安装最新稳定版,再根据报错信息逐步锁定兼容版本。SymPy 是纯 Python 数学库,表达式非常复杂时算力消耗很大,因此需要配合超时控制使用。

3.3 项目目录结构

一个清晰的项目结构能减少调试成本。下面是一个适合作为起点的结构:

astra_solver/ ├── .env ├── requirements.txt ├── config.py ├── models.py ├── tools.py ├── solver.py ├── evaluator.py ├── dataset/ │ ├── calculus.json │ ├── number_theory.json │ ├── optimization.json │ └── ... └── reports/ └── evaluation_result.csv
  • config.py:读取环境变量和全局配置。
  • models.py:定义统一的题目、答案、工具调用模型。
  • tools.py:封装 SymPy 计算函数。
  • solver.py:核心求解循环,负责调用模型和工具。
  • evaluator.py:批量评估脚本。
  • dataset/:准备好的人工标注题目集。
  • reports/:保存每次评估的结果。

3.4 环境自检脚本

完成依赖安装后,先写一个最小脚本确认 API 连通性:

import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) resp = client.chat.completions.create( model="gpt-4o-mini", # 换成你账号里可用的模型名 messages=[{"role": "user", "content": "返回字符串 ok"}], max_tokens=10, temperature=0, ) print(resp.choices[0].message.content)

如果输出ok,说明 Key、模型名和网络链路都可用。如果报错,根据状态码判断:401表示 Key 无效,429表示限流,404表示模型名称不可用。这一步看似简单,却能避免后面求解脚本里反复排查环境问题。

4. 核心实现:用 OpenAI Function Calling + SymPy 做数学求解管线

4.1 题目与答案的数据模型

为了让求解结果可以保存、浏览和自动化检查,先用 Pydantic 定义统一的数据结构:

from typing import List from pydantic import BaseModel, Field class ToolCall(BaseModel): name: str arguments: dict class Solution(BaseModel): problem_id: str category: str reasoning: str = "" answer: str = "" tool_calls: List[ToolCall] = Field(default_factory=list) verified: bool = False verification_message: str = ""

tool_calls记录模型调用过的工具和参数。这个字段在调试时很有用:如果某道题验证不通过,可以回看模型到底调了哪个函数、传了什么参数。

4.2 工具层:把数学计算交给确定程序

tools.py中封装几个核心数学工具。这里用 SymPy 的parse_expr把字符串转成表达式,再交给 SymPy 计算:

import sympy as sp from sympy.parsing.sympy_parser import parse_expr def _to_expr(text: str): cleaned = text.strip().replace("\\", "") return parse_expr(cleaned) def sympy_simplify(expression: str) -> str: expr = _to_expr(expression) return str(sp.simplify(expr)) def sympy_integrate(integrand: str, variable: str = "x") -> str: x = sp.Symbol(variable) expr = _to_expr(integrand) return str(sp.integrate(expr, x)) def sympy_solve(equation: str, variable: str = "x") -> str: x = sp.Symbol(variable) expr = _to_expr(equation) return str(sp.solve(sp.Eq(expr, 0), x)) def check_equality(expr_a: str, expr_b: str, variable: str = "x", mode: str = "expression") -> bool: x = sp.Symbol(variable) a, b = _to_expr(expr_a), _to_expr(expr_b) if mode == "indefinite_integral": # 不定积分结果之间可以差一个常数,比较导数更可靠 return sp.simplify(sp.diff(a, x) - sp.diff(b, x)) == 0 return sp.simplify(a - b) == 0

check_equalitymode="indefinite_integral"是处理积分问题时的关键:sin(x)^2/2-cos(2x)/4看起来不同,但求导后相等,因此都算正确。

需要说明的是,parse_expr会解析模型生成的字符串,这在本地实验环境可用,但生产环境不能直接信任模型输出。建议增加白名单校验,只允许字母、数字、括号、运算符和少量数学函数名。

4.3 系统提示词与工具描述

系统提示词要明确告诉模型:必须优先调用工具,不准编造工具未返回的结果。示例:

SYSTEM_PROMPT = """你是一名数学解题智能体。请按以下步骤工作: 1. 先用自然语言写出推理思路。 2. 遇到积分、化简、解方程时,调用提供的 SymPy 工具计算。 3. 工具返回结果后,把结果整理到最终答案中。 4. 不要编造工具未返回的内容。 5. 最终以 JSON 格式输出:{"reasoning": "简要推理", "answer": "最终答案"} """

同时,在 API 请求中注册工具描述,让模型知道可以调用哪些函数。这里只展示一个工具的结构,完整代码可以加入sympy_simplifysympy_solve

TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "sympy_integrate", "description": "使用 SymPy 计算不定积分,例如 x*sin(x)。", "parameters": { "type": "object", "properties": { "integrand": {"type": "string", "description": "被积表达式"}, "variable": {"type": "string", "description": "积分变量", "default": "x"} }, "required": ["integrand"] } } } ]

工具描述写得越清楚,模型选择工具时就越准确。尤其是参数说明,宁可多加几个字,也不要让模型去猜。

4.4 求解主循环

核心求解循环的逻辑是:调用模型,如果模型返回工具调用请求,就执行对应工具并把结果回传;如果模型返回纯文本,就解析 JSON 并进入验证阶段;验证不通过则把错误信息反馈给模型,让它重新求解。

import json import os from dotenv import load_dotenv from openai import OpenAI from models import Solution, ToolCall import tools load_dotenv() TOOL_MAP = { "sympy_simplify": tools.sympy_simplify, "sympy_integrate": tools.sympy_integrate, "sympy_solve": tools.sympy_solve, } class MathSolver: def __init__(self, model="gpt-4o-mini", temperature=0.0, timeout=60): self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) self.model = model self.temperature = temperature self.timeout = timeout def solve(self, problem: str, category: str = "", problem_id: str = "unknown") -> Solution: messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": problem}, ] solution = Solution(problem_id=problem_id, category=category) for _ in range(5): response = self.client.chat.completions.create( model=self.model, messages=messages, tools=TOOL_SCHEMAS, temperature=self.temperature, timeout=self.timeout, ) message = response.choices[0].message if getattr(message, "tool_calls", None): messages.append({ "role": "assistant", "content": message.content or "", "tool_calls": [tc.model_dump() for tc in message.tool_calls], }) for tc in message.tool_calls: args = json.loads(tc.function.arguments or "{}") result = TOOL_MAP[tc.function.name](**args) solution.tool_calls.append(ToolCall(name=tc.function.name, arguments=args)) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": str(result), }) continue if not message.content: solution.verification_message = "模型返回空内容" return solution try: parsed = _extract_json(message.content) solution.reasoning = parsed.get("reasoning", "") solution.answer = parsed.get("answer", "") except Exception as exc: solution.verification_message = f"解析最终结果失败: {exc}" return solution break else: solution.verification_message = "达到最大迭代次数仍未给出最终答案" return solution solution.verified, solution.verification_message = self._verify(solution) return solution def _verify(self, solution: Solution): if not solution.answer: return False, "答案为空" if not solution.tool_calls: return False, "缺少工具调用,无法验证" # 按工具类型分派验证逻辑 for tc in solution.tool_calls: if tc.name == "sympy_integrate": ref = tools.sympy_integrate(**tc.arguments) variable = tc.arguments.get("variable

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

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

立即咨询