数学题做完不会验算,算到一半把题目条件记岔,这是大语言模型做数学题最常见的两个毛病。标题里带“AI for Math速成”,老读者应该明白,这个系列前面几篇把数学数据、提示词、微调都已经过了一遍,这一期专门聊聊怎么把单个模型升级成一个能规划、能调工具、能自我检查的数学Agent。一句话概括就是:从LLM到Agent,本质上是让模型从“凭记忆答题”变成“用工具解题”。这套思路不仅适合想给数学解题系统做落地的人,任何在处理复杂推理任务的场景——金融数据解读、代码报错排查、物理题推导——都可以借用同一个框架。这篇文章会沿着设计动机、模块拆解、落地代码、评测收敛、常见坑的顺序往下走,全程给可复现的做法。
1. 为什么单靠LLM做数学总是差一口气
先别急着堆Agent,得先把问题的病根看清楚。我最早做数学解题的时候,用的是最直接的路子:把题目扔给大模型,让它一步步写推理过程。效果看着还行,小学应用题可以拿高分,但一上到竞赛题和大学数学,就开始露馅。不是不会,是“会但不严谨”——经常思路完全正确,中间某一步代数变换错了,或者多位数乘法口算出问题,整道题全盘皆输。这其实是语言模型的先天缺陷:它擅长的是“词的组合”而不是“符号的精确运算”。
1.1 数学推理对模型的三个苛刻要求
数学和其他生成任务最大的不同,是它对“每一步”都有硬约束。写作文错一个字不影响整体,但数学里错一个符号,结论就废了。具体来说有三重考验。
第一重是逻辑链条要闭链。模型在生成时是一个词一个词往外蹦的,它的注意力窗口有限,一旦推导超过十步,前面的条件会产生“注意力衰减”,写到后面把前面某个前提忘了,这在数学上就是致命的。
第二重是算术和代数要零容错。8位以内的加减乘除、符号展开、因式分解,这些对人类来说机械但可靠的步骤,对语言模型反而是最不擅长的地方。它没有内置的运算器,全靠token概率“猜”,猜错一个数字是常有的事。
第三重是验证能力。人类做完题会回头检查:把解代回原方程,看两边相等不相等。但纯用LLM做这个自查,它会倾向于“自己怎么算的就怎么说对的”,这是模型固有的确认偏误——它不是在验算,而是在续写一个呼应前文的答案。
1.2 能力边界测试:模型擅长什么,不擅长什么
我在早期做了一个小实验,把同一批数学题分成三类跑纯LLM:概念解释、计算求解、证明推导。结果很有意思。
概念解释类(比如“解释什么是特征向量”)表现最好,因为这类问题在训练数据里出现过成千上万次,模型本质是在做“语义回忆”。计算求解类,尤其涉及多位乘除法、复合函数求导后化简的,错误率明显上升,因为它是真在“猜数”。证明推导类最难,推理链一长,经常出现逻辑断层,甚至自己发明一个不存在的定理。
所以结论很清晰:LLM在数学上真正的价值不是“计算器”,而是“解题策略生成器”。它能理解题意、给出一个大致可行的思路框架、判断哪条路可能走得通,但具体的计算和严密的推导,应该交给确定性的工具完成。这也是做数学Agent最重要的心智转变——别把模型当全能选手,把它当队长,真正的“苦力活”让队友干。
1.3 解题的本质切换:从“背诵答案”到“调用能力”
这个切换很关键。传统的LLM数学方案,本质是在让模型“想起答案”;而Agent方案是让模型“组织解题过程”。前者依赖预训练阶段见过多少类似题,后者依赖工具调用的可靠性和循环验证的严密性。
举个直观的例子:“求两个矩阵的乘积并验证是否可逆”。纯LLM的做法是凭空生成矩阵乘法的结果。Agent的做法是调用符号计算库完成乘法,再通过计算行列式判断可逆性。后者每一步都有据可查,错在哪一步一目了然,还能把中间结果打印出来给用户看。所以Agent不是智商升级,是工作方式升级——从“独立答题”变成“监督解题生产线”。认识到这一点,后面所有的模块设计就有了逻辑起点。
2. Agent整体架构拆解:大脑、手脚和检查员
既然定位变成了“队长+队友”模式,架构上就要明确:谁是大脑,谁动手,谁验收。我在搭建自己的数学Agent原型系统时,参考了常见的ReAct模式,做了本地化调整,核心是五个模块:规划器、执行器、工具集、短期记忆、验证器。它们的配合关系不复杂,但每一环都需要仔细设计。
2.1 模块职责:为什么这样分工
规划器(Planner)就是那个LLM大脑,负责读题、定策略、分解步骤。它输出的是一个“行动序列”,比如“先计算导数,然后求根,判断单调性”。执行器(Executor)负责接住规划器下发的指令,去调度具体工具——调用符号计算库、执行Python代码、查公式表。工具集(Tools)是一组确定性的程序,包括代码解释器、符号计算内核、图形绘制、数值计算等,它们不接受模糊指令,天然精确。
短期记忆负责保存中间结果。很多Agent跑着跑着忘记前面算到哪,就是因为没有把“子目标完成情况”显式记录下来。验证器(Verifier)独立于规划器存在,它的任务是对最终结果做反代测试、单位检查、边界验证。把验证和生成分开,才不会被模型的“自我感觉良好”带偏。
2.2 核心循环流程:思考、行动、观察的闭环
整个Agent的运行是一个循环,逻辑上和人类的做题过程很一致:
- 规划器读取题目,生成当前子目标(例如“化简方程左边”)。
- 执行器把子目标翻译成具体工具调用,交给工具集。
- 工具集返回精确结果,写入短期记忆。
- 观察器把结果格式化成上下文,反馈给规划器。
- 规划器判断是否完成子目标,决定是继续下一步还是终止。
- 全部步骤完成后,验证器对最终答案做独立校验。
这个循环听起来简单,但实际工程里“格式化反馈”这一步最容易被忽视。工具返回的结果是一堆矩阵数字,直接塞回给规划器,模型会被噪音干扰,忘记自己在干嘛。正确做法是把结果提炼成一句话摘要加关键数值——事件过程要完整,信息要有节奏。我在实际中踩过这个坑,后面会展开讲。
2.3 为什么选择确定性工具而非“再问一次模型”
有一个很容易被新手绕进去的问题:有些计算,比如那个矩阵乘法,我再让模型算一遍、给两个答案取相同值不就完了吗?多模型投票确实能降低错误率,但解决不了系统性问题——如果模型因为某个概念理解错了,它的所有输出会在同一个地方错。投票只能过滤随机错误,不能过滤系统性错误。
而确定性工具,比如符号计算库,遵循的是严格的算法规则,不存在“状态不好算错”的情况。只要你的调用参数传对了,结果就是正确的。这就把“随机性”从最关键的运算环节中彻底剥离了,只在“策略选择”层面保留LLM的灵活性。这个架构选型的底层逻辑,说穿了就一句话:把容易出错但需要创造力的部分交给模型,把必须精确但机械的部分交给程序。
3. 核心落地实操:从零搭一个可运行的数学Agent
理论说太多容易飘,真正动手你会发现另一堆麻烦。我在这里给出一套我从本地原型系统里积累的具体实践方案,尽量做到拿来就能改。
3.1 环境准备与最小依赖
搭建一个数学Agent不需要重型框架,我的建议是用最轻的Python环境起步。核心依赖就三块:一个能调用大模型的HTTP库或者官方SDK、一个符号计算库(用于精确代数运算)、一个代码解释器(用于数值计算和图线绘制)。调试阶段,我推荐直接在Notebook里跑循环,不用一上来就上工程框架,降低出问题的范围。
一个可以快速起步的最小目录结构大概是:
math_agent/ agent.py # 主循环逻辑,规划器+执行器 tools.py # 工具集:符号计算、数值计算、绘图 llm_api.py # 统一的大模型调用封装 memory.py # 短期记忆存储,维护子目标状态 prompt.py # 规划器和验证器的提示词模板每一块代码都控制在几百行以内,重点是跑通“思考→行动→观察”的最小闭环,再逐个加模块。
3.2 关键代码示例:Agent主循环的骨架实现
下面这一段是我实际调试中用的简化版主循环。它没有做精巧的状态管理,但足够展示Agent的本质逻辑。核心是用一个while循环控制迭代次数,防止Agent跑飞,然后每一步根据规划器的输出决定调用哪个工具。
# agent.py — 最小Agent循环骨架 import json from tools import symbolic_simplify, numeric_eval, plot_function def run_agent(question, max_steps=10): memory = {"question": question, "steps": [], "result": None} for step in range(max_steps): # 1. 规划器:根据当前记忆生成下一步行动 planner_output = llm_call( system="你是数学解题规划器。只输出JSON动作,不要解释。", prompt=format_planner_prompt(memory) ) action = json.loads(planner_output) # 2. 执行器:分发到具体工具 if action["tool"] == "symbolic_simplify": output = symbolic_simplify(action["expr"]) elif action["tool"] == "numeric_eval": output = numeric_eval(action["expr"]) elif action["tool"] == "plot": output = plot_function(action["expr"]) elif action["tool"] == "finish": memory["result"] = action["answer"] break else: output = f"未知工具: {action['tool']}" # 3. 把观察结果压缩后写回记忆 memory["steps"].append({ "tool": action["tool"], "input": action.get("expr", ""), "output": summarize_output(output) }) # 4. 验证器独立检查 verification = verify_solution(memory) return memory["result"], verification这个骨架有三个设计点值得注意。
第一个是规划器必须“只输出JSON”,不要自由文本。否则模型容易在“思考”里夹带计算过程,又退化成硬算模式。我通过提示词强制约束,并且在解析时用异常捕获兜底——模型偶尔会输出不规范JSON,要能容错重试。
第二个是每次工具返回结果后,要对输出做“摘要化”(summarize_output)。这一步在骨架代码里只有一行,但非常关键。比如符号计算返回一个巨长的表达式,我会在摘要里保留“化简结果=2x+1”这种精炼信息,同时截断过长的中间表达式,避免上下文爆炸。
第三个是验算要在循环外单独做。主循环跑完拿到答案,再调用验证器反代检查,两者逻辑不复用,从设计上避免“又当运动员又当裁判员”。
3.3 工具集的设计:符号计算与数值计算的分工
工具集是整个Agent可信度的基石。我的经验是分两层:符号计算层处理代数推导问题,数值计算层处理具体数值和绘图问题,两者不要混用。
符号计算层我推荐用符号计算库,它能处理因式分解、求导积分、方程求解等代数任务。调用的时候有一点需要注意:表达式解析要容错。模型生成的表达式字符串经常有多余空格、中文括号甚至绘制符残留,需要先做清洗。我习惯写一个safe_parse函数,把常见异常全部捕获,返回给规划器一个明确错误信息而不是让整个Agent崩溃。
数值计算层则负责代值计算、逼近求解和画函数图像。比如题目要求“在某个区间内找零点”,符号方法求不出解析解的时候,就用数值方法迭代逼近。实现上就是常规Python库,没有特别多坑,但要注意数值误差的展示格式——保留六位有效数字就好,多了噪音反而干扰模型判断。
有一类特殊工具:公式查询器。有些竞赛题会用到冷门定理,模型可能记错。我会在工具集里放一个结构化的公式库,Agent需要时按名称检索。实测下来,这比让模型死背公式靠谱得多,属于“把外部知识做成可检索工具”的思路。
3.4 验证器的设计与反代测试
验证器是我认为整个Agent里性价比最高的模块,一个简单的验证函数,能堵住一半以上的低级错误。它做的事情很直接:把最终答案代回原始条件,检查等号是否成立、不等式是否满足、定义域边界是否正确。
具体到一个“解方程”的场景,验证器的逻辑是这样的:拿到Agent给出的解x=2,把2代回原始方程两边,算差值绝对值,如果低于阈值就判定通过。如果在定义域内,还需要逐个检查边界值。验证器本身不用LLM,全部是确定性代码,因此它的结论是可信任的——这就是“独立验证”的价值所在。
但验证器能做的事情有限,它只能验证“已知答案是否正确”,没法验证“推导过程是否严谨”。所以我的Agent在输出最终答案时,会同时给出“过程摘要+验证结果”,让用户自己判断。这有点像做实验写报告:结论背后要有支持证据,读者可以复核。这也是Agent相比纯LLM输出的最大优势——过程透明、结论可验证。
3.5 一步到位:完整运行的一个解题实例
光说代码骨架可能还不够直观,我来走一遍完整实例。假设题目是:“解方程 2^(x+1) = 3^x,并判断有几个解”。这个题涉及到指数方程和对数,纯LLM容易在取对数化简时出错,非常适合Agent来做。
- 第1步:规划器读题后决定,先对等式两边取自然对数,转换为线性方程。输出动作:调用符号计算工具,表达式为“log(2**(x+1)) - log(3**x)”,意图是化简。
- 第2步:符号计算库返回结果“log(2)·x + log(2) - log(3)·x”。这个结果显示化简后的形式,虽然不是传统的最简式,但已经包含核心信息。
- 第3步:规划器观察结果后,决定下一步解这个关于x的线性方程。它让工具库调用solve函数,输入表达式“log(2) + x(log(2) - log(3)) = 0”。
- 第4步:工具库返回“x = log(2)/(log(3) - log(2))”,约等于1.7095。
- 第5步:验证器上场。它把x代回原方程两边,分别算出2^(x+1)和3^x的值,计算相对误差,判定通过。
- 第6步:Agent输出“一个解,x≈1.7095,验证通过”。
这个例子你拿纯模型跑一遍,大概率也能得到正确答案,但如果数值复杂一点、特判多一点,纯模型的翻车率马上上来。Agent方案的价值,在复杂任务上会体现得指数级明显——因为它把每一步都放在了可检验的基础上。
4. 提升数学Agent能力的三个进阶方向
跑通最小闭环之后,你会发现效果已经很不错了,但要让它稳定地处理更难的问题,还有三个方向需要投入。这三个方向不是相互独立的,按照性价比排序,先做提示工程,再做验证器升级,最后才是模型微调。
4.1 提示工程进阶:给Agent立一套“解题规范”
很多Agent发挥不稳定,卡在提示词的设计上。规划器和验证器需要的是不同风格的提示词,不能一套走天下。
规划器的提示词核心是“决策而不是计算”。我会明确告诉它:你不需要执行计算,你的任务是决定下一步做什么,把计算交给工具。还有一条重要的约束:如果判断某一工具返回的结果不对劲,可以要求重新调用或换一种策略,不要硬着头皮继续。
验证器的提示词核心是“怀疑而不是确认”。如果让验证器也用“检查答案并返回通过”,它就会站在Agent那边找借口。我的做法是:明确告诉验证器“如果发现任何异常,立刻报告”,并且给它独立的工具权限,它可以自己重算一遍关键数值。
这样拆分后,Agent的判断链路更清晰:规划器是进攻手,验证器是防守手,两者在提示词层面就形成了正交。这是最简单、见效最快的优化手段,新Agent到手先调提示词,比换模型划算多了。
4.2 过程监督:从只查结果到监控每一步
默认验证器只看最终答案,但很多难解的题,错因不在最后一步,而在中间的某一步推导。一旦中间融入了错误的假设,后面全对也是白搭。所以要引入过程监督——给Agent的每个核心步骤都打一个“可信度分数”。
做法可以是这样的:每完成一个子任务,把该子任务的输入、输出、使用工具、执行耗时记成一条日志。验证模块不只看最终答案,还抽查若干中间步骤,对代数变换做数值代入检查。例如化简后的表达式,我会用随机取几个数分别代入化简前后两侧,如果不等,立刻标注“该步骤有疑点”。
这样做的代价是计算量增加了一些,但换来的是错误定位能力。当一个Agent跑错了,你能精确到“第三步化简有问题”,调试效率完全不同。对于竞赛数学题这种多步骤推理场景,过程监督是最值得投入的一项能力。
4.3 换个更强的大脑:数学模型的选型与微调取舍
Agent的效果上限,最终还是受制于规划器的聪明程度。如果规划器连思路方向都判断不了,再好的工具也是白搭。所以选型很重要:通用的模型在“语义理解”上强,但“数学推理”能力中等,尤其是涉及多步推导时,指令遵循能力会下降。
我自己的调研结论是:优先选择在数学评测基准上表现较好的推理增强模型,它们接受了更多数学数据训练,更习惯符号和推导表达。如果预算允许,还可以在垂直场景做小规模微调——用一批带“动作注释”的解题轨迹微调规划器,让它学会“见到哪种题型,应该优先尝试哪种工具序列”。
但要泼一盆冷水:微调投资大、周期长,而且如果提示词体系和工具设计没理清,微调并不能根治“策略混乱”的问题。我的建议是,先花两周把提示词和验证器打磨到位,再考虑微调。绝大多数场景,前者提升幅度已经非常可观。
5. 常见问题与排查技巧实录
任何Agent系统,运行起来都会有一堆幺蛾子。这一节把我实测中遇到的高频问题、成因和解决方案整理出来,可以当速查表用。
5.1 问题速查表
| 现象 | 常见原因 | 排查思路与解决方案 |
|---|---|---|
| Agent反复调用同一个工具停不下来 | 规划器陷入局部循环,观察结果没有改变它下一步决策 | 在提示词里加“如果某工具重复调用三次,结果仍未变化,换一种策略或直接结束”;同时给最大步数硬限制 |
| 工具调用时格式出错 | 模型输出的JSON不规范,或表达式字符串无法解析 | 在调用工具前加解析层,捕获所有异常;提示词里给一个“正例+反例”格式,并开启重试机制 |
| 符号计算返回结果与预期差太远 | 表达式语义和模型理解不一致,比如变量名混淆 | 检查提示词里是否定义了变量的规范写法;在工具层做变量名自动统一 |
| 验证器误判(该过不过、不该过过了) | 阈值设置不合理,或没检查定义域 | 把判据分成多条独立规则:代回测试、边界检查、量纲检查;每条规则单独报告,不要合成一个模糊分数 |
| 上下文太长导致Agent“失忆” | 中间结果堆叠太多,模型注意力被稀释 | 增加摘要机制,把每一步输出提炼成短句;超过长度时启动“滚动压缩”,把历史步骤压成一行总结 |
| 输出答案虽然对但过程不可信 | 模型反向编造步骤,也就是“答案对了但思路是假的” | 在最终报告里强制列出“工具调用记录”,贴出每一步的入参和出参;对用户透明,AI造的假过程一眼就能被识别 |
5.2 我的调试顺序建议
如果你刚搭好Agent,开始跑测试,我建议按这个顺序做问题排查:第一步,只跑题目,观察循环是否正常走完,检查有没有死循环和工具调用崩溃;第二步,加入验证器,看最终结果准确率提升了多少,同时收集验证失败案例,这些案例是提示词优化的最佳素材;第三步,针对验证失败的题目做“行为轨迹回放”,从日志里找Agent在哪个环节跑偏了,对症下药。
在这个阶段,我一再提醒自己:不要把Agent当黑盒。每次运行都保存完整日志,包括每一步的规划器原始输出、工具入参、出参和耗时。这些日志是调试的命根子,跑了一百道题之后,你会发现自己对Agent行为模式的理解完全不一样了。
5.3 稳定运行的一些小技巧
最后分享几个我用真金白银换来的小技巧,不高深,但能让系统稳一个档次。
第一,给所有外部工具调用加超时控制。符号计算库偶尔会遇到超复杂表达式导致卡死,给每个函数调用套上超时保护,超时返回错误信息,不要让Agent卡在一步上整个挂起。
第二,正确使用随机采样参数。规划器如果总选同一条路,可以适当调高采样温度增加探索;但验算环节必须把参数设为确定性,否则逐字输出的小抖动会影响最终答案比对的可靠性。
第三,建立“结果置信度”概念。Agent跑完一道题,除了给答案,还应该输出验证通过与否、调试日志条数、总步数这些元信息。用户看到“验证失败”比看到“错误答案”体验好得多——至少Agent知道自己不行,而不是硬撑。这个诚实机制,也是Agent走向可信赖的第一步。
6. 一些没有写进代码里的体会
从LLM到Agent这一步,我最大的感触是:一定要接受“模型会犯错”这个前提。很多人做Agent,潜意识里还是想找一颗“什么都会的聪明大脑”,可真正的工程恰恰相反——它假定大脑一定会犯错,然后用流程、工具、验证把所有错误隔绝在最终结果之外。这和写复杂软件时要加单元测试是同一个道理,没有人指望编译器不报错,而是靠测试把错误拦截在交付之前。
在做这个数学Agent原型的过程中,我反复体会到“工具调用记录”是一种非常有价值的副产品。它让AI系统的推理过程变得可以复盘、可以审计、可以追溯。不管是给学生讲题,还是给领导汇报方案,这个透明性都比“一个神奇的神秘答案”有价值得多。如果后面你想更进一步,可以试着把Agent的日志结构化沉淀下来,变成一套解题过程的知识库——这又是另一个值得展开的话题了。