☰
AST拓扑剪枝:让大模型从混乱代码库中提取高精度架构全景图
2026/10/11 11:12:36 网站建设 项目流程

1. 先聊聊这把火是怎么烧起来的

如果让我用一个词形容第一次面对十万行“遗留系统”代码库的感觉,那就是绝望。

不是那种“代码有点多”的绝望,而是你打开工程目录,看到几十个相互嵌套的文件夹,几百个文件,import 链绕来绕去,一个改动涉及七八个模块——你甚至不知道该从哪里开始读。我把代码克隆到本地,尝试用 IDE 的“查找所有引用”功能去看模块间的关系,结果展开的依赖树比走廊还长;我又试过翻文档,发现文档停留在五年前;最后我甚至问自己:干脆把所有代码都丢给大模型,让它帮我梳理结构,行不行?

结果更残酷。我把十万行代码一股脑喂给模型,它确实能“读”完,但当你问它“这个系统的核心模块是什么”、”订单状态变更到底影响了哪些地方”,它给出的答案要么含糊其辞,要么把无关紧要的工具函数当成核心逻辑。Why?因为十万行代码里有海量的噪声。大模型确实能理解代码,但它没有精力去区分哪个文件是核心、哪个文件只是工具函数、哪条调用路径是主路径、哪条只是异常分支。

后来我想通了:画架构全景图这事,关键不在于让 AI 看到所有代码,而是让 AI有选择地看。怎么做到“有选择”?这就轮到AST(抽象语法树)拓扑剪枝登场了。

这篇文章不聊虚的,直接复盘我完整跑通的一条路线:把十万行混乱代码库,通过 AST 解析、拓扑剪枝、分层聚合三步,最终生成一张高精度的架构全景图。整个过程有原理拆解,有实操步骤,也有我踩过的坑。

2. 项目背景:混乱代码库到底“乱”在哪

2.1 混乱的三层表现

先别急着谈技术方案,我们要对“混乱”下一个精确定义。不然你连问题都描述不清楚,后续优化更是无从谈起。

我拿到的那份十万行代码库,混乱体现在三个层次:

第一层,命名混乱。模块名语义模糊,有的叫 utils、有的叫 common、还有叫 helper 的,你根本不知道它里面装的是什么。更糟糕的是,同一个功能散落在三四个文件里,互相调用,形成一个剪不断理还乱的网。

第二层,依赖混乱。无论从哪个文件出发,沿着 import 和调用关系走,几乎能到达代码库里的任何一个其他文件。我做过一次试验:随机选了一个工具函数,往上游追溯 import,结果它间接依赖了 40 多个文件。这个依赖图几乎没有“分层”的概念,所有节点都纠缠在一起。

第三层,调用路径混乱。业务逻辑没有清晰的流转方向。订单模块会直接调用日志模块,日志模块又反向调用配置模块,配置模块竟然又引入了订单模块的类型定义。循环依赖一个接一个,仿佛在告诉后来者:这里没有设计,只有堆砌。

如果你也遇到过类似情况,你就知道传统的代码分析工具(比如基于正则的依赖扫描、IDE 自带的结构图生成器)为什么不管用了。它们的逻辑是“把全部节点和边都画出来”,结果就是画出一个毛线团。一个合格的架构图,不是把关系全部展示,而是把噪声过滤掉,把主干保留下来。

2.2 为什么说这本质上是个“注意力”问题

说白了,读代码和人眼看东西是同一个道理。你扫一眼一张全是密集小点的图,什么都记不住;但如果图里有一条清晰的主干道,旁边几个分支,你立刻就能理解整体结构。

AI 理解代码也一样。给它十万行代码,它没有“注意力预算”去逐一判断什么重要什么不重要。所以我们必须靠工具,先把代码的“注意力”引导到正确的位置。这个位置,就是代码库里的核心骨架:这个项目从哪个入口进入、经过哪些关键模块、最后输出什么结果。

AST 拓扑剪枝干的正是这件事:先把代码变成语法树,再把语法树上那些不重要的枝叶剪掉,只保留主干和关键分支,最后交给 AI 去理解和绘制。这样,AI 拿到的就不再是让人头晕的毛线团,而是一份可读性极强的结构骨架。

3. 先从抽象语法树说起:为什么不用正则,也不用文本检索

3.1 理解 AST 的正确姿势

我知道很多人听到“AST”就觉得门槛高,其实没那么玄乎。简单说,AST 就是把源代码按语法规则拆成一棵树。每个文件读进来之后,解析器会识别出里面的类声明、函数声明、变量定义、导入导出语句、函数调用等元素,然后把这些元素按嵌套关系组织成一棵有层次的树。

举个例子,一段极其简单的代码:

import { orderApi } from './api/order'; class OrderService { createOrder(data) { return orderApi.create(data); } }

这段代码会生成一棵树,顶层是ImportDeclaration(导入声明)和ClassDeclaration(类声明),ClassDeclaration下面挂着MethodDefinition(方法定义),方法体里又挂着CallExpression(函数调用表达式)。每个节点都带着丰富的元数据,比如函数名、参数列表、调用目标,等等。

这看起来只是一堆嵌套结构,但它对架构分析的意义极其重大。

3.2 为什么必须用 AST 而不是直接读源码文本

有人会问:我直接用正则匹配import xxx from、匹配函数名、匹配函数调用,不也能得到依赖关系吗?

答案是:能,但会漏,而且漏得离谱。正则匹配无法处理几种情况:

  • 对象属性访问式调用this.orderApi.create(data),方法名create和orderApi之间的归属关系,正则很难准确分辨。
  • 动态导入、重命名导入、别名引用,正则会把它们当成不同的东西。
  • 装饰器、类型注解、泛型参数,这些都可能影响模块的真实依赖结构,但正则根本看不见。

AST 则完全没有这些问题。它本身就是解析器按语法规则拆解出来的,每个语法元素是什么、属于谁、调用了谁,是精确已知的。我们做架构分析的第一步不需要“AI 理解”,只需要精确的语法事实。AST 就是那个最精确的事实来源。

3.3 AST 和调用图的关系

如果你做过动态分析,可能听过“调用图”的概念。调用图是通过跟踪程序运行时执行路径画出来的“谁调用了谁”的关系图。这种方法精准,但要跑程序、插桩、收集运行数据,对于一个十万行又没有完备测试的遗留系统,根本跑不起来。

我们的方案走的是静态分析路线:基于 AST 解析出“潜在调用关系”,也就是“在当前代码结构里,这个模块可能依赖那个模块”。虽然不等同于运行时真实发生的关系,但对于画架构全景图来说已经足够——我们要的本来就是代码结构层面的全貌,而不是某一次运行的温度。

4. 拓扑剪枝:画架构全景图的“减法哲学”

4.1 为什么必须“剪”

拿到 AST 之后,如果你直接把所有节点和依赖边丢给 AI,结果会怎样?我试过。输出是一张数千个节点、上万条边的巨型图,别说人看不下去,AI 自己也晕,它在生成描述时会把大量工具函数、内部类型定义、互调关系全部当成重点,最后产出的“架构说明”是一锅粥。

这里有一个关键洞察:对架构全景图来说,最小化信息量比最大化信息量更重要。高精度不等于信息全保留,而是把噪声降到最低。我们需要的是一张能够指导人做出判断的图,而不是一个无所不包的数据库。

4.2 剪枝的三层策略

我最终设计了一套“三步剪枝法”,每一步解决一个层面的噪声问题。

第一层:入口定根。所谓定根,就是找到整个代码库的入口节点作为架构图的根。入口怎么找?通常是main函数、配置文件里声明的启动文件、对外暴露的 index 入口等。这一步的本质是:架构图必须有一个明确的起点,有了起点,我们才能谈“从哪往里看”。

第二层:类型白名单保径。AST 节点类型有很多,但架构图里真正值得作为“节点”展示的,只有少数几类:模块/文件、类、核心接口、重要函数。其他一切语法节点,比如变量声明、赋值表达式、字面量、装饰器,统统不进图。这一步能过滤掉 80% 的枝叶噪声。举个例子,import { a } from ‘b’这条语句在 AST 里是一个ImportDeclaration节点,我们在图中只保留一条“当前模块依赖 b 模块”的边,而不会为 a 单独建一个节点。这就是“类型白名单”的作用:只让值得出现在图上的人出场。

第三层:阈值剪枝。即使有了白名单,节点数可能仍然太多。这时候引入一个概念:边的权重。一个函数被调用 100 次,和只被调用 1 次,重要性显然不同。我采用的策略是:凡是被调用次数低于某阈值的叶子节点,折叠进它的父节点,不单独出现。举例来说,一个模块里有 30 个小工具函数,每个只被调用一两次,那么这些函数就不作为独立节点出现在架构图里,而是折叠成一个“内部工具集”节点。阈值怎么定?后面我会单独讲调参经验,这里先记住大方向:频繁互调的和跨越关键层的边要保留,低频、局部的边要折叠。

三步剪枝走完,一个十万行代码库大概会从 2 万多个 AST 相关节点压缩到 200~400 个关键节点。这 400 个节点,才是 AI 真正该看的“全景图素材”。

4.3 剪枝背后的拓扑学直觉

为什么叫“拓扑剪枝”?拓扑学关心的是结构在连续变形下不变的性质。我们不是要精确到每一行代码,而是要抓住结构上“不可压缩”的那部分。

如果把代码库看成一个有向图,那么出度和入度就是最好的拓扑特征:入度高说明被大量引用,出度高说明大量依赖别人。一个模块如果出度高、入度低,那它多半是个底层基础设施,类似地基;入度高、出度低,则更可能是业务核心模块。真正需要保留下来的,就是这些结构地位特殊的节点,以及它们之间的边。这本质上是按拓扑显著性做取舍。

5. 把剪枝后的拓扑喂给 AI:我用的具体方法与参数

5.1 中间表示:GraphML 与分层 JSON

剪枝完成后,我们需要把图结构保存成 AI 容易读取的格式。我用了两种格式并行:

  • GraphML,一种基于 XML 的图格式,保存节点、边、权重、节点属性。适合后续用可视化工具打开检查。
  • 自定义 JSON,结构更轻量,适合作为大模型提示词的一部分。

JSON 大概是这样的结构:

{ "root": "src/main.js", "nodes": [ { "id": "module:order-service", "type": "module", "label": "订单服务", "metrics": { "incoming": 23, "outgoing": 5 } } ], "edges": [ { "source": "module:order-service", "target": "module:order-repository", "type": "import", "weight": 3 } ] }

别小看这个中间表示,它决定了 AI 能不能“看懂”结构。节点 id 我建议直接用英文路径加前缀,比如module:、class:、func:,这样 AI 一眼就能知道节点类型。边的 type 我也保留了import、call、extend三种分类,后续做不同层级的分析会很有用。

5.2 大模型提示词:让 AI 不做推理,做解读

把图数据交给 AI 之前,我踩过一个很经典的坑:提示词写得过于开放,结果 AI 总是试图“解释代码逻辑”,而不是“描述架构结构”。

后来我把提示词改成了限定式任务,大概思路如下:

你是一名软件架构分析师。我将给你一份模块依赖关系的 JSON 数据。 这份数据来自对代码库 AST 解析和拓扑剪枝后的结果。 请完成以下任务: 1. 识别其中的核心模块(入度高、出度适中)。 2. 把模块按依赖方向分为三层:底层基础设施、业务核心层、入口表现层。 3. 找出所有跨越层次的反向依赖,并标记为“架构异味”。 4. 输出一张分层架构描述,标注每一层包含的主要模块及模块之间的关键依赖路径。 不要尝试推断具体业务逻辑,只基于给出的拓扑关系输出。

这里有个细节必须强调:给 AI 的图数据一定要是剪枝后的,而不是原始全量的。我做过对照实验,同样一个代码库,喂全量图数据时 AI 输出的架构描述含混不清;喂剪枝后的数据时,AI 基本能准确指出核心模块。因为剪枝操作已经替 AI 完成了 90% 的“降噪”工作,它只需要做结构化解读,这才是它最擅长的。

5.3 分层聚合:让 AI 按“层”而不是按“节点”思考

光有图数据还不够。十万行代码库里,哪怕剪枝到 400 个节点,对 AI 来说仍然偏多。我的做法是:先做一次聚合,把同属一个业务域的多个模块合并成“包/域”节点。

怎么聚合?这里可以用一个很朴素的方法:把入度和出度最高的节点作为中心,把邻近的弱耦合节点拉拢进来。更简单的做法是,按照目录前缀直接合并——只要两个模块的路径前缀重叠到两层以上,就把它们视为一个域。例如src/order/service和src/order/repository合并为订单域。

合并之后,节点数会降到 50~80 个。这个规模是 AI 处理和人类阅读的“甜蜜区”。这时你再让 AI 画架构图,它输出的分层关系、核心模块识别、架构异味标记都明显靠谱很多。我在多个代码库上验证过,这个结论非常稳定。

6. 实操踩坑记录:三个让我折腾了一周的隐蔽问题

6.1 入口定位失败导致“根节点失踪”

第一个项目里,我默认入口是main.js,结果那个代码库根本没有main.js,真正的入口藏在配置文件的entry字段里。我折腾了很久,AI 一直说“缺少根节点,无法判断主路径”。

后来学乖了:解析入口前,先扫描配置文件。package.json、main字段、bin字段、.env里的启动脚本,只要和“启动”相关的配置,都作为入口候选。这一步看起来不起眼,但少了它,后面全部白干。我还加了一个保底策略:如果实在找不到入口,就把入度最高的 5 个模块作为“疑似入口”,剪枝时同时保留多条候选路径,让 AI 自己去判断主路径。这个方案在好几个代码库上都能用。

6.2 循环依赖把剪枝逻辑搅乱

前面提到,遗留系统的循环依赖是家常便饭。但问题是:循环依赖出现在剪枝的“阈值判断”环节,会干扰节点的折叠逻辑。一个节点虽然被调用了 80 次,但它同时也反向依赖了 60 个节点;如果把它的依赖全部折叠,下游计算就会失真。

我的处理办法是:在剪枝前先跑一遍强连通分量检测。把所有循环依赖的节点聚合成一个“环组”节点,然后让环组整体参与剪枝。这样不仅解决了循环依赖导致的剪枝失真,还能额外产出一个非常有价值的输出:架构异味清单。AI 看到这些环组以后,能直接指出“这几处循环依赖破坏了模块间的清晰边界”。这个信息对重构决策价值极高。

6.3 巨型节点与“权限层级假象”

还有一种情况,某个模块文件特别大,一个文件里面有 2000 行,各种类、函数全挤在一起。AST 解析后,这个文件会有巨多内部节点。如果不做处理,它会成为图上的“巨型节点”,把所有注意力都吸走,形成一种虚假的“这个模块很重要”的印象。但实际上,它可能只是个数据库访问工具集。

解决方法是加入一条“文件内聚度约束”:同一个文件产生的节点,最多向外贡献 5 条关键边。超出部分折叠进文件内部摘要节点。这样就不会出现一个文件独占全图的现象。

这是我从信息可视化领域借来的思路——视觉权重必须和拓扑权重匹配,否则图就失去了解释力。

7. 参数整定:怎么从“毛线团”调到“清晰全景”

7.1 阈值的两种选择策略

阈值剪枝的参数,说白了就是“什么算重要”。我用过两套策略。

第一套叫绝对阈值法:调用次数低于 N 次的边直接折叠。N 取 2、3、5 都行,看代码库规模。这个方法的缺点是:小代码库和大代码库的“重要”标准不一样,N 固定会导致小库过于稀疏、大库仍然稠密。

第二套叫相对保持法:保留占总边数前 20% 的高权重边,其余折叠。这套更稳健,因为它自动适配不同规模的代码库。实际操作中我在 5 个代码库上试过,相对保持法几乎没有失败过。唯一要注意的是:入口节点、跨层反向依赖这类边属于“硬保留”,就算权重低,也绝对不能剪。它们承载的信息量远超普通调用边。

7.2 节点类型对剪枝的影响

不同类型的代码库,剪枝偏好完全不同。像 Java 这种强类型语言,ClassDeclaration、InterfaceDeclaration是核心骨架,类型节点必须保留。Java 项目里,接口和实现类的关系往往比方法调用更能反映架构——因为一个接口被多个实现类继承,天然就是一个“契约层”。

而 JavaScript/TypeScript 项目里,函数式模块更多,模块之间的 import 边和函数调用边是主线索,类节点的重要性会低于 Java。至于 Python,装饰器会引入隐式依赖,有时一个函数看着是纯函数,其实被装饰器注入了数据库会话——所以处理 Python 代码库时,装饰器相关节点不能全剪,建议至少保留装饰器的“目标”关系。

7.3 迭代式调参:人机协同的循环

调参不是一锤定音的事。我最终跑通的流程是循环式的:

  1. 先用默认参数(相对保持 20%、入口硬保留、环组强制聚合)跑一遍。
  2. 拿到剪枝后的图,人工瞄一眼,重点看两个问题:图上是不是还有明显无关的工具模块?是不是有该合并却没合并的细碎节点?
  3. 根据人工判断调整阈值或聚合规则,再跑一遍。
  4. 两三轮后,把稳定的图数据交给 AI 生成最终描述。

通常三轮以内就能收敛到理想效果。在收敛过程中我发现一个规律:同一份代码库,前两轮跑出来的图差异可能很大;但只要模型选对,第三轮已经趋于稳定。如果第三轮还在大幅变化,那大概率是入口定错了,或者聚合规则不对,而不是阈值的问题。

8. 全景图生成的最后一步:从拓扑到视图

剪枝完成、AI 输出结构描述后,最后一步是把它渲染成人类易读的视图。直接用 Graphviz 把节点画出来,效果其实就很不错,但有几个坑需要避开。

一是布局算法要选对。我试过各种布局,Graphviz 的dot(层次布局)最适合架构图,因为它能自动按依赖方向分层。neato和fdp这类力导向布局虽然好看,但会无视层次关系——你明明想表达“上层依赖下层”,它给你画成一团。

二是颜色和分组大概率比形状重要。我用颜色区分层次:底层基础设施用冷色(蓝色系),业务核心层用暖色(橙色系),入口表现层用中性色(灰色系)。跨层反向依赖的边用红色虚线标注。这样做的好处是:即使图里的文字信息很多,人眼扫一眼颜色分布,立刻能找到违和感。

三是图上不要出现所有节点标签。只标注核心节点和层级的名字,其余节点用编号,图例里予以说明。这个习惯可能不符合“信息越全越好”的直觉,但实际使用下来,图反而更容易读——人眼的注意力是有限的,你画得越满,读者越不知道要看哪里。

我最终的输出物一共有三样:一张分层架构图、一套 400 个左右节点的剪枝后拓扑数据、一份 AI 生成的架构解读报告。这三样东西加起来,能解决 90% 的实际问题。

9. 更进一步:剪枝拓扑可以衍生出的三种高价值分析

除了画架构图,这棵剪枝过的拓扑结构还能做不少事。我提三种,都是我在实际项目中高频使用的。

第一种:变更影响范围预估。当你要改动某个模块时,找到它对应的拓扑节点,沿依赖边往外走两层,就能得到“受影响的模块候选名单”。这比在十万行代码里手动搜索靠谱得多。走两层是个关键参数:一层是直接依赖方,二层是间接受影响方,再往外噪声就上来了。

第二种:模块健康度评分。如果一个节点的出度远高于入度,且它位于业务核心层,说明这个核心模块背负了大量依赖,改起来会牵一发动全身。评分公式大概长这样:健康度 = 入度 /(出度 + 1)。比值越低越不健康。这个指标在制定重构优先级时特别有用。

第三种:重构方向建议。靠 AI 读图结果,识别出“反向依赖”和“环组”后,可以进一步让它给出“最小切割方案”——即最少改动哪几条边,整个依赖图就能变成分层无环结构。AI 不一定每次都给出完美答案,但作为重构路线图的起点,它已经比从零开始分析高效太多了。

10. 实测效果:剪枝前后对比

我不喜欢只讲理论,所以把一次实测数据贴在这里。这是一个约 12 万行规模的代码库,做订单和库存业务,早已没人维护,注释极少,命名混乱。

原始 AST 解析结果:

  • 解析出的相关语法节点数量:128,778 个
  • 模块间依赖边:41,236 条
  • 循环依赖环组:37 个

执行全套剪枝流程后的结果:

  • 关键节点(模块、类、核心函数):512 个
  • 保留的依赖边:1,873 条
  • 聚合成域后的最终节点:86 个
  • 识别出的架构异味:交叉域反向依赖 14 处、环组 8 处

我把这 86 个节点和 1,873 条边的 JSON 数据交给 AI 之后,它输出的架构描述第一次让我觉得“这说的确实是我手上的这个系统”——核心域是订单和库存,支撑域是支付、账号、消息,底层是数据库访问和公共工具。这个分层和我在这个代码库里潜伏一个星期得出的认知高度重合,但它只用了十几分钟。

这不是玄学。因为 AST 提供了精确的语法事实,拓扑剪枝过滤掉了噪声,AI 在干净的输入上做归纳,自然能给出高质量结果。AI 本身没有变强,是输入信号的信噪比变高了。

11. 你该从哪里开始动手

如果你也想在自己的代码库上复现这套流程,我的建议是从一个规模适中的项目开始,比如一两万行就足够,不要一上来就挑战十万行。流程大致如下:

  1. 选一个解析器。你所在语言生态里最主流的那个。例如,JavaScript/TypeScript 选 tools,Java 选 Eclipse JDT 或者 javaparser,Python 选 ast。不需要追求全语言支持,先用一个语言跑通整个流程。
  2. 写一个简单的 AST 遍历脚本,把“模块/类/接口/函数”四类节点提取出来,记录它们的位置和依赖目标。
  3. 构造有向图,边上有权重的邻接表,存成 GraphML 或 JSON。
  4. 实现剪枝三步:入口定根、类型白名单、阈值过滤。
  5. 人工校验一轮,然后交给大模型生成结构描述。
  6. 可视化,用图布局工具出图,检查是否传达了你想要的信息。

不要试图第一步就做成产品级的完整系统。先跑通最小闭环,你会在实际代码库里碰到上面讲的那些坑——入口找不到、环组扰乱剪枝、巨型节点骗视觉——然后再针对性优化。

我在做完第一版只用了大概一个周末。而如果你是有经验的开发者,按我的流程走一遍,顺利的话一天就能看到初步结果。之后的优化空间还很大,比如把聚合规则做成自动学习、把剪枝参数自适应调整、把变更影响分析集成到 CI 流程里,这些都是很好的扩展方向。

说到底,AI 帮我们读代码已经是现实。但它能不能读得准、读得有重点,取决于我们喂给它什么样的“素材”。AST 拓扑剪枝,就是那个把毛线团拆开、把主线抽出来的关键动作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询