AI 架构图生成实战:基于 XML 中间格式的可编辑系统设计 1. 这个项目到底要解决什么问题项目代号叫 next-draw.io AI 架构图。当时起这个名字没有绑定任何具体商业产品就是 next下一代加 draw绘图加 io输入输出三个词的组合。真正做起来之后才发现这名字歪打正着点中了架构图工具最核心的三个痛点怎么把输入变成输出怎么降低画图门槛怎么让图能持续迭代维护。这个项目的目标是让 AI 根据一段需求描述甚至一段代码直接生成一份结构化、可继续编辑的系统架构图而不是给一张只能看、不能改的静态图片。如果你和我一样经历过“开会前两小时还在手动补箭头”“新同学入职读架构文档读到怀疑人生”“代码改了三轮图还是半年前那版”这些场景那你应该知道这类工具缺得有多离谱。1.1 架构图为什么总是画不出来先说最扎心的现实不是大家不会画图而是画图的成本收益严重失衡。手动拖拽一个框、填一个名字看起来只要十秒钟但一张完整的系统架构图光节点就有二三十个连线几十条还要分组、标注、调色、对齐。等你终于把框摆好、线连完需求已经变了于是你开始新一轮拖动、缩放、对齐。这种重复劳动极其消耗耐心所以大部分团队的架构图要么画得很浅要么根本不更新。更深一层的问题是架构图应该是“持久资产”但实际上它往往成为“一次性产物”。真正的系统在演进接口在变化模块在拆分合并而图不会自己跟着变。于是技术文档里的架构图经常成为面试官最喜欢问的“哪张图才是对的”的笑话。这个项目要解决的就是让架构图可以像代码一样被生成、被版本管理、被自动化检查和更新。换句话说把画图从“手工建模”变成“半自动编译”。1.2 next-draw.io 的核心目标我在设计目标时给自己列了四条硬性要求少一条都会让项目失去价值首先输入要低门槛。用户可以直接写一句人话比如“用户端通过网关访问后端服务后端服务依赖订单库和用户库”也可以粘贴一段代码目录甚至给一份接口清单。工具要做的是理解这些信息而不是要求用户先学会某种复杂的建模语法。其次输出必须可编辑。AI 画完图人还要改。所以生成结果不能是位图不能是一张 PNG而应该是带有节点、连线、分组、样式属性的文本格式方便用户在已有基础上继续调整。第三结果必须能入库协作。架构图要放进 git 这类版本管理系统里每次改动都能清晰对比哪些模块新增了哪些依赖被删掉了。图片格式做不了这件事只有文本化、结构化格式能胜任。第四要支持迭代修复。AI 第一次生成的结果总是会有点偏差要么漏了一个模块要么把边连错了。工具需要有自检和重试机制让用户说一句“再检查一下网关和鉴权服务的关系”就能做局部修正而不是整张推倒重来。有了这四条后面所有技术选型都围绕它们展开。你会发现大部分设计取舍其实都是被这些目标倒逼出来的。2. 整体设计思路先选对路线再谈实现2.1 三条技术路线我为什么选 XML 中间格式做 AI 生成架构图输出格式是第一个要拍板的决策。我认真对比过三条路线各有各的坑。第一条路线是让 AI 直接生成图片。这个最直观模型用一张 PNG 或者 SVG 把图画出来。但问题非常明显坐标、文字准确度不可控节点边界经常糊成一团而且生成结果完全不可编程、不可 diff用户想改一个字都得重新生成。这就违背了“可编辑”和“可入库”两条核心目标。第二条路线是生成文本描述类的 DSL 图表。这类语法适合简单流程图写起来也很快但对企业级系统架构图来说表达能力偏弱。泳道、分组、跨层依赖、复杂样式都要靠各种技巧去凑。而且 DSL 本身是一个大生态将来想扩展自定义组件成本会越来越高。第三条路线是生成结构化的绘图 XML 中间格式。XML 听起来老气但它有不可替代的好处本质上描述的是一张数据表节点是行边是关系样式是属性。解析容易校验容易扩展也容易。我把 AI 直接生成最终绘图格式作为远期目标但在当下阶段项目内部先定义一套精简的 XML 中间结构让模型把“实体”“关系”“分组”“样式”语义化地表达出来后续再加一层薄薄的转换器把它映射成真正能导入第三方工具做可视化的文件。三条路线的对比我实测下来是这样输出形态成本可编辑性入库存档复杂图支持位图/矢量图低差基本重画差二进制无法 diff差文字经常错文本 DSL中中文本可改中diff 但语义弱中分组交叉很费劲XML 中间结构高强节点属性全保留强结构化 diff 清晰高能带分组和自定义属性如果你见过网页 HTML 和最终渲染结果之间的关系就很容易理解我的选择。HTML 不是给你直接看的最终画面它是一种中间结构浏览器拿到之后做布局、样式、绘制。XML 架构图也是这样AI 只负责把“网页语义”写好布局和绘制交给后面的引擎两者解耦之后每一边都能做好自己擅长的事。2.2 核心工作流拆解整个项目流程被我拆成五个步骤顺序是固定的缺一步都会多米诺骨牌式崩塌。第一步输入处理。把用户输入的需求文本、代码结构、接口清单统一拼接成一条结构化 prompt。这一步看起来简单但实际上大部分生成质量都取决于 prompt 里有没有把“什么是节点、什么是边、什么是分组”定义清楚。第二步结构生成。把 prompt 发给大模型要求它返回 XML 中间结构。注意不是让它返回示意图而是返回一张有节点、有边、有分组的结构化数据。模型的角色被限定为“信息抽取器”而不是“画家”。第三步解析清洗。大模型输出不可能次次完美可能带 markdown 包裹可能在 XML 里混入解释文字可能节点 id 不唯一。这一步要用脚本把原始输出提取出来解析成内存对象并且对不合法内容做修正或丢弃。第四步布局计算。拿到节点和边的信息之后我并不直接用模型建议的坐标而是把实体关系交给一个独立的分层布局引擎重新计算每个节点的位置。这一步能避免大量“节点叠在一起”“连线横穿所有模块”的惨案。第五步渲染回写。把布局之后的节点坐标、宽高、样式、分组信息写回到与第三方工具兼容的绘图 XML 文件。用户在画图界面打开就能看到一张完整图拖一拖就能继续调整。这个流程相当于把 AI 的能力限制在“理解关系和生成结构”上而把需要确定性的活交给算法。实践证明这样分工的成功率远高于“一步到位”的做法。2.3 模块划分与数据流项目代码按职责划分成六个模块各自只做一件事提示词管理模块负责拼 prompt内部维护几个版本的模板可以针对文本输入、代码输入、多轮修正输入分别调用不同的模板片段。解析模块负责把模型返回的字符串变成 Python 对象里面有容错逻辑比如自动补全缺失的结束标签、剔除多余的解释文本。实体标准化模块负责统一命名和 id。模型有时候会把“User Service”和“用户服务”当成两个不同实体这个模块负责做大小写归一、同义词合并确保同一个真实系统只有一个节点。布局引擎模块接收标准化后的实体关系输出坐标和尺寸。它的算法不需要多豪华但必须稳定不能因为输入顺序变化就让图整体乱跳。渲染写回模块负责把内存模型映射成最终 XML 文本包括节点坐标、样式颜色、分组层级。最后是自检回读模块生成完毕之后重新解析一遍 XML统计节点数、边数、孤立节点数量低于阈值就自动重新生成一次。这一层守卫非常管用它能拦截大量“看似生成了实际结构是坏的”情况。数据流是单向的文本进中间结构生成清洗标准化布局渲染最后落盘。每一步的输出都是下一步的输入所有中间格式都必须严格校验这能避免错误在后端被放大。3. 核心实现细节从提示词到可编辑图3.1 一个稳定的提示词模板先说结论提示词不要写太多花哨的废话重点是把“信息格式”讲清楚。我踩过一个大坑最初让模型“发挥创意”结果十个结果十个结构都不带重样的。后来我把提示词收敛成固定模板效果立刻稳定。我现在用的是类似下面这种结构你是一名系统架构信息抽取器。 请把用户输入转换为可编辑架构图的中间描述。 中间描述格式要求 diagram group idg1 label接入层/ node idn1 label用户端 groupg1 typeclient/ node idn2 label网关服务 groupg1 typeservice/ storage idn3 label用户数据库 typedatabase/ edge fromn1 ton2 labelHTTPS/ edge fromn2 ton3 labelJDBC/ /diagram 约束 1. node、storage、group 的 id 必须全局唯一。 2. edge 只能引用已经存在的 id。 3. type 只能取 client、service、storage、message、external 五种。 4. 不要输出任何解释文字只输出 XML。这个模板把“游戏规则”先定死让模型只做填鸭式抽取。看起来没什么技术含量但非常有效。type 限定为五种是有意为之因为后端的颜色和形状就是按这五种类型去映射的类型越少系统越好维护。关于输入部分我会在模板后面动态追加用户原始描述同时提醒模型“不要给用户描述里没提到的东西编造依赖关系”。这个提醒很关键没有它模型就会脑补出一堆现实中并不存在的中间件图一多简直没法看。3.2 解析和清洗模型输出不是金标准模型返回的文本再干净也必须经过一道容错清洗。最常见的状况是它把 XML 放在 markdown 代码块里有些还带 xml 标签有些则直接裸输出。我的第一步是用正则把代码块里的内容抽出来。import re def extract_xml(raw: str) - str: # 先尝试匹配带 标记的代码块 pattern r(?:xml|XML)?\s*(.*?) match re.search(pattern, raw, re.S) if match: return match.group(1).strip() # 没有代码块时尝试直接找 diagram 开头的内容 match re.search(rdiagram[\s\S]*/diagram, raw, re.S) if match: return match.group(0) return raw拿到 XML 字符串之后再用标准库的 ElementTree 解析成内存树。解析这一步经常挂掉原因也确实有意思模型会输出类似diagram标签没闭合、属性值没加引号、边引用了不存在的 id。所以解析后必须立刻做一轮结构校验。我通常在解析后打一份“结构摘要”比如打印出总共有几个节点、几条边、哪些边引用了未知 id。这些信息能明确告诉我这次生成的图是否具备继续处理的基础。如果发现未知 id就直接把这条边丢弃因为布局引擎没法处理悬空引用。清洗还有一个容易被忽略的点必须对节点 label 做 XML 转义。如果用户输入文本里包含或直接拼进 XML 会导致解析失败。用 Python 的xml.sax.saxutils.escape把特殊字符转掉哪怕生成结果里看不到区别文件本身是稳定的。3.3 布局方案别依赖大模型给的坐标最初我觉得大模型既然能把关系和层级抽出来那么顺带给它几个绝对坐标也是顺手的事。实验做下来我发现模型对坐标的直觉极差经常输出“节点A在 x80节点B在 x820但它们还有同一层编号”这种自相矛盾的数据。而且即使坐标基本合理只要用户加一个节点整体布局就彻底乱掉。所以我做了个重要决定模型输出里关于坐标的信息全部忽略只保留节点、边、分组、类型、label。真正的位置计算完全交给代码里的分层布局引擎。实现的思路不复杂根据边关系建立一个“依赖层级”。没有入边的节点归为第 0 层下游节点按层级 max(所有上游节点层级) 1计算。每一层的节点按顺序排列同一层内横向均匀分布。纵向间距固定比如 80px。横向间距按 label 长度估算每个节点宽度至少 120px防止文字挤压。连线的路由交给绘图工具自带的自动路由布局引擎只需要保证节点不互相重叠。写出来的核心函数大致是这样def assign_level(edges, node_ids): depth {nid: 0 for nid in node_ids} indeg {nid: 0 for nid in node_ids} graph {nid: [] for nid in node_ids} for src, dst in edges: graph[src].append(dst) indeg[dst] 1 queue [nid for nid in node_ids if indeg[nid] 0] for nid in queue: for child in graph[nid]: depth[child] max(depth[child], depth[nid] 1) indeg[child] - 1 if indeg[child] 0: queue.append(child) return depth这就是一次拓扑排序的变体。遇到环状依赖时拓扑排序会留下未处理节点我的处理方式是给这类节点统一分配一个“循环依赖层”避免脚本死循环。对于架构图来说环不应该出现在业务依赖里如果出现了图反而会帮你暴露问题。布局算法稳定之后用户体验就非常可预期。无论跑多少次同样的输入永远不会产生结构漂移不同的图这对后续做 diff 和版本对比非常重要。3.4 样式注入与实体标准化大模型生成节点样式属性时非常随性今天给“用户端”填红色明天给“用户端”填蓝色后天可能直接不填。样式不稳定会让人对工具产生极强的不信任感。我的方案是把样式属性从模型输出里剥离完全交给代码决定。项目里维护一个简单的类型到样式的映射表TYPE_STYLE { client: rounded1;whiteSpacewrap;fillColor#E3F2FD;strokeColor#1976D2;, service: rounded0;whiteSpacewrap;fillColor#E8F5E9;strokeColor#2E7D32;, database: shapecylinder3;whiteSpacewrap;fillColor#FFF8E1;strokeColor#F57F17;, message: shapemessage;whiteSpacewrap;fillColor#F3E5F5;strokeColor#7B1FA2;, external: shapecloud;whiteSpacewrap;fillColor#ECEFF1;strokeColor#455A64;, }这样用户看到同一类型节点永远是同一套视觉表达图面色彩整洁也便于团队在评审时快速过滤信息。实体标准化模块则是解决“同一个东西被当成三个节点”的问题。常见情况包括英文字母大小写差异比如users-db和UsersDB中英文混写比如“订单服务”和“Order Service”还有缩写差异。我的处理策略先统一成小写去掉特殊符号再把等价词映射列表交给固定函数遇到匹配就合并同一个 id。这个模块不能做太激进否则容易把两个本来独立的服务合并掉。所以我的原则是只对“完全一致但写法不同”做合并对“看起来像但不一定相同”保留为独立节点把选择权留给用户。用户可以在图上看到结果发现不对劲再手动合并比工具自作聪明要安全得多。4. 实操过程搭一个能跑通的最小版本4.1 环境准备与文件结构整套最小版本不需要前端页面也没必要引入复杂框架。我强烈建议第一步先做成命令行工具跑通核心链路之后再考虑 Web 界面和交互。准备一个普通 Python 环境版本 3.10 以上就行不需要额外依赖重型库。大模型 API 调用我用标准库里的 urllib 实现避免引入一堆依赖。文件结构安排如下next-draw-project/ ├─ prompts/ │ └─ diagram_prompt.txt ├─ src/ │ ├─ llm_client.py │ ├─ parser.py │ ├─ layout.py │ ├─ writer.py │ └─ cli.py └─ examples/ └─ demo_input.txt如果你的机器联网不方便也可以用本地模型只要保持输入输出格式不变后端是谁其实不关键。架构上把 llm_client 做薄就保留了随时切换后端的能力。4.2 核心代码骨架先看调用大模型的部分。假设你有一个兼容通用消息格式的接口代码大概这样import json from urllib.request import Request, urlopen def call_llm(prompt: str, endpoint: str, token: str) - str: payload json.dumps({ messages: [{role: user, content: prompt}], temperature: 0.2, max_tokens: 4000, }).encode(utf-8) req Request(endpoint, datapayload, headers{ Content-Type: application/json, Authorization: fBearer {token}, }) with urlopen(req, timeout120) as resp: data json.loads(resp.read().decode(utf-8)) return data[choices][0][message][content]接着是 pompt 读取和主流程def build_prompt(user_input: str) - str: template open(prompts/diagram_prompt.txt, encodingutf-8).read() return template \n\n用户输入\n user_input def main(): user_input open(examples/demo_input.txt, encodingutf-8).read() prompt build_prompt(user_input) raw call_llm(prompt, ENDPOINT, TOKEN) xml_text extract_xml(raw) model parse_middle_xml(xml_text) model normalize_entities(model) xmlish write_editable_diagram(model) with open(output.xml, w, encodingutf-8) as f: f.write(xmlish) summary validate_diagram(xmlish) print(summary)用这样一个简单的 90 分脚本已经能走完“文本进XML 出”的完整链路。我建议第一次跑的时候尽量使用短小、边界清晰的输入比如“A 服务调用 B 服务B 服务连接数据库”先验证链路通顺再逐步增加复杂度。4.3 接入文档和版本库命令行版本跑通之后下一步一定是把它接进团队文档流程。因为输出的是纯文本 XML所以可以直接扔进 git。每次有人调整架构描述并重新生成 diff 里能清楚看到哪个节点变了、哪条边断了。这个体验是位图完全给不了的。在此基础上我在自己的项目里加了一个小脚本读取 XML 后输出一行摘要nodes12, edges15, groups3, unlinked_nodes0这个摘要可以作为 CI 检查的一部分比如节点数不小于阈值、不存在孤立节点。一旦有人手工删改 XML 导致依赖断裂检查立刻报警。架构图和代码库慢慢形成互相约束的关系图不再是摆设。可视化打开的问题可以先由第三方工具的无损导入解决。中间结构 XML 经过一层映射之后写入绘图工具兼容格式工具内部有自动布局能力打开时再执行一次“整理布局”就能得到非常干净的图。我的经验是不要让项目试图覆盖所有画布交互那个工程量不适合个人项目第一阶段去做。5. 常见问题与排查手册5.1 XML 解析失败这是我在接入各种大模型后遇到概率最高的错误。典型表现是脚本报错syntax error: line 3, column 0看一眼原始输出发现模型把解释文字加在 XML 前面或者少了个闭合标签。处理优先级按下面这个表来现象常见原因处理方式一直解析失败模型输出被 markdown 包裹先用正则剥离代码块标签缺闭合上下文过长导致模型截断降低 max_tokens 或拆分输入属性没加引号模型语法能力波动手动用正则修正让属性值统一被引号包裹被当标签解释输入文本包含特殊字符对 label 做 XML 转义根节点多了一个模板要求只输出一个 diagram截取第一个完整diagram块碰到解析失败不要急着改模板先打印一份原始返回内容的前 500 个字符看看结构。很多时候问题出在输入文本带了很多代码片段导致模型输出被截断而不是提示词写得不够好。5.2 节点重叠和布局偏移所有第一次接出来的图都会出现“节点全部叠在左上角”或者“边穿过五六个节点”这种状况。原因很直接你用了模型返回的坐标而模型根本不理解布局空间。解决办法只有一个不要续用模型坐标改成重启布局引擎。我每次换输入都强制刷新坐标甚至故意在 prompt 里不提供坐标信息就是防止模型“顺手”填出几个坐标把我误导了。如果布局引擎跑完依然重叠检查分隔距离是否太小。我在实现里把横向间距设为 40px纵向间距设为 80px节点高度固定 40px覆盖大多数情况。遇到图特别大间距可以调成按节点数量比例缩放。5.3 实体重复和关系丢失“用户服务USER-SERVICE后端用户中心”这三个名字同时出现在生成的图里是最让人崩溃的场景。我第一次碰到以为是模型抽风后来才知道是没做标准化。标准化模块上线之后重复率下降得很明显。我试过最极端的情况输入一段包含 500 行日志描述的资料模型把同一个数据库重复建了四个节点。归一化处理配合边引用清理最后保留一个节点所有边都指向那一个 id。关系丢失的问题更多出现在边长过长的时候。模型在生成最后几条边时突然忘记了前面定义的节点 id于是引用了一个不存在的 id。这种边会被校验模块直接丢弃从结果上看就是“少了一条线”。我的处理方法是让提示词在结尾重复提醒“所有 id 必须是前面出现过的 id”同时关闭自回归模型无关紧要的想象力效果立竿见影。5.4 生成慢和结果不稳定调用大模型生成 20 个节点的图一次可能要二十秒以上用户体验很差。我的优化思路不是改模型参数而是控制输出范围。在提示词模板里加一条“节点数量不超过 25 个”让模型在信息抽取阶段做取舍。超过规模的输入先让模型做分层摘要再分块生成局部图最后再用布局引擎把几块图合并到一起。这个方法牺牲了一定的“全自动”但稳定性和可读性都回来了。不稳定问题还有一个隐藏推手模型内置的随机性。虽然我用 temperature0.2但不同时间运行同一输入仍然可能得到细微差别。后来我在注意力机制之外显式要求模型“按输入顺序输出节点”把单体节点与它们的边绑定在相邻位置。实测下结果几乎不再跳变。6. 一些实测体会和后续扩展方向6.1 我反复测试后的几个结论做了这个项目之后我对“AI 生成架构图”这件事有两个很明确的判断。第一大模型擅长提取关系不擅长做空间设计。让它罗列系统有哪些模块、模块之间怎么调用它能做得又快又好但让它协调几十个节点的坐标它瞬间退化成随机数生成器。因此架构上必须把“关系提取”和“空间布局”彻底分离谁擅长什么就干什么。第二后处理永远比调 prompt 更可靠。你可以把提示词写到完美但模型是非确定性的下一次输出可能就任性而解析、校验、标准化这些代码是确定性的一旦写好就稳定。我的成功率从最开始三成不到到现在基本九成以上真正的转折点是写了一个严格的后处理校验链而不是花了更多时间打磨 prompt。多轮迭代也很值得做。第一版生成的结构即使有遗漏第二步让模型“检测当前图和输入的一致性”大概率能把缺掉的边补回来。该环节我建议最多做两轮再多会给模型引入幻觉让它生成不存在的依赖。6.2 接下来可以做的新功能当前版本只是个骨架我下一步想优先做几个方向。第一个方向是把代码仓库直接喂进去。解析源码里的类、函数、服务注册信息自动生成服务依赖图这比让用户写自然语言描述要精确得多。关键难点从文本抽取变成了语法解析但底层工作流完全不用改。第二个方向是局部编辑的对话式体验。现在生成完只能手动改 XML不够友好。如果用户说“把订单服务的颜色改掉”“把缓存节点放到数据库旁边”模型能在保留大部分结构不变的前提下做局部 patch这个工具就真的很像团队里的架构师助理了。第三个方向是架构演进对比。既然图是文本、能入库、能 diff那就能自动对比前后两版架构图标出新增模块、失效依赖、不稳定节点。这事对大型系统评审太有用了我后续会优先投入。最后再分享一个我在实际使用中发现的小技巧永远在生成结果里保留一个“未知依赖”虚拟节点。当模型拿到一条没有来源的边时不要直接丢弃而是挂到一个虚拟节点上提醒用户这里可能漏了系统。这样你不会在批量生成时因为丢边而悄悄损失语义架构评审时还能“被迫”解释清楚。这个小改动值得任何做 AI 绘图工具的同学抄走。