1. 从一句话到一张图:这个项目到底在解决什么问题
画架构图这件事,做过的人都知道有多折磨。产品经理丢过来一段需求描述,说“帮我画个系统架构图”,你打开 Visio 或者 draw.io,拖方块、连箭头、调对齐、改配色,半小时过去了,图还没成型。更别提改需求的时候——加一个模块,所有连线要重排,布局全乱。我见过太多团队,架构图永远停留在第一版,因为维护成本太高,没人愿意动。
这个项目的核心思路很直接:你只需要说一句话,比如“画一个电商系统的微服务架构图,包含网关、用户服务、订单服务、支付服务、库存服务和消息队列”,系统自动生成一张可以在浏览器里打开、可以拖拽、可以点击查看详情的交互式架构图。整个链路是:自然语言输入 → AI Agent 解析语义 → 输出结构化 JSON → 渲染成 HTML 可交互页面。听起来简单,但每一步都有不少门道。
适合谁来参考这篇内容?如果你是后端开发、架构师、技术负责人,想快速把脑子里的架构想法可视化;如果你是前端开发,想了解怎么用 JSON 驱动动态图形渲染;如果你对 AI Agent 应用开发感兴趣,想找一个完整的、有实际产出的练手项目——这个方向都值得花时间研究。它不依赖什么高深技术,核心就是JSON 数据结构设计 + HTML/SVG 渲染 + AI Agent 编排,但组合起来能解决一个真实存在的效率问题。
我实际跑过几轮之后最大的感受是:关键不在于 AI 有多聪明,而在于你怎么设计 JSON Schema 和渲染逻辑。AI 只负责把自然语言翻译成结构化数据,剩下的交给确定性的渲染引擎。这个分工思路,是整件事能稳定跑通的前提。
2. 整体架构设计:为什么选 JSON 做中间层
2.1 三层解耦:输入层、转换层、渲染层
整个系统我把它拆成三层,每层职责单一,互不干扰。
输入层就是用户的一句话描述,可能很粗糙,比如“帮我画个三层架构图”,也可能很详细,比如“画一个包含 CDN、负载均衡、应用服务器集群、Redis 缓存、MySQL 主从的 Web 架构”。输入层不需要做任何预处理,直接把原始文本传给下一层。
转换层是 AI Agent 的核心工作区。它接收自然语言,输出一个严格符合预定义 Schema 的 JSON 对象。这个 JSON 里包含节点(nodes)和边(edges)两个核心数组,每个节点有 id、label、type、group 等字段,每条边有 source、target、label 等字段。为什么用 JSON 而不是直接让 AI 输出 HTML?因为 JSON 是结构化的、可校验的、可程序化处理的,而 HTML 是一坨字符串,AI 生成的 HTML 布局几乎不可控,改起来也麻烦。
渲染层拿到 JSON 之后,用 JavaScript 动态生成 SVG 或 Canvas 图形,绑定交互事件。这一层完全不依赖 AI,是纯确定性的代码逻辑。你给同样的 JSON,永远得到同样的图。
提示:三层解耦的最大好处是,你可以单独替换任何一层。比如换个更强的 AI 模型,或者把渲染层从 SVG 换成 Canvas,其他层不受影响。
2.2 为什么 JSON 是最合适的中间格式
有人可能会问,为什么不让 AI 直接生成 Mermaid 或者 PlantUML 代码?那些也是结构化的文本格式,渲染起来也方便。
我试过 Mermaid 方案,问题在于:Mermaid 的布局引擎是黑盒,你很难精确控制节点位置和连线走向。而且 Mermaid 的交互能力有限,想实现“点击节点弹出详情面板”这种功能,基本做不到。PlantUML 更偏向静态图,交互性更弱。
JSON 的好处在于,它把“图的结构”和“图的呈现”彻底分开了。结构就是 nodes 和 edges,呈现就是渲染层的事。你可以在 JSON 里给每个节点加任意自定义字段,比如description、tech_stack、owner,渲染层根据这些字段决定要不要显示 tooltip、要不要变色、要不要加图标。这种灵活性是 Mermaid 和 PlantUML 给不了的。
另外,JSON 的校验非常成熟。你可以用 JSON Schema 定义一套规则,AI 输出的结果直接跑一遍校验,不合格就重试。这比校验一段 Mermaid 代码容易多了。
2.3 AI Agent 在链路中的角色定位
这里要澄清一个概念:AI Agent 不是万能的,它只做它擅长的事。在这个项目里,AI 擅长的是“理解自然语言描述的系统结构”,不擅长的是“精确计算坐标和布局”。所以我把 AI 的角色严格限定在“自然语言 → JSON”这一步,后面的布局计算、渲染、交互全部交给代码。
具体来说,AI Agent 的工作流程是这样的:
- 接收用户输入的自然语言描述。
- 识别出系统中有哪些组件(节点),以及组件之间的调用关系(边)。
- 给每个节点分配一个合理的 type(比如 service、database、queue、gateway)。
- 按照预定义的 JSON Schema 输出结构化数据。
- 如果输出不符合 Schema,自动重试或报错。
这个流程里,Prompt 的设计非常关键。我后面会详细讲怎么写出让 AI 稳定输出合格 JSON 的 Prompt。
3. JSON Schema 设计:让 AI 输出可控的结构化数据
3.1 节点和边的字段定义
先看我实际用的一套 Schema,简化版长这样:
{ "title": "电商系统微服务架构", "description": "包含网关、用户服务、订单服务、支付服务、库存服务和消息队列", "nodes": [ { "id": "gateway", "label": "API 网关", "type": "gateway", "group": "接入层", "description": "负责路由转发、鉴权、限流" }, { "id": "user-service", "label": "用户服务", "type": "service", "group": "业务层", "description": "用户注册、登录、信息管理" } ], "edges": [ { "source": "gateway", "target": "user-service", "label": "HTTP" } ] }每个字段都有明确用途:
id:节点的唯一标识,用于边的 source 和 target 引用。必须是英文、无空格、短横线分隔。label:节点显示名称,可以是中文。type:节点类型,决定渲染时的颜色和图标。常见类型有 gateway、service、database、queue、cache、external。group:节点所属层级或分组,渲染时可以用不同背景色区分。description:节点详细描述,点击时显示在侧边栏或 tooltip 里。edges里的source和target必须引用已存在的节点 id,label是连线上的文字说明。
注意:
id字段一定要强制 AI 用英文,否则中文 id 在后续程序化处理时容易出编码问题。我踩过这个坑,AI 有时候会输出中文 id,导致边引用对不上。
3.2 用 JSON Schema 做输出校验
光定义字段还不够,你得让 AI 知道什么算合格。我建议写一份正式的 JSON Schema,在 Prompt 里附上,同时在代码里也做一次校验。
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["title", "nodes", "edges"], "properties": { "title": { "type": "string" }, "nodes": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["id", "label", "type"], "properties": { "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, "label": { "type": "string" }, "type": { "enum": ["gateway", "service", "database", "queue", "cache", "external"] }, "group": { "type": "string" }, "description": { "type": "string" } } } }, "edges": { "type": "array", "items": { "type": "object", "required": ["source", "target"], "properties": { "source": { "type": "string" }, "target": { "type": "string" }, "label": { "type": "string" } } } } } }这份 Schema 的作用有两个:一是放在 Prompt 里让 AI 照着填,二是代码里用ajv之类的库做校验。校验不通过就触发重试,重试时把错误信息也塞回 Prompt,让 AI 知道哪里错了。
3.3 处理 AI 输出不稳定的几种策略
AI 输出 JSON 不稳定是常态,我总结了几个应对策略:
策略一:强制 JSON 模式。很多模型 API 支持response_format: { type: "json_object" }这样的参数,开启后模型只会输出合法 JSON,不会夹带解释文字。这个一定要开。
策略二:Few-shot 示例。在 Prompt 里给两到三个完整的输入输出示例,让 AI 模仿。示例要覆盖不同复杂度的场景,比如一个简单三层架构、一个微服务架构、一个带数据流的架构。
策略三:分步生成。如果一次性生成完整 JSON 容易出错,可以拆成两步:先让 AI 列出所有节点,确认后再让 AI 补充边关系。这样每步的复杂度降低,准确率会提升。
策略四:后处理修复。代码里写一些容错逻辑,比如自动补全缺失的type字段(默认设为service),自动把中文 id 转成拼音或哈希值,自动去重。
我实测下来,策略一 + 策略二组合使用,成功率能到 90% 以上。剩下的 10% 靠策略四兜底,基本不会出现完全不可用的情况。
4. 渲染层实现:从 JSON 到可交互 HTML
4.1 技术选型:SVG vs Canvas vs DOM
渲染层有三个选择:SVG、Canvas、纯 DOM。
SVG是最合适的。每个节点就是一个<rect>或<circle>,每条边就是一个<path>或<line>。SVG 元素天然支持事件绑定,点击、悬停、拖拽都很容易实现。而且 SVG 是矢量图,缩放不失真,导出也方便。
Canvas性能更好,适合节点数量特别多的场景(比如上千个节点)。但 Canvas 里每个图形不是独立元素,事件处理需要自己算坐标,交互实现复杂度高很多。对于架构图这种通常几十个节点的场景,SVG 完全够用。
纯 DOM就是用<div>加绝对定位来画图。简单场景可以,但连线处理很麻烦,不推荐。
我最终选了 SVG,核心原因是交互实现简单。每个节点绑定click事件,点击时显示详情面板;绑定mouseover事件,悬停时高亮相关连线。这些在 SVG 里都是几行代码的事。
4.2 自动布局算法:分层与力导向的取舍
布局是渲染层最核心的部分。JSON 里只有节点和边的关系,没有坐标,坐标得靠算法算出来。
常用的布局算法有两种:分层布局和力导向布局。
分层布局适合有明确层级关系的架构图,比如接入层、业务层、数据层。算法逻辑是:根据边的方向做拓扑排序,把节点分配到不同层,同一层的节点水平排列。这种布局出来的图很规整,适合展示系统架构。
力导向布局适合展示复杂网络关系,节点之间的连线没有明确方向。算法逻辑是模拟物理系统,节点之间有斥力,边有引力,迭代到稳定状态。这种布局出来的图比较自然,但不够规整。
对于架构图场景,我推荐分层布局为主,力导向为辅。具体做法是:先根据节点的group字段分层,同层内用力导向做水平排列,避免节点重叠。这样既有层次感,又不会太死板。
如果你不想自己实现布局算法,可以用现成的库,比如dagre(分层布局)或d3-force(力导向)。dagre特别适合架构图,它专门为有向图设计,支持节点大小、边标签、层级间距等参数。
4.3 交互功能实现:拖拽、缩放、点击详情
交互功能是“可交互架构图”的核心卖点。我实现了以下几个功能:
拖拽节点。给每个节点绑定mousedown、mousemove、mouseup事件,拖动时更新节点的transform属性。同时要更新与该节点相连的所有边的路径,否则连线会断。这个逻辑稍微有点绕,但写一次就够了。
画布缩放和平移。给 SVG 外层容器绑定wheel事件实现缩放,绑定mousedown+mousemove实现平移。缩放时要注意以鼠标位置为中心,而不是以画布中心,否则体验很差。
点击节点显示详情。点击节点时,右侧滑出一个面板,显示节点的label、type、description等信息。如果 JSON 里有更多自定义字段,也可以在这里展示。
悬停高亮。鼠标悬停在节点上时,高亮该节点及其直接相连的边和节点,其他元素降低透明度。这个功能对理解复杂架构图特别有帮助。
导出为 PNG。用html2canvas或 SVG 转 Canvas 的方案,把当前画布导出成图片。这个功能用户很喜欢,方便贴到文档或 PPT 里。
提示:拖拽节点后,最好提供一个“重置布局”按钮,一键恢复到自动布局的初始状态。用户拖乱了之后不用手动调回来。
4.4 样式与主题:让架构图看起来专业
架构图好不好看,配色和字体占一半。我总结了几条经验:
- 节点颜色按 type 区分。gateway 用蓝色,service 用绿色,database 用橙色,queue 用紫色,cache 用红色,external 用灰色。这样一眼就能看出组件的角色。
- 连线用曲线而不是直线。曲线更柔和,交叉时也更容易区分。SVG 的
<path>用贝塞尔曲线,控制点根据节点位置动态计算。 - 字体用无衬线体。中文用“思源黑体”或“苹方”,英文用“Inter”或“Roboto”。字号不要太小,节点内文字 14px 起步。
- 加一点阴影和圆角。节点加
rx="6"的圆角和轻微阴影,看起来更有质感。 - 背景用浅灰或白色。不要用花哨的背景,架构图的核心是信息传达,不是艺术创作。
5. 完整实操流程:从零跑通一句话生成架构图
5.1 环境准备与依赖安装
这个项目不需要太重的环境,我用的技术栈是:
- Node.js 18+:跑后端服务和构建工具。
- OpenAI SDK 或兼容接口:调用 AI 模型做自然语言解析。任何支持 JSON 模式输出的模型都可以。
- D3.js 或 Dagre:做布局计算。
- 原生 SVG + JavaScript:做渲染和交互,不需要 React 或 Vue,保持轻量。
初始化项目:
mkdir arch-diagram-gen && cd arch-diagram-gen npm init -y npm install openai dagre d3如果你用其他模型,把openai换成对应的 SDK 就行。核心逻辑不变。
5.2 Prompt 设计与 AI 调用
Prompt 是整个项目的灵魂。我反复调了很多版,最终稳定下来的结构是这样的:
你是一个架构图生成助手。用户会用自然语言描述一个系统架构,你需要将其转换为 JSON 格式的架构图数据。 输出必须严格遵循以下 JSON Schema: (这里粘贴前面定义的 Schema) 要求: 1. 节点 id 必须用英文小写字母和短横线,不能有中文。 2. 每个节点必须有 id、label、type 三个字段。 3. type 只能是 gateway、service、database、queue、cache、external 之一。 4. 边必须有 source 和 target,且必须引用已存在的节点 id。 5. 根据系统描述合理推断层级关系,用 group 字段标注。 6. 只输出 JSON,不要输出任何解释文字。 示例输入:画一个简单的三层 Web 架构,包含负载均衡、两台应用服务器和一台数据库。 示例输出: { "title": "三层 Web 架构", "nodes": [ {"id": "lb", "label": "负载均衡", "type": "gateway", "group": "接入层"}, {"id": "app-1", "label": "应用服务器 1", "type": "service", "group": "应用层"}, {"id": "app-2", "label": "应用服务器 2", "type": "service", "group": "应用层"}, {"id": "db", "label": "数据库", "type": "database", "group": "数据层"} ], "edges": [ {"source": "lb", "target": "app-1"}, {"source": "lb", "target": "app-2"}, {"source": "app-1", "target": "db"}, {"source": "app-2", "target": "db"} ] } 现在请处理以下输入: {用户输入}调用代码大概长这样:
import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); async function generateDiagramJson(userInput) { const response = await client.chat.completions.create({ model: "gpt-4o", response_format: { type: "json_object" }, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: userInput } ], temperature: 0.3 }); return JSON.parse(response.choices[0].message.content); }temperature设低一点,0.2 到 0.4 之间,保证输出稳定。太高了 AI 会发挥创意,加一些你没要求的节点。
5.3 布局计算与 SVG 渲染
拿到 JSON 之后,先用dagre算布局:
import dagre from "dagre"; function computeLayout(json) { const g = new dagre.graphlib.Graph(); g.setGraph({ rankdir: "TB", nodesep: 60, ranksep: 80 }); g.setDefaultEdgeLabel(() => ({})); json.nodes.forEach(node => { g.setNode(node.id, { width: 160, height: 60 }); }); json.edges.forEach(edge => { g.setEdge(edge.source, edge.target); }); dagre.layout(g); return json.nodes.map(node => { const pos = g.node(node.id); return { ...node, x: pos.x, y: pos.y }; }); }rankdir: "TB"表示从上到下布局,适合分层架构。nodesep和ranksep控制节点间距和层级间距,根据节点数量调整。
然后生成 SVG:
function renderSvg(nodes, edges) { const svg = document.getElementById("canvas"); // 绘制边 edges.forEach(edge => { const source = nodes.find(n => n.id === edge.source); const target = nodes.find(n => n.id === edge.target); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", `M${source.x},${source.y} C${source.x},${(source.y+target.y)/2} ${target.x},${(source.y+target.y)/2} ${target.x},${target.y}`); path.setAttribute("stroke", "#999"); path.setAttribute("fill", "none"); svg.appendChild(path); }); // 绘制节点 nodes.forEach(node => { const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); rect.setAttribute("x", node.x - 80); rect.setAttribute("y", node.y - 30); rect.setAttribute("width", 160); rect.setAttribute("height", 60); rect.setAttribute("rx", 6); rect.setAttribute("fill", getColorByType(node.type)); svg.appendChild(rect); // 文字省略... }); }5.4 交互事件绑定与状态管理
交互部分的核心是维护一份“当前状态”,包括节点位置、缩放比例、选中节点等。每次交互后更新状态,然后重新渲染受影响的元素。
const state = { nodes: [], edges: [], scale: 1, selectedNode: null }; function onNodeClick(nodeId) { state.selectedNode = nodeId; showDetailPanel(nodeId); highlightRelated(nodeId); } function onNodeDrag(nodeId, dx, dy) { const node = state.nodes.find(n => n.id === nodeId); node.x += dx; node.y += dy; updateNodePosition(nodeId); updateRelatedEdges(nodeId); }状态管理不需要引入 Redux 或 MobX,一个普通对象就够了。关键是每次修改状态后,只更新受影响的 SVG 元素,不要全量重绘,否则拖拽会卡。
6. 常见问题与排查技巧实录
6.1 AI 输出 JSON 解析失败怎么办
这是最常见的问题。表现是JSON.parse报错,或者解析出来的对象缺字段。
排查步骤:
- 检查是否开启了 JSON 模式。如果没开,AI 可能在 JSON 前后加解释文字,比如“好的,以下是生成的 JSON:”。开启 JSON 模式后这个问题基本消失。
- 检查 Prompt 里是否有明确的 Schema。没有 Schema 约束,AI 会自由发挥,字段名可能对不上。
- 加一层容错解析。用正则提取第一个
{到最后一个}之间的内容,再解析。这能处理 AI 偶尔加前后缀的情况。 - 校验失败后自动重试。把校验错误信息拼回 Prompt,让 AI 修正。重试次数设 2 到 3 次,超过就报错让用户重新描述。
提示:我习惯在代码里加一个
sanitizeJson函数,先做基础清洗(去 markdown 代码块标记、去首尾空白),再解析。这个函数帮我省了很多事。
6.2 节点重叠和连线交叉怎么处理
节点重叠通常是因为布局参数不合适。dagre的nodesep和ranksep调大一点,给节点留足空间。如果节点数量多,考虑把画布尺寸调大,或者允许用户手动拖拽调整。
连线交叉是分层布局的固有问题,很难完全避免。几个缓解办法:
- 调整节点顺序,把关联紧密的节点放在相邻位置。
- 用曲线代替直线,交叉处视觉上更容易区分。
- 给连线加不同的颜色或虚线样式,区分不同类型的关系。
- 提供“高亮路径”功能,鼠标悬停某个节点时,只显示与它相关的连线。
6.3 复杂架构图渲染性能优化
节点超过 100 个时,SVG 渲染会开始变慢。优化方向有几个:
- 减少 DOM 操作。用
DocumentFragment批量插入,或者用虚拟 DOM 库。 - 简化图形。节点用简单的矩形,不要用复杂的路径或渐变。
- 按需渲染。只渲染视口内的节点,视口外的暂时不渲染。这个实现起来复杂,但效果显著。
- 考虑 Canvas。如果节点真的很多(500+),换 Canvas 渲染,用
OffscreenCanvas做离屏渲染。
不过说实话,架构图通常不会超过 50 个节点。超过这个数量,图本身的可读性就很差了,应该考虑拆分成多张图。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| JSON 解析失败 | AI 输出夹带解释文字 | 开启 JSON 模式,加 sanitize 函数 |
| 边引用不存在的节点 | AI 编造了 id | 校验边引用,自动删除无效边 |
| 节点 id 是中文 | Prompt 约束不够强 | 在 Schema 里加 pattern 校验,强制英文 |
| 布局太挤 | nodesep/ranksep 太小 | 调大间距参数,或增大画布 |
| 拖拽后连线断开 | 没更新边的路径 | 拖拽时同步更新相关边的 d 属性 |
| 缩放后模糊 | SVG viewBox 没更新 | 用 transform 缩放,不要改 width/height |
| 导出 PNG 空白 | 跨域或样式丢失 | 用 SVG 序列化 + Canvas 绘制,内联样式 |
7. 扩展方向:这个项目还能怎么玩
跑通基础版本之后,我试了几个扩展方向,都挺有意思。
方向一:支持多轮对话修改。用户说“把数据库换成 MongoDB”,系统在现有 JSON 基础上修改,而不是重新生成。实现方式是维护对话历史,每次修改请求带上当前 JSON,让 AI 输出 diff 或完整的新 JSON。
方向二:从代码仓库自动生成架构图。扫描项目目录,识别出服务、模块、依赖关系,自动生成 JSON。这个对微服务项目特别有用,能快速看清服务之间的调用关系。
方向三:导出为多种格式。除了 PNG,还可以导出 SVG、PDF,甚至生成 Mermaid 或 PlantUML 代码,方便嵌入到文档里。
方向四:实时协作。多人同时编辑一张架构图,用 WebSocket 同步状态。这个复杂度高一些,但团队场景下很有价值。
方向五:接入更多数据源。比如从 Excel 文件读取组织架构数据,生成组织架构图;从数据库读取表结构,生成 ER 图。核心逻辑不变,只是输入源从自然语言换成了其他格式。
我个人最看好的是方向二和方向五。从真实数据源自动生成架构图,比让用户手动描述更可靠,也更有实用价值。自然语言输入适合快速原型和头脑风暴,但正式文档里的架构图,还是应该从代码或配置里自动生成,保证和实际系统一致。
最后分享一个小技巧:如果你想让 AI 生成的架构图更符合团队规范,可以在 Prompt 里加一段“团队架构图规范”,比如“所有对外服务必须标注为 external 类型”“数据库节点必须放在最底层”。这样生成的图不需要二次调整就能直接用。我试过把团队的命名规范和分层规范写进 Prompt,效果立竿见影,返工率降了一大半。