1. 十万行代码库的架构困局与破局思路接手一个十万行级别的代码库最让人头疼的往往不是某个具体 bug而是我根本不知道这东西是怎么跑起来的。函数调用层层嵌套模块之间互相引用改一处崩三处新人上手周期动辄以月计算。这种场景下传统的架构图工具基本失效——它们要么依赖人工维护没人会坚持更新要么只能画出静态的目录结构毫无意义要么生成的图密密麻麻像一团毛线球根本没法看。我最近在做一个代码库分析项目时就遇到了这个典型问题。目标是用 AI 自动生成高精度的架构全景图让开发者能一眼看清模块边界、调用链路和核心枢纽。但直接让大模型去读十万行代码结果只有两种要么上下文塞不下要么输出一堆似是而非的废话。真正让这件事跑通的是一套基于AST抽象语法树拓扑剪枝的处理管线。这篇文章就把这套方案的完整思路、核心实现和踩过的坑从头到尾讲清楚。先说结论AST 负责把代码变成结构化的图拓扑剪枝负责把图压缩到 AI 能理解、且信息不丢失的规模最后 AI 负责把结构翻译成人能看懂的架构描述。三者缺一不可。这套方案适合任何需要理解大型代码库的场景——不管是接手遗留系统、做架构评审还是给 AI 编程助手喂上下文都能直接复用。2. 为什么不能直接把代码丢给 AI2.1 上下文窗口的硬约束很多人第一反应是现在大模型上下文都上百万 token 了十万行代码撑死也就几十万 token直接塞进去不就行了这个想法在实际操作中会撞上三堵墙。第一堵墙是有效上下文衰减。模型标称支持 100 万 token不代表它在 100 万 token 的输入上还能保持推理质量。实测下来当输入超过某个阈值后模型对中间部分内容的注意力会显著下降也就是业内常说的lost in the middle现象。你塞进去的代码它可能只认真看了开头和结尾。第二堵墙是信噪比极低。十万行代码里真正决定架构的可能是几百个核心函数和它们之间的调用关系剩下的大量是工具函数、配置、测试、注释、样板代码。这些内容对理解架构几乎没有贡献却会稀释模型的注意力。第三堵墙是成本。每次分析都塞十万行代码token 消耗是天文数字迭代几次成本就失控了。而且代码一变就得重新分析根本没法做增量。2.2 直接摘要为什么也不靠谱那退一步先让 AI 分段摘要再把摘要拼起来我试过效果很差。问题在于摘要会丢失结构信息。一个函数被摘要成处理用户数据但它在调用链中的位置、被谁调用、调用了谁这些拓扑关系全丢了。最后拼出来的架构图模块之间的连线要么缺失要么是模型脑补的精度根本没法保证。架构的本质是关系不是内容。你要画的是谁依赖谁数据怎么流动哪些是核心枢纽这些信息必须从代码的结构中提取而不是从代码的语义中猜测。这就是为什么必须走 AST 这条路——AST 天然保留了结构关系。2.3 AST 拓扑剪枝的核心价值AST 拓扑剪枝的思路其实很朴素把代码解析成语法树从树中提取出函数、类、模块之间的依赖关系构建一张有向图然后对这张图做剪枝保留架构关键节点和边丢弃噪声。打个比方这就像给一座城市画交通主干图。你不需要画出每一条小巷只需要标出主干道、枢纽立交和关键路口。AST 就是城市的完整地图拓扑剪枝就是提取主干道的过程最后 AI 拿着主干图去描述这座城市的交通格局。这套方案的优势在于三点结构保真关系来自代码本身不是猜测、规模可控剪枝后节点数能压到 AI 舒适区、可增量代码变了只需重新解析变化的文件。3. AST 解析与依赖图构建的实操细节3.1 工具选型为什么是 tree-sitter解析 AST 的工具很多Python 有内置的ast模块Java 有 JavaParserJavaScript 有 Babel。但面对多语言混合的代码库我最终选了tree-sitter。原因很直接它支持几十种语言解析速度快容错性强代码有语法错误也能解析出部分结构而且提供统一的查询接口。对比一下常见方案方案多语言支持容错性速度增量解析语言内置 parser单语言差中不支持Babel/JavaParser单语言中中部分支持tree-sitter多语言强快支持tree-sitter 的增量解析能力特别关键。十万行代码全量解析一次可能要几十秒但后续只解析改动的文件几毫秒就能完成。这对需要频繁分析的场景是刚需。安装很简单Python 环境下pip install tree-sitter tree-sitter-languages3.2 从语法树提取依赖关系解析出 AST 只是第一步关键是从树中提取出我们需要的依赖关系。我定义了三种核心关系调用关系函数 A 调用了函数 B导入关系模块 A 导入了模块 B继承关系类 A 继承了类 B用 tree-sitter 的查询语法S-expression可以精准匹配这些模式。以 Python 为例提取函数调用的查询大概长这样import tree_sitter from tree_sitter_languages import get_language, get_parser PY_LANGUAGE get_language(python) parser get_parser(python) # 查询所有函数调用 call_query PY_LANGUAGE.query( (call function: (identifier) func_name) ) def extract_calls(source_code): tree parser.parse(bytes(source_code, utf8)) captures call_query.captures(tree.root_node) calls [] for node, name in captures: calls.append({ callee: name, line: node.start_point[0] 1 }) return calls这段代码会返回文件里所有的函数调用点。但注意这里拿到的只是调用了某个名字还需要结合作用域分析才能确定到底调的是哪个函数——同名函数在不同模块里很常见。这一步我用了简单的符号表来消歧先收集所有文件里定义的函数和类建立名字到定义位置的映射再回填调用关系。3.3 构建全局依赖图单个文件的依赖提取完后要把所有文件的结果合并成一张全局有向图。节点是函数/类/模块边是依赖关系。我用 networkx 来管理这张图import networkx as nx G nx.DiGraph() # 添加节点附带元信息 for func in all_functions: G.add_node( func[id], typefunction, filefunc[file], linesfunc[line_count], complexityfunc[cyclomatic_complexity] ) # 添加边 for call in all_calls: if call[caller] in G and call[callee] in G: G.add_edge(call[caller], call[callee], typecall)这里有个细节值得说节点 ID 的设计。我一开始用函数名做 ID结果同名函数直接冲突。后来改成文件路径::类名::函数名的格式唯一性就保证了。这个格式还有个好处从 ID 就能反推代码位置调试时特别方便。构建完的图十万行代码大概会产生 3 到 8 万个节点边数在 10 到 30 万之间。这个规模直接喂给 AI 依然太大必须剪枝。4. 拓扑剪枝把图压缩到 AI 的舒适区4.1 剪枝的核心原则剪枝不是随便删节点删错了架构就失真了。我定的原则是保留架构关键节点保留关键路径合并同质节点丢弃叶子噪声。具体来说一个节点是否重要看几个维度度数入度和出度都高的节点通常是核心枢纽介数中心性处于多条最短路径上的节点是模块间的桥梁代码规模行数多、复杂度高的节点往往承载核心逻辑入口标记被标记为 API 入口、main 函数、路由处理器的节点必须保留4.2 基于中心性的节点筛选networkx 内置了中心性计算直接拿来用# 计算介数中心性 betweenness nx.betweenness_centrality(G) # 计算度数中心性 degree_cent nx.degree_centrality(G) # 综合评分 scores {} for node in G.nodes(): scores[node] ( 0.5 * betweenness[node] 0.3 * degree_cent[node] 0.2 * G.nodes[node].get(complexity, 0) / 100 ) # 按分数排序取 Top N top_nodes sorted(scores.items(), keylambda x: -x[1])[:500]这里有个经验值十万行代码保留 300 到 800 个核心节点比较合适。太少会丢失架构细节太多 AI 又处理不过来。我一般先按分数取 Top 500然后根据实际生成的图再微调。4.3 关键路径保留与叶子剪除光保留高分节点还不够因为架构的连通性靠的是路径。如果两个核心节点之间的路径被剪断了架构图就散了。所以第二步是保留核心节点之间的最短路径core_nodes [n for n, _ in top_nodes] paths_to_keep set() for i, src in enumerate(core_nodes): for dst in core_nodes[i1:]: try: path nx.shortest_path(G, src, dst) paths_to_keep.update(path) except nx.NetworkXNoPath: continue同时把那些只被调用、从不调用别人、且度数极低的叶子节点剪掉。这些通常是工具函数、getter/setter、简单的数据转换对架构理解贡献很小。剪枝完的图节点数通常能压到原来的 5% 到 10%边数压到 10% 左右但核心架构信息基本无损。4.4 社区检测与模块聚类剪枝后的图还需要做一步模块聚类把功能相近的节点归到一起这样 AI 才能识别出模块这个概念。我用的是 Louvain 社区检测算法import community as community_louvain partition community_louvain.best_partition(G.to_undirected()) # 按社区分组 modules {} for node, comm_id in partition.items(): modules.setdefault(comm_id, []).append(node)每个社区就是一个候选模块。给每个模块起个名字可以用社区内高频的文件名或类名架构图的骨架就出来了。5. 让 AI 画出高精度架构全景图5.1 给 AI 的输入怎么组织剪枝和聚类完成后我们要把图结构翻译成 AI 能理解的文本格式。这里我试过几种方案最终选了结构化 JSON 自然语言描述的混合格式。JSON 部分描述图的拓扑{ modules: [ { id: module_1, name: 用户认证模块, nodes: [auth/login.py::LoginHandler, auth/token.py::TokenValidator], size: 12 } ], edges: [ {from: module_1, to: module_2, type: calls, weight: 34} ], core_nodes: [ {id: auth/login.py::LoginHandler, in_degree: 15, out_degree: 8} ] }自然语言部分补充关键节点的语义信息比如每个模块的职责、核心函数的签名和注释摘要。注意不要把所有节点的注释都塞进去只给核心节点。注释太多会稀释重点而且很多注释质量堪忧反而误导模型。5.2 提示词的设计要点提示词的核心是约束输出格式和明确分析维度。我用的模板大致是这样你是一个架构分析专家。下面是一个代码库经过剪枝后的拓扑结构 包含 N 个模块和 M 条依赖边。 请基于这些信息输出一份架构全景图描述要求 1. 按模块划分说明每个模块的职责 2. 标注模块之间的依赖方向和依赖强度 3. 识别出核心枢纽模块和关键调用链路 4. 指出潜在的架构风险如循环依赖、单点枢纽 输出格式为 Markdown模块用二级标题依赖关系用表格。关键点在于明确要求 AI 基于给定的拓扑输出而不是自由发挥。我踩过的坑是如果不加约束AI 会开始推测代码功能编造出根本不存在的模块。加上仅基于给定信息的约束后准确率大幅提升。5.3 架构图的可视化渲染AI 输出的文本描述最后要渲染成图。我用的是 Mermaid 的 flowchart 语法注意这里说的是最终产物用 Mermaid 渲染不是我在文章里画 Mermaid 图。让 AI 直接输出 Mermaid 代码然后前端渲染graph TD A[用户认证模块] --|34次调用| B[用户数据模块] A --|12次调用| C[日志模块] B --|8次调用| D[数据库访问层]这样生成的架构图模块边界清晰依赖关系有量化标注比传统工具画的图信息密度高得多。而且因为是 AI 基于真实拓扑生成的不会出现图很漂亮但和代码对不上的情况。6. 实操中踩过的坑与排查技巧6.1 动态调用和反射的漏网之鱼AST 只能解析静态的调用关系动态调用比如 Python 的getattr、JavaScript 的obj[method]()是抓不到的。我一开始没注意这点生成的架构图漏掉了好几个关键模块。解决办法是补充启发式规则扫描字符串常量如果某个字符串和已知函数名匹配就推测可能存在动态调用加一条虚线边标注疑似动态调用。虽然不精确但比完全漏掉强。6.2 循环依赖的识别与处理大型代码库里循环依赖很常见但在有向图里循环依赖会让拓扑排序失败也会让 AI 困惑。我的处理方式是先检测强连通分量把循环依赖的节点合并成一个超级节点在架构图里标注为循环依赖组并列出组内成员。sccs list(nx.strongly_connected_components(G)) for scc in sccs: if len(scc) 1: # 这是一个循环依赖组 print(f循环依赖: {scc})6.3 常见问题速查表问题现象可能原因排查方向解决方案架构图模块缺失剪枝阈值过高检查保留节点数降低阈值增加保留节点依赖关系错误同名函数消歧失败检查符号表用完整路径做节点 IDAI 输出编造模块提示词约束不足检查提示词加仅基于给定信息约束图太密看不清边太多统计边数按权重过滤低权重边解析速度慢全量解析检查是否增量启用 tree-sitter 增量解析动态调用丢失AST 静态局限扫描字符串常量加启发式规则补边6.4 几个实操心得心得一先小后大。不要一上来就分析十万行代码先拿一个几千行的小模块跑通全流程确认每个环节的输出符合预期再放大到全库。我一开始直接上全库结果某个环节出错排查了半天才发现是解析器版本问题。心得二保留中间产物。AST、依赖图、剪枝后的图、AI 输入、AI 输出每一步都存下来。出问题时能快速定位是哪一步的锅。我习惯用 JSON 存每一步的结果方便对比。心得三人工校验核心模块。AI 生成的架构图核心模块的职责描述一定要人工核对。我遇到过 AI 把日志模块描述成用户管理模块的情况原因是两个模块的节点在图上挨得近AI 混淆了。核心模块错了整张图的参考价值就大打折扣。心得四增量更新比全量重跑香。代码库是活的每天都有提交。全量重跑一次要几分钟增量更新只需要解析改动的文件然后局部更新图结构几秒钟搞定。这个优化在频繁分析的场景下体验提升巨大。7. 这套方案还能怎么扩展跑通基础流程后我又做了几个扩展效果都不错。扩展一结合 Git 历史做架构演化分析。把每个版本的依赖图都生成出来对比不同版本的差异就能看出架构是怎么演化的——哪些模块在膨胀哪些依赖在增加哪些循环依赖是新引入的。这对技术债务管理特别有价值。扩展二接入 AI 编程助手做上下文注入。当开发者问这个函数改了会影响什么时直接从依赖图里查反向依赖把受影响的模块和调用链喂给 AI回答的准确率比让 AI 自己猜高得多。扩展三架构风险自动预警。设定规则比如单节点入度超过 50 就告警新增循环依赖就告警在 CI 流程里自动跑把架构问题挡在合并之前。这套方案我从头到尾跑下来最大的感受是AI 不是用来替代结构分析的而是用来放大结构分析的价值。AST 和拓扑剪枝负责把代码的骨架抽出来AI 负责把骨架翻译成人能理解的语言。两者配合才能从十万行混乱代码里画出一张真正有用的架构全景图。如果你也在做类似的事情建议先从一个小模块开始把管线跑通再逐步放大——这个过程中积累的经验比任何文档都值钱。