数学场景下LLM为何不靠谱?Agent+工具闭环才是解法
如果你也试过让大语言模型帮忙解数学题多半遇到过这种场面它先是给出一段看起来极其专业的解答步骤你信心满满地往下读结果最后一步的答案和标准答案差了十万八千里——再让它检查一遍它甚至还能信誓旦旦地告诉你“重新检查后确认无误”。这种“流利地胡说八道”在数学场景里格外致命因为数学只认结果不认语气。这期AI for Math速成进入第四篇我想认真聊一件事为什么数学场景下LLM单打独斗不够用以及为什么Agent结构是更靠谱的解法。如果你正在做AI教育产品、数学解题工具或者单纯想搞明白“LLM加工具”到底怎么落地这篇就是为你准备的。1. 面对数学LLM的会说话为什么变成不会做1.1 语言模型的本质是接话不是推导先说一个被很多人忽略的事实大语言模型的训练目标是预测下一个Token它最擅长的其实是把一段话接得“像那么回事”。你在对话框里输入一道题它生成的下一个词根本不是在执行数学规则而是在做一个概率选择——在所有可能出现的文本里挑一个最“顺”的片段继续写下去。这个过程和人类做推导是两码事人类从公理出发按逻辑规则一步步变形LLM只是让接出来的句子在语法上、语义上看起来不违和。用一个不太严谨但很形象的类比一个口才极好的导游可以对着地图说得头头是道你让他估算两座山之间的实际距离他报出的数字大概率是靠“听起来差不多”蒙的。数学恰恰是所有知识里最不买“说话好听”账的领域。题目答案唯一判分机制非黑即白哪怕论证过程再漂亮只要一个符号错了整道题就是错的。这种“硬正确性”放大了LLM的弱点它输出的流畅性和正确性之间几乎没有任何保证关系。更麻烦的是LLM在生成错误步骤时自己往往毫无感知。这不是态度问题而是机制问题模型内部没有“算一算再回答”的模块它的每一步选择都在模仿训练数据里的模式而不是真的在解方程。你在数学题上追问它“你确定吗”它甚至可能基于同样的概率机制给出另一个同样流利的错误答案。1.2 一道积分题如何让我认清现实我最初做AI数学工具时也天真地以为“让模型多看看例题多给点Few-shot提示它就能学会”。直到我拿一道很基础的积分题反复测试才彻底改变想法。题目是求不定积分∫ (2x3)/(x²3x1) dx。这类题的标准做法是换元。观察分子2x3恰好是分母x²3x1的导函数所以令ux²3x1du(2x3)dx原积分直接变成∫1/u du ln|u|C ln|x²3x1|C。听着不难对吧我连续让同一个模型试了几轮它的表现让我大跌眼镜有时它能说“令u等于分母”但接下来会写成ln|分母/2|凭空多个1/2有时它干脆跳过换元直接用分部积分硬算最后得出一堆乱七八糟的分式还有一次它第一行写对了第三行却把u代回去的时候少写了平方项。整个过程像极了一个学生在考场上“假装会做”每一步看起来都有模有样但合在一起根本经不起推敲。我后来又试了纯数值计算的题目比如两阶矩阵乘法。2x2的矩阵公式清清楚楚C11A11B11A12B21就这么几个乘法和加法模型偶尔还是会把某一项漏掉或者把行和列对应错。真正让我死心的是错误不是随机的每次错的地方还不太一样根本没有一个稳定的纠错规律可循。那次测试之后我得出一个结论数学场景里LLM不适合当“生产最终答案的人”。它更适合当“调度员”——负责把问题拆开、决定该调用什么东西、判断下一步动作而真正的计算和变形必须交给不会“胡说”的工具去做。2. LLM在数学上的三块硬伤以及为什么工具是必然解药2.1 硬伤一数值计算本质是猜第一块硬伤最直观让LLM做精确数值计算它不是在算而是在猜一个“看起来像答案的数字”。比如20位以内的整数乘法、带小数的浮点运算、多步除法模型给的答案经常会在某一位莫名其妙地错掉。这背后同样是Token预测的机制——数字在它眼里只是文本片段它知道1和2“长得像”但并不知道1加2等于3这件事是确定性的。处理办法其实很朴素把计算任务从模型身上摘走。最简单的方法是让它生成Python表达式然后用解释器去执行稍复杂一点可以用任意精度计算库。我现在的做法是给Agent配一个数值计算工具规定任何多步数值计算必须走工具模型不直接输出数字。这样“猜”的问题就彻底消失了工具的答案永远精确唯一的风险只在于模型把输入表达式传错。2.2 硬伤二符号计算环节容易变形失忆第二块硬伤更隐蔽也更容易误导人。符号计算——比如多项式展开、因式分解、求导、化简——表面上是“写式子”看上去LLM应该擅长但实际上它对符号变形的处理极不稳定。举个例子因式分解x²-5x6正确答案是(x-2)(x-3)。模型可能给出正确答案也可能给成(x-2)(x3)或(x-6)(x1)中间项的符号稍微一变整道题就毁了。再比如把(x1)^5展开成多项式二项式系数1、5、10、10、5、1它一般能记住但展开到第三项、第四项时指数或系数偶尔会写串。问题在于符号变形本质上要求每一步都严格保持等价关系任何一步失忆都会让整个结果废掉。而LLM恰恰没有“验算上一步”的固有能力。这个场景的解药是计算机代数系统比如开源的SymPy。我提到的那个换元积分题交给SymPy就是一条命令的事情它直接返回ln(x²3x1)精确且可复现。Agent在这里的角色就变成了识别用户输入属于“求积分”任务然后调用符号积分工具把工具返回的结果整理成答题步骤。模型负责“知道该求积分”工具负责“真的求出积分”。2.3 硬伤三多步推导长距离漂移第三块硬伤是长链推理漂移。一道证明题或者复合函数求导第一行还对第三行开始偏最后一步彻底飞走。我观察到的规律是中间步骤一旦出现一个微小错误后面的所有步骤都会基于这个错误继续推而且模型完全不会回头发现。这很像人类学生做题时“一步错步步错”但人类至少还有可能在某一步觉得“不对劲”LLM连这种直觉都没有。应对漂移的思路有一条我很推荐不要依赖模型内部一步步推导把推理过程“外化”。什么叫外化就是让模型把中间步骤写成代码、写成工具调用每算一步都让工具返回确定的结果。比如让模型完成“先求导再令导数等于零再解方程”的流程就不要让它一口气输出整个解答而是规定它第一步调用求导工具拿到结果后第二步调用求根工具每一步工具都返回确定值模型只负责解读这些值并决定下一步。这么一来长链推理被拆成了短链漂移的生存空间被压缩得极小。顺带提一句自洽性验证也有用——让模型对同一个问题生成多个解答然后投票选出出现最多的答案。这在选择题、判断题上能提升正确率但我觉得它只是“用重复对抗随机错误”治标不治本。真正治本的办法还是把推理拆开、交给工具逐步确认。到这里你应该也感觉到单靠模型内部的能力天花板是有限的想突破这个天花板就必须把计算、变形、验证分包给外部工具——而这正是Agent的起点。3. Agent闭环让LLM从写答案变成做实验3.1 为什么数学是Agent最自然的应用场景先想想数学高手解题时在做什么。拿到一个问题他不会直接就写答案而是先读题、想思路、列式子、在草稿纸上算几步、验算一下发现不行就换个方向再来。整个过程其实是一个不断“尝试-观察-调整”的实验闭环脑子负责出思路手负责算和写眼睛负责看结果对不对。LLM现在缺的就是这个闭环中的“手”和“眼睛”。它只有一张嘴输出一段文本之后既看不到自己写了什么符号也无法确认结果是否真的成立。Agent结构正好补上这一环LLM依然是“大脑”负责生成策略和判断外部工具是“手”负责精确计算、符号变形、画图、验证工具返回的结果则是“眼睛”让模型看到行动输出再决定下一步。用一个场景类比LLM单独做数学题就像一个学生叫助手“你直接告诉我答案”助手只能凭记忆空说Agent则是一个研究助理工作台助手可以翻参考书、按计算器、写程序和验算。你觉得哪种方式更靠谱答案不言而喻。3.2 最小闭环规划→行动→观察→再规划一个数学Agent的最小闭环其实只需要四步规划LLM读问题决定下一步要做“什么动作”比如先简化解、求导、求根或者查一个公式。行动把“动作”翻译成一次具体的工具调用例如调用符号积分工具传入被积函数表达式。观察工具执行后返回结果这个结果会作为新消息放回对话历史里。再规划LLM基于观察结果判断任务是否完成如果没完成则继续走规划步骤。这里的关键点是循环不是预先把步骤写死的而是由模型逐轮决定的。模型可以根据上一步的实际输出灵活调整。比如它第一步让符号计算工具化简一个表达式发现化简结果仍然很复杂它可以决定改用数值方法来求近似解如果化简结果很简单它可能直接决定输出答案。这个灵活性正是“Agent”和“脚本”的区别——脚本是固定动作序列Agent是由决策驱动的动态流程。3.3 三件套工具、记忆、验证器把Agent用于数学我建议最少配齐三样东西工具层、记忆层和验证层。工具层是Agent的外部能力集合。按用途可以分成几类数值计算类负责精确算术和浮点运算符号操作类负责积分、求导、化简、因式分解方程求根类负责找数值解或符号解可视化类负责画函数图像帮助模型“看一眼”函数形态还有知识检索类负责查公式和定理。记忆层解决的是“做过什么”的问题。Agent在循环中会产生一系列中间步骤如果不做记忆管理要么上下文很快被塞满要么模型开始重复之前做过的失败尝试。我的做法是维护一个简短工作记忆当前目标、已尝试的路径、最近一次工具结果。每轮结束后把关键信息压缩成一两行摘要防止模型在长流程里“失忆”。验证层是数学Agent里被我放在最高优先级的部分。别的领域的Agent可以容忍“差不多”数学不行。验证器最好用可执行断言而不是让模型自己判断。比如得到积分结果后验证器可以对结果求导再看导数和被积函数是否等价解方程之后把根代回原方程看误差是否小于阈值。只有这种“工具级的裁判”才能真正判对错。反思机制可以作为第四件套加上当验证器判错时把错误信息反馈给模型让模型基于错误原因重新规划。这相当于给学生一本错题本——不是让他重新做一遍而是让他看着错误反思自己的解题路径哪里出了问题。我后面实测中发现反思是提升正确率非常有效的一招但要注意别让模型陷入反复反思的怪圈否则会浪费大量Token。4. 从零搭一个数学Agent的最小骨架4.1 先想清楚边界不是要证明世界难题而是要解题闭环在动手之前必须明确一个目标我们不是要造一个能攻克数学前沿问题的系统而是要造一个“在可离散化为工具调用的问题上稳定不翻车”的解题器。这个边界很重要它决定了系统设计的重点。基于这个边界我的核心原则是三句话LLM做路线规划工具做精确计算验证器做最终裁判。模型不直接输出答案而是输出“下一步该调用哪个工具、传入什么参数”工具返回的确定结果再一步步被模型汇总结论。这样即便模型某个判断失误工具层和验证层仍然能把错误拦截在出口处。4.2 工具层的实际选择我用Python写了一个最小的工具注册表。每个工具都定义三样信息名字、用途描述、参数说明。为什么描述这么重要因为模型全靠描述来决定调不调用描述写得含糊它就会乱传参数。TOOLS { eval_numeric: { desc: 计算某个数学表达式的数值。expression为合法Python表达式例如3**2 4。, params: {expression: str} }, sympy_calculate: { desc: 对表达式执行符号运算支持integrate积分、diff求导、simplify化简、factor因式分解。mode指定运算类型。, params: {mode: str, expression: str} }, find_root: { desc: 在区间[a, b]上寻找函数f(x)的数值根。f使用Python表达式表示变量为x例如x**3 x - 1。, params: {f: str, a: float, b: float} }, }我把这套注册表放进系统提示词里然后告诉模型遇到数学计算先看工具注册表能交给工具的任务绝对不要自己算。这个设定乍一听很简单但它带来的效果非常明显——模型直接从“答题者”变成了“工具调用者”。4.3 规划层的提示词与结构化输出有了工具下一步就是约束模型的输出格式。经验之谈千万不要让模型自由发挥写大段话一定要规定结构化输出。我用的格式是JSON包含thought、action、action_input三个字段。{ thought: 这个表达式需要先化简判断是否可以直接求积分, action: sympy_calculate, action_input: { mode: integrate, expression: (2*x 3)/(x**2 3*x 1) } }主循环的伪代码也非常直白for step in range(MAX_STEPS): response llm_chat(messages) cmd parse_json(response) if cmd[action] finish: return cmd[result] result execute_tool(cmd[action], cmd[action_input]) messages.append({ role: observation, content: f工具返回: {result} })这段代码里有两个细节我后来非常受益一是把工具返回结果用“observation”角色注入模型天然把“观察结果”当成外部信息来处理二是设定MAX_STEPS上限防止Agent在复杂问题上无限循环。我一般设8到10步超过就直接让模型基于已有信息输出“最优猜测”保证系统总能在有限时间内结束。4.4 一个完整例子跑通求x³x1在[0,1]上的根为了让骨架更直观我拿一个很具代表性的题目走一遍完整流程。题目是求方程x³x1在区间[0,1]内的根。第一轮LLM读完题输出thought“这个方程不能直接手算需要找数值根区间是[0,1]。”然后action调用find_root传入fx**3 x - 1a0b1。工具返回0.6823278038281921。到这里如果是一个“直接答题”的模型它可能已经直接输出答案了。但我这个Agent没那么简单LLM收到结果后第二轮的thought是“我需要验证一下这个根是否正确”于是它再次调用eval_numeric计算f(0.6823278038281921)的值。工具返回约-1.9e-16。这个结果非常接近0LLM第三轮才判断“误差在可接受范围任务完成”然后输出结论x约等于0.6823且误差已经过数值验证。不知道你注意到没有整个过程最值钱的不是那一步求根——计算器也能做——而是Agent自己决定“求完根还要验算”。这就是从LLM到Agent的质变它不再给一个孤立的答案而是给出一个有验证链路、有置信度的结论。用户拿着这个结果可以放心引用。5. 实测里踩过的坑和现在的处理办法5.1 当LLM生成工具参数时参数也会传错理想很丰满现实很骨感。第一次跑完整闭环时我发现在“模型做决策”这一步会出现新的错法模型不是自己算错而是把工具参数传错。最典型的是调用sympy_calculate时把mode写成integrate却把表达式写成含有多个变量的工具报错说变量不明确或者求根时该传a0、b1却传成了a1、b0区间方向反了。我的解决办法是双管齐下。第一把工具描述写得更“啰嗦”明确写出参数类型、边界含义、常见错误示例甚至直接写“a必须小于b”第二工具执行出错时把错误信息原封不动放回observation里让模型基于错误信息重新生成调用。由于很多错误是参数层面的模型看完错误信息后大概率能自己修正。我加了“同一工具连续失败3次就换策略”的兜底规则防止一直撞墙。5.2 工具调用失败后Agent会反复撞同一堵墙这个坑更隐蔽某个工具调用连续失败模型却固执地用完全一样的参数重试。我第一次遇到是求根工具因为区间端点处函数值同号而报错LLM收到错误后第二轮的输出和第一轮完全一致——同一个工具、同一组参数仿佛错误信息没被读到。这个问题如果不管Agent会白白烧掉好几轮Token。对策有两层。第一层是在系统提示词里规定“如果某个工具连续两次返回错误说明当前思路无效必须换一种方法。”第二层是在代码里硬编码限制对同一工具的连续失败次数计数达到2次后注入一条人工提醒“你正在重复失败操作请换策略”。顺带提一嘴模型对“错误信息”的敏感度其实没有我们想象中那么高尤其当错误信息很长时它容易忽略关键句。所以我每次只把工具返回的错误摘要放进observation不塞完整堆栈。5.3 上下文被过程撑爆早期步骤被忘光Agent一轮轮跑下来observations越堆越多很快把上下文窗口占满。更要命的是模型在后期会遗忘早期步骤的关键信息——比如它一开始想好的边界条件到了第五轮已经完全想不起来了这等于长链漂移在Agent形态里又复活了。我现在的做法是引入轻量级摘要机制。每一轮结束后把当前状态压缩成一行摘要比如“已在[0,1]找到候选根0.6823误差1.9e-16待最终确认”然后把历史消息里的早期内容删掉只保留摘要和最近两三条observation。这样既控制长度又保证关键状态始终在上下文里。要注意的是摘要生成本身也靠LLM这就等于让模型“自己记笔记”比堆原始历史可靠得多。5.4 让LLM验证对错等于没有验证最后一个坑可能最有普遍性我以为让Agent“自我检查”能提升准确率结果发现LLM检查完还会说“结果正确”。有一次求积分工具返回结果是正确的但模型在检查环节说“分子分母有公因子简化后应该再加一项”硬生生把一个正确答案改成了错误答案。这说明让模型验证等于让一个不可靠的裁判去判题。我现在的原则是所有验证必须由工具执行模型只能解读工具的验证结果。比如验证积分验证器调用sympy对结果求导然后化简并与被积函数做差差为0就通过验证方程根就是把根代回表达式看绝对值是否小于阈值。模型要做的只是看验证器返回的“PASS”或“FAIL”而不是自己重新推一遍。这条原则立下来之后系统的稳定性上了一个档次错误不会再在验证环节被“重新引入”。最后说点我个人体会。这套最小骨架跑通之后我对“AI做数学”的预期发生了根本变化不再奢望模型直接给出可靠答案而是把它当成一个能自我安排验证流程的调度员。数学Agent做得好不好往往不取决于模型本身有多强而取决于工具层描述是否清晰、验证器是否严格、记忆管理是否到位。如果你也想上手试试我建议先把工具注册表和验证脚本写扎实再让LLM只负责聪明地编排。从LLM到Agent本质上就是把推理从模型内部搬到外部世界——模型说方法工具做计算验证器给结论三方配合才能让数学题真正解明白。