
很多人一听到“让 AI 画架构图”第一反应是“不就是把需求丢给对话式大模型让它生成一段 Mermaid 嘛”。我一开始也这么想直到自己动手做了一个叫 next-ai-draw-io 的小项目——它的目标不只是让 AI 吐出图片而是真正替你把架构图画成可编辑、可继续改、能直接进已有文档体系的工程文件。这篇文章就把我做的这个项目的完整思路、核心实现、踩坑记录都摊开讲项目本身不大但里面关于格式选型、布局算法、提示词设计这些细节沉淀下来还是有不少值得聊的东西。先说结论架构图这件事真正的痛点从来不是“画不出来”而是“画出来之后没法改、没法复用、没法纳入协作流程”。这也是我最终放弃纯 Mermaid 路线、转而锚定 draw.io XML 格式的核心原因。Mermaid 适合快速表达逻辑但它是文本渲染样式、对齐、微调都很痛苦而 draw.io也就是 diagrams.net的 XML 格式本质上是带坐标、带样式、带层级关系的图形描述语言AI 只要生成规范的 XML用户拿到的就是一个真正意义上的 .drawio 文件——双击打开所有节点都能拖、能改、能套用团队模板。1. 整体设计与思路拆解1.1 先想清楚AI 替人画架构图到底解决的是什么问题做这个项目之前我先整理了一下自己平时画架构图的流程。传统做法大概是这样的先在白板上把系统拆成模块理清调用关系然后打开绘图工具一个框一个框地拖一条线一条线地连中途还要反复调整对齐、配色、分组遇到架构评审要改方案又得整片整片重画。真正花费时间的其实不是“构思”而是“落笔”和“返工”。市面上已有的 AI 绘图方案大多只解决到“生成图片”这一步生成的图要么是位图无法编辑要么是 Mermaid 源码但渲染出的布局经常不如人意。我想要的是一个能理解“订单系统依赖库存系统通过消息队列异步解耦”这种描述然后输出一个结构清晰、样貌规整、还能二次编辑的架构图文件。所以 next-ai-draw-io 的产品定义就一句话把自然语言描述转为符合 draw.io 格式规范的结构化 XML 文件。这个选择还有个附加好处draw.io 的 XML 本质上是公开的 mxGraphModel 格式它不锁定在某个私有工具里。文件可以用 VS Code 打开、可以放进 Git 做版本管理、可以用脚本批量校验、可以自动导入到很多在线协作白板里。一套格式打通了绘图、评审、存档、复用整个链路。1.2 为什么不是 Mermaid也不是纯 PNG这可能是这个项目最重要的一个决策。我做了一个小对比Mermaid 的优点是人眼可读、生成容易、版本管理友好但它本质是“用文本描述图”渲染引擎才是真正决定布局的主体。问题恰恰出在这个“主体不受控”上AI 能生成代码但没法控制它被渲染成什么样遇到复杂一点的架构图Mermaid 的自动布局能把箭头绕成蜘蛛网。PNG 这类位图就更不用说了看着是一张图改起来等于重画。架构图这种东西在设计阶段几乎每天都要变——加一个模块、调一条链路、合并两个服务——位图方案在协作上是灾难。draw.io 的 XML 就不一样了。它是精确到每个节点的 x、y、width、height 和图元样式的描述语言。AI 只要生成结构正确的 XML出来的图就是可控的节点位置是我算好的连线走的是我定好的路线样式风格统一。用户后续想调整也只是改属性而不是推翻重画。1.3 项目整体架构从自然语言到坐标图元整个 next-ai-draw-io 的处理链路可以拆成这么几层第一层是输入理解层。接收用户输入的自然语言描述用大模型抽取里面的“实体”和“关系”。所谓实体就是一个个架构组件比如“API 网关”“订单服务”“MySQL”所谓关系就是组件之间的连接包括连线两端、关系类型同步调用、异步消息、数据读写。第二层是结构建模层。把抽取出来的实体和关系组织成一张有向图。这里有比较多的细节要处理层级怎么分上游、下游还是平级、归属怎么定哪些节点属于同一个逻辑分组、孤立节点怎么摆。第三层是坐标计算层。这也是我自己写代码而不是完全丢给大模型的关键部分。大模型擅长理解语义但在“把 18 个节点排得整整齐齐”这件事上并不可靠。坐标计算用了传统的分层布局思路按拓扑序分层、层内居中、交叉最小化输出每个节点的精确位置。第四层是格式生成层。把布局结果用 draw.io XML 模板序列化生成 stylesheet、mxCell、mxGeometry 这些图元定义。这一步看似模板化其实最容易出错后面我会专门讲。2. 核心细节解析与实操要点2.1 mxGraphModel 格式读懂 draw.io 的“图纸语言”其实 draw.io XML 没有想象中难它的核心是mxfile包裹一个diagram内部是mxGraphModel。所有图形元素都是mxCell包括节点、连线还有分组容器。一个最简节点大概是这种感觉mxCell idnode-1 value订单服务 stylerounded1;whiteSpacewrap;html1; vertex1 parent1 mxGeometry x120 y80 width120 height60 asgeometry/ /mxCell一条连线则是mxCell idedge-1 value styleedgeStyleorthogonalEdgeStyle; edge1 parent1 sourcenode-1 targetnode-2 mxGeometry relative1 asgeometry/ /mxCell看到没这里的每一笔都是精确的节点的坐标、宽高、样式写在 geometry 里连线通过 source 和 target 引用节点的 id不直接写坐标由 draw.io 自己按正交线段去渲染。有个细节特别容易忽略容器节点和普通节点在 parent 属性上不同。普通节点的 parent 是1代表画布根节点但如果要把几个服务画在一个分组框里那么这个分组框本身是一个容器节点而里面的所有子节点 parent 都要设为容器节点的 id同时还要给子节点的 mxGeometry 加一个 relative1 属性。这个层级关系一旦搞错draw.io 打开文件会直接把节点渲染到画布外面去。我给这个项目定了一个比较稳的编码约定所有 id 用语义化命名比如 svc-order、db-mysql-1不用数字 id。原因很简单AI 生成的时候能记住语义 id不容易出现引用错乱而且生成的 XML 文本人可以审查、比较、再编辑。2.2 布局算法让 AI 生成的不是“一坨节点”试想一下如果直接把大模型输出的节点堆到画布上坐标全是随机值出来的图会非常灾难——节点互相重叠、连线穿来穿去。所以布局这一步不能依赖大模型自由发挥需要用确定性的算法来兜底。我用的方案是分层有向图布局思路不复杂。第一步根据实体之间的依赖关系建图依赖方向统一理解为“被依赖方在下游”第二步用拓扑排序确定每个节点的层级深度比如 API 网关在第 0 层订单服务在第 1 层数据库在第 2 层第三步同一层的节点从左往右排列按层内序号分配 x 坐标第四步层与层之间按最大层宽度计算 y 坐标偏移。计算逻辑大致是def compute_layer_layout(levels, node_width120, node_height60, h_gap40, v_gap80): layout {} max_width max(len(level) for level in levels.values()) total_width max_width * node_width (max_width - 1) * h_gap y 0 for depth in sorted(levels.keys()): nodes levels[depth] level_width len(nodes) * node_width (len(nodes) - 1) * h_gap start_x (total_width - level_width) / 2 x start_x for node in nodes: layout[node] (x, y, node_width, node_height) x node_width h_gap y node_height v_gap return layout这里的 max_width 取了所有层里最宽的那一层用来确定整个画布的视觉中心每一层用自己的 level_width 计算起始 x这样能保证每层都是居中的。效果就是最终图整体居中、层间留白均匀看起来比大多数自动布局都顺眼。连线部分用了 draw.io 的orthogonalEdgeStyle也就是正交折线风格。这个风格好就好在它不要求 AI 提前给出连线的路由点draw.io 自己会基于源节点和目标节点的相对位置自动绕线。这大大降低了 AI 生成“好看连线”的难度。2.3 样式规范不搞花哨但要有统一的视觉语言架构图最大的问题是风格不统一。AI 如果自由发挥样式每个节点的填充色、圆角、边框粗细都不一样会很乱。我在样式这块直接做了白名单约束。节点样式字段我固定成几个维度形状矩形、圆角矩形、椭圆、菱形、填充色、边框色、字体色、是否加粗。然后按组件类型做了分类规范。比如应用服务圆角矩形浅蓝填充深蓝边框数据库/存储圆柱体形状浅绿填充深绿边框中间件消息队列、缓存圆角矩形浅黄填充深黄边框外部系统普通矩形浅灰填充深灰边框这个分类的好处一是美观二是让架构图有“语义色彩”——扫一眼颜色就知道哪个是服务、哪个是存储。代码里做了一张类型到样式的映射表生成 XML 时按类型去查。容器分组的边框我用了虚线这样既区分了逻辑边界又不会喧宾夺主。字体的选择上统一用 Helvetica14 号不支持中文渲染的问题draw.io 会自己做字体回退这个不用担心。2.4 提示词设计如何让大模型稳定输出结构化数据next-ai-draw-io 的第一个版本是直接让大模型输出完整 XML。后来发现两个问题一是 XML 特别容易在中途被截断token 超限二是大模型在计算坐标时经常“发挥不稳定”一会儿靠左一会儿靠右。于是我把流程改了先让大模型只输出 JSON也就是抽取出来的实体和关系坐标交给代码算。提示词的核心结构大概是这样的从以下系统描述中抽取架构组件输出 JSON不要解释不要包含其他内容。 输出格式要求 { nodes: [ {id: 唯一标识, label: 展示名称, type: service|database|middleware|external|group} ], edges: [ {source: 源节点id, target: 目标节点id, label: 关系描述} ] }这里有两个小技巧比较关键。一是“不要解释不要包含其他内容”这句约束能有效防止大模型输出前后缀文字省掉解析时剥离噪音的麻烦二是 type 字段不要开放自由字符串用枚举值这样后面样式映射才能准确。对于分组信息我会在 JSON 里允许一个可选的 container 字段。如果一个节点有 container那它的 parent 就是那个容器 id。这样能处理“核心链路”和“支撑服务”这样的逻辑分区。3. 实操过程与核心环节实现3.1 环境准备与依赖清单我用的是 Python 3.10 FastAPI做一个本地 HTTP 服务。其实如果是自己用纯 CLI 也够但做成服务后面可以接 Web 前端、接团队机器人通用性更好。核心依赖其实只有两个大模型的 SDK 用来做语义抽取xml.etree 做 XML 序列化。布局算法完全是自己写的没引第三方图形库因为 for 循环就算得过来。如果做前端的同学还可以直接在前端用drawio的嵌入模式预览生成的.drawio文件实现“输入描述 → 看到架构图”的完整闭环。3.2 核心代码JSON 解析与 XML 序列化这里提供一段可以直接落地跑的简化版核心代码。我在项目里用xml.etree.ElementTree来构建 XML然后用indent()做格式化保证输出文件可读。import xml.etree.ElementTree as ET from xml.dom import minidom def build_drawio_xml(nodes, edges, layouts, groups): mxfile ET.Element(mxfile, {host: app.diagrams.net}) diagram ET.SubElement(mxfile, diagram, {id: page-1, name: 架构图}) graph ET.SubElement(diagram, mxGraphModel, {dx: 1200, dy: 800, grid: 1}) root ET.SubElement(graph, root) ET.SubElement(root, mxCell, {id: 0}) ET.SubElement(root, mxCell, {id: 1, parent: 0}) for gid, gname in groups.items(): gx, gy, gw, gh groups_layout[gid] ET.SubElement(root, mxCell, { id: gid, value: gname, vertex: 1, parent: 1, style: rounded0;dashed1;verticalAligntop;whiteSpacewrap;html1; }).append(ET.Element(mxGeometry, { x: str(gx), y: str(gy), width: str(gw), height: str(gh), as: geometry })) for n in nodes: x, y, w, h layouts[n[id]] parent n.get(container, 1) cell ET.SubElement(root, mxCell, { id: n[id], value: n[label], vertex: 1, parent: parent, style: type_style_map[n[type]] }) geo ET.Element(mxGeometry, { x: str(x), y: str(y), width: str(w), height: str(h), as: geometry }) if parent ! 1: geo.set(relative, 1) cell.append(geo) edges sorted(edges, keylambda e: e[source]) for e in edges: ET.SubElement(root, mxCell, { id: fedge-{e[source]}-{e[target]}, value: e.get(label, ), edge: 1, parent: 1, source: e[source], target: e[target], style: edgeStyleorthogonalEdgeStyle;rounded0; }).append(ET.Element(mxGeometry, {relative: 1, as: geometry})) rough ET.tostring(mxfile, encodingunicode) parsed minidom.parseString(rough) return parsed.toprettyxml(indent )代码里有两个我反复验证过的细节。第一个是parent ! 1时给子元素加relative1的操作不加这个属性draw.io 会忽略容器内节点的相对坐标导致节点跑位。第二个是给边 id 加了固定前缀edge-避免和节点 id 冲突同时用“源-目标”的组合保证同一对节点之间不会生成两条重复边时 id 冲突。如果你想让架构图更美观可以在这个基础上加子布局比如允许一个容器节点内部独立跑一次布局算法再把结果相对于容器左上角偏移。这个我实现了效果不错但首版完全可以不做。3.3 大模型解析与异常兜底大模型输出 JSON 不是 100% 可靠的。我遇到过几种情况输出了 Markdown 代码块包裹的 JSONJSON 里混入了注释字段名被翻译成了中文id 里有空格导致 XML 格式问题。这些都需要在接入层处理。我的做法是在解析前先做一个预处理函数import json, re def extract_and_parse_json(raw_text): text raw_text.strip() text re.sub(r^(?:json)?\s*|\s*$, , text) start text.find({) end text.rfind(}) if start -1 or end -1: raise ValueError(no json found) text text[start:end 1] return json.loads(text)这段代码的意图很简单先剥掉大模型可能套上的代码块外壳再用首尾大括号切出 JSON 主体避免了解析整个字符串时被前后缀文字干扰。实际跑下来一个这样的兜底函数能省掉 80% 的格式异常问题。JSON 解析成功后还不能直接进布局。我会做一轮严格校验每个节点必须有 id、label、type每个边必须有 source、target且 source 和 target 必须存在于节点集合里type 必须是白名单枚举值。任何一条不满足直接报错返回提示不静默容错。因为架构图这东西错了就是错了宁可让用户改描述重试也不要生成一个引用了不存在节点的坏文件。3.4 让架构图从“单页”到“多页”做第二版的时候我发现真实架构图往往不是一张图能放下的。一个系统可能有整体架构图、部署架构图、数据流图三个视图。硬塞在一页里图会非常拥挤。所以后来给 JSON 格式加了一个可选的pages字段一次解析可以产出多个页面每个页面有独立的名字、节点列表、边列表。XML 侧就是多生成几个diagram节点的事draw.io 打开后会显示底部多页签非常方便。这里有个体验细节不同页面里的重复节点比如同一个数据库在“部署视图”和“数据流视图”都出现我给它分配不同的 id 后缀避免跨页引用混乱。宁可让两个页面的节点是两份独立定义也不要搞全局引用因为 draw.io 对跨页引用的支持并不友好。4. 常见问题与排查技巧实录4.1 XML 被截断或结构不完整这是第一版最容易 hit 的问题。大模型输出长 XML 时快到 token 上限就会截断导致文件后半部分缺失draw.io 直接打不开。排查方法很简单文件生成后用 XML 解析器过一遍解析不了就说明结构坏了。我写了一个校验函数在返回文件前自动跑一遍。修复思路有两个方向彻底解决走新流程大模型只输出 JSONXML 由代码拼装XML 长度完全可控不会再截断兜底方案如果 XML 解析失败提示用户“请简化描述后重试”并输出当前已解析出的部分信息帮助定位是哪一部分描述有歧义。注意如果你调试时发现 draw.io 能打开文件但有些节点缺失基本可以确定是 XML 被截断优先检查文件尾部有没有完整的/mxfile闭合标签。4.2 连线拓扑混乱我遇到过一个很经典的症状生成的图节点排得整齐但连线乱成一团A 节点连到 B 的线在图上绕了一个大圈。原因几乎都是节点和连线的坐标/引用不匹配。排查顺序按这三步走检查边的 source 和 target 是否存在且唯一检查是否出现了循环依赖。布局算法最怕环一旦有环拓扑排序会崩或者某条边直接反向。解决办法是布局前先做环检测有环时按“先到先得”原则忽略某一条边并在返回结果中标记检查连线是否都使用了orthogonalEdgeStyle。如果没有draw.io 默认直线连接节点一多直线穿过其他节点就是必然。4.3 分组容器内的节点飞出边界这个问题在分组嵌套的时候特别常见容器的几何信息写在父节点上但子节点没有相对坐标的概念结果就是 draw.io 计算子节点位置时用相对值还是绝对值搞错了。我的统一处理规则是容器子节点的几何坐标一律存相对值也就是相对容器左上角的偏移同时加relative1容器的宽高则根据子节点的绝对坐标算出来再微调加 padding。def compute_container_layout(children, padding30): min_x min(pos[0] for pos in children.values()) min_y min(pos[1] for pos in children.values()) max_x max(pos[0] pos[2] for pos in children.values()) max_y max(pos[1] pos[3] for pos in children.values()) return min_x - padding, min_y - padding, (max_x - min_x) 2 * padding, (max_y - min_y) 2 * padding原理很简单先把所有子节点排好再用子节点的边界反推容器的边界。这样容器永远能包住内容不会出现节点在框外面的尴尬情况。4.4 中文字符渲染乱码draw.io 对中文支持本身没问题容易栽坑的是 XML 声明的编码。如果你用 Python 的ET.tostring输出 unicode 字符串再写入文件时没指定encodingutf-8Windows 默认编码写的文件就可能是 GBKdraw.io 打开直接乱码。解法就一行with open(arch.drawio, w, encodingutf-8) as f: f.write(xml_content)另外 XML 声明里最好明确写?xml version1.0 encodingUTF-8?双保险。4.5 常见问题速查表现象可能原因排查方法draw.io 打不开文件XML 截断、闭合标签缺失用 XML 解析器校验文件是否完整文件打开了但节点重叠坐标计算未执行或布局异常检查拓扑排序是否有环、层级是否正确节点都在画布左上角堆着父节点 id 引用错误或 relative 属性缺失检查容器节点 parent 和 geometry relative节点显示但连线不显示边的 source/target 指向不存在节点校验节点集合检查 id 是否有空格中文乱码文件编码不是 UTF-8统一以 UTF-8 编码写入文件样式全部丢失style 属性为空或被覆盖检查样式映射表确认 type 命中白名单5. 经验总结与扩展思路这个项目做到最后我自己最大的收获倒不是“AI 能画图”这个结果而是发现了一条更适合工程化的路径让大模型做语义理解让代码做结构计算两者各干自己擅长的事。这个思路不仅适合画架构图也适合其他很多 AI 辅助生成场景。实际跑下来的体会是AI 在“理解‘订单系统依赖库存系统并发送领域事件’这句描述并提取出三个实体和两条关系”这种任务上表现非常稳定而在“18 个节点怎么排不重叠”这种纯计算任务上指望 AI 反而不靠谱。架构图的最终质量由布局算法兜底而不是靠提示词碰运气。如果你也想在团队内部引入类似能力可以从一个很小的场景入手先只支持单页、三五十个节点以内的架构图把布局、校验、格式这块打磨稳再逐步加分组、多页、子布局、样式定制。这种项目天然适合做成一个内部工具嵌入到现有文档流程里去。最后分享一个小技巧生成完.drawio文件后记得再让代码自动跑一次“导入验证”。具体做法是用 draw.io 的命令行工具做一次静默转换比如转成 SVG如果转换成功说明文件结构基本没问题如果转换失败说明文件有问题可以直接拦截不用等用户手动打开才发现。这个小验证我在项目里是作为 CI 的一环跑的省了很多后续沟通成本。这个方向进一步做下去可以支持更多画布类型也可以把描述层做成对话式——用户先让 AI 生成初版架构图再基于图上的节点继续追问“如果把缓存加在订单服务和数据库之间图会变成什么样”也就是实现架构图的交互式迭代。这个我下个版本已经在做了。