最近手头有个内部工具,核心链路就是题目里这条:AI -> Mermaid -> 图形可视化UI。用户用一句话描述“我想看用户从登录到下单的完整流程”,大模型自动生成Mermaid代码,前端拿到这段代码丢给渲染器,一张清晰可交互的流程图就出来了。实践下来这套方案很能打,也踩了不少坑。这篇文章我会把链路拆解开,讲清楚为什么选这条技术路线、Mermaid语法里哪些细节容易翻车、提示词怎么调才能稳定输出、UI层集成时需要注意什么,最后附上我整理的排查手册。内容适合正在做AI Agent、智能文档、低代码平台,或者单纯想给项目加一个“一句话生成图表”功能的朋友参考。
1. 整体设计思路:为什么是“AI + Mermaid + UI”这条链路
1.1 方案选型的底层逻辑
先聊选型。市面上可视化方案太多了,ECharts、PlantUML、D3.js、Canvas手绘、SVG生成,为什么最终选了Mermaid这条链路?核心原因是:AI生成文本的能力远强于生成图形。
让大模型直接输出SVG或者PNG,看起来可行,实际用起来问题很大。SVG结构复杂,一丁点坐标偏移整张图就乱了,AI输出的SVG经常需要手动修坐标,投入产出比极低。让AI生成图片则更不可控,token消耗大,文字识别容易出错(尤其是中文),图片还不可编辑,用户想改动一个节点就得重新生成整张图。
Mermaid走的是“中间语言”路线。AI只需要输出结构化文本,文本本身是人类可读的,也是语言模型最擅长的输出形式。前端拿到这段文本后,由mermaid.js负责解析渲染成图形。这样一来,AI做它擅长的事(语义理解、结构提取),UI做它擅长的事(渲染、交互),各司其职。
还有个容易被忽略的点:可维护性。Mermaid代码可以存进数据库,用户下次打开页面直接从库里捞出来渲染,想改就改文本,不需要保留任何图片文件。对比一下直接存SVG,文本方案的存储开销几乎可以忽略。
1.2 链路中的角色分工
这条链路里每个环节都有自己的职责边界,拆清楚才不会在后面出问题。
AI层负责两件事:第一,理解用户的自然语言描述,提取出实体、关系和流程顺序;第二,把这些结构化的语义写成符合Mermaid语法的代码。这里有个容易踩的坑——AI经常会“自由发挥”,在图表里加入用户没提过的节点,或者把逻辑关系搞反。所以提示词里一定要强调“忠实于原文描述,不要增加知识库中与描述无关的内容”。
Mermaid层是中间语言层,它定义了“什么是合法图表”。这一层的价值在于,它把“语义结构”和“视觉呈现”解耦了。同样的flowchart代码,换个主题就是另一副面孔,但逻辑结构完全不变。这意味着你可以在不改AI代码的情况下,给用户提供风格切换、主题换肤、夜间模式等功能。
UI层负责最终呈现。这一层要考虑的问题就多了:动态更新时怎么避免页面闪烁、如何绑定节点点击事件、图表超出容器时怎么缩放、渲染失败时怎么优雅降级。这些细节我在第四章会详细展开。
1.3 数据流转架构
整条链路的数据流转是这样的:
- 用户输入自然语言描述(可以是一句话,也可以是一段话)
- 前端调用大模型API,返回Mermaid代码文本
- 前端把Mermaid代码传给mermaid.js的
mermaid.render()方法 - 渲染成功后得到SVG,插入页面指定容器
- 用户在UI上看到图表,可以进行交互(点击节点、导出图片、编辑代码等)
这里要特别注意一个兜底设计:大模型生成Mermaid代码不可能100%正确,语法错误是家常便饭。所以我的做法是,在渲染之前先调用mermaid.parse()做一次预检,语法不过就直接进入“修复流程”——把错误信息连同原始需求一起丢回给大模型,让它重写。实测下来,这个“AI自我修复”机制能把成功率从75%提升到97%左右,成本增加不高但体验提升非常明显。
2. Mermaid图形语法核心细节解析
2.1 高频图类型与适用场景
Mermaid支持的图类型很多,但实际项目里高频使用的就那么几种。我给团队内部整理了一个速查表,帮你快速判断用户需求应该映射到哪种图。
| 图类型 | 语法关键字 | 典型场景 | 示例 |
|---|---|---|---|
| 流程图 | flowchart/ graph | 业务流程、操作步骤、逻辑分支 | 用户登录到下单流程 |
| 时序图 | sequenceDiagram | 多角色交互、API调用链、消息传递 | 微服务调用链 |
| 类图 | classDiagram | 面向对象设计、系统结构 | Java类关系展示 |
| 状态图 | stateDiagram-v2 | 状态机、订单状态流转 | 订单从创建到完结 |
| 甘特图 | gantt | 项目排期、任务规划 | 产品迭代计划 |
| 饼图 | pie | 占比统计、数据分布 | 流量来源分布 |
| 思维导图 | mindmap | 头脑风暴、知识梳理 | 产品功能拆解 |
选错图类型是AI生成阶段最常见的错误。用户说“帮我画个订单流转图”,AI给了时序图,虽然也能表达流程,但状态流转用状态图更直观。所以在提示词里我加入了一步“语义分类”——让AI先判断应该用哪种图,再输出代码。这个策略后面会细说。
2.2 节点定义与关系连线语法要点
flowchart是上手最快也是翻车最多的类型,语法细节值得单独拎出来讲。
节点定义有三种写法:
A[普通矩形节点] // 默认节点形状 B(圆角节点) // 圆角矩形,常用于流程开始/结束提示 C{菱形判断} // 菱形,用于条件分支连线方式也很多样:
A --> B // 实线箭头 A --- B // 实线无箭头 A -.-> B // 虚线箭头 A ==> B // 粗线箭头 A -- 文本 --> B // 带标签的连线我自己在生成流程逻辑时,重点会约束两点。第一,节点ID尽量用英文或数字组合,中文放到显示文本里,比如A[登录页面]比登录页面[登录页面]稳得多。因为有些特殊字符作为ID会导致解析报错,而AI又特别喜欢用中文当ID,这是高频错误源。第二,分支和合并逻辑要明确,避免出现悬空的节点或循环指向自身的边。
子图(subgraph)也是常用的语法,可以把流程分组:
subgraph 认证阶段 A[登录页面] --> B[验证码校验] end subgraph 业务阶段 C[用户首页] --> D[商品浏览] end注意,Mermaid v10.0之后官方建议用subgraph id[标题]这种写法,旧的subgraph 标题在严格模式下会警告。如果AI生成的是旧语法,新版mermaid.js可能解析失败或样式异常,这个问题排查起来有点隐蔽。
2.3 样式定制与主题切换
默认的邓紫棋色(其实是默认主题的紫色)不一定符合产品视觉,我自己一般会把主题切到base或neutral再定制颜色。
mermaid.initialize支持全局主题设置:
mermaid.initialize({ theme: 'base', themeVariables: { primaryColor: '#e6f7ff', primaryBorderColor: '#1890ff', primaryTextColor: '#333', lineColor: '#1890ff', fontSize: '16px', }, fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif', });给单个节点或连线设置样式,可以在Mermaid代码里用classDef和style指令:
classDef important fill:#f6f9ff,stroke:#2f6fed,stroke-width:2px; class A,B important; linkStyle 0 stroke:#ff4d4f, stroke-width:3px;这里有个经验之谈:优先用classDef做批量样式定义,而不是每个节点单独写style。一方面代码更简洁可控,另一方面AI生成带大量style的代码时容易出错,不如让它只负责结构,样式统一由前端主题方案覆盖。这样图表代码的可迁移性也更强——同一段Mermaid代码,放到不同的产品里,通过主题变量就能适配各自的视觉规范。
3. AI生成Mermaid代码的提示词策略
3.1 大模型输出不稳定的根因
AI生成Mermaid代码翻车,根子在大模型对“语法正确性”没有自校验能力。它只是根据训练数据中的模式推测“看起来应该这样写”,实际上一个多余的引号、一个错误的关键字、一对没闭合的ASCII方括号,都会让整个图表渲染失败。
另外还有版本兼容问题。Mermaid的语法版本间差异不小,比如老版本graph BT的别名写法,新版本已不推荐;style A这种直接改节点样式的语法,新的初始化方式下也可能不生效。大模型的训练语料里混杂了各个版本的写法,生成时不会关心你用的是哪个版本。
我在项目里用的解决思路是双管齐下:用提示词约束输出格式降低基础错误率,用前端兜底校验和AI自我修复解决漏网之鱼。
3.2 一个稳定可复制的提示词模板
经过多轮迭代,下面这套提示词模板在我这边效果最稳定,你可以直接抄去用:
角色:你是一位资深软件架构师,擅长用Mermaid代码表达复杂流程和设计。 任务:根据用户用自然语言描述的内容,生成标准Mermaid图表代码。 要求: 1. 首先根据描述内容选择最合适的图类型(flowchart/sequenceDiagram/classDiagram/stateDiagram-v2/gantt/mindmap之一)。 2. 只输出Mermaid代码,用```mermaid代码块包裹,不要输出任何解释文字。 3. 节点ID使用英文数字组合,中文只放在节点的显示文本中。 4. 确保所有箭头、括号、引号闭合,语法严格符合Mermaid v10+规范。 5. 不要添加描述中不存在的节点或关系,忠实于原文语义。 6. 节点文本控制在20个汉字以内,超出则精简。 7. 若描述信息不足以保证图形清晰,可在代码块的HTML注释中列出需要补充的信息。 用户描述如下: {userInput}第7条是我特意加的。因为用户的需求经常很模糊,比如“帮我画出登录模块的流程图”,但登录模块的具体步骤是什么没人知道。让AI在注释里列出补充信息,比让它瞎猜要靠谱得多。前端可以在渲染结果旁边渲染这些注释,引导用户补充完整。
3.3 代码后处理与自动修复机制
提示词再强,也不能保证100%无错。我设计了三级兜底:
第一级是清理。用正则把AI返回内容里的````mermaid和``` `围栏剥离,只保留中间的代码块。有时候AI会输出“以下是您需要的代码:”这类废话,正则一并处理。这一步能省掉不少低级问题。
第二级是语法预检。调用mermaid.parse(code)主动校验,注意这个方法在v11里是mermaid.parse(),老版本是mermaid.parseError或直接调用无返回值。校验通过才渲染,不通过进入第三级。
第三级是AI自我修复。把原始需求+当前出错代码+错误信息一起发给大模型,让它重写。这里要注意重新附加提示词模板,因为AI接续上下文时容易丢失原始任务设定,导致输出格式又变回第一轮那种松散状态。修复请求通常是:
你之前生成的Mermaid代码有语法错误,错误信息如下: {errorMsg} 当前代码: {code} 用户原始需求: {userInput} 请严格修正语法,只输出修正后的代码块。这个循环最多重试两次,两次还不行就提示用户补充信息或更换图类型。实测单次修复成功率在80%以上,两轮累计可以到95%以上,剩下的5%基本是需求本身含糊导致,这时候硬生成没有意义。
4. UI层集成与渲染实操
4.1 React/Vue中动态渲染Mermaid
我前端用的React,所以先以React为例给你一套可直接运行的实现。
Mermaid在纯前端项目里有个特性,它既可以扫描整个DOM自动渲染(mermaid.run()),也可以指定单个图表渲染(mermaid.render())。对于动态更新场景,绝不能直接反复调用run(),因为它会把页面上所有符合条件的内容重新渲染一遍,还会生成重复的ID导致报错。正确姿势是每次渲染都生成一个新的ID,单独调用render()。
import mermaid from 'mermaid'; import { useMemo } from 'react'; mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'base', fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif', }); async function svgFromCode(code) { const id = `mmd-${Date.now()}-${Math.floor(Math.random() * 10000)}`; // render返回的svg字符串不含最外层容器标签,需要自己包一层 const { svg } = await mermaid.render(id, code); return svg; }在组件里,比较稳的写法是这样:
function Diagram({ code }) { const html = useMemo(async () => { try { await mermaid.parse(code); return await svgFromCode(code); } catch (err) { return `<div class="error-tip">图表解析失败,请检查输入内容。原因:${err.message}</div>`; } }, [code]); // 用一个状态保存异步结果 const [svgHtml, setSvgHtml] = useState('正在生成图表...'); useEffect(() => { let canceled = false; svgFromCode(code) .then((svg) => !canceled && setSvgHtml(svg)) .catch((e) => !canceled && setSvgHtml(`图表解析失败:${e.message}`)); return () => { canceled = true; }; }, [code]); return <div dangerouslySetInnerHTML={{ __html: svgHtml }} />; }等一下,我上边代码块里其实写混了useMemo和useState两种思路。实际项目中我只用useState方案,因为useMemo不适合放异步操作——React的useMemo设计目标是同步纯函数,放async会导致内存泄漏和渲染不稳定。这个坑我踩过,React官方不推荐,你也别踩。
Vue的写法类似,只不过把生命周期钩子换成watch:
watch(code, async (newCode) => { await mermaid.parse(newCode); const { svg } = await mermaid.render(`v-${Date.now()}`, newCode); graphContainer.value.innerHTML = svg; });4.2 节点交互:点击、悬停与回调绑定
Mermaid渲染出的SVG本身是静态的,要让节点可交互,需要给SVG内部元素绑定事件。mermaid.render会返回svg字符串和bindFunctions回调函数,后者专门用来绑定事件函数。
const { svg, bindFunctions } = await mermaid.render(id, code); // 先把svg插进DOM container.innerHTML = svg; // 再调用bindFunctions绑定回调 if (bindFunctions) bindFunctions(container);这样一来,Mermaid代码里用click关键字写的交互才能生效:
click A handleNodeClick "点击查看详情"在初始化时注册回调:
const handleNodeClick = (nodeId) => { // 根据节点ID做业务跳转、弹窗等 console.log('点击了节点:', nodeId); }; mermaid.initialize({ securityLevel: 'strict', startOnLoad: false, theme: 'base', });这里有个严格限制:securityLevel为strict时,handleNodeClick必须预先定义在全局作用域(window上),否则绑定会失败。如果设成loose,安全性会下降,但事件绑定的灵活性增加。我的建议是生产环境保持strict,确实需要自定义交互时把处理函数显式挂到window,再在Mermaid代码里引用。
4.3 性能优化:避免UI卡顿的实用手段
一开始我把这个功能集成到内部平台时,有个明显的卡顿问题:用户一修改描述文字,图表就整个重新渲染,界面闪得厉害。后来加了两层优化。
第一层是渲染防抖。让用户在输入框停止输入800ms后再发起生成请求,前端用useDeferredValue配合请求取消机制,避免连续请求。
第二层是简单限流。设置一个“最近2秒内只允许发起一次请求”的开关,超过直接忽略。因为大模型API时延本来就在1到3秒之间,用户也没办法瞬间连发几十次,所以防抖加限流足够应付绝大多数场景。
还有一个容易被忽视的性能杀手:图表的节点数。Mermaid在节点数量超过50个时,渲染时间指数级上升,生成的SVG也会非常庞大,拖慢整个页面。这种场景我建议在提示词阶段就做约束——让AI优先使用subgraph进行分组,并且提示“如果节点过多,请抽象层级,只展示关键节点,细节用子页面承载”。这比在UI层硬扛要聪明得多。
4.4 导出图片与复制代码
图表生成之后,用户经常想导出PNG或SVG给别人用。Mermaid官方推荐用mermaid.render拿到SVG字符串后,自行序列化为图片。
一个简单的实现是把SVG转成Canvas再导出PNG:
function svgToPng(svgElement) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const xml = new XMLSerializer().serializeToString(svgElement); const svg64 = btoa(unescape(encodeURIComponent(xml))); const img = new Image(); img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); const a = document.createElement('a'); a.download = 'diagram.png'; a.href = canvas.toDataURL('image/png'); a.click(); }; img.src = 'data:image/svg+xml;base64,' + svg64; }这个方案有个注意点:SVG里的字体是系统字体渲染出来的,在Canvas里绘制可能字体丢失或偏移,导出效果和页面预览略有差异。要彻底解决需要把自定义字体嵌入成<text>的style或配置合适的font-family,细节较多,但一般场景够用。
复制Mermaid源码到剪贴板就简单了,直接用navigator.clipboard.writeText(code)。这个需求听着简单,但往往用户提得很多——“我想把这张图贴到Wiki里”,这时候提供“复制代码”按钮能省他们很多事。
5. 常见问题与排查技巧实录
5.1 图表渲染失败,报Syntax Error
这是最频繁的报错。原因可细分为几类:
- AI输出代码块没有完全剥离干净,
```mermaid围栏混进了代码中 - 节点ID包含中文或空格,被解析成非法字符
- 箭头写错了,比如
A- ->B这种中间有多余空格 - 括号或引号不闭合
排查方法三步走:第一步,在AI输出后立刻console.log原始代码文本,看看有没有多余围栏或中文引号。第二步,单独在Mermaid Live Editor里粘贴这段代码,看能不能复现报错,排除渲染环境干扰。第三步,如果确认代码有问题,走我前面说的AI自我修复流程,把错误信息原样丢给它。
我实际工作中发现一个特别隐蔽的坑:LLM输出中偶尔会混入全角符号,比如中文冒号:而不是英文冒号:。肉眼根本注意不到,但解析器会直接挂掉。老实说没有直接好的提示词办法强制英文半角,最有效的就是让AI重新作文本规范化处理,或者在前端写个replace函数把常见全角符号替换成半角。
5.2 中文显示问题:乱码、缺字、换行异常
Mermaid对中文的支持其实是靠浏览器字体渲染的,本身没有内置字体映射。如果你的UI框架盖了一层样式,字体优先级被外部CSS覆盖,中文可能变成方块或字体怪异。
解决方案是在初始化时指定字体:
mermaid.initialize({ fontFamily: 'PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif', });更稳妥的是在页面全局样式中给Mermaid生成的svg强制继承字体:
.mermaid svg text { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; }还有一个换行问题:Mermaid默认情况下,节点文本里的换行符(<br/>)才会换行,普通的空格不会。AI生成的节点文本如果有长串英文URL,会撑宽节点。解决的技巧是让AI在提示词里知道“不要输出过长文本,URL用一个短名称替代,具体链接写到注释里”。
5.3 主题不生效或样式错乱
现象是:初始化里配置了themeVariables,但生成的图表颜色没变化,或者部分是默认色。
这种问题八成是初始化时机不对。mermaid.initialize()必须在第一次render()之前调用,且不能调用两次——第二次调用会让第一次的主题配置被覆盖回默认值。我见过有同事在组件里每次都调用initialize,结果主题永远不稳定。
另一个原因是securityLevel: 'strict'会过滤掉SVG里部分样式相关的属性。如果你发现classDef设置的样式在strict模式下没生效,可以先在linkStyle上用内联样式试试,若内联可行那就确认是被过滤了。严格模式下Mermaid会清理SVG内部的JavaScript和事件相关属性,对CSS有些限制。实在需要富样式,可以权衡切到loose模式,但只要没特殊交互需求,还是建议保持strict。
5.4 安全性问题:XSS与外部资源注入
Mermaid官方文档明确警告,渲染基于HTML的SVG存在xss风险。特别是不受信任用户输入的代码里可以嵌入<img onerror>或<script>标签。Mermaid使用securityLevel: 'strict'能过滤HTML标签和JavaScript,这层保护是默认开启的,坚持使用strict模式就可以高枕无忧。
但还有一个隐藏风险:Mermaid代码中可能包含外部图片链接引用。比如在flowchart节点里写A["<img src=x onerror=...>"],strict模式下HTML会被过滤,但有些Mermaid的第三方插件扩展了口子。所以权限设计上,我建议把“生成Mermaid代码”的环节收口在服务端或安全沙箱中,前端展示时再降级用strict渲染。
我自己在内部系统里还加了一个额外的文本过滤步骤——把Mermaid代码里出现的<、>、&在文本节点中先实体化,这样即使AI抽风写了一堆HTML进来,最终也只是显示成普通文本。
5.5 结合热词痛点:UI层卡顿的处理
搜索热词里有个“UI界面卡顿”,在Mermaid场景下这个卡顿主要体现在三处:图表初始化加载阻塞、图表动态更新时页面重排、以及大图渲染时主线程长时间占用。
第一处,上线前把mermaid.js做代码分割,只在真正用到的页面动态import,别一股脑打进首屏bundle。第二处,不用每次重新渲染整个图表,而是复用已经渲染好的SVG,只更新内部节点文字或样式。更激进的做法是“diff-minimal update”,但Mermaid没提供增量渲染API,所以实际操作往往是把图表拆分成多个子图,用户只看当前需要的那部分。第三处,节点极多的场景(上百个),主线程压力确实大,目前可行的缓解方案是把Mermaid渲染包在requestIdleCallback里延后执行,或者使用Web Worker配合Canvas渲染——但注意Mermaid本身依赖DOM,跨Worker使用比较复杂,前期不建议搞,先靠限制节点数和懒加载解决。
5.6 易语言、C#等非前端环境下的推进建议
热词里出现了“C# task更新ui”、“易语言子线程操作ui控件”这类搜索记录,这说明很多开发者想在不同语言环境里接入类似功能。我的建议是:这类环境里不要自己硬解析Mermaid,而是把“AI生成Mermaid代码 + Mermaid渲染”整体封装成一个本地服务或者嵌入WebView组件,通过IPC与主语言通信。
比如C#侧用WebView2加载一个前端页面,C#把用户需求通过invoke传进去,页面内部完成AI请求、渲染、交互,再把结果通过回调回传。这样UI层卡顿、渲染兼容问题都集中在WebView的现代浏览器内核里,比用WinForm手动画图靠谱太多。易语言同理,找一个轻量级的CEF/WebView支持库,把图表模块作为独立页面嵌入即可。
写在最后
从头到尾撸了一遍AI生成Mermaid到前端渲染的全链路。我自己做下来最大的感受是:这条链路真正难的不是渲染,而是如何让大模型稳定输出“刚好能用”的Mermaid代码。提示词模板、语法预检、AI自我修复,这三件套一次次把成功率和用户体验往上推。
最后再分享一个小的经验技巧:我一直给团队强调,不要在提示词里让AI“自由发挥”样式相关的代码。大模型生成的结构性代码(节点、连线、分组)质量远高于它生成的样式代码(颜色、布局参数)。把所有样式相关的控制权都收回到前端初始化配置里,你会在排查问题的时候省下一大半的力气。
这个项目的下一步我打算加入“多AI协作”能力——让一个模型负责语义提取,一个模型负责Mermaid语法修正,两个模型互相校验。等跑完这轮再回来继续分享。