代码化图表设计:让架构图随代码仓库一起演进
我见过太多项目里的架构图最后都从“文档”变成了“装饰品”。六个月前画的系统架构图上面还挂着一堆早就下线了的服务新同学入职问微服务之间怎么调老员工只能回复“你去看代码吧”。这不是执行力问题而是图表设计diagram-design的方法从一开始就偏了我们把“画图”当成了一个交付动作而不是一套需要长期维护的设计体系。今天想聊的这套做法主题就是 diagram-design把图表当成代码工程来设计、生成和维护让架构图、流程图、部署图跟着代码仓库一起演进而不是散落在各处的 png/jpg/drawio 文件。整个过程不复杂用到的也都是开源工具但踩过的坑、验证过的方法论值得完整记下来。无论你是写技术文档的工程师还是需要对复杂系统做梳理的架构师或者纯粹想让自己画的图“活得久一点”这篇都可以作为一份可参考的实操手册。1. 一张过期的架构图为什么比没有图更危险先说一个最容易被低估的问题图表过期不是“文档没更新”而是它会在你和团队之间制造系统性误导。维护过项目文档的人大概率都有过这样一个瞬间打开项目的 architecture.md发现里面那张架构图还是半年前的图上挂着两个已经下线的服务、一个改了名的中间件新引入的队列完全没出现。你问同事同事说“啊那个图应该没更新吧你看代码就行”。这句话听起来无害实际上信息成本极高——整张图已经不可信了它的所有信息都需要读者对照代码二次验证。而二次验证的代价大多数情况下比直接读代码还高。1.1 图表设计的目标不是“画得漂亮”而是知识传递不失真我们把 diagram-design 拆开看diagram 是结果design 是过程。这个过程包含了你打算向读者传递什么信息、以什么顺序传递、哪些元素需要被强调、哪些细节必须被隐藏。这里有一个很多人的误区觉得图表设计是视觉工作于是把大量精力花在配色、圆角、图标是否精美上。但图表在技术场景里的本质是信息压缩——把系统里复杂的调用关系、依赖方向、数据流向压缩成人类视觉可以快速扫描的几何表达。压缩是会丢失信息的而设计水平的高低就看你在压缩过程中丢掉了什么、保留了什么。举个例子。一张部署架构图核心信息是“请求从入口进来经过哪几层最终落到数据库”那图中最重要的视觉线索应该是链路方向和层级关系。但如果你为了好看给每个服务都配了高饱和度的图标、加了投影和渐变读者第一眼看到的往往是最花哨的模块而不是那条核心链路。这就是典型的知识传递失真视觉层级和信息层级打架读者被迫花费额外精力去解码。图表设计要解决的第一问题是“读者能否在两分钟内复述这张图想表达的核心结论”而不是“这张图放到演示文稿里好不好看”。1.2 从手绘图到代码化没法版本管理的东西注定没法维护我们团队之前用过一段时间 draw.io大家齐心协力把架构图、时序图、网络拓扑都画进去了。头一个月效果很好第二个月开始出现分歧有人改了图导出 PNG 贴到 Wiki 上但没更新源文件有人直接下载了 PNG 编辑到了第三个月源文件和导出的图片已经彻底不一致团队干脆默认“图是参考具体以代码为准”。这个问题不是工具的问题而是工作流的问题。draw.io 这类 GUI 工具并不差但它天然把图表定义和图表产物绑定在一起——你保存的是一个私有格式的文件这个文件没法在代码评审里被 diff没法用文本工具检索也没法和特定版本的服务代码形成关联。最终的结果就是图表的生命周期在它创建完成那一刻就结束了。代码化图表设计为什么有效因为它把一张图的全部信息写成纯文本文本进入 Git 仓库就能获得版本管理、代码评审、分支合并、历史追溯这些成熟工程能力。渲染器只是一个解释器负责把文本转换成图片。图片本身不再被直接编辑它只是一份构建产物。这意味着两件事第一任何人修改图表都必须通过“编辑源码 → 提交 PR → 评审 → 合并”的路径每一步都有记录不合理的变更会被拦下来第二图表的源文件和代码文件放在一起服务代码改了、图表源码没改评审的时候一眼就能发现过期问题被提前暴露而不是事后补救。注意这里说的“代码化”和“程序员用代码画图”是两回事。核心不是用编程语言替代鼠标而是让图表进入和其他代码一样的工程流程。哪怕你用的是 Mermaid 或者 PlantUML 这类声明式语法只要定义文件进了仓库就已经比手工维护图片前进了一大步。2. 图表设计工具选型主流代码化方案一次看透确定要代码化之后下一步就是选型。我接触过的方案不少这里不打算把所有工具都列一遍只聊五款我实际用过、并且在团队项目里验证过的方案Mermaid、PlantUML、GraphvizDOT、Python Diagrams、Structurizr。2.1 选型四问表达力、渲染质量、生态、自动化选图表工具之前先问自己四个问题比直接看星标数可靠得多。第一问语法表达力。你的场景需要画什么类型的图如果只是流程说明Mermaid 的语法足够如果要做 UML 时序图、类图PlantUML 的领域模型更完整如果要画云架构图Python Diagrams 的云厂商图标体系几乎是现成的。第二问渲染质量。工具生成的图片排版是否稳定节点之间的连线会不会因为文字长短变化而乱窜这一点直接决定你愿不愿意把这张图放进正式的交付文档。第三问生态集成。有没有官方的编辑器插件能不能在 GitHub 或者 GitLab 的 Markdown 里直接渲染渲染时需要的系统依赖是否容易安装Mermaid 在 GitHub 原生渲染这一点就让它成了很多技术文档的首选。第四问自动化难度。图表能不能跑在 CI 里在每次合并前自动渲染、自动校验、自动发布文本定义的文件天生适合自动化但不同工具对“无人值守渲染”的支持程度不一样。Python Diagrams 需要安装 Graphviz 渲染引擎Mermaid CLI 需要 Node.js 环境PlantUML 需要 Java 环境。这些差异在本地开发时感觉不大放到 CI 里就是每次流水线多出来的系统依赖。下面这组对比是我在团队内部做选型分享时用过的表格现在直接放出来方案核心优势主要限制适合场景Mermaid语法极轻、GitHub 原生渲染、社区庞大复杂布局控制弱、自定义样式能力一般文档内嵌流程图、时序图、甘特图PlantUMLUML 覆盖完整、时序图/类图专业需 Java 环境、默认渲染风格偏传统软件开发阶段的 UML 建模Graphviz DOT布局算法强大、可定制程度高语法底层、手写门槛高层级架构图、依赖关系图Python Diagrams云架构图标资源丰富、用 Python 描述架构只适合架构图、局限在云平台场景云架构图、部署图、系统概览图Structurizr基于 C4 模型、架构描述即代码需要先理解 C4 建模思想、灵活性有边界软件架构文档体系的长期建设2.2 五款方案的互补关系不存在通吃所有场景的“银弹”这五款工具不是竞争关系更像各管一段。Mermaid 的生态最好GitHub 直接渲染意味着零成本阅读但它对“强结构、多分支”的复杂图支持并不好——节点一多布局就开始飘。PlantUML 在 UML 这个领域是王者类图、时序图的表达能力远超 Mermaid但它的渲染风格不管怎么调都带一股“上世纪标准建模”味放进现代产品文档里视觉上有点违和。Graphviz DOT 是所有工具里布局算法最强的尤其适合“节点层级关系明确的图”比如服务依赖树但它的语法确实简陋写起来像在调一个配置程序。Python Diagrams 是这里最“工程师向”的方案它允许你用惯了的编程语言来描述架构。它内置了 AWS、Azure、GCP、阿里云、Kubernetes 等大量图标资源写出来的架构图天然和云厂商的术语体系对齐。Structurizr 则是围绕 C4 模型设计的架构建模工具它的思路不是“画一张图”而是“维护一个架构模型然后从模型自动导出多视角的图”。如果你所在团队打算认真建设软件架构文档体系Structurizr 的模型化思路值得提前了解。2.3 我的组合方案按图类型分场景不搞全家桶项目里我的默认组合是这样技术文档中的流程图、时序图、状态图一律 Mermaid。直接嵌 MarkdownGitHub/GitLab 私有化部署也能渲染阅读成本最低。云架构图、部署图、系统概览图一律 Python Diagrams。云厂商图标齐全代码可模块化和我们的微服务代码库风格统一。UML 类图、时序图软件设计阶段PlantUML。虽然视觉老一点但用它做设计推演时表达精准。长期维护的架构文档Structurizr DSL 打底再按需导出 C4 各层视图。这套组合里Mermaid 和 Python Diagrams 是主力占比大约七成。并不是说其他方案不好而是团队的项目里日常最高频的图是“描述一段流程”和“描述一套系统的部署结构”这两类图用对应方案处理效率和可维护性在项目里得到了反复验证。3. 用 Python Diagrams 搭一条架构图设计流水线工具选型定了之后最难啃的是落地。这一节我们拿 Python Diagrams 走一遍全流程从环境准备到第一张架构图再到如何把图设计得可维护。3.1 环境准备Python 环境、Graphviz 渲染器、项目目录设计Python Diagrams 本质上是一个 DSL 库它最后调用 Graphviz 完成实际排版渲染。所以环境准备分两步缺一不可。第一步安装 Python 库pip install diagrams第二步安装系统级渲染引擎 Graphviz。macOS 上执行brew install graphvizUbuntu/Debian 系执行sudo apt-get install graphviz安装完验证一下dot -V能输出版本号就说明环境没问题。这里有个新人高频报错ExecutableNotFound: failed to execute [dot, -V]不用查别的就是 Graphviz 没装或者不在 PATH 里。接下来是项目目录设计。这部分很多人不在意但恰恰是决定图表能不能长期维护的关键。我现在的标准结构是这样diagrams/ ├── src/ │ ├── common.py # 公用节点、样式配置 │ ├── order_service.py # 订单服务架构图 │ ├── payment_service.py # 支付服务架构图 │ └── infra.py # 基础设施拓扑图 └── out/ # 渲染输出目录不入库 ├── order_service.png ├── payment_service.png └── infra.pngsrc 目录按业务域拆文件每个文件只负责一张图common.py 存放所有图共用的样式和自定义节点out 目录是渲染产物加进 .gitignore因为图片属于构建产物不需要进版本库。这套设计对应到工程实践上的好处图变更影响面小、新增图不影响已有产物、CI 可以做增量渲染。3.2 第一张图一个可编译的微服务架构图先看一个完整可运行的示例。假设我们要画一个最简单的订单服务架构DNS 入口 → 负载均衡 → 应用服务 → 数据库/对象存储/消息队列。from diagrams import Diagram, Cluster from diagrams.aws.network import Route53, ELB from diagrams.aws.compute import EC2 from diagrams.aws.database import RDS from diagrams.aws.storage import S3 from diagrams.aws.integration import SQS from common import default_graph_attr, default_node_attr with Diagram( 订单服务架构, showFalse, directionLR, graph_attrdefault_graph_attr, node_attrdefault_node_attr, ): dns Route53(dns) lb ELB(负载均衡) with Cluster(应用层): api EC2(order-api) worker EC2(order-worker) db RDS(订单数据库) storage S3(静态资源) queue SQS(订单消息队列) dns lb api api db api queue worker db api storage worker queue在 diagrams/src 目录下执行python order_service.py输出文件会生成到diagrams/out/order_service.png。showFalse表示生成后不自动打开图片适合批量化脚本执行directionLR让整个图按从左到右的流向布局。代码本身不难理解创建节点对象然后用运算符表示“流向”。Cluster可以把相关节点框进一个虚线框里表达逻辑上的分组。3.3 可维护性设计样式封装与模块化拆图能画出图只是第一步。真正把 diagram-design 落到团队流程里需要解决的第二个问题是图多了以后样式怎么统一节点定义怎么复用这俩问题不解决项目里会有二十张图、二十种风格。样式统一的手段有两个全局属性和自定义节点类。全局属性写在 common.py 里用graph_attr和node_attr控制default_graph_attr { fontsize: 14, bgcolor: white, pad: 0.5, ranksep: 0.6, nodesep: 0.6, } default_node_attr { fontname: Helvetica, fontsize: 12, fixedsize: false, }所有图统一引用这两个字典视觉风格就能保持一致。ranksep和nodesep控制节点之间的水平/垂直间距数值越大图越疏松适合信息密度高的场景。自定义节点类用于把团队内部使用的中间件统一封装。比如公司内部用了一套基于 Kafka 的消息平台我们不想每次都写死一个第三方图标可以建一个自定义节点from diagrams import Node class KafkaMQ(Node): _provider custom _type message-queue _icon_dir resources/icon-kafka.png封装之后画图时直接用KafkaMQ(订单消息)代替每次都去查图标路径。以后团队换消息中间件只需要改 common.py 里的封装所有引用它的架构图会同步更新。这个收益在图纸数量超过十张之后会非常明显省掉的重复工作量是量级的差距。模块化拆图的思路也是一样每个业务域一个文件单文件只画单张图图里超过 20 个节点时强制拆子图。一个 20 节点的图人的视觉已经需要来回扫视才能抓住结构了拆成 4 张 5 节点的子图每张图都一目了然。4. 提高图表可读性的四个设计原则与自检方法工具部分讲完了回到设计本身。图表的可读性不是天赋是可以按原则逐条检查的工程问题。我在实践里反复验证过四个原则分别处理信息量、复杂度、视觉语义和交付质量。4.1 一张图只讲一件事宁可拆图不要堆图技术团队最常见的失败案例是把一堆信息塞进一张图里既想表达部署拓扑又想表达核心业务调用链还想表达数据存储的冗余关系最后画出来一张意大利面。我自己的判断标准是一张图如果能在两分钟内被完整读明白它就是合格的超过这个时间就该考虑拆图。拆图不是简单的缩小范围而是按“视角”拆分——部署视角画一张调用链路视角画一张数据流视角画一张三张图互相引用而不是互相叠加。举个例子一个订单系统如果只画一张图节点会包含 DNS、LB、网关、四五个微服务、数据库、缓存、消息队列、对象存储再加外部支付回调数一下超过 15 个节点大量连线交叉基本不可读。拆成“部署架构”“核心下单链路”“支付回调链路”三张图之后每张图的信息量下降了一半以上反而能把关键依赖关系画清楚。4.2 用层次而不是关系硬扛复杂度很多人在画图时是“平铺”思维把涉及的组件全部列出来然后连线。组件少的时候没问题一多就失控。这里推荐引入 C4 模型思想不管你是否完整使用 StructurizrC4 的分层逻辑都对图表设计有指导意义。C4 是四个字母Context系统上下文、Container容器、Component组件、Code代码。分别对应四种细致程度。画任何一张图之前先问自己我这幅图站在哪个层次系统上下文层只画用户和外部系统之间的边界适合给非技术人员看容器层画应用、数据库、消息队列这些可独立部署的单元这是架构师日常最常用的视角组件层画某个容器内部的模块划分比如订单微服务拆成 controller、service、repository代码层画类级别的关联绝大多数项目根本用不到这一层画了就是过度设计。层次意识能帮你在动手前就控制图的复杂度边界避免一张图试图跨越多个抽象层次。4.3 颜色、箭头、注释让视觉元素也参与表达图表里的每个视觉元素都在传递信息。颜色、箭头方向、线型、注释文字这些都要像代码里的命名一样具备语义一致性。颜色方面我建议全图主色调不超过五种并且每种颜色有明确的语义。比如蓝色表示业务服务绿色表示数据存储橙色表示外部依赖灰色表示非关键路径组件。颜色必须有图例说明否则读者无法解码。图例可以放在图的底部用一行文字说明。箭头方向是所有图表里必须严格检查的元素。箭头代表依赖方向还是数据流向全图要统一。如果有的箭头表示“调用”有的箭头表示“数据回传”最好用实线/虚线区分并在图例里写清楚。很多图不可读最大问题就是箭头语意混乱。注释文字要克制。节点旁边的文字只放必要的说明比如“此处走 Redis 缓存”。任何一句话写出来之前先问这句话是读图的人自己推导不出来的吗如果是废话宁可删掉减少视觉噪音。4.4 交付前的可读性自检清单画完图不等于设计完成。每次提交前我会按下面这份清单过一遍不看任何额外说明只听图能否在两分钟内复述核心结论所有箭头的语义是否统一数据流/调用/依赖是否有箭头没有标注或语义模糊节点数量是否超过 15 个超过的话能否拆分颜色是否超过五种是否有图例说明是否有误导性信息比如已下线节点、过期的服务名图能否脱离作者独立被理解这七条全部过掉图才算达到交付标准。现实中大部分“不干净的图”都栽在前三条语义不统一、箭头瞎画、节点数量失控。5. 把图表设计接进 CI让文档图和代码同步进化到了这一步整个体系已经接近完整代码化定义 → 工具渲染 → 设计规范约束。但还缺最后一环——让图表在整个项目生命周期里自动保持新鲜。5.1 文档图失效的三种典型场景先梳理一下我见过的最常见的三种失效模式确认之后你会发现问题不在人在流程。第一种是“一次性图”。历史原因画的架构图没有源文件只有一张 PNG。三个月后架构调整没人想去改它因为改了得重画。第二种是“私有文件图”。所有图表源文件放在个人电脑或某个网盘某个共享文件夹里团队其他人看不到或者看到了也不敢用。第三种是“无责任图”。公司没有规定谁负责维护架构图也没有在代码评审里对图表变更进行审查于是它自然演变成一个没人认领的孤儿文档。这三类失效模式的共同点是什么图表没有进入团队的“默认工作流”。它游离在代码之外游离在评审之外游离在部署之外自然就失去了生命力。5.2 接进 CI让每张图都有渲染检查、变更提示和版本归档解决方案听起来不复杂把图表源码放进代码仓库然后让 CI 在每次 PR 时自动渲染、自动检查、自动发布。我在团队里接的 GitHub Actions 大概长这样name: render-diagrams on: pull_request: paths: - diagrams/**/*.py jobs: render: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install graphviz run: sudo apt-get install graphviz fonts-noto-cjk - name: Install diagrams run: pip install diagrams - name: Render all diagrams run: | for f in diagrams/src/*.py; do python $f done - name: Upload rendered images uses: actions/upload-artifactv4 with: name: diagram-outputs path: diagrams/out/注意这里的几个设计点paths限定只有 diagrams 目录变更时才触发流水线避免每次代码提交都跑一遍渲染。渲染命令用循环遍历所有 Python 文件后续新增图脚本不需要改流水线配置。渲染产物以 Artifact 形式上传团队成员能在 PR 页面直接下载最新的图不需要在本地执行任何命令就能看到效果。如果图片后续还要发布到内部文档站在这个步骤之后增加一个发布步骤即可。这套流程跑起来之后图表就不再是“个人产出物”而是一个有自动化保障的工程资产。5.3 集成过程中的几个坑位与处理办法CI 集成流程本身不复杂但实际落地时一定会遇到几个隐蔽的问题提前说出来帮你避坑。第一个坑中文字体。Diagrams 默认渲染的字体对中文支持不好CI 的 Ubuntu 镜像默认也没有中文字体渲染结果里中文全部变成方框。解决办法是在 CI 里安装fonts-noto-cjk并在 node_attr 里把fontname指定为中文字体名比如Noto Sans CJK SC。本地 macOS 环境下则要改成PingFang SC或Arial Unicode MS。这个差异只影响渲染结果不影响源文件是本地踩过最多次的坑。第二个坑图片产物到底要不要入库。我建议不入库理由很直接PNG 是二进制文件入库之后每次渲染输出变化都会造成大量无法阅读的 diff反而污染仓库。正确的做法是把源码入库、把产物作为 CI Artifact 或者发布到文档站。如果团队强烈希望在 Wiki 或文档平台展示就在发布流程里做映射而不是让人去仓库里翻。第三个坑渲染路径和输出目录的稳定性。Diagrams 脚本内部会拼接输出路径我一开始把输出路径直接写成out/结果 CI 工作目录和本地工作目录不一致时运行就报错或者把图片写到怪位置。后来统一改成基于文件路径计算输出目录不再依赖运行时的当前工作目录。具体做法是在每个脚本开头声明自己的输出路径import os from diagrams import Diagram OUTPUT_DIR os.path.join(os.path.dirname(__file__), .., out) OUTPUT_PATH os.path.join(OUTPUT_DIR, order_service) os.makedirs(OUTPUT_DIR, exist_okTrue) with Diagram(订单服务架构, showFalse, directionLR, outformatpng): ...这样无论在本地还是 CI渲染输出位置永远一致。第四个坑Graphviz 版本差异导致布局漂移。这个问题比较隐蔽。本地 Graphviz 版本和 CI 版本不一致时同一份源码渲染出来的布局可能不同——节点位置漂移、连线走向变化。如果你对图纸有严格的视觉要求最好在 CI 配置里固定 Graphviz 版本或者接受这些微小差异只把图当作信息载体而不是像素级作品。经过这四步——环境准备、第一张图、可维护性封装、CI 集成——整个代码化图表设计流程也就闭环了。团队里现在每张架构图都有自己的源码文件评审时可以在 PR 里看到图表的变更新同学不用再靠口口相传去理解系统结构。最后分享一个小习惯。我在每张图的脚注里都会写一行“本图由 diagrams/src/xxx.py 自动生成修改请编辑源码不要直接改图片”。这行字在最初被同事当作废话直到有人直接改了 PNG 被我妹发现、只能重新渲染覆盖的时候才明白它是给维护者留的善意提醒。图表设计这件事工具和规范可以帮你走完九十九步但最后一步永远是人的习惯——把每一张图都当作需要持续维护的代码来对待它才不会在某个版本迭代里悄悄变成墙上的装饰品。