AST拓扑剪枝结合大模型:混乱代码库架构还原实操指南 第一次拿到那个十万行的老项目源码时我真的在电脑前愣了很久。没有统一缩进、文件夹命名随缘、到处都是复制粘贴改出来的同名函数现有工具导出的依赖图更是一张蜘蛛网谁看谁崩溃。后来我换了一条路用AST抽象语法树把源码还原成可计算的语法骨架再用拓扑剪枝把与架构无关的细节砍掉最后交给大模型去绘制架构全景图。效果比我预想的好很多——那些隐藏在混乱代码里的核心模块、数据流向、循环依赖居然都能清晰浮出水面。如果你也在为大型遗留系统画架构图发愁这篇文章就是我的完整实操记录。1. 先想清楚十万行混乱代码库到底难在哪1.1 混乱代码库的三层病症动手之前得先承认一个现实十万行代码的复杂度不是“大”而是“乱”。我接触过不少遗留系统真正让架构分析失效的通常不是代码量而是三层叠加的混乱。第一层是结构乱。文件不分层职责不分离一个类里既做业务又做持久化一个函数写了两百行全局变量随意读写。模块之间的依赖方向完全随缘今天A调B明天B又回调A形成一层又一层环。你很难用“上层调用下层”的模型去套它。第二层是命名乱。同一个业务实体在三个文件里叫Order、orderInfo、trade_order还有一堆只改数字复制的函数OrderService2、OrderService3。这种命名对人和机器都不友好。人读起来会误解传统工具按名字聚类也会把本不相关的东西绑到一起。第三层是信息乱。注释要么没有要么严重滞后文档可能还停留在五年前的第一版架构。代码是唯一可靠的真相源但散布在十万行里靠人肉翻阅根本不现实。等你拼完所有细节可能已经过了一个月业务又变了。所以做架构还原的第一步不是选工具而是接受一个事实我们面对的是一个无约定、无文档、无历史的“三无系统”。在这样的系统里任何依赖“约定”的方案都会失效。1.2 传统架构分析工具为什么会失效很多团队一开始会试IDE自带的依赖分析或者打开类图插件。我最初也试过结论是根本没法看。IDE导出的依赖图会把所有调用关系都画出来包括那些“工具函数被五十个地方调用”的边。十万行代码里这种噪声边少说有上万条屏幕上一团线根本分不清哪个是主线。更麻烦的是传统静态分析工具大多依赖几个假设目录分层清晰、包名即是模块边界、依赖方向有规则。可混乱代码库一条都不满足。你告诉工具“所有utils只能被上层调用”工具只能找出几千个违规却无法告诉你这些违规里哪些是真正的架构风险哪些只是历史遗留的无害依赖。还有一类工具试图用“依赖矩阵”来聚类模块比如把所有互相调用的文件聚成一团。但单纯按调用频率聚类会把大量偶然调用也当成强绑定。比如一个全局配置对象被几百个模块读取它就会成为所有聚类的中心把完全无关的业务模块联系在一起最后形成一堆四不像的“簇”。说白了传统工具擅长“如实记录”不擅长“判断重要程度”。但架构图本质上是一种高层抽象我们需要的是删繁就简的能力。这正好是AI可以做、但必须给它喂对原料的场景——直接喂完整源码不行喂传统依赖图也不行这就是AST加拓扑剪枝出场的原因。2. AST拓扑剪枝的工作原理先让AI读懂再让AI取舍2.1 为什么AI需要AST而不是原始源码如果你直接把十万行源码塞给大模型要不了多久就会撞上两个硬墙上下文窗口不够以及细节噪声太大。就算模型支持超长上下文它也会平均分配注意力把大量篇幅浪费在某个工具函数的实现上反而看不清真正的调用骨架。AST就相当于把代码从“文本形态”变成“结构形态”。在看一段代码的时候我们需要的不是每一行是怎么写的而是哪些地方声明了函数、哪些地方调用了谁、哪些类继承了谁。AST把这些关系显式地抽出来注释丢掉格式化细节丢掉函数内部的局部运算也丢掉。我们只保留语法层面的骨架。但这里要补充一句AST本身解决的是语法结构还没到语义层面。要还原架构还需要符号表和类型信息。比如一个同名函数在不同文件里定义AST不会自动告诉你这是两个不同的函数你需要解析作用域并把它们区分开一个调用语句调用的是哪个重载版本也需要类型推断才能确定。所以我的做法是先用解析器拿到全工程AST然后在这个基础上做一轮符号收集把每个函数、类、全局变量都分配一个唯一ID再把所有跨文件的调用、继承、字段引用整理成结构化记录。这个记录已经比源码小几个数量级但仍然不是“架构图”因为边数依旧太多。于是下一步就是拓扑剪枝。2.2 拓扑剪枝在做什么从依赖泥潭到主干图这里说的“拓扑”不是数据结构里那个“拓扑排序”而是指依赖图本身的连接结构。拓扑剪枝的核心思路是在保留图的主要连通结构的前提下去掉对架构判断没有贡献的节点和边。具体到我常用的流程可以拆成五个动作。第一步是聚合。把函数级节点合并到类级或文件级节点。十万行代码可能有五六千个函数但架构图里我们通常只需要看到几十个模块。函数级节点太细AI分不清重点文件级节点通常是合适的粒度。第二步是环压缩。把强连通分量一个互相依赖的节点集合压缩成单个“超级节点”。为什么会这么做因为架构图需要表达“这个子图的内部高度耦合”而不是把所有环内部边都一根根画出来。压缩之后图就清爽了。第三步是聚类。依据调用边的密度、命名相似度、同目录归属等特征把节点聚成候选模块。这一步不需要很精确因为后面AI会帮我们做语义判断但候选模块的质量直接决定AI的上限所以我会用社区发现算法跑一遍再用目录信息修正。第四步是边界过滤。模块之间的边往往是噪声最重的地方。一个模块可能引用另一个模块的十几个工具函数但架构上只需要知道这两个模块有依赖关系。所以我只保留每个模块之间的Top K条依赖边K通常取5到10。边多了AI就会开始纠结“为什么要分这么多条线”。第五步是路径提取。在剪枝后的模块内部再挑出为数不多的几个关键类或关键函数作为代表相当于给AI一个“看内部结构的窗口”。比如某个模块对外暴露了十个函数但真正会被很多外部模块调用的只有两三个那么这两三个就是代表。把上面这些动作串起来的伪代码大概长这样def prune_dependency_graph(dep_graph, k30): # 1. 先把强连通分量压缩成超级节点 g collapse_scc(dep_graph) # 2. 用社区发现算法划分候选模块 communities detect_communities(g) # 3. 把同一社区内的子节点合并成一个聚合节点 super_nodes merge_communities(g, communities) # 4. 只保留跨模块边中权重最高的 k 条 cross_edges extract_cross_edges(super_nodes) important_edges top_k_edges(cross_edges, k, weightcompute_edge_importance) # 5. 根据保留下来的边界点反推模块内部需要保留的代表节点 kept_nodes infer_representative_nodes(super_nodes, important_edges) return ArchitectureSummary(super_nodes, important_edges, kept_nodes)每一步都有取舍。比如“环压缩”会损失内部细节但架构图本来也不需要展示这些细节反而保留环的状态才能提醒我们这里需要治理。“聚类”可能分错但AI在后面生成架构图时可以基于模块职责做二次细化。整个过程的目的是让输入模型的信息从“噪声驱动”变为“结构驱动”。2.3 剪枝后的信息反而更准少即是多的原理我在最初的实验里踩过一次坑当时不舍得剪觉得信息越全AI应该画得越准。结果把一个包含上万节点的依赖图丢给模型它输出了一张看起来像样的分层图但里面全是幻觉——它把很多不相关的模块强行塞进了同一个层因为依赖图里它们有太多边模型根本无法判断谁才是核心。后来做了A/B对比同一套代码一份是完整依赖图一份是剪枝后的摘要分别让同一个模型生成架构图。完整版输出一团乱麻剪枝版却能给出明确的核心层、支撑层和边缘模块。原因其实很简单AI和人一样在信息过载时容易把“出现频率高”误判为“重要”。一个被到处引用的工具函数出现频率很高但它不是架构核心真正重要的是那些连接模块、传递主流程的骨干路径。剪枝不是删除信息而是把信息转化成模型更容易处理的形式。就像地图软件不会把每栋楼每个垃圾桶都画给你而是挑出主干道和关键地标。对AI来说一份经过拓扑剪枝的摘要等于给它一张只留主路的交通图它自然更容易推断出城市的整体格局。所以我在实践中得出一个经验架构图的精度很多时候不取决于你给模型多少细节而取决于你替模型过滤掉多少细节。剪掉九成无关节点模型给出的架构判断反而更稳定。3. 实操流水线从源码到架构图的完整步骤3.1 工具准备与解析层设计先说工具选择。做这条路线的底线是不要自己写语言解析器。解析任意一门现代编程语言都是巨大的工程更别提要解析几门语言的混合项目。我常用的方案是“通用语法树解析器”加“符号解析增强”的套路。通用解析器的好处是覆盖语言多、容错性不错遇到语法不合规的旧代码也能尽量还原AST。但它给的是语法树不是符号表。所以我会在AST基础上再跑一个符号收集脚本把跨文件引用关系理清。对于像Python这种动态语言别名、动态调用都不容易静态解析我还会额外采集运行时调用链作为静态依赖的补充。对于大型语言模型我一般选用代码理解能力较强的模型并且要求它输出Graphviz的DOT语言或PlantUML。如果你手里的模型偏弱也没关系后面会讲怎么调整提示词让它输出更笨拙但更可靠的文本结构。链条大概是这样源码目录 - 通用AST解析 - 符号表构建 - 依赖图生成 - 拓扑剪枝 - 架构摘要 - 大模型生成 - 人工复核这里最关键的是“依赖图生成”和“拓扑剪枝”两步它们决定了最终喂给模型的信息质量。我建议把每一步的中间结果都落盘方便后面调试。尤其是剪枝后的摘要每次生成架构图之前都确认一遍别偷懒。3.2 第一步AST解析与符号抽取我有一次拿到的是混合语言项目有Java、Python和一批SQL脚本。统一的做法是每种语言用对应的解析配置跑一遍输出统一的中间表示。下面以函数级符号收集为例写个简化逻辑def collect_symbols(ast): symbols {} edges [] for node in walk(ast): if node.type function_declaration: # 记录当前函数并关联到它所在的文件 fname qualify_name(node.file, node.name) symbols[fname] FunctionSymbol( filenode.file, start_linenode.start_line, end_linenode.end_line, paramsnode.params, return_typenode.return_type, ) elif node.type call_expression: # 当前上下文是某个函数体记录一条调用边 caller current_context[-1] callee resolve_simple_name(node.callee_name, current_scopenode.scope, symbolssymbols) if caller and callee: edges.append(CallEdge(callercaller, calleecallee, linenode.line)) return symbols, edges这段伪代码展示的是一种非常朴素的做法遇到函数声明就登记遇到函数调用就记一条边。但实际工程里你会遇到很多问题重名函数、通过对象实例调用、lambda回调、反射或eval。这些规则之外的调用AST解析阶段一概看不见。所以我会在解析完之后做一层“模糊度标记”凡是没法静态解析到具体目标的调用单独存一份。后面剪枝和提示词生成时这些模糊调用可以作为一种“需要注意的依赖”告诉AI。千万不要静默丢弃否则架构图会很完美但和真实系统对不上。3.3 第二步构建依赖图与执行拓扑剪枝符号抽取完成之后你会得到一个节点几万、边几万的初始依赖图。不要直接拿去画图先把边分类函数调用、类继承、接口实现、字段类型引用、全局变量读写、数据库表访问。不同类型边的语义不一样剪枝时的权重也不一样。我的经验是数据库表访问和消息事件流这两类边往往比普通函数调用更能反映架构边界。很多所谓的“服务拆分”本质上是把数据库访问边界划清楚。所以我会给这种边更高的权重确保它们不会在剪枝中被轻易丢弃。剪枝参数方面我通常用一套启发式规则先跑一遍再人工微调。规则包括扇入极高比如被超过30个模块调用的节点优先作为公共服务或基础设施节点不参与普通模块聚类。跨模块边只保留Top KK初始设为30。如果模块实在很多再适当放开到50。太多的话模型几乎必然输出“一团线”。强连通分量大于3个节点的全部压缩并单独列一张“循环依赖清单”。同一目录、调用关系紧密的节点默认合并成一个候选模块外部边界只看候选模块之间的连接。剪枝不是一次性的。我通常会剪完一版后快速看一眼保留的边里有没有明显错误。比如有一条边把一个UI类连到了数据库驱动类但中间隔着一个巨大的业务类这种边要么是剪错了要么是真正值得注意的坏味道。留到后面让人工确认。3.4 第三步组装AI提示词并生成架构图这是很多同行最容易忽略的一步。他们以为把GraphML或者JSON依赖图直接塞给模型就行结果输出惨不忍睹。模型不擅长解析杂乱键值对它更擅长读懂接近自然语言的清单。所以我会把剪枝后的结构整理成下面这种形式模块摘要已按耦合度聚类 - module order-core 代表文件: order/service.py, order/model.py, order/handler.py 推测职责: 订单创建、订单状态流转 外部调用: - payment-gateway, - inventory-core - module payment-gateway 代表文件: pay/client.py, pay/callback.py 推测职责: 第三方支付对接、回调处理 外部调用: - accounting-core喂给模型之前我会加一句明确的角色和任务说明比如请基于以上剪枝后的模块依赖摘要完成三件事 1. 识别可以合并到同一子系统层的模块 2. 标出最可疑的循环依赖、跨层访问和上帝对象 3. 用DOT语法画一张简洁的分层架构图不要画函数级细节。为什么要强调“不要画函数级细节”因为模型有时会自作聪明把摘要里提到的代表文件扩展成很细的框。这会让架构图又变成一张工程文件图失去“全景”意义。在提示词里给出输出边界比事后反复修改有效得多。另外我会跑两轮第一轮让模型分析并给出疑问标记第二轮把疑问点的原始文件片段补充给它让它修正模块职责描述。这样模型对不确定的内容有机会自我纠正准确率会明显提升。3.5 第四步人工复核与迭代闭环AI给出的架构图再漂亮也要做人工复核。我的办法是抽关键依赖验证选10到15条剪枝保留后的核心边回到源码里确认这条调用是否真实存在、方向是否正确、是否有中间层被省略掉。别贪多核心边能对上整体图就基本可信。复核后大概率要迭代。常见情况是模型把两个同名模块合并了或者把一个公共依赖误当成上层模块。遇到这种错误我通常不会直接改图而是去调整模块摘要的描述把模块的文件路径和职责写得更具体然后重新生成。十次里能修好七八次剩下实在改不好的手动拖一下节点位置就好。完整闭环跑完之后我建议大家把剪枝参数、AI提示模板、人工发现的问题都记录下来。因为同样的代码库以后还会变化下次架构评审、重构拆服务时这套流水线可以快速复用。每一次人工修正也在间接优化未来的剪枝结果。4. 工具选型与落地配置4.1 解析器与模型选择解析器方面我见过两种流派。一种是语言专用解析器优点是符号表、类型信息非常准确但面对多语言项目要拼凑多套工具链维护成本高。另一种是通用语法树解析器覆盖语言多、容错强但拿不到类型和语义信息。我自己是混合用用通用解析器做全量粗分析再用语言专用工具对核心路径做精查。模型选择上代码能力较强的模型表现明显更好。它们不仅理解AST摘要里的技术名词还更容易输出结构化的Graphviz。如果只能用通用文本模型建议把提示词里的模块名改成更贴近业务的名字比如把order-service改成“订单系统”模型理解起来会轻松不少。这个细节我试过很多次值得你留意。4.2 关键参数与经验阈值参数这东西不同代码库差别很大。但有几个经验阈值可以作为起始参考。控制台我会写成一张表方便你对着调。参数项初始建议值调整方向节点聚合粒度文件级十万行以下可以用类级十万行以上用文件级更稳跨模块保留边数K30模块多时提到50超过50后AI开始分不清主次只对跨模块边剪枝模块内部保留代表节点3至5个/模块代表节点太多会诱发AI画内部细节强连通分量阈值大于3个节点压缩小于等于3个节点可以保留避免损失太多信息模型temperature0温度调高会得到多样化架构图但可复现性差迭代轮数2至3轮超过3轮边际收益很低不如人工介入这些参数不是真理但可以当起点。每换一个代码库都先用一个小模块跑通再放大到全量。4.3 为什么不建议做成全自动无人校验很多人希望“上传代码直接出架构图”但我必须泼一盆冷水全自动在混乱代码库上不可靠。因为剪枝会丢信息AI会补全信息这个“补全”过程必然伴随幻觉。如果你把架构图直接当成唯一真相后续重构决策就会被误导。所以我的落地模式是“半自动”机器负责剪枝、聚类、生成候选架构图人负责审核心边、重点环、可疑跨层调用。大约是七成自动三成人工。刚开始你觉得人工很累但跑过一轮你会发现真正需要人工判断的就是那么十几个点。这已经比从零读代码高效太多。5. 模拟实战十万行交易系统的架构还原5.1 项目现状与目标我不是拿真实生产项目来展示但为了说明完整流程我用一个典型的模拟项目X一个交易系统代码量约12万行跨Java、Python和SQL脚本三门语言模块目录超过50个无任何架构文档。历史遗留的共享数据库、消息队列、定时任务全混在一起。项目目标是搞清楚三件事系统是否能按业务边界拆成微服务哪些模块之间存在隐藏的循环依赖哪些模块直接访问数据库造成数据耦合。5.2 剪枝还原过程先跑全量解析共识别出约4200个函数和类初始依赖边有3.8万条。如果直接画根本没救。接着用社区发现聚类得到17个候选模块比原来的50个目录少了很多说明代码的实际耦合度和目录结构确实不一致。之后做环压缩发现4个明显的强连通分量其中最严重的是一个订单与额度管理之间的双向调用环。跨模块边压缩到57条再用TopK保留到28条。这28条边组成了架构图的主干。然后我把候选模块摘要和这28条边喂给模型生成出来的架构图把我吓了一跳它居然正确识别出了“核心交易引擎”“风控接口”“额度管理”“消息队列接入”“数据访问层”“外部网关”六个子系统比原目录结构清晰得多。下面是简化后的结果示意。子系统参与的主要模块关键依赖问题标记核心交易引擎order-core, trade-executor依赖风控和额度管理与额度管理互相调用风控服务risk-check, rule-center依赖数据访问层没有明显问题额度管理quota-center, quota-rule依赖核心交易引擎也被其反向调用存在循环依赖消息队列接入mq-listener, event-dispatcher依赖订单和账户模块两个业务逻辑耦合在同一个消费者里数据访问层db-dao, shared-db几乎所有模块直接访问没有统一数据入口外部网关thirdparty-gw, callback只依赖订单模块相对干净生成这个图只花了几分钟比人工梳理节省了至少三天时间。5.3 发现的问题与架构结论架构图出来之后我们重点验证了三个发现。第一个发现是“所有模块直连数据库”。这不是靠调用关系发现的而是靠数据库表访问边统计出来的。模型在摘要里看到了十几张表被多个模块同时访问自然得出“数据层没有收敛”的结论。第二个发现是“订单状态机与额度管理双向循环”。这在代码里其实藏得很深因为在文件级别上看它们是两个目录互相调用也不多但顺着剪枝保留的代表节点往里查发现订单回滚时需要额度模块做释放而额度模块冻结时需要订单模块查状态最终形成一个环。第三个发现是“消息队列消费者耦合”。这个光靠AST和剪枝并不容易看出来因为它是通过消息事件流连起来的。我在依赖图里额外抽取了“事件订阅”这种边才让模型意识到同一个监听器里处理了两种不同业务。人工抽检了10条关键依赖有8条与源码完全一致2条有误。错误原因都是同名类在不同包导致AI混淆。修正方式是把这些类的完整限定名写进模块摘要重新跑一轮错误就消失了。这个案例说明AI不能保证100%正确但当目标是要在几小时内摸清一座三无代码库的大致架构它绝对能打。更重要的是它给出的问题线索给后续人工深挖提供了非常精准的方向。6. 常见问题与避坑清单6.1 常见故障快速排查表跑这条流水线时会遇到不少麻烦。我把踩过的坑整理成一张速查表遇到问题直接对着找思路。现象可能原因解决办法解析阶段大量文件失败代码包含预处理宏、模板语法或非标准扩展开启解析器容错模式对失败文件单独配置方言初始依赖图爆炸内存不够节点粒度太细函数级节点太多先用文件级聚合再构建依赖图剪枝后图变成一堆孤立模块聚类参数太高把弱连接模块也拆开降低模块尺寸限制保留一些“弱但存在”的跨模块边AI输出层次不清所有模块平铺提示词没要求分层跨模块边保太多限制层次数量让模型按核心/支撑/边缘输出AI编造了源码里不存在的模块摘要里模块名不唯一语义模糊在模块名后标注代表文件路径重新生成关键依赖被剪掉了只按调用次数剪枝忽略了数据流和事件边给数据库访问、消息订阅边更高的权重多次生成结果不一致模型temperature太高固定temperature为0必要时固定随机种子6.2 几个我用真金白银换来的心得第一先小规模调参再全量跑。我刚开始直接全量十万行解析慢不说剪枝参数也定得稀里糊涂结果跑出一张没法看的图。后来先抽一个最乱的模块大约一万行把“保留多少边”“聚类到什么粒度”调好再套到全量上。省下的时间比多花的那一下午多得多。第二千万不要只看函数调用关系。架构边界最大的敌人是数据库耦合其次是消息队列和共享缓存。如果依赖图里没有数据流边AI画出来的架构图会“看起来很干净”实际却不准确。我会把所有数据库表访问和事件订阅也建模成依赖边哪怕这些信息在AST里需要额外解析。第三坏味道要当作一等公民告诉AI。我早期提示词只说“请生成架构图”模型输出一张干净的图把循环依赖和跨层调用都美化了。后来改成“请优先识别循环依赖、跨层访问和上帝对象”输出质量立刻不一样。AI需要知道你的目标不仅是描述现状还要诊断问题。第四剪枝摘要里一定要保留可追溯的文件路径。这个细节让我避免了很多次AI幻觉。模块名可以抽象但每个模块至少要保留一个代表文件路径比如src/main/java/com/xxx/order/core/OrderService.java。这个路径会让模型在判断模块职责时有一个锚点不容易把两个同名类搞混。最后每次生成结果都留档。AI输出有随机性即使temperature为0一次细微的提示变化也可能导致不同结构。把每次的输入摘要、提示词、输出图都存下来出问题的时候才能回溯是哪一步丢了关键信息。我自己在这套流程上前后改了四版才跑通。最核心的认知是AI画架构图不是一个“把代码丢给模型”的处理而是一种信息策展。模型只是在帮你做最后一道归纳真正决定准确率的是你喂给它的剪枝图。你替它想清楚哪些信息重要它才能替你画出一张真正能用的架构全景图。如果你也在面对一座意大利面代码库建议找一个相对独立的模块先试这个流程。切一小块跑通剪枝再给模型生成第一版草图哪怕一开始不完美也会让你快速看到旧代码里那些被埋没的边界。最后再分享一个很实用的小技巧剪枝后的摘要里一定要保留有代表性的文件路径就这一步AI的错误率会明显下降。