
我们团队去年接了一个中台项目技术方案评审的时候我发现了一个特别有意思的现象架构师在共享文档里贴了一张系统交互图底下评论区的讨论完全不在一个频道上。有人指着红色箭头问“这里是同步还是异步”有人问“这个方块到底代表服务还是数据库”。那张图改到第7版的时候已经没有人敢动了因为图里的连线像蜘蛛网一样编辑一次要花半小时清理布局。后来我花了三个下午把整套架构图从绘图软件里搬进了代码仓库。从那以后所有的图都开始被评审、被测试、被自动校验甚至能像代码一样在合并请求里看差异。这篇文章就把我在这套“diagram-design”实践中沉淀下来的思路、工具选型、工程化方法和踩坑记录全部摊开来讲。如果你也在维护那些越改越乱的架构图、流程图、部署图这篇文章应该能给你一条新的路。1. 为什么我最终把图表当成代码来设计先说结论图表和代码本质上都是信息组织方式区别只在于一个用图形表达一个用文本表达。但绝大多数团队在维护图表时用的还是“画图”的思维而不是“设计”的思维。1.1 传统绘图工具里的架构图为什么总是越改越乱我相信你见过这种场景某个同事用绘图软件画了一张漂亮的系统架构图发到群里的时候所有人都在点赞。三个月后系统新增了两个服务他打开原文件准备改结果发现连他自己都忘了当初那些虚线是什么意思。传统绘图软件的问题不在于“能不能画图”而在于它存下来的是一堆“视觉对象”而不是“关系数据”。你往画布上拖了一个矩形、画了一条箭头软件只知道“这里有个矩形、那里有条线”它不知道这个矩形代表订单服务也不知道那条箭头代表“调用关系”。于是图里的元素和真实的系统之间没有约束系统变了图不知道图改了系统也不报错多人协作时谁都可以拖一块新的形状进来但没有人能保证这块形状和已有的连接关系在逻辑上是对的版本管理基本靠覆盖和另存为一张图的终稿、终稿2、最终版3能堆满一整个文件夹。最要命的是当图里的元素超过二三十个的时候手动调整布局的时间会呈指数级增长。因为你需要同时保证不重叠、不交叉、对齐、分组合理。这个任务对人是负担对计算机反而是强项。1.2 当图表变成文本管理的逻辑就全变了我在尝试把图表搬到代码仓库之后最大的感受是文本化只是第一步“把图当代码设计”才是核心。一旦图表以文本存在它就拥有了一系列以前想都不敢想的能力可读性不用打开软件在代码编辑器里就能读懂一张图的逻辑可追溯每一次改动都有提交记录谁改的、为什么改一目了然可评审图表的改动可以像代码一样走Merge Request评审人在评论里直接指出“这个依赖方向画反了”可校验写一段脚本就能检查图里有没有孤立的节点、有没有悬空的连接、有没有不符合规范的颜色使用可复用一个通用组件画一次用的时候引入不用每个项目重画一遍。这就是“diagram-design”的核心思想把画图变成写代码把图表的生命周期纳入软件工程的管理体系。它并不是要你放弃视觉表达而是让你用更可靠的方式去维护视觉表达。2. 工具选型的真实权衡从 Mermaid 到专属 DSL很多人在接触“图表代码化”之后第一个问题就是到底用什么工具我给我的答案从来不是“越强大越好”而是“越匹配你的维护场景越好”。2.1 四类方案横向对比我结合自己在项目里的实际体验把常用方案梳理成下面这张表方案代表工具适合场景语法门槛布局控制力渲染风格轻量文本图表Mermaid内嵌在文档、Markdown 中快速画流程图/时序图/状态图极低较弱自动布局不稳定简洁现代专业建模语言PlantUMLUML 类图、用例图、时序图等标准建模低中等支持自定义偏工程风声明式图形Graphviz / DOT复杂拓扑结构、树、依赖关系图中等强自动布局算法可选朴素但严谨自由绘图 文本同步Excalidraw / draw.io 文本化存储原型图、白板讨论、审美优先的图无完全手动可控性强手绘风格或自由风格这四类方案的取舍点在于你是更在意“画出来的图好看”还是更在意“图能被自动维护”。Mermaid 的优点是上手快、渲染好看缺点是自动布局引擎在复杂图里经常“发挥失常”。我见过一张 30 个节点的时序图Mermaid 渲染出来之后序列的顺序完全乱了。PlantUML 在 UML 标准上更严谨但语法细节比 Mermaid 繁琐。Graphviz 的布局算法最强大但你要接受它的默认审美——中规中矩不花哨。Excalidraw 和 draw.io 让你完全掌控视觉但正因为掌控度高协作时的冲突概率也高。2.2 我的选型结论一个仓库里允许同时存在多种图如果你问我哪一个最好我的答案是不要只选一个而是按图的性质分层选型。我自己在项目仓库里同时用了三种业务流程图、状态机图、简单的系统上下文图用 Mermaid。这类图变化快、需要频繁嵌入文档Mermaid 的效率和可读性最合适。类图、时序图、用例图这种有标准建模语义的用 PlantUML。它和 UML 标准对齐评审团队理解成本低。上报依赖关系、数据流向这种节点多、关系密的图用 Graphviz 的 DOT 语言。它能把节点之间的复杂拓扑自动排布清楚省去手动拖拽的心智负担。我见过一些团队试图把所有的图都统一到一个工具上最后的结果往往是要么用 Graphviz 画流程图画到怀疑人生要么用 Mermaid 硬画类图画到语法爆炸。工具的边界就是图的边界尊重边界才是工程化的开始。2.3 什么时候我会坚持不用代码画图这里也要说句公道话代码画图不是银弹。有些场景下手动绘图永远是更优解。比如产品原型图。原型图的核心在于快速表达交互细节、视觉层级每个元素的位置、间距、大小本身就是信息的一部分。这种信息用 Mermaid 或者 PlantUML 是表达不了的——布局即语义。另一类是头脑风暴时的白板图它的价值在于“画的时候的思考过程”而不在于最终成品这种情况下用手绘画布是最自然的方式。所以我在团队里定了一条规矩凡是会进入知识库、进入交付文档、会被持续维护的图必须用文本方式管理凡是只用于临时讨论、现场推演、过期即弃的图随便用白板工具画。这条规矩让团队的图资产都沉淀在了代码库里而临时讨论也不会被工具束缚。3. 一个可落地复制的 diagram 工程骨架如果只是把一张图从绘图软件搬到文本文件那还没到位的。“diagram-design”要发挥价值需要把它当成一个独立的工程来组织。下面是我沉淀下来的一套目录和规范。3.1 目录结构与文件命名我在仓库里专门建了一个 diagrams 目录结构大致是这样docs/ └── diagrams/ ├── README.md # 整个图表库的说明和索引 ├── system/ │ ├── context.mmd # 系统上下文图Mermaid │ ├── container.puml # 容器图PlantUML │ └── deployment.dot # 部署拓扑Graphviz ├── business/ │ ├── order-flow.mmd │ └── payment-state.puml └── shared/ ├── components/ │ ├── auth-boundary.puml │ └── logger-boundary.puml └── scripts/ ├── check-diagrams.sh └── render-all.sh文件命名我强制要求用kebab-case后缀区分语言类型.mmd代表 Mermaid.puml代表 PlantUML.dot代表 Graphviz。这样做的原因很简单在后缀里标明语言类型CI 脚本就能根据后缀自动选择对应的渲染器和校验器不需要在配置里维护一份“哪些文件用什么工具”的映射。3.2 README 是图库的“路由表”一个 diagram 仓库最容易遇到的问题就是图越来越多后来的人根本不知道哪张图是当前的权威版本。我在这里吃过亏。有次一个新同事要画支付链路的图在目录里找到了 4 个名字里带 “payment” 的文件每个都长得不太一样他选了一个最新的后来才知道那个文件已经被废弃了三个迭代周期。后来我强制要求 diagrams/README.md 里必须有一张索引表记录每张图的文件名、用途、维护人、最后更新时间、关联代码模块。规则只有两条索引表过时比没有索引表更糟每次图的变更必须同步更新 README任何一张图被废弃时要么删除文件要么在文件头部注释里标记deprecated并在 README 里注明替代图的路径。这个习惯让图库的可导航性提升了非常多尤其是面对跨团队共享时阅读者能在 30 秒内定位到要找的图。3.3 约定颜色与形状语义让图“自带注释”很多人画架构图时颜色完全凭心情。昨天心情好订单服务用蓝色今天心情差订单服务用紫色。结果一张图里颜色至少承载了三种以上的含义有按层级分的有按状态分的还有按负责人分的。阅读者看到颜色后靠猜。我在团队里建立了简单的颜色语义规范绿色系稳定的、已上线的服务橙色系建设中、即将上线的模块灰色系已废弃、计划移除、处于过渡期的组件红色系故障点、需要重点关注的风险区域蓝色系外部依赖非本团队维护的系统。形状语义也有约定矩形表示服务/模块圆角矩形表示外部系统菱形表示决策点圆柱表示数据存储。这套语义的威力在于当团队所有人都遵守同一套视觉语言时图的“信息密度”会成倍提高。一张图不需要额外的文字解释读者只要看到颜色和形状就能在脑海中建立起对系统状态的第一判断。规范怎么落地很简单——把它写进 README 和代码提交模板里。我在 review 图表的合并请求时第一眼先看颜色是否符合语义规范不符合直接打回重写。4. 画图之前先拆清楚四层设计法工具选好了、工程结构搭好了真正开始“设计”一张图时很多人又会陷入一个典型的误区一上来就拖节点、连线画到一半发现这张图既想要表达架构层级又想要表达调用时序还想表达数据流转最后画出一个四不像。我自己的方法是画图之前先在脑子里拆四层。这四层拆清楚了动笔只是时间问题。4.1 第一层明确这张图的服务对象一张图到底是给谁看的直接决定了图里能放多少信息。面向老板汇报的图只需要体现“边界”和“价值”服务用 3 到 5 个大方块表示就够了不需要画到数据库实例级别。面向后端同事技术评审的图图里必须包含服务、数据库、消息队列、关键接口连调用方向都要准确。面向新同学入门的图反而要在图里额外标注“哪个模块负责什么”甚至加上简单的文字描述。我见过最多的失败案例就是一个人画图时脑子里只有“我要把系统完整地画出来”却忘了看图的人需要多快、多准确地获取决策信息。这张图是为评审准备的结果画成了宣传海报那张图是给新人做指引的结果画成了技术大杂烩——这个信息的错配比画的丑要致命得多。所以现在每开始画一张图我都会先写一行注释标注它的目标读者和核心目标。比如# 目标读者后端评审会 # 核心目标确认订单创建过程中各个服务的调用顺序和异常分支4.2 第二层确定抽象级别与边界同一套系统在不同图里可以有不同的抽象粒度。上下文图关注的是“系统在哪、和谁交互”容器图关注的是“系统内部有哪些大模块”组件图关注的是“模块内部又有哪些组件”。我见过很多初学者把这三层画进一张图图面马上爆炸。确定抽象级别时有一个很实用的问题清单可以自检这张图需要出现多少个节点如果超过 30 个它大概率过于复杂需要拆分哪些细节应该留到更高精度的子图里而不是塞进当前这张图图里的每一个节点是不是都属于统一抽象级别如果某一层是服务、另一层是接口那层级就不统一。画了几年图我最大的体会是图的美感很大程度来源于是否遵守了抽象边界。边界清楚才可能干净边界混乱再怎么调对齐都救不回来。4.3 第三层把“关系”画出来而不是把“线条”画出来连线是图表设计里最容易被低估的元素。线条方向画反、箭头语义不统一、实线虚线混用这些都是评审中反复出现的问题。我给团队定的规则是实线箭头表示同步调用虚线箭头表示异步消息/事件双向箭头尽量少用因为绝大多数所谓的“双向”实际上都是各自独立的单向交互拆开画在语义上更准确在每一条线上都要标注动作词——是“调用”还是“推送”还是“订阅”。没有标注动作的线等于没有信息的线。曾经在评审的时候同事指着一张图问我这条蓝色虚线的箭头是不是代表订单服务在调用库存服务我当时一愣因为那条线我本来想表达的是“库存扣减事件通过消息队列推送给订单服务”。图上的连接如果不加动作词读者只能靠猜猜错的代价是评审会白开。从那以后我对箭头语义的规范就变得极度严格。4.4 第四层文字决定图的长期生存能力最后想聊一个反直觉的点很多人以为图表的主角是图形但我发现真正决定一张图能否长期活下来的是图里的文字。图上节点的命名必须一看就懂。svc-order-v2这种命名读者要猜半天。换成订单服务(OrderService)之后哪怕是不熟悉代码库的人也能对上号。节点的描述可以在例图代码里写成注释但最终渲染出来的标签必须简短、无歧义、可读。图表注释里面更要写清楚“为什么”。等到半年后有人改动这张图注释里如果有这么一段话就能避免一次架构事故# 为什么订单搜索走独立的读取服务 # 因为主库的连接池无法承受高频查询对写入链路的冲击。 # 临时改走主库会导致写入超时任何优化都必须先经过 DBA 评审。带上下文的图才算进入了“可维护”的层次。这么一条注释能让后来人不再犯重复的错误。5. 自动化让图表像代码一样被评审和校验“diagram-design”最过瘾的地方在于图一旦变成文本就能跑自动化。我在 CI 里至少做了三件事每一件都实打实地省过团队的时间。5.1 语法检查与渲染失败的早期发现Mermaid 和 PlantUML 的语法并不复杂但手写的时候很容易漏一个括号、少一个引号结果就是整个文档渲染失败。以前这种问题要等文档发布的时候才会被发现现在我在 CI 里加了一步扫出所有.mmd和.puml文件分别调用对应的 CLI 工具做语法解析和渲染测试只要有一张图渲染不出来构建就直接失败。这套检查非常轻量跑完几十张图也就一两分钟。但就是这一两分钟帮我们拦截了至少五六次“合并文档后整页图表集体挂掉”的惨剧。给 CI 加这类检查没有任何技术难度但收益是持续而稳固的。5.2 用脚本维护跨图一致性架构图最多的地方也是最容易撒谎的地方。系统里有 10 个微服务架构图里却只画了 7 个图上的消息队列叫order-event-bus真实代码里对应的 Topic 叫trade-order-events。这些不一致靠人工是盯不过来的自动化脚本可以帮你。我在 repo 里维护了一个expected_services.txt内容来自部署配置或服务注册中心导出的服务清单。CI 里跑一个脚本解析每张 Mermaid/PlantUML 图的节点名和清单做比对。缺少的节点输出 warning完全对不上的输出 error 并阻断合并。更暴力一点的做法是图里的关键标签直接引用代码里的常量。比如 Mermaid 里加一个%% { constant: services.order } %%这样的注释渲染前由脚本替换成对应服务的真实名字。这样图和代码之间就有了约束关系代码改名后渲染脚本会报错强迫你同步更新图。5.3 版本间的 diff 评审代码合并的时候大家都会看 diff但图的 diff 以前是没法看的。传统绘图软件里两个人改了同一张图合并后要么覆盖要么手工重新整理。而在文本图表的世界里一张 Mermaid 图就是几行文本diff 清清楚楚。我鼓励团队在提交图表修改时用文字描述改动点。比如一个合并请求里写把支付超时重试从同步改成异步新增一个 retry-topic 节点。评审人只需要对照 diff 看这一处改动是否符合架构设计评下来的效率比看一张大图高得多。我还为此在提交模板里加了一栏“改动摘要”提醒提交者说明图中改变了哪些关系。6. 真实项目里的踩坑记录这一路走过来坑没少踩。挑几个有代表性的写下来给大家做个参考。6.1 中文渲染和字体布局问题Mermaid 在默认配置下渲染中文有时候会出现文字被截断、或者节点宽度不够导致换行错乱的问题。解决方法是显式配置字体和节点宽度。我的 Mermaid 配置里一般都会设--- title: 订单系统上下文图 config: theme: neutral fontFamily: PingFang SC, Microsoft YaHei, sans-serif ---同时在写节点 label 时尽量控制长度必要的时候用换行而不是让渲染引擎自动挤。PlantUML 的中文问题主要集中在默认字体上通常在启动命令里指定-charset UTF-8并且安装好中文字体就能解决。Graphviz 对中文的支持相对原始需要显式配置fontnameMicrosoft YaHei否则中文字符会显示成方块。6.2 复杂图的“布局漂移”问题文本图表最让人头疼的一个问题就是你改了一行代码布局可能会整体漂移。有时候你只是加了一个小节点渲染出来的图全都乱套了原来舒服的对齐全没了整个图需要重新“驯服”。解决方案没有银弹。我的经验是在 Mermaid 里尽量显式声明节点之间的层级关系不要完全依赖自动布局如果一定要能精确控制位置Graphviz 是更合适的选择它的rank分组能力比 Mermaid 强很多复杂节点拆成子图子图内部自己排布再把子图当作一个整体放到总图里去。布局漂移这件事一开始以为是工具的 bug后来想明白了布局也是图的一部分该手动约束的时候就得手动约束完全交给算法等于把审美交给随机数。6.3 多人协作时的“审美分叉”代码有 formatter风格统一靠 lint。图表没有统一的 formatter审美分叉几乎一定会出现。有人喜欢密集排版有人喜欢大量留白有人喜欢所有节点都上色有人喜欢只用黑白。我用两条机制来解决渲染结果预览图进仓库。改动图表时CI 会把渲染出来的最新 PNG/SVG 一并提交评审人直接看预览图而不是只读源码。这比每个人本地渲染再截图方便太多。规范即自动化。节点名称、线条颜色、箭头类型能自动校验的尽量用脚本校验不能自动校验的写在 README 并向全员广播。6.4 局部图和总览图怎么联动大系统里通常需要一张总览架构图和若干张局部细节图。常见的问题是总览图更新了局部图没跟上两处信息对不上。我现在的做法是总览图里只保留“接口”不保留细节。总览图节点上标注的是“详见 modules/order-flow.mmd”这样的指引而不是把细节直接复制一份。这样总览图和细节图之间有唯一的关联路径信息不会出现两份副本漂移。说白了就是让每张图只维护一个真相不同图之间通过引用而非复制来连接。最后再分享一点个人心得说真的从“在画布里拖方块”过渡到“在编辑器里写图表代码”前一两周的效率是下降的。一张以前半小时能画完的图用代码可能要写一个晚上而且渲染出来的布局可能还没有手工画的顺眼。这个阵痛期会劝退不少人我当时也怀疑过自己是不是在为了工程化而工程化。真正让我改变想法的是三个月后的一次架构调整。旧架构要下线一个核心服务牵一发动全身。放在以前我需要打开一堆画布文件手动检查哪些服务调了它、哪些流程被它依赖。但这次我在代码仓库里用一行搜索把所有的图表文件扫了一遍凡是引用这个服务节点名的地方统统暴露了然后再用脚本把已废弃的节点标成灰色。那次调整前后花了半天全程没有打开过任何绘图软件。所以我的建议是不要为了追求形式上的统一而立刻全量迁移。挑一个你维护得最痛苦、最经常改的架构图把它改成文本形式放进仓库配上一个最简单的 CI 校验先跑一个月试试。等你体会到“图的改动可以被追溯、被评审、被自动检查”带来的安全感之后你会主动把其他的图也搬进来的。图表设计的本质不是把图画得多好看而是让图里的信息在团队里流动得更准确、更持久。这一点代码化的方式给了一个非常好的答案。