YAOTU INSIGHTS

AST拓扑剪枝+AI推理:从十万行混乱代码库生成高精度架构图

AST拓扑剪枝+AI推理:从十万行混乱代码库生成高精度架构图
翻到那套老代码的第一眼我整个人是有点懵的。11.8 万行散布在 390 多个文件里包名从common、utils、misc到function什么都有最离谱的是一个名为utils的包里塞了四十多个互相引用并依赖数据库访问的类。更要命的是文档早已和代码脱节架构文档画的是三年前的结构而实际上这几个模块早就绕着互相依赖了。就是在这种“十万行混乱代码库”面前让我像读小说一样一行行读源码来搞懂它根本不现实我需要的是先“看见”它——用图的方式看见整个系统。于是就有了这篇博客的主题当代码库乱到一定程度与其靠人肉翻代码不如先让程序把所有代码关系解析出来再去掉噪声、保留骨架最后让 AI 基于这张图画出“高精度架构全景图”。项目整体思路就是四个字先建图再剪枝然后让 AI 在干净的数据上做推理。1. 先定义“架构全景图能看懂”的标准再动手读代码很多团队一提“架构图”就直接上百度的脑图画板一个模块一个框人工连线。问题是十万行代码里模块之间至少有上万条依赖关系你根本不可能手工画得完就算画得完画出来的也大概率是美化过的“主观想象版架构”而非代码的真实状态。所以在做这个项目之前我先把“能看懂”定义成四个可以被验证的问题整个系统到底有哪些核心模块边界在哪里模块之间的依赖方向是什么谁依赖谁哪里出现了循环如果我要改某一个业务能力影响面覆盖哪些模块哪些模块是枢纽性的、耦合度极高、重构时必须优先处理的对象。只有能把上面四个问题用一张图回答清楚那张图才配叫“高精度架构全景图”。否则它只是一张“彩色网络拓扑”跟路由器上抓的包一样真实但没有价值。有了标准之后接下来的问题就是怎么用技术手段把代码变成一张能回答上述问题的图并且保证图上的信息足够精确、可追溯。答案就是标题里前半个短语AST 拓扑剪枝。要理解它得先把 AST 是什么、它如何变成依赖图这件事说清楚。这里也说一句大实话我不是第一次试图用代码可视化来理解历史系统以前也见过别人直接把 IDE 的依赖图截图出来结果都是一团乱麻缩得越小越看不清放得越大越找不到重点。真正的问题从来不是“图的像素不够”而是信息没有被筛选。剪枝就是为了解决这个信息密度问题而存在的。1.1 为什么直接“全文阅读”不是一个选项先算一笔账11.8 万行假设一个人专注读代码、每分钟读 40 行读完一遍大约需要 2950 分钟也就是接近 50 个小时。就算一次性读懂读完之后你的脑子里依然没有一个“全局的、可以度量耦合程度”的架构图。因为人脑记忆不是图数据库读完之后只剩下几个大块记忆很难精确定位每一条依赖路径。这也是代码库越大、维护成本越高的核心原因之一人类从线性文本中重建非线性依赖关系的能力存在天然瓶颈。反观程序化解析一个稍加优化的解析器处理 10 万行只依赖语法分析的代码通常在几十秒内即可完成。如果要连跨文件的引用关系也解析掉大概 2 到 3 分钟内能跑完一整轮。这个时间成本是完全可接受的而且可以反复增量跑。所以我从一开始就把方案设定为“先让机器建立全景底图再借助 AI 理解语义”这个顺序不能反。2. AST 解析与代码图建模把十万行从文本变成可计算的结构做这个项目的核心依赖是一条完整的数据链路文本源码 → 词法分析 → 语法分析生成抽象语法树AST → 符号解析和引用解析 → 生成依赖图 → 图剪枝 → 图算法/模型识别 → 全景图展示。第二步到第五步是我们这种应用型项目最需要花功夫的地方。AST 本身我不过多展开因为“抽象语法树”是编译原理教材里的基础概念简单说它就是源码经过解析之后生成的一棵结构化树树上的节点代表代码里的函数声明、变量定义、方法调用、类定义等语法元素。有了这棵树程序就“看懂”了代码里谁是定义、谁在使用而不是把所有东西当成一串字符串。2.1 跨文件依赖是怎么建立起来的真正让“十万行代码”变成一张图的关键不在单个文件的 AST 本身而在于跨文件引用解析。我用一个很朴素的思路来做这件事先把每个文件解析成 AST再遍历 AST 中所有的 import、require 或者模块声明语句找到“当前文件引用了哪个其他文件/导出符号”同时再扫描函数/类内部的调用点确认“这个函数调用具体指向哪里的定义”。为了控制规模我定了一个节点粒度的规则文件是第一级节点函数/方法是第二级节点。建图时先建文件级别的粗粒度图用于全景总览当某个文件需要被深入分析时再动态展开成函数级别的子图。这种两层粒度设计非常实用否则直接在十万行代码上生成函数级别的图数据量会撑爆可视化界面。当时我还认真做过一次规模估算贴在这里给大家一个直觉印象一个 10 万行库按平均单文件 260~300 行算大概是 330~400 个文件如果是业务系统函数/方法数量通常在 3500~5000 之间如果直接生成函数级依赖图节点上万边数能到 2 万到 3 万条就算只用文件级粒度边数也常常有 5000~8000 条。这意味着就算只画文件级依赖图屏幕上这 8000 条边也足够糊成一坨了。这就顺理成章地把我们引到了下一步剪枝。图能建出来不代表图能直接看要让人看图获得信息必须做拓扑剪枝。2.2 符号解析中的命名冲突与清理建图环节最耗时也最容易翻车的其实是符号解析而不是 AST 解析。尤其是旧代码里的全局函数、动态语言中常见的模块互相 require、运行时动态生成类名这些都会导致“一条调用语句不知道指向谁”的窘境。我当时的做法是分层级兜底先从 AST 内部找同文件的定义优先使用同文件解析结果找不到就去全库索引里查导入路径映射仍然找不到的先打标记“未知引用”在图上用虚线节点表示等整轮解析结束再统一统计“未知引用”占比一般低于 3% 说明解析质量可信。这一点非常关键。很多人在建依赖图时只看“能画出来”就满意了却没有区分“确定边”和“猜测边”。等到剪枝和 AI 分析阶段一条错误的指边可能让整个聚类结果跑偏。所以我在建图阶段就要求所有解析结果必须带可信度来源要么来自 AST 结构绑定要么来自导入映射不允许无中生有。3. 拓扑剪枝不是删边而是保留“结构骨架”先亮观点剪枝的目的不是把图变简单而是把图变“有信息量”。在依赖分析这件事上边的数量和信息量并不是正比关系。太多带噪声的边只会让核心结构淹没在视觉噪音里。所以核心原则是保留能承载结构信息的边删掉与结构无关或重复的边。我在实际项目里用了四种剪枝策略按优先级排列如下剪枝策略针对的边保留逻辑风险提示同模块内部边折叠一个包目录内部的引用边内部依赖不影响模块间结构折叠为权重包目录划分不一定可信需搭配模块识别修正高扇出节点只留 Top-K 条边工具类、公共父类等被大量依赖的节点保留最重要连接避免星型结构刷屏Top-K 太小会丢真实依赖语义需要凭结果手感微调弱连通分支只保留代表性边只经过单路径可达的冗余边用来减少传递依赖的重复表达会把一条链变成“直跳”需保留跳数标记环内弱边优先裁剪出现在循环依赖圈中、连接强度低的边突出环是否存在而不是环内每个细节环不能完全消除否则会丢失依赖方向信息3.1 “结构性边”和“噪声边”怎么区分如果你上手这个项目你会发现剪枝最难的其实不是剪枝算法本身而是怎么定义“结构性边”。我最后给出的是一个可落地的操作性定义一条边被删除之后如果两点之间的可达性、模块的出入度排名以及循环依赖集合都不发生变化那它就是候选噪声边。比如一个utils类被 300 个文件引用如果它不对任何模块边界、任何依赖闭环产生影响那么把它画成 300 条边只会掩盖真正重要的依赖关系。正确的做法是把 300 条边折叠成一个“高扇出节点”并在图例里注明它被 300 个地方依赖。我用相似度指标给每个节点算了一个“枢纽值”枢纽值 出度 入度x 连接组件数。枢纽值最高的 20 个节点必须是图上的重点高亮节点哪怕剪枝也不能把它们代表的依赖关系全部抹掉。这里其实是把“删边”变成了“重新分配边的视觉权重”这个观念上的转变很重要。3.2 剪枝参数怎么调看连通性曲线在第一版剪枝流程跑通之后我发现自己被一个新问题困住了K 值保留前几条边到底设多少才好不同代码库的差异极大有的库 100 条边就能看清有的库 500 条边依然一团模糊。我的经验是不要凭肉眼看图来调参要画一条“边数-连通性曲线”。具体做法是把剪枝参数从 0 逐步加大每走一步统计当前剪枝后图里的连通组件数量和连通阈值当边数下降到某个值之后连通组件数量开始明显增加、枢纽节点的可达覆盖率大幅下滑时这个点就是“结构损毁点”。取结构损毁点之前的 1.2~1.5 倍边数作为剪枝目标区间就能得到一个“保留骨架、去掉脂肪”的合理平衡。顺带说一句剪枝不是一次性的。我在项目里设计了两个层级第一层是在建图时做规则剪枝第二层是在生成展示图时做视觉剪枝。这两者的目标不一样——规则剪枝要保数据正确性视觉剪枝要保可读性。AI 做架构识别时用的是第一层剪枝后的图给人看的全景图是第二层剪枝后的图。如果把两件事混在一起做很可能为了可读性丢失关键数据然后模型就分析错了。4. AI 的角色从剪枝图到架构全景图现在图已经剪得比较干净了大概从最初的 8000 条边降到了一个 600 节点 / 700 边的中等规模图。但是 600 个节点依然太多人是否能在 30 秒内看出系统框架依然心里没底。这就是 AI 登场的地方。很多人一听到“AI 画架构全景图”就直接理解为“把代码喂给大语言模型让它生成架构描述”。这其实是个认知误区。大语言模型在理解十万行代码时会触及上下文长度的硬约束而且它对局部文本的“幻读”很容易覆盖真正广泛存在的依赖关系。所以我更信任一种混合方法图结构负责客观事实大语言模型负责语义摘要与命名。具体实现拆成三步走第一步对剪枝后的依赖图做图嵌入/表示学习把每个节点变成一个高维向量。相似依赖模式的节点在向量空间里会自动靠近。第二步在不告诉它任何人工假设的前提下对向量做无监督聚类让 AI 自己发现“哪些文件是一伙的”。这一步会得出候选模块。第三步把每个簇内部最具代表性的文件注释、函数名、模块路径拼接起来喂给大语言模型做语义摘要让它给这个簇起一个像人话的名字比如“订单状态流转模块”或“用户鉴权中间件”。这样既保留了依赖图的客观性又借用了大模型的语义理解两者互补。4.1 模块聚类遇到的边界模糊问题尽管社区发现和聚类算法很成熟在真实混乱代码里还是会遇到一堆奇怪边界案例。最常见的是“同一个包里住着两个毫无关系的模块”因为历史代码经常把不相关的东西胡乱塞进同一个目录。如果直接按目录分模块聚类结果必然失真如果完全不看目录只按依赖关系分又会把一些明明过去有关联但现在已经解耦的模块硬拉在一起。我当时的处理方法是给大语言模型第一个“混合特征”把目录路径、文件依赖向量、函数名语义向量拼接起来让模型自己判断“这两个文件是否应该属于同一个模块”。实测下来混合特征比单一特征准确率高出不少模块划分与人工评审的一致性能够达到令团队满意的程度。4.2 架构模式自动识别聚类只是第一步真正让人感叹的是 AI 在“识别架构模式”上的能力。我在剪枝后的图上不仅把节点聚了群还给重要的边界配置了一个“边界强度”分数用来判断模块之间的依赖是强耦合还是弱耦合。这之后大模型的第二次推理用一句话就能说清楚“这是一张有 X 个簇、簇间边权矩阵如下、簇内主要功能是……的图请判断它是分层架构、事件驱动架构还是微服务架构并指出哪些模块是核心枢纽。”我第一次跑这个时心里其实犯嘀咕就凭一个简单的边权矩阵它能识别出什么有效信息结果它给出的结论和我们团队人工分析几乎完全一致核心交易链路是一个强耦合的大簇周边一圈辅助功能以弱边挂在它上面整体是“中心辐射型”而非严格分层。这类结论在过去的分析流程里需要三个工程师开两天的评审会才能达成AI 把它压缩成了五分钟。这里也解释了标题中“AI 画出全景图”的一个关键技术前提AI 如果不是基于一份经过剪枝的高质量拓扑图而是直接阅读原始代码文本它很难有足够大的视野去感知模块边界。AST 拓扑剪枝相当于给 AI 戴上了一个“广角眼镜”把混乱的细节遮挡起来把重要的结构特征暴露出来。全景图的精度很大程度取决于这件“眼镜”磨得够不够干净。5. 精度怎么验证拿人工基准和真实改动记录来验收架构图做得再好看如果答案和真实系统形态相差太远那也只是另一种形式的“花架子”。在项目收尾阶段我需要回答一个最尖锐的问题怎样证明这张 AI 架构全景图是“高精度”的我的做法是双重验证大家以后也可以直接复用这个思路。第一重验证是人工基准对照。我找了三名对这套系统有多年经验的后端开发请他们各自手工画一份模块划分图汇总成“人工基准答案”然后拿 AI 聚类结果和这三份人工答案计算一致性指标。在只使用目录名做聚类的同类工具中一致性大约在 0.5~0.6 之间而我们基于剪枝依赖图 大语言模型的方案最终一致性做到了 0.72 左右。这个数字谈不上完美但作为辅助分析工具已经足够可靠。第二重验证更让我兴奋是“利用真实改动记录做回测”。我拉取了系统最近一年的版本库提交历史提取“同一提交里同时被修改的文件集合”。如果 AI 聚类是准的那么现实中经常一起修改的文件应该大概率落在同一个 AI 划分出的模块里。用这个天然的真实变更轨迹数据我算出回测命中率发现大约 86% 的“共同修改对”落在同一个模块内明显高于随机基线。5.1 项目中我对“精度”的几个反直觉体会做完整条链路之后我对“高精度”有了几个不太一样的理解在这里分享给后来者第一高精度不等于“和人工画的一模一样”。人工架构图带有强烈的主观场景视角不同人会画出不同风格的图而 AI 画的图追求的是“能够稳定复现、可被数据验证的架构形态”。第二剪枝参数会显著影响后续精度评估。如果剪枝过狠枢纽节点的引出边太少聚类时会丢失大量“模块间连接”信息反之剪枝过轻噪声边会把两个本来无关的模块拉在一起。所以我强烈建议所有想做同类项目的团队把剪枝参数暴露为一个可调的配置项而不是写死在一个魔法数字里。第三现阶段 AI 还不能准确理解所有代码语义。尤其是一些依赖宏或者代码生成器的项目解析出来的结构图可能与其真实运行时结构相差甚远。如果你恰好要处理的是这类项目请把“生成器生成部分”和“手写部分”分开建图再合并分析不要指望 AI 自己去猜测这种隐藏逻辑。5.2 落成什么产品形态团队才真正愿意用最后聊聊输出物。我自己在工具落地这件事上吃过亏只做分析不出图老板不买账只出图不给数据溯源工程师又觉得不可信。所以最终交付物是一个网页版架构查看器左侧是剪枝后的模块拓扑全景图右侧是任何一条边所对应的真实代码引用位置。用户可以从全景图一步步点进去先看模块再展开模块里的文件再从文件跳转到具体的一行 import 语句。整个过程不需要离开浏览器也不需要分析者打开 IDE 一行行找依赖。团队试用一周之后评价出奇地一致——“以前走读一个模块至少要一个小时现在十分钟就能看懂它挂在系统里的哪个位置都不用去翻那些又长又乱的源码注释了。”说实话这个项目做到后面真正产生价值的不只是那张 AI 画出来的架构图而是整条“从代码文本到结构化事实”的流水线。我把这条流水线沉淀成了一个内部工具每逢新同事入职、架构评审、技术债盘点都会把它跑一遍。后来我甚至又加了一个自动对比功能连续跑两个版本的代码库把模块边界的变动直接 diff 出来精确到哪一次提交导致了两个模块之间的依赖出现或消失。这已经远远超过最初“画一张图”的期待了。如果你手头也有一套十万行级别的混乱系统架子我的建议只有一条不要把精力花在“把代码改整洁”的宏大计划上而是先花两到三周时间把它的全景图准确画出来。架构图一旦高清下一步的重构决策就会瞬间变得清楚许多。