大模型+符号计算:构建数学求解智能体的工程实践 📅 发布时间:2026/9/1 18:29:09 👁 浏览次数: 当标题里出现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 CallingSymPy 做符号计算。验证模型给出的答案至少要被一个独立工具重新计算确认。评估在 10 类数学题目上统计通过率并记录失败样本供后续优化。这个边界足够小适合学习也足够完整适合作为生产原型的起点。2. 为什么大模型直接回答数学题会失败先理解推理与验证分离的必要性2.1 语言模型的本质是“生成下一个 token”不是“计算正确答案”大语言模型的工作方式是根据已有的上下文预测下一个最有可能出现的 token。这个过程在自然语言任务里表现很好因为自然语言的判断标准是“通顺”“合理”“符合常见模式”。但数学题不一样数学要求的是精确符号操作。(ab)^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_integrate、sympy_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.csvconfig.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_keyos.getenv(OPENAI_API_KEY)) resp client.chat.completions.create( modelgpt-4o-mini, # 换成你账号里可用的模型名 messages[{role: user, content: 返回字符串 ok}], max_tokens10, temperature0, ) 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_factorylist) 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) 0check_equality的modeindefinite_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_simplify、sympy_solveTOOL_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, modelgpt-4o-mini, temperature0.0, timeout60): self.client OpenAI(api_keyos.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_idproblem_id, categorycategory) for _ in range(5): response self.client.chat.completions.create( modelself.model, messagesmessages, toolsTOOL_SCHEMAS, temperatureself.temperature, timeoutself.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(nametc.function.name, argumentsargs)) 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