news 2026/10/7 5:09:45

教学智能体函数绘图实战:从模型随机生成到确定性代码执行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
教学智能体函数绘图实战:从模型随机生成到确定性代码执行

接手教学智能体这个项目的时候,我压根没把“画函数图”这个需求当回事。无非就是让学生输入一个函数,返回一张图嘛,在我原本的设想里,这撑死就是一个下午的活。结果真做起来才发现,这里面的坑深得离谱。从表达式解析报错,到图表中文全变方块,再到同一个问题每次画出来的图都不一样,足足折腾了快两天才把方案彻底跑稳。今天这篇就把完整的折腾过程、踩过的坑和最终定型的做法写下来,给正在做智能体、尤其是做教学类智能体的朋友一个参考。

1. 需求没那么简单:教学智能体要的不只是一张图

1.1 大模型会说图,但不会画图

学生问“老师,帮我画一下 y = x² - 2x + 3 的图像”,这事对大模型来说其实很尴尬。你让它用文字描述这个函数长什么样,它能说得头头是道:开口向上、对称轴 x=1、顶点坐标是 (1, 2)。但你要它真给你一张图,它做不到。

为什么?因为大模型的强项是语义理解和文本生成,不是数值计算和精确渲染。你可以让它在文本里近似描述一个图形,但它不具备“画布”的概念。即便现在很多多模态模型能生成图片,那也是“生成一张长得像函数图的图”,而不是“根据数学表达式精确渲染出正确的曲线”。我实测过几个声称支持绘图的多模态模型,让它们画同一条抛物线,有的顶点位置明显不对,有的甚至是随手画的波浪线,这在教学场景里是绝对不行的——学生是要拿这张图去对答案的,图像差一个像素就可能造成误导。

所以教学智能体里的“画函数图”,从根上就不能指望大模型直接“画”,必须把它拆成“模型理解用户意图 + 程序执行绘图”两步。谁负责思考,谁负责计算,从一开始就要分清楚。

1.2 三条技术路线的性价比对比

在动手写代码之前,我把能想到的实现路线全部列了一遍,大概有下面三条:

路线开发成本覆盖能力聊天窗口展示稳定性
静态图片库 + 检索匹配低很差好极高
第三方图表服务 API中中差中
本地/云端执行代码绘图中高高好中高

先说第一种,静态图片库。这个思路是预先渲染好所有常见函数图像,比如一次函数、二次函数、反比例函数、三角函数等等,然后让大模型根据用户问题去匹配对应的图片。听着很省事,但覆盖能力实在堪忧。教学场景里学生问的问题千变万化,“带参数的函数”、“两个函数的交点”、“自定义定义域的函数”,每一种变化都得预生成一张图,这基本上是不可能的。我自己试了一个下午就放弃了,因为光是二次函数 y = ax² + bx + c,a、b、c 稍微变几个值,图像就完全不同,静态库根本无法穷举。

第二种,调用第三方图表服务。丹斯莫这类在线图表工具确实画得好,但接 API 有几个麻烦:一是函数表达式要转换成人家规定的语法格式,出错了很难排查;二是返回结果往往不是一张可以直接塞进聊天窗口的 PNG 图片,而是一段参数拼接的 URL 或者交互组件,在消息流里展示很别扭。而且每次画图都要发起外部网络请求,链路一旦不稳定,教学现场就卡住了。

第三种,也是我最终选的路线——在可控环境里执行 Python 代码,用 matplotlib 这类库把函数图像渲染出来。它的优势很直接:完全可控、可定制,样式想怎么调就怎么调;渲染结果是标准的图片文件,聊天窗口直接就能展示;而且现代智能体平台大多提供了代码节点或工具调用机制,落地不算难。缺点也有,就是你必须解决两件事:代码能不能安全执行,以及代码能不能稳定生成。这两个问题,就是后面两天折腾的核心。

2. 为什么代码执行成了唯一靠谱的方案

2.1 大模型直接画图的三重限制

我一开始也抱着侥幸心理,想看看有没有办法让大模型输出一个“自包含”的图像描述,比如 SVG 或者 MathML,然后前端渲染。试了一圈,结论是这条路比想象中更不好走。

第一重限制是数值精度。SVG 是矢量格式,理论上可以描述一条精确的抛物线路径,但那是建立在“模型知道这条曲线的每个关键点坐标”的前提下。实际上,大模型让我手写路径控制点,它给的坐标往往只能让曲线“看起来像那么回事”,真要放大检查,顶点位置、曲率、渐近线全是偏的。数学函数图像最讲究的就是数值关系,曲线偏移几个像素,函数性质就变了。

第二重限制是连续性问题。函数图像往往涉及大量密集采样点,尤其是处理快速变化的区域。让模型生成几十个坐标点本身就很吃力,更别说几十万个点。你不可能靠“生成点列表”这种模式画出一张光滑的 y = sin(x) 图像,模型的上下文窗口和输出长度都撑不住。

第三重限制是规则遵守。模型就算承诺“我输出 SVG”,在长输出过程中也容易漂移,一会儿忘记加命名空间,一会儿把颜色值写错,一会儿在路径里混入非法字符。这些错误在聊天场景里还凑合能忍,在精确绘图场景里就是硬伤。

所以结论很明确:想让智能体稳定地画出函数图,必须把“绘图”这件事从大模型的能力范围里拿出来,做成一个确定性的程序能力。

2.2 代码执行架构:把“思考”和“计算”分开

代码执行方案的核心思想,简单说就是“各司其职”。

大模型只做自己擅长的事:理解用户的自然语言请求,判断用户想问的函数是什么、有没有特殊的定义域要求、需不需要标注某些特殊点。然后把这些信息转化成结构化的绘图请求。

执行引擎做自己不擅长但很擅长的事:拿到这个结构化请求后,解析数学表达式、在定义域内采样、处理奇点和边界、调用绘图库渲染图片、返回图片链接。这些步骤全部由确定性代码完成,每一步都可复现、可调试、可校验。

我当时用的是主流智能体平台的代码节点。说白了,平台提供一个沙箱环境让你写 Python 代码,然后把这个能力封装成一个工具。用户问函数图,智能体先判断该调用哪个工具,再把关键参数填进去,代码节点负责跑出图片。整个链路听起来很简单,但实际踩坑的过程一点不简单。

真正帮我把思路拽回来的是后来一个习惯:凡是能被写成确定性代码的逻辑,绝不让大模型自由发挥。模型只出参数,代码来出结果。这个原则贯穿了后续所有设计。

3. 两天里真实踩过的坑,我按时间顺序记下来了

3.1 第一轮翻车:表达式解析和数学记号冲突

第一个让我心态崩掉的坑,是表达式的解析。学生写“y = x^2”是天经地义的数学记法,但 Python 根本不认识这个写法,因为^在 Python 里是按位异或运算符。于是智能体第一次执行绘图代码的时候,直接抛了一个 SyntaxError,学生看到的是“抱歉,我暂时无法生成图像”。

一开始我还以为是模型生成的代码不稳定,就多调了几次,结果发现就算模型自己把^换成**,也还会遇到新的问题。比如学生写“y = 2x + 1”,Python 里必须写成2 * x + 1,那个省略掉的乘号,模型偶尔会补上,偶尔就意识不到。还有学生写“y = sin(2x)”,如果直接扔给math.sin,连隐式乘法带函数前缀一起处理,麻烦更大。

排查的过程其实有点绕。我先打印出了模型生成的 Python 代码,发现它自己在做各种字符串替换和拼装,但规则总是覆盖不全。后来我意识到问题根源出在架构上:你让模型自己处理数学记号和 Python 语法的转换,等于让它做一件容错率极低的事。正确的解法是把这个转换逻辑写死在代码里,用规则和解析库去兜底。

最终我引入了sympy这个符号计算库,配合几个正则替换规则,专门处理数学表达式的规范化。比如把^换成**,用正则匹配数字后直接跟字母或括号的情况补上乘号。经过规范化之后,无论是x^2、2x还是sin(2x),都能被正确解析成 Python 可执行的表达式。

3.2 第二轮翻车:中文字体全变方块

表达式问题解决了之后,图像终于能画出来了。结果第二张图就让人哭笑不得——函数曲线是对的,但图例、标题全部变成了一个个小方块。说白了就是 matplotlib 默认字体不支持中文,中文全部渲染成了“□□□”。

这个坑最折磨人的地方在于:我在本地调试的时候完全正常,因为本地系统里装了中文字体;但一旦部署到平台的代码节点里,运行环境是最小化的 Linux 容器,系统里压根没有 SimHei、微软雅黑这类中文字体。所以图表里的“二次函数”四个字,全变成了方框。

我第一次尝试是直接在代码里指定plt.rcParams['font.sans-serif'] = ['SimHei'],但代码节点环境里没有这个字体,指定了也白搭。后来我查了容器的系统字体目录,发现里面只有一个特别朴素的字体,而我又没有权限随便往系统里塞字体文件。最后采用了一个务实的方案:图片内部尽量不出现中文,标题和图例全用英文或者纯数学符号。比如标题就写y = x^2 - 2*x + 3,图例写f(x),这些英文字符在任何 Linux 字体下都能正常显示。至于中文解释,放在图片前后的大模型回复文本里,反正模型生成文字又不费劲。

这里顺手还解决了一个跟中文字体相关的小问题:负号的显示。matplotlib 默认的字体里,负号有时候会显示成一条横杠甚至乱码,需要设置plt.rcParams['axes.unicode_minus'] = False,才能保证坐标轴上的负数正常显示。这个问题如果不注意,画出来的图特别掉价。

3.3 第三轮翻车:坐标轴范围让图像完全走样

字体问题解决了,第三轮打击来自坐标轴范围。

我一开始的绘图脚本非常耿直:拿到函数表达式,从 x = -10 到 x = 10 采样,然后直接画。遇到 y = x² 这种函数没问题,但遇到 y = 1/x 这种在 x=0 处有奇异点的函数,直接在 x=0 附近取到无穷大,画出来的图像包含一条几乎垂直的直线,把整个图都带偏了。还有 y = tan(x),本来渐近线之间那些规律性的曲线,会因为绘制范围太宽而糊成一团,图像完全没法看。

这个问题的本质,是数学函数的数值特征和线性采样之间的矛盾。你不知道用户会输入什么函数,也不知道它的值域宽到什么程度,如果让模型自己决定坐标范围,它给出的数值经常和函数实际形态对不上。

我的处理方式分成两步。第一步,在做数值计算的时候用numpy算出一整组采样点,然后把超过阈值(比如 |y| > 10⁶)的点直接记为 NaN,这样这些点就不会被画出来,也就不会产生那种吓人的竖直断层线。第二步,在计算完有效点之后,根据实际 y 值的最小值和最大值,自动微调纵轴范围,避免图像离坐标轴太远或者太挤。这套逻辑用在 1/x、tan(x) 这类带有奇异点的函数上,效果立竿见影。

顺带还处理了一个相关需求:有时候用户会指定“只看 x 从 0 到 5”的范围。如果模型都按默认的 -10 到 10 画,用户会觉得智能体根本没听懂。所以我把 x 的范围也做成了参数,模型可以从对话里提取用户指定的定义域,提不出来才用默认值。

3.4 第四轮翻车:模型自由生成代码的不确定性

前三轮坑虽然费时间,但至少思路清晰。真正让我差点推倒重来的,是模型的“自由发挥”。

最初我的方案是:大模型根据用户的函数请求,自行生成一段完整的 matplotlib 绘图代码,然后交给执行节点跑。你猜怎么着?同一个问题,连问三次,模型给出了三种完全不同风格的代码。第一次是plt.plot(x, y),第二次是ax.plot(x, y),第三次干脆忘了保存图片,直接调用了plt.show()。在无图形界面的服务器环境里,plt.show()什么都不返回,图像文件根本没生成,智能体对外只能说一句“绘图失败”。

更离谱的是,有一次它在代码里把import numpy as np写成了import nupy as np,执行直接崩掉。这种错误在本地调试时完全复现不出来,因为它每次生成代码都是重新采样的。

到那个时候,我终于做了一个决定:不再让模型写代码了。

这个决定的背后,是对大模型能力边界的重新认识。模型生成代码的能力确实强,但它不具备“稳定输出同一套代码风格”的承诺。在交互式编程场景里,这种灵活性是优点;但在生产链路里,这种随机性会变成事故源。我需要的是稳定:同一个函数,今天画是这样,明天画还是这样。

4. 最终方案:只让模型填空,不让模型写代码

4.1 工具参数协议设计

第四轮翻车之后,我把整个实现思路推倒重来,最后定下来的方案,核心就一句话:模型只输出一套结构化的绘图参数,绘图逻辑全部由固定代码完成。

这套参数我设计成了 JSON 格式,长这样:

{ "expression": "x**2 - 2*x + 3", "x_min": -5, "x_max": 5, "grid": true, "title": "y = x^2 - 2x + 3", "show_axis": true }

字段解释一下。expression是经过规范化处理的函数表达式,必须能被sympy正确解析;x_min和x_max是横轴范围,用户没指定时用默认值 -10 到 10;grid控制是否显示网格线;title只是展示用的标题,可以是纯数学写法;show_axis控制是否显示坐标轴参考线。

这里最关键的是expression这个字段。模型不需要自己拼 Python 代码,它只要从用户的自然语言里把“数学表达式”这个要素提取出来,再转成规范格式就行。比如用户说“帮我画二次函数 y=x²-2x+3”,模型只需要输出上面那个 JSON。

我一开始还担心模型会把表达式里的中文或者特殊符号带进来,实际测试发现,只要在提示词里明确“只输出 JSON 对象,不要输出任何其他内容”,绝大多数情况下模型都能很好地按要求填空。就算偶尔输出格式非法,也可以做一层容错解析,而不是直接让整个链路崩溃。

4.2 固定绘图引擎的核心实现

参数协议定了,剩下的工作就是写好那个固定的绘图引擎。下面是我简化后的核心代码,算是整个方案最能抄作业的部分:

import matplotlib matplotlib.use('Agg') # 无图形界面环境下必须使用非交互式后端 import matplotlib.pyplot as plt import numpy as np import sympy as sp import re def normalize_expression(expr: str) -> str: """数学表达式规范化:把数学记号转成 Python 可执行的形式。""" expr = expr.strip() expr = expr.replace('^', '**') # 处理隐式乘法:数字后直接跟字母/括号,或者右括号后直接跟数字/字母 expr = re.sub(r'(\d)([a-zA-Z(])', r'\1*\2', expr) expr = re.sub(r'([a-zA-Z)])(\d)', r'\1*\2', expr) return expr def plot_function(expr: str, x_min: float, x_max: float, title: str, grid: bool = True, show_axis: bool = True, output_path: str = 'function_plot.png'): expr = normalize_expression(expr) x = sp.Symbol('x') try: f = sp.lambdify(x, sp.sympify(expr), modules=['numpy']) except Exception as e: raise ValueError(f"表达式解析失败: {e}") xs = np.linspace(x_min, x_max, 2000) ys = f(xs) # 处理奇异点:把绝对值过大的值置为 NaN,避免画出竖直断线 ys = np.where(np.abs(ys) > 1e6, np.nan, ys) # 如果全为 NaN,则说明范围内没有有效点,给个默认范围避免空图 if not np.isfinite(ys).any(): ys = np.zeros_like(xs) plt.figure(figsize=(8, 6)) plt.plot(xs, ys, linewidth=2.5) if show_axis: # 画出 x 轴和 y 轴的参考线 plt.axhline(0, color='black', linewidth=0.8) plt.axvline(0, color='black', linewidth=0.8) if grid: plt.grid(True, linestyle='--', alpha=0.6) plt.title(title) # 保证负号正常显示 plt.rcParams['axes.unicode_minus'] = False # 自动调整纵轴范围:只用有效点来算 finite_ys = ys[np.isfinite(ys)] if len(finite_ys) > 0: y_min, y_max = np.min(finite_ys), np.max(finite_ys) margin = (y_max - y_min) * 0.1 if y_max != y_min else 1.0 plt.ylim(y_min - margin, y_max + margin) plt.savefig(output_path, dpi=150, bbox_inches='tight') plt.close() return output_path

这段代码里,有四个地方是整个方案稳定性的关键。

前面已经反复说的规范化函数,是所有解析问题的总闸门。re.sub处理隐式乘法的时候,用正则区分“数字后跟字母或左括号”和“右括号后跟数字或字母”。这个写法简单,测试下来能覆盖学生输入里九成以上的写法。

那个sp.lambdify(x, sp.sympify(expr), modules=['numpy'])是核心中的核心。sympify把表达式文本转成 SymPy 的符号表达式对象,lambdify再把符号表达式变成一个可调用的 NumPy 函数。这个组合的好处是,无论表达式里带不带三角函数、对数函数,它都能自动映射到 NumPy 对应的函数实现,不需要我手动写一大串if判断。

奇异点处理也非常重要。学生输入y=1/x的时候,如果不做掩膜,x=0附近会产生一个无穷大的值,图像就是一条凌乱的竖直线。np.where(np.abs(ys) > 1e6, np.nan, ys)这行代码会把超出合理范围的点全部丢掉,曲线在奇异点附近自然断开,这才是数学上正确的画法。

自动纵轴范围这段代码,是海量调试之后加进去的。最开始图的上下边缘经常贴住曲线,或者顶部空一大片,学生看着很别扭。有了finite_ys这个变量专门过滤掉 NaN 点,再留出 10% 的边距,图的视觉效果稳定了很多。

4.3 安全性与执行性能的注意点

让智能体执行代码,安全性一定是逃不开的问题。我虽然把生成完整代码的权力收了回来,但expression本质上还是由模型输出的,和恶意输入也只是一线之隔。所以执行环境必须做安全兜底。

如果是在 Coze、Dify 这类平台内置的代码节点里跑,平台本身已经做了沙箱隔离,我只补充了两件事:一是对表达式字符串的长度做限制,超过 200 个字符直接拒绝,防止有人用超长表达式消耗计算资源;二是在sympify之前加一层简单的关键字检查,拦截import、open、exec这类明显的危险操作。

如果是自己部署服务,一定要把绘图进程关在 Docker 容器之类的最小化环境里面,只给它必要的依赖库,不给它访问外部网络的能力。毕竟教学场景面对的可能是未成年人,安全上多谨慎都不为过。

性能方面,2000 个采样点对 matplotlib 来说是很轻的负载,单张图渲染耗时基本在几百毫秒以内。真正占用时间的通常是大模型生成 JSON 参数的过程,但那本身就不是这段代码能优化的,交给智能体平台去处理就好。

5. 复盘与建议:给教学类智能体开发的几条实在经验

5.1 对“模型随机性”的再认识:能少写一行代码就少写

这次折腾最大的教训,不是哪一个技术点,而是对模型能力边界的一次重新校准。模型很强,但它的强是“生成内容”的强,不是“保证输出确定性”的强。尤其是当你需要连续几十次、几百次调用都保持输出格式一致时,模型的自由生成能力就成了风险源头。

说白了,让大模型写 Python 代码,它能写好,但它写一百次可能有一百种风格。在教学智能体里,我们需要的是稳定,不是创意。凡是能被规则定义清楚的逻辑,一律从模型能力里拿出来,用代码固定住。模型只处理语义理解层面的事,也就是“用户到底想画什么、用户有没有提特殊要求”。输出端尽量收敛成填空题,最好收敛到只有一个 JSON 对象。

这次演示的函数图功能,已经是把“模型参与度”压缩到了最低:模型输入是自然语言,输出是 JSON 参数,中间的表达式规范化、曲线采样、坐标范围计算、图片渲染全部是确定性代码。这样设计之后,同一个函数无论问多少遍,画出来的图都是一样的。

5.2 给教学场景智能体的三条避坑建议

第一条建议,是做好函数表达式的容错。教学场景里,学生输入的表达式五花八门,有从教材抄的,有从搜索框复制的,有自己随手打的。对输入做过度的严格要求是不可能的,只能靠解析层去兜底。正则替换加符号计算库,是目前我试过最有效的组合。

第二条建议,是图片之外一定要保留原始数据。我自己踩过一次:学生问“这个函数和 y=x 的交点在哪里”,智能体画了两条曲线,学生盯着图看了半天,还是不知道具体坐标。后来我在工具设计时加了“附加计算”的字段,函数图生成之后,智能体可以额外调用一段求解交点的代码,把坐标值返回在文字里。图是给人看的直观信息,具体数值才是教学里要落地的答案。

第三条建议,是记录每一次绘图请求的完整日志。不要小看这个动作。以前没有日志的时候,学生反馈“智能体画的图好像不对”,我只能干瞪眼,因为没有证据去复现他到底输入了什么、模型当时提取了什么参数。加了日志之后,每次请求的原始文本、规范化后的表达式、最终渲染的图片路径都记录下来,大部分“图不对”的问题,肉眼一看日志就能定位到底是模型提取错了参数,还是绘图代码本身做了错误的数学假设。

5.3 后续还能怎么扩展

函数图这个功能跑稳之后,我又想到了几个自然而然的扩展方向。

第一个是图像叠加。现在只是画单个函数,但教学里经常需要对比函数图像,比如“画一下 y=x² 和 y=x³ 的图像对比”。这个只需要让参数协议里支持一个表达式数组,固定绘图引擎去循环绘制,模型那边要做的只是把多个表达式都提取出来,难度不大。

第二个是交互式坐标点标注。比如用户问“这个函数的顶点在哪里”,智能体可以额外返回一个顶点坐标,然后在图上用红点标出来。这个功能需要绘图引擎在画完主曲线之后,根据传入的特殊点数组追加散点图。从模型角度说,只是多输出一个special_points字段的事。

第三个是动图支持。像“感受一下 y = ax² 当 a 从 0.1 变到 2 时图像怎么变化”这种需求,静态的单一图片已经不够了,得输出一组图片甚至 GIF 动画。这个让代码生成多帧图片再合成就行,模型的职责依然是输出参数列表。

好,这篇折腾记录就写到这里。其实最想分享给各位的,不是那几百行 Python 代码,而是“所有教学功能都要以稳定为前提”这个贯穿始终的原则。学生看到的每一次结果,都是在给智能体的能力打分,交错一次图,可能就失去一份信任。先把确定的部分全部用代码锁死,再让模型去处理它真正擅长的语言理解,这才是教学智能体能越用越好用的根基。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 5:09:44

DocResearch 实战:Agent Loop 与 Agentic RAG 构建带引用的调研报告

1. 从一条命令说起:DocResearch 到底在解决什么问题第一次看到 DocResearch 这个项目名,我脑子里蹦出来的画面特别具体:深夜十一点,你对着一个空白的 Markdown 文件,手边开着十几个浏览器标签页,PDF、网页、…

作者头像 李华
网站建设 2026/10/7 5:09:16

AI日报生产全流程:从信息筛选到自动化工具链的工程实践

1. 一份AI日报背后到底在解决什么问题每天早上打开手机,AI圈的信息像洪水一样涌过来:某实验室放出新模型、某大厂开源了工具链、某篇论文在圈内刷屏、某个产品悄悄更新了定价。信息本身不缺,缺的是"今天到底哪几条跟我有关"。我做A…

作者头像 李华
网站建设 2026/10/7 5:09:16

具身智能嵌入式开发必学:C语言动态数组原理与工程实现

如果问一个打算入行具身智能的人,学习路线应该从哪里开始,网上十有八九会告诉你先学Python和PyTorch。这个答案不能算错,但你真跑到实验室里,面对一台机械臂、一块主控板、一堆传感器,就会发现另一套逻辑:底…

作者头像 李华
网站建设 2026/10/7 5:08:51

Java面试官拆解简历优化:技术栈分级、项目量化、基础自测

做了多年Java面试官,每年经手筛掉的Java简历少说也有上千份。最近又在集中筛选简历,发现一个老问题依然普遍:技术栈堆了十几行,项目经历却只有三五行,一问关键技术点就支支吾吾。说实话,很多候选人不是技术…

作者头像 李华
网站建设 2026/10/7 5:06:47

用scrcpy与ADB搭建免费多手机群控投屏工作台

做设备批量管理或者电脑端同时盯多台手机的时候,很多人第一反应是买一堆昂贵的硬件投屏盒子,或者去用那些收费的云控平台。其实还有一条更轻量、更隐蔽、完全免费的路子,就是直接撸起袖子用开源工具自己搭一套。这篇文章就是专门讲这个的&…

作者头像 李华