ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

扣子工作流实战:智能体画函数图的三层架构与避坑指南

扣子工作流实战:智能体画函数图的三层架构与避坑指南 1. 为什么“画函数图”这件事值得折腾两天1.1 一个看似简单的需求背后藏着三层坑“让智能体画函数图”这句话第一次听的人多半会觉得没什么难度——不就是给个大模型让它输出一段绘图代码然后跑一下吗我一开始也是这么想的。结果从接到这个需求到最终跑通前后搭进去将近两天。踩的坑分布在三个层面意图识别层、代码生成层、渲染回传层。任何一层没处理好用户看到的要么是一段没法运行的代码要么是一张糊到看不清坐标轴的图片要么干脆就是一句“抱歉我无法绘制图像”。先把场景说清楚。我做的这个教学智能体面向的是中学数学和大学基础课的学生。用户输入类似“画一下 y sin(x) 在 -2π 到 2π 的图像”或者“帮我看看这个二次函数开口方向”智能体需要理解意图、生成绘图代码、执行代码、把图片返回给用户最好还能顺带解释一下图像特征。这个链路里扣子Coze工作流负责编排代码节点负责执行 Python 绘图逻辑插件负责图像理解最后通过对话把结果吐出来。为什么值得写一篇复盘因为我在搜索相关资料时发现大部分教程停留在“怎么建一个 Bot”的层面真正讲到“让 Bot 稳定输出函数图”这种具体能力的少之又少。而这类需求在教学、数据分析、工程汇报里又特别常见。所以我把这两天的折腾过程完整拆开包括方案选型、参数计算、代码细节、排查记录希望能让后来的人少走弯路。1.2 适合谁来参考这篇复盘如果你属于下面几类人这篇内容应该能直接抄作业正在用扣子或类似平台搭建教学类、答疑类智能体需要让智能体具备“画图”能力已经能让智能体生成代码但卡在“代码跑不起来”或“图片传不回来”想搞清楚工作流里代码节点、插件节点、大模型节点各自该承担什么职责对智能体开发有兴趣想通过一个具体案例理解“平台搭建”和“纯 Python 搭建”的差异。我尽量不堆术语遇到必须解释的概念会用生活化的类比说清楚。下面从整体设计思路开始讲。2. 整体方案设计与选型考量2.1 为什么最终选了“工作流 代码节点”而不是纯提示词最开始我试过最省事的办法直接在 Bot 的提示词里写“当用户要求画函数图时请输出一段 Python 代码用户可自行运行”。实测下来这个方案在教学场景里基本不可用。原因有三个。第一用户不会运行代码。我的目标用户是学生他们期待的是“输入一句话看到一张图”而不是拿到一段代码再去本地配环境。第二大模型输出的代码不稳定。同一个“画 sin 函数”的请求十次里可能有三次忘记导入 matplotlib两次把linspace的参数写反还有一次用了不存在的库。第三无法保证图像质量。没有统一的坐标轴、标题、网格设置出来的图五花八门。所以纯提示词方案只能作为“兜底”真正要稳定必须把绘图逻辑固化到工作流里。工作流的好处是流程确定、参数可控、每一步的输入输出都能调试。大模型只负责“理解用户想画什么”把自然语言转成结构化参数剩下的交给代码节点执行。这就是我最终采用的架构。2.2 三层架构意图解析、代码执行、结果回传整个链路我拆成三层每层职责清晰方便单独排查问题。第一层意图解析层。由大模型节点承担。用户说“画个抛物线 y 等于 x 平方减 2x 加 1”大模型要把它转成结构化数据比如{function: x**2 - 2*x 1, x_min: -5, x_max: 5, title: 二次函数图像}。这里的关键是约束输出格式我用了 JSON Schema 来强制大模型按字段返回避免它自由发挥。第二层代码执行层。由代码节点承担。拿到结构化参数后用 Python 的 matplotlib 或 numpy 生成图片。这一层是纯确定性逻辑不涉及大模型所以稳定性最高。代码节点里我会做参数校验、异常捕获、图片保存。第三层结果回传层。把生成的图片以合适的方式返回给用户。扣子里图片可以通过文件变量或 URL 传递我实测下来用“上传到资源库再返回链接”的方式最稳直接返回 base64 在某些客户端会显示异常。这三层里第一层最容易出问题因为大模型的输出有随机性第二层最考验代码细节第三层最容易被忽略但恰恰是“用户能不能看到图”的关键。2.3 平台搭建与纯 Python 搭建的差异搜索热词里有人问“利用平台构建的智能体与用 Python 构建的智能体有什么不一样”我借着这个项目说说体会。纯 Python 搭建比如用 FastAPI 加 OpenAI SDK自由度最高任何逻辑都能自己写但你要自己处理对话管理、上下文、部署、并发。平台搭建比如扣子把对话管理、插件生态、工作流编排都封装好了你专注在业务逻辑上。代价是有些底层能力受平台限制比如代码节点的运行时长、可用库的版本、文件传递方式。具体到“画函数图”这个需求平台方案的优势是快——我半天就能把工作流搭起来劣势是调试信息有限代码节点报错时日志不如本地详细。所以我的做法是先在本地用 Python 把绘图逻辑调通确认参数和输出没问题再原样搬到代码节点里。这样能把“平台问题”和“代码问题”分开排查效率高很多。3. 核心细节解析与实操要点3.1 意图解析怎么让大模型稳定吐出结构化参数这是整个项目里最花时间的地方。大模型很聪明但也很“随性”。你让它输出 JSON它可能给你包一层 markdown 代码块可能多加几个字段也可能把x_min写成xmin。我的解决办法是三重约束。第一重在系统提示词里明确字段和类型。我会写清楚“你必须返回一个 JSON 对象包含 function字符串Python 表达式、x_min数字、x_max数字、title字符串四个字段不要输出任何其他内容。”第二重用平台的结构化输出能力。扣子的部分模型节点支持指定 JSON Schema开启后模型会严格按 schema 返回。这个功能一定要用能省掉大量解析异常。第三重代码节点里做容错解析。即使前面两层都做了我还是会在代码里加一段“清洗”逻辑去掉可能的 markdown 标记、把常见错误字段名映射到正确字段、给缺失字段填默认值。比如用户没说范围我就默认-10到10。这里有个经验不要让大模型直接输出 Python 代码。我试过让大模型生成完整绘图代码结果它经常在字符串里用中文引号或者把plt.show()和plt.savefig()混用导致代码节点执行失败。改成“大模型只输出参数代码节点用固定模板绘图”之后稳定性从大概七成提升到接近百分之百。3.2 绘图代码的关键参数与计算过程绘图代码看着简单但要让图“好看且正确”有几个参数必须算清楚。采样点数量。画函数图本质是把连续函数离散成有限个点再连线。点太少曲线会变成折线点太多计算慢且文件大。我的经验值是1000 到 2000 个点。对于sin(x)这种平滑函数1000 个点足够对于有剧烈变化的函数比如tan(x)在渐近线附近需要更多点但同时要做断点处理否则会出现竖直的连线。坐标范围。用户给的范围要校验。如果x_min x_max要交换或报错。范围过大比如-10000到10000会导致大部分区域是平的这时候我会提示用户缩小范围或者自动裁剪到函数有意义的区间。y 轴范围。这是容易被忽略的点。如果函数有极值比如y 1/x在 0 附近趋于无穷自动的 y 轴范围会把图压扁。我的做法是计算 y 值后用分位数裁剪比如取 1% 和 99% 分位作为 y 轴上下限把极端值截掉保证主体曲线清晰。断点检测。对于tan(x)、1/x这类有间断点的函数相邻两点的 y 值差如果超过某个阈值比如整体 y 范围的 50%就在这两点之间断开不连线。这个逻辑我用 numpy 的diff实现效果很好。下面是我代码节点里的核心片段可以直接参考import numpy as np import matplotlib matplotlib.use(Agg) # 无界面环境必须设置 import matplotlib.pyplot as plt def plot_function(func_str, x_min, x_max, title): # 采样 x np.linspace(x_min, x_max, 1500) # 安全求值只允许数学函数 safe_dict {k: getattr(np, k) for k in [sin,cos,tan,exp,log,sqrt,abs]} safe_dict[x] x y eval(func_str, {__builtins__: {}}, safe_dict) # 断点处理 dy np.abs(np.diff(y)) threshold (np.nanmax(y) - np.nanmin(y)) * 0.5 y_plot y.copy() y_plot[np.where(dy threshold)[0] 1] np.nan # 绘图 fig, ax plt.subplots(figsize(8, 5), dpi120) ax.plot(x, y_plot, color#2563eb, linewidth2) ax.axhline(0, colorblack, linewidth0.8) ax.axvline(0, colorblack, linewidth0.8) ax.grid(True, linestyle--, alpha0.4) ax.set_title(title, fontsize14) ax.set_xlabel(x) ax.set_ylabel(y) fig.tight_layout() fig.savefig(/tmp/function_plot.png) return /tmp/function_plot.png这段代码里matplotlib.use(Agg)是必须的因为代码节点通常没有图形界面不设置会直接报错。eval的安全处理也很重要我限制了内置函数只放行数学函数避免执行恶意代码。3.3 图片回传为什么 base64 经常显示不出来图片生成后怎么让用户看到这一步我卡了最久。最开始我把图片转成 base64 字符串直接塞进对话返回。本地测试没问题但换到实际客户端图片要么不显示要么显示成一段乱码文本。后来我改成先上传到平台资源库拿到文件 ID 或 URL再返回。扣子里有对应的文件上传能力代码节点生成图片后通过 API 上传拿到链接最后在回复里用 markdown 图片语法展示。这个方案稳定得多。还有一个细节图片格式和大小。PNG 清晰但文件大JPG 小但函数图有文字压缩后容易糊。我最终用 PNGdpi 设 120一张图大概 100 到 200 KB兼顾清晰度和传输速度。如果平台对文件大小有限制可以降到 dpi 100。提示代码节点的临时文件路径在不同环境可能不同建议用系统临时目录如/tmp并确保有写权限不要硬编码某个绝对路径。4. 实操过程与核心环节实现4.1 从零搭建工作流的完整步骤我把整个搭建过程按顺序列出来你可以照着做。第一步创建 Bot 并定义人设。在扣子里新建一个 Bot人设写清楚“你是一个教学助手擅长解释函数图像”。这一步影响的是对话风格不影响绘图能力。第二步创建工作流。工作流里放三个节点大模型节点意图解析、代码节点绘图、插件节点可选图像理解。节点之间用变量连接。第三步配置大模型节点的输出格式。在提示词里写清楚字段要求并开启结构化输出。如果平台支持直接贴 JSON Schema。第四步写代码节点的逻辑。把上面那段绘图代码放进去注意输入变量名要和上游节点输出对应。代码节点里加 try-except任何异常都返回一个友好的错误信息而不是让工作流直接崩掉。第五步配置文件回传。代码节点输出图片路径后接一个上传节点或插件把图片转成可访问的链接。第六步联调测试。用几个典型输入测试简单多项式、三角函数、有断点的函数、用户没说范围的情况。每个 case 都记录输入和输出方便定位问题。4.2 参数计算实例以 y x² - 2x 1 为例拿一个具体例子走一遍看看参数怎么定。用户输入“画一下 y x² - 2x 1”。大模型解析后返回{function: x**2 - 2*x 1, x_min: -5, x_max: 5, title: 二次函数 y x² - 2x 1}。这里 x 范围是我在提示词里设的默认值因为用户没说。代码节点拿到后生成 1500 个点x 从 -5 到 5步长约 0.0067。计算 y 值最小值出现在 x1 处y0x-5 时 y36x5 时 y16。y 的范围是 0 到 36。如果直接按这个范围画曲线在 x1 附近会显得很平因为 y 轴被 36 撑开了。这时候分位数裁剪就派上用场取 y 的 1% 到 99% 分位大概会裁到 0 到 30 左右主体曲线更清晰。当然对于这种简单函数也可以不做裁剪让用户看到完整范围。我的策略是如果 y 的极值超过中位数绝对偏差的 10 倍就裁剪否则保留。最终生成的图里坐标轴、网格、标题齐全用户一眼就能看出这是个开口向上的抛物线顶点在 (1, 0)。4.3 实操现场一次完整的调试记录记录一次典型的调试过程让你感受下排查思路。现象用户输入“画 tan(x)”返回的图中间有一条竖直的线把渐近线连起来了。排查先看代码节点日志发现 y 值里有极大的数比如 1e15这是 tan 在 π/2 附近的值。相邻两点的 y 差超过了阈值但我的断点逻辑写的是dy threshold而 tan 的跳变是从正极大到负极小diff是负的取绝对值后确实超阈值应该被置为 nan。检查代码发现我置 nan 的位置是np.where(dy threshold)[0] 1这个逻辑对但 threshold 用的是nanmax - nanmin而 nanmax 本身就是 1e15导致 threshold 巨大反而没触发。解决改用稳健统计量用np.nanpercentile(y, 99) - np.nanpercentile(y, 1)作为阈值基准这样极端值不会影响阈值。改完后tan 的渐近线正确断开。经验处理含极端值的函数时永远不要用 max/min 做阈值要用分位数。这个坑我在数据分析里踩过没想到画图又踩一次。5. 常见问题与排查技巧实录5.1 高频问题速查表问题现象可能原因排查方法解决方案代码节点报 ModuleNotFoundError环境缺少 matplotlib/numpy看日志具体缺哪个库平台一般预装常用库缺的话换用内置库或联系平台图片返回但显示为空白文件路径错误或权限不足检查代码节点输出路径是否存在用系统临时目录确保写权限大模型输出无法解析为 JSON提示词约束不够或模型随机性打印大模型原始输出开启结构化输出代码里加清洗逻辑曲线出现竖直连线断点未处理检查 y 值是否有极端跳变用分位数阈值做断点检测图片模糊dpi 太低或格式压缩检查 savefig 参数dpi 设 120 以上用 PNG 格式用户输入中文函数表达式大模型未转成 Python 语法看解析后的 function 字段提示词里要求转成 Python 表达式如“平方”转“**”工作流超时采样点过多或计算复杂看代码节点耗时降到 1000 点避免复杂符号运算5.2 独家避坑技巧技巧一本地先跑通再搬平台。平台代码节点的调试体验远不如本地 IDE。我习惯在本地 Jupyter 里把绘图逻辑、异常处理、边界情况全部测一遍确认无误后原样复制。这样能把问题范围缩小到“平台配置”而非“代码逻辑”。技巧二给大模型留“退路”。提示词里加一句“如果用户输入无法解析为函数返回 function 字段为空字符串”。代码节点检测到空字符串就回复“抱歉我没理解这个函数请换一种说法”。这比让大模型硬编一个错误表达式要好。技巧三图片命名带时间戳。多个用户并发时如果都用同一个文件名会互相覆盖。我在代码里用time.time()生成唯一文件名避免串图。技巧四限制函数复杂度。用户可能输入sin(x)/x exp(-x)*cos(10*x)这种复杂表达式计算量大且容易出数值问题。我在代码里加了表达式长度限制和计算超时保护超过就返回简化建议。技巧五日志要打全。代码节点里把输入参数、中间结果、异常信息都打到日志里。平台日志查看不方便但出问题时有日志和没日志的排查效率差好几倍。5.3 关于“智能体面试”和“智能体架构”的延伸思考搜索热词里有“智能体面试”和“智能体架构”我借着这个项目说两句。面试里如果被问到“你怎么设计一个能画图的智能体”很多人会答“用大模型生成代码”。这个答案在面试官眼里是减分的因为它忽略了稳定性和工程化。更好的回答是分层意图解析用大模型执行用确定性代码回传用平台能力。每一层都有明确的输入输出和容错机制。架构上这个项目体现了一个通用原则把不确定的部分和确定的部分分开。大模型是不确定的所以只让它做它擅长的“理解”绘图是确定的所以用代码固化。这个思路可以迁移到很多智能体场景比如自动生成文章、客服问答、数据分析。6. 这个项目后续还能怎么扩展跑通基础绘图后我又加了几个扩展效果不错分享给你。扩展一多函数对比。用户输入“把 sin(x) 和 cos(x) 画在一起”大模型解析出函数列表代码节点循环绘图用不同颜色区分加图例。这个改动不大但教学场景很实用。扩展二图像理解反推函数。用户上传一张函数图用平台的图像理解插件识别曲线特征再让大模型推测可能的函数表达式最后绘图验证。这个链路长但很有意思适合做进阶功能。扩展三动态范围调整。用户说“放大看 x 在 0 到 1 的部分”大模型解析出新的范围重新绘图。这要求工作流支持多轮参数更新我用对话变量存当前函数和范围每次只改范围。扩展四导出高清图。教学场景里老师可能需要把图放进课件我加了一个“导出高清版”的选项dpi 提到 300文件大但清晰。这些扩展里我觉得多函数对比性价比最高代码改动小用户感知强。如果你也在做教学智能体建议优先加这个。最后说个我在实际使用中的体会智能体画函数图这件事难点从来不在“画”本身而在“让用户用自然语言稳定地触发画图并拿到正确结果”。把大模型的不确定性和代码的确定性结合好这个思路一旦跑通很多类似需求都能套用。我踩过的那些坑希望你都绕过去。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进