☰
从LLM到数学Agent:用验证闭环破解大模型数学幻觉
2026/10/14 4:48:12 网站建设 项目流程

做过数学题的人应该都体会过那种诡异感:让大模型直接“裸解”一道不定积分,十次里可能有两三次能给出漂亮答案,步骤条理清晰,但自己去验算的时候,发现它对不上原函数求导的结果;或者是看起来每一步都没毛病,最后一步符号莫名其妙反了。这个现象见得多了之后,我反而不再迷信“模型越强,数学越好”的说法了。原因很简单:大模型在答数学题时,更像是在做基于海量文本模板的模式补全,而不是像计算器那样按确定性规则一步步推进。真正可行的速成思路,不是逼它变得更聪明,而是把它放进一个能自我验证、能调用外部工具的闭环回路里。这也是AI for Math速成系列第四篇要聊的主题:从单纯使用LLM到搭建数学Agent,我们到底在结构上改变了什么,需要迈过哪些关键节点。

1. 为什么直接问大模型“解数学题”会卡在及格线

1.1 LLM的数学“直觉”本质上是在做模式补全,不是在做计算

很多刚接触大模型数学能力的人,都会先被它的“流畅推导”迷惑。你问一个常见题型,比如分部积分、二阶常系数微分方程、数列极限,它能写出相当接近教材风格的答案。原因不是它内部装了一个数学引擎,而是训练语料里这类题型的解答太多了,它把这些文本的共性模式学得很好。只要题目形态足够“经典”,它可以用统计概率把正确路径复现出来。

问题出在题目嵌套了额外条件,或者需要在推导过程中做一个不常见的变量替换时。我实测中最典型的情况是:开头第一步完全正确,到第三步引入一个复杂的参数关系时,模型开始按记忆中的近似题型脑补,然后画出一条“看起来合理”的推导线。由于自然语言没有像数值求值那样的自检机制,后面每一步都是在错误前提上继续生成的,整个推导就成了一座漂亮的危楼。

这里有个关键认知:LLM的数学表现是“生成流畅性”和“计算正确性”的混合物,而不是纯粹的计算过程。把它当作一个能提出解法的思考助手很有价值,但把它当作一个不会犯错的计算终端,会在不知不觉中埋下大量隐患。这也是后面所有设计思路的出发点——让思考部分归模型,让验证部分归确定性工具。

1.2 三个典型翻车类型:符号错位、步骤跳变、答案自信地错

我总结了一下平时踩过的坑,基本可以归成三类。

第一类是符号错位。比如一个代数化简步骤,模型把“x² - 2xy + y²”当成平方差去处理,或者积分中出现参数符号时搞混积分变量和参数。这类错误往往从中间某一步开始,而且一旦符号错了,后续结果会异常“自然地”继续错下去,不会像程序那样直接报崩溃。

第二类是步骤跳变。两个关键步骤之间,模型省略了最关键的一段推导,直接输出“由上式可得”的结论。我一开始还以为是模型默认读者能看懂,后来对照检查才发现,它跳过的往往是最需要严格论证的部分,而且很多时候跳过去之后结论并不成立。这在高数选择题里不太容易暴露,因为选项已经提供了很多提示;一旦面对证明题或计算量较大的题,就容易露馅。

第三类最迷惑人,就是答案自信地错。模型在最后写出一个完整表达式,还会附带“经检验满足条件”这类话术。可当你真的把它代回原题,发现结果有偏差时,它甚至会反过来质疑你的验算。这种对自身输出缺乏真正反省能力的特性,决定了我们不能让LLM只依靠自身完成数学过程,必须用外部验证约束它。

1.3 现实能力边界:哪些题能裸问,哪些题必须加防护

在谈Agent之前,我先给一个很实用的能力分级参考,方便你给手头的题分类:

任务类型LLM直接求解加验证工具后
常见积分、微分、数列极限等基础题能用,但偶尔出错可靠,验证成本低
多步符号化简、含参讨论容易在中间步骤崩掉值得做成Agent
需要多次尝试、反推、回溯的构造性证明基本拿不准是Agent的主场
题目条件需要通过图形或离散对象理解描述性理解尚可,计算易失配建议结合图模型或离散枚举

我建议你在真正决定要不要把某类数学题交给大模型之前,先做一个小规模侦察测试:找20道同类型的题,让模型直接作答,手工验算正确率。如果正确率超过九成,裸问也可以接受;如果只有七八成,就要开始考虑加验证回路;如果更低,说明题型超出了它的稳定控制区,直接上Agent反而能节省大量纠错时间。

2. 从“会推理”到“会验证”:给LLM接上第一个工具

2.1 让模型先给可执行方案,再交给代码执行去核对

让模型输出“自然语言步骤”只是推理过程的开始,不是终点。真正有效的做法是:让模型在给出思路的同时,额外输出一段可执行的验证代码,然后由代码执行器去核对它的中间结论与最终答案。

举个例子,一道定积分题,模型给出的自然语言步骤可能有一条是“原函数为xeˣ − eˣ”,这个表述本身没法被自动检查。但如果你要求它额外输出代码,把原函数求导并和原被积函数做比较,那验证就成了确定性过程。原函数求导和原题一致,这条结论就被验证通过;不一致,立即被标记为待修正。

这样设计的核心好处是,把模型“说得对”转换成程序“算得对”。大模型可以对任何命题说得头头是道,但程序不会给任何情感分,算不出来就是算不出来,不一致就是不一致。

2.2 一个最小可用的“模型+执行器”流程

下面给出一个可以用在个人项目里的最小搭建流程。这里的call_llm只是一个占位函数,你实际使用时接入你熟悉的推理服务或本地部署模型即可。

def call_llm(system_prompt, user_messages): # 接入你自己的推理服务,或使用本地模型接口 # 返回模型生成的文本 return response_text def extract_code_block(text): start = text.find("```python") if start == -1: return None code_start = start + len("```python") code_end = text.find("```", code_start) if code_end == -1: return None return text[code_start:code_end] def execute_math_code(code): namespace = {} # 注意:如果你执行的是不可信脚本,务必放进隔离环境, # 这里只展示最小验证逻辑 try: exec(code, namespace) return namespace.get("result", "代码运行成功,但没有定义 result 变量") except Exception as exc: return f"运行异常: {type(exc).__name__}: {exc}" prompt = """ 请完成下面的定积分计算题: \\int_0^1 x e^x dx 要求: 1. 写出推导步骤。 2. 用 Python 代码验证最终结果,代码里必须把最终答案赋给变量 result。 """ text = call_llm("你是一个严谨的数学解题助手", prompt) code = extract_code_block(text) if code is None: print("模型没有输出代码块,请重新生成") else: print(execute_math_code(code))

这个流程看起来简单,但已经改变了数学任务的鲁棒性:模型负责提出思路和代码,程序负责验证和计算。哪怕模型推导过程中写了些不靠谱的文字,只要最终结果变量被程序验证通过,就说明答案本身经得起检查;反之,程序报错也会成为下一轮修正的线索。

2.3 为什么优先选代码验证而不是让模型自己检查

很多人遇到模型答错,下意识会对它说“再检查一遍”。实测下来,这种做法有时有效,但更多时候只是让模型换一种方式重写同一个错误结论。原因在于,模型的“自查”依赖的仍是同一套概率生成逻辑,很难真正发现自身隐藏的模式缺陷。它不像人做检查那样,会刻意用不同的方法再推一遍。

代码验证器在这方面天然更有说服力。第一步求导、第二步代入数值区间、第三步比对符号,每一步都按已经过验证的算法执行,结果不会随着模型温度或提示措辞发生变化。当然,代码本身也可能写错,所以我的建议是让代码结果与人工抽查配合:对每道被Agent判为“通过”的题,随机抽出两到三道再人工核对,用来发现代码构造中的盲区。

3. Math Agent的骨架设计:思考-行动-观察三条主链

3.1 Agent不是“多轮对话”,而是明确的状态机

聊到Agent时容易有个误解,以为在多轮对话里让模型“记住上下文”就完成了Agent化。对数学任务而言,这远远不够。真正的数学Agent应该是一个状态机:有当前假设、已有已验证结论、待处理分支、失败动作记录,以及工具执行后的观察结果。每一轮都在“思考—行动—观察”的循环里推进,而不是简单地延续自然语言。

一个最简循环可以这样描述:Agent先读取题目,生成当前的想法;然后决定调用哪个动作,比如执行符号验证、代入数值或拆分引理;工具把结果返回给Agent;Agent根据结果决定继续、回溯还是终止。为了让这个循环可控,我建议让Agent每一步都以结构化JSON输出,这样程序才能可靠地读取它的动作意图:

{ "thought": "这一步应该验证原函数求导是否正确", "action": "symbolic_check", "target": "F'(x) = x e^x", "observation": "待补充" }

有了这个结构,后面接工具调度、状态记录和失败回退都方便得多。纯粹的自由文本对话,在复杂问题上会很快失控。

3.2 动作库设计:哪些工具值得挂到Agent上

数学Agent的动作库不需要很庞大,但每一个动作都必须是确定性的、可重复执行的。我个人按优先级排序经常会这样选:

工具类型用途接入优先级
符号计算/化简求导、积分、通分、因式分解验证最高
数值估算对复杂式子的取值进行近似验证高
规则/恒等式校验三角恒等式、对数运算规则高
约束求解不等式范围、边界条件检查中
离散枚举组合题、数论小规模验证中

这里有一个容易踩的坑:把“让LLM再生成一段解释”当成动作。解释不是工具,它不产生可验证的结果,只会增加噪声。真正配挂在Agent上的,是那些能给出确定结论的模块——程序说“通过”就是通过,说“不通过”就是不通过,不跟模型讨价还价。

3.3 记忆与中间记录:数学推导如何保存和回滚

数学推导往往是一条很长的链,Agent需要记住自己已经确认了哪些前提、否定了哪些路径。我建议用一个轻量级状态对象来做记忆管理,而不是把它们都塞进对话上下文里。对话上下文不仅消耗token,还容易让模型把早前的错误结论当成有效前提继续用。

class MathSolverState: def __init__(self): self.assumptions = [] # 当前活着的假设 self.verified = set() # 验证通过的结论 self.rejected = set() # 验证失败的结论 self.last_tool_result = None self.round = 0 def mark_rejected(self, statement): self.rejected.add(statement) # 如果某个被拒绝的结论依赖某个假设,可以一并标记怀疑 def rollback_to(self, safe_point): # 具体实现取决于你如何记录历史动作, # 核心思想是把状态回滚到最近一个可证明安全的节点 pass

这样设计的好处是,Agent在尝试新思路时如果撞了墙,可以明确回滚到某个已验证的节点,而不是带着污染继续往下推。后续所有动作都基于verified集合里的内容,推导路径的可追踪性会大幅提升。

4. 数学专项Agent的Prompt与调度策略

4.1 针对不同数学任务,设定不同的推理协议

不同数学任务适合的Agent策略完全不同。刚开始我试图用一套通用Prompt包打天下,后来发现这会导致大量无效动作。现在我会根据题型切换协议:

  • 数值计算题:强制要求代码输出,并指定精度范围,比如误差小于1e-8才算通过。
  • 符号推导题:强制拆成“主干结论”和“引理验证”两步,每个引理都要单独交给符号工具检查。
  • 证明题:要求Agent先列举需要的关键断言,再逐个确认断言之间的推导关系,最后才连成完整证明。
  • 应用题/建模题:先让Agent把实际问题转化成数学模型和变量定义,模型建完再做求解。

这些协议不是额外的提示装饰,而是给Agent划定了动作边界。协议越清晰,Agent在遇到歧义时就越不容易胡来。

4.2 多分支并行:让Agent同时尝试两条路,再合并证据

数学里经常出现“这条路能走通,但不知道哪条更快”的情况。这时候让Agent死磕一条路是很低效的,我通常采用多分支并行:让Agent针对当前问题同时生成两条独立解法,分别交给工具验证,然后再合并证据。

举例来说,一道数列极限题,一条思路是夹逼,另一条思路是Stolz定理。两个分支独立完成推导和数值验证后,如果结果一致,置信度会明显高于单条路。即便结果不同,也能为后续回滚提供更多线索。实现上不需要真的开并行线程,只需要在状态机里维护多个分支,让Agent每次决策时选择置信度更高的分支推进即可。

4.3 错误反馈与自修正循环的收敛条件

有了工具反馈,Agent的自修正才成为可能。但自修正如果不加边界,很容易变成无限循环。我的经验是提前定义好三条收敛条件:

第一,验证通过条件:最终答案被确定性工具验证,且每个关键中间断言都有对应检查。第二,证据耗尽条件:Agent尝试了N次修复,仍然返回工具报错,停止继续尝试,改为换工具或换解题方向。第三,反例确认条件:一个断言已被明确的数值反例推翻,标记为rejected,不再允许后续步骤依赖它。

没有这些条件,Agent会在同一条死路上反复打转,既浪费时间又消耗预算。有一回我忘了设最大动作数,它连续尝试了十几轮,每次都用几乎相同的错误代码重新提交,最后只是换了句提示词。收敛条件表面上只是控制机制,实际上是在帮Agent建立“什么算做完”的判断。

5. 落地避坑:评估、成本与结果判读

5.1 第一个坑:用“最终答案对”当唯一指标,你会被假话骗了

评估Agent效果时,最容易犯的错误是只看最终答案是否正确。我见过不少案例:Agent最终答案对了,但过程里已经有错误的中间步骤。如果只看结果,你会误以为这套流程很可靠,然后把它放到更复杂的题上,最终在某道题上彻底翻车。

更可靠的评估方式是给每个中间步骤评分:哪一步是工具验证通过的,哪一步是模型推断跳过的,哪一步存在反例。用一个“过程通过率”来评价Agent,比只对准答案更有指导意义。具体做法是,准备一份约30题的评测集,包含简单到困难不同难度的题,逐题记录Agent的每一步结果,人工复核后计算“关键步骤可验证”的比例。

5.2 第二个坑:Agent把问题拆得太碎,反而算得更差

另一类常见问题是过度分解。Agent为了追求“每一步都被验证”,把一道很简单的题拆成十多个子步骤,每次子步骤都调用模型重新生成。这会造成两个后果:模型在多次调用之间产生不连贯的假设,以及每步引入的微小误差不断累积,最终答案反而比不拆时更差。

我的经验是,让模型尽量负责策略性拆解,把重复性计算全部卸载给确定性工具。只在需要判断方向时调用模型,计算和化简直接用工具完成。拆解粒度控制在“人类数学草稿纸上的步骤”这个级别就好,不要细到每个变量赋值都单独去问模型。

5.3 第三个坑:工具报错被模型忽略后的补救策略

模型拿到工具报错后,最常见的反应是嘴上道歉,然后把同一段代码稍微改两个变量名再交上来。这个问题很隐蔽,因为报错信息已经被观察到了,但模型并没有真正理解报错的原因。

补救策略是:在观察结果里强制注入“你必须改变动作类型,禁止使用相同代码结构”,并且要求模型在下一轮先解释报错原因,再提交新代码。如果解释不清,就让它直接回到更早的状态点重推。这一招能把很多无效循环拦在早期。

5.4 评估成本与模型选型的平衡建议

数学Agent对模型推理能力的要求比普通对话高不少,但也没有必要每次都上最贵的模型。我的成本策略是:初始规划阶段用高能力模型把战略方向给定下来,后续验证和局部修正在一个能力中等但响应更快的模型上完成。整个Agent里,真正决定成败的往往不是模型有多强,而是验证工具链覆盖得有多全。

我还习惯在每道题结束之后,把“题目摘要、最终答案、验证工具返回、人工复核结论”压缩成一条记录存下来。攒够一二百条记录之后,就能看到哪些题型经常失败、哪些工具调用频率最高。这些数据才是下一轮迭代的真正依据。


最后分享一个我实际项目里特别受用的习惯:接到一道数学题时,不要先想着让Agent去“解题”,而是先确认它“能不能被验证”。如果一道题连可验证的中间结果都定义不出来,Agent的推理链条就很难监控。先把题的验证机制设计好,再让模型去发挥推导能力,这套从LLM到Agent的路才算真正走通。后续你可以尝试在这个骨架上加入更多工具,比如约束求解、图论枚举、形式化证明检查器,数学Agent的边界会继续往外扩一步,但核心的“思考—行动—观察—验证”闭环,始终是它最值钱的部分。

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

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

立即咨询