开头先交代一下背景。我最近在做代码架构治理的时候,又重新折腾了一圈代码可视化工具,从最开始的依赖分析脚本到各种商业平台,最后还是把codegraph用成了日常主力。这工具名字起得挺直白,就是 code + graph,把代码库结构变成一张可以查询的图。但真正用起来之后我发现,它的价值反而不在"画图好看",而在"把依赖关系变成可检索、可对比、可审计的数据",这个定位和很多只会出图的工具是完全不同的。
这篇文章我打算不按官方文档的目录来写,而是按我实际使用的路径走一遍:先说它到底解决什么痛点,再讲安装初始化需要注意的细节,然后是核心命令graphify的完整工作逻辑,接着是真实项目里的应用效果和排查链路,最后给一套可以抄作业的进阶配置。整个过程会有大量命令输出和原理解释,也会把我踩过的坑原原本本写出来,希望能帮正在选型或者已经被工具搞得头大的朋友少走几段弯路。
1. 这个工具真正解决的痛点:代码依赖从来不是"看得见"的问题
先聊需求。做过大型项目的人应该都有这种体验:一个仓库存续五六年,几百个模块互相 import,真正梳理依赖关系的时候,全靠 IDE 的"Find References"一个一个点,点半天脑子里还是一团乱麻。我以前带一个新的同学接手老项目,光是讲清楚"这个服务到底依赖了哪些模块、被哪些模块依赖、能不能安全改掉"就得花掉一整天。
传统方案的毛病是显而易见的。IDE 的调用关系只能解决"单个符号从哪里来"的微观问题,你看到一个函数被引用了二十次,但你想知道的是"这二十个引用分布在哪些模块、哪些服务、哪些层次",这个信息 IDE 无法直接汇总。而架构评审需要的是宏观视图:整个仓库的依赖箭头画出来之后,哪些模块是核心枢纽、哪些模块是孤岛、哪两个模块之间偷偷形成了循环依赖,一眼就能定位。
codegraph做的就是把代码解析成结构化数据,把"文件、模块、函数、类"这些符号定义成节点,把"import、call、extends、implements"这些关系定义成边。画出来只是它的最小功能,真正值钱的是它把原本散落在源码里的依赖关系,变成了可以查询、过滤、追踪、对比的一等公民数据。
我用下来最大的感受是,它不是在"画代码",而是在"索引代码"。就像搜索引擎把全网页面做成倒排索引,你随手输入一个模块名就能知道它影响谁、被谁影响,这种检索能力对重构和架构治理来说是决定性的。所以如果只想找个工具把代码库变成一张漂亮的图发朋友圈,那codegraph可能有更轻量的替代品;但如果要做依赖治理、死代码排查、模块边界检查,它这个"图数据库 + 查询语法"的思路才是核心价值。
2. 安装与初始化的细节:环境要求、三个坑和一份最小配置
先声明一下,我这边用的版本是基于 Node.js 生态的 v0.8.2,通过 npm 全局安装。不同版本、不同底座的codegraph命令细节可能会有差异,但整体工作流大同小异。
2.1 环境准备与安装
安装本身没什么花活,一条命令搞定:
npm install -g @lichee/codegraph装完之后检查版本:
codegraph --version # v0.8.2但第一次跑之前,必须先确认几个前置条件,否则后面全是坑。
第一是语言解析器。codegraph的底层依赖各种语言的语法解析能力,JavaScript/TypeScript 是内置的,Python、Java、Go、Rust 这些语言则需要额外的解析器支持。我用的这个版本对 Python 和 Java 支持得比较好,Go 的部分特性(尤其是泛型和接口推断)偶尔会有解析遗漏。安装完二进制之后,建议先跑一下codegraph doctor看看各语言支持状态:
codegraph doctor第一次跑的时候我什么都没看直接扫描,结果 Java 项目的输出里一堆类关系缺失,查了半天才发现是解析器没有装全。这个命令的存在就是用来避坑的,该花半分钟看一定要看。
第二是文件系统大小写敏感的问题。如果你在 macOS 上用的是默认的 APFS 分区,大小写不敏感,而代码库里如果同时存在Foo.ts和foo.ts(这种情况在 Git 仓库里真的会出现),解析器生成的符号 ID 可能碰撞。这个坑我后面会详细说。
第三是.gitignore的同步。codegraph默认会尊重 Git 忽略规则,但也允许自定义忽略。这里容易出问题的场景是:你的node_modules或者构建产物没有被 ignore,扫描的时间指数级上升,输出图还会无比嘈杂。所以初始化的时候一定要先确保.gitignore是完整的。
2.2 初始化命令的完整过程
安装好之后,进入项目根目录执行初始化:
codegraph init这个命令会做三件事:
- 检测当前目录的版本控制状态和语言混合情况;
- 生成
.codegraph/config.yaml配置文件; - 创建默认的存储目录(我这边是
.codegraph/storage/),后续索引数据都会放在这里。
看一下生成的默认配置:
# .codegraph/config.yaml version: 1 storage: engine: sqlite path: .codegraph/storage/codegraph.db scan: languages: [auto] ignore: - "**/node_modules/**" - "**/dist/**" - "**/build/**" - "**/.git/**" follow_symlinks: false max_file_size_kb: 512 graph: include_imports: true include_calls: true include_inheritance: true include_implementation: true deduplicate_edges: true这个配置我建议逐项理解,而不是直接照抄。storage.engine用的是 SQLite,意味着所有图数据都落在本地单文件里,没有服务端依赖,这对我这种喜欢在服务器上跑批量任务的场景很友好。scan.follow_symlinks默认关掉,防止循环符号链接把扫描进程拖死。max_file_size_kb默认 512KB,大于这个的文件会被跳过,遇到过有些代码生成器产出的巨型单文件,解析器直接卡死,调大这个值的代价是内存上升,建议默认就行。
初始化完成之后可以先跑一次扫描:
codegraph scan扫描完成后,你会看到输出统计,比如解析了多少文件、提取了多少符号、建立了多少依赖边。这些数字是后续所有操作的底数,建议截图保存一下,后面调优配置的时候好对照。我第一次扫描一个 30 万行左右的仓库时,耗时大概 40 秒,生成了约 10 万个符号节点和 30 多万条依赖边,这个量级在 SQLite 里查询基本无感。
2.3 最容易踩的三个初始化坑
先把坑说完,免得大家重复交学费。
第一个是扫描目录不是 Git 仓库根目录。很多人直接cd到子目录里就执行init,导致 ignore 规则匹配的是子目录的相对路径,把一堆源码文件当成白名单漏掉了。你会在输出里看到扫描文件数远小于预期。正确做法是切到仓库根目录再执行。
第二个是语言检测被构建产物干扰。有些项目会在generated/或vendor/目录下放大量第三方源码,如果ignore没有覆盖,扫描时间会以倍数膨胀。建议初始化之后先看scan的统计,如果符号数量比你预期的多一个量级,多半就是没忽略干净。
第三个是浅克隆仓库扫描不全。我遇到过同事拿git clone --depth 1的仓库来扫描,结果大量的跨版本引用关系因为文件不完整而解析失败。codegraph只关心当前工作区的文件状态,但如果文件本身是残缺的(比如 submodule 没拉全),图谱的数据天然有洞。扫描之前先确认git submodule update --init --recursive跑过一遍了。
3. 核心命令graphify的工作逻辑:从源码到图谱的四级管道
graphify是codegraph的核心子命令,热搜里也有这个词,说明大家在搜的都是这个东西到底怎么用。我第一次用它的时候以为就是把scan的结果导成一张 PNG,跑完之后发现完全不是这么回事。
scan阶段做的是提取,graphify阶段做的是构建和裁剪。它会把原始符号数据重新组织成一张有向图,然后根据你的目标选择输出何种格式。命令的大致形态:
codegraph graphify --format dot --focus src/core --depth 3 codegraph graphify --format json --output graph.json codegraph graphify --format html --open输出格式支持dot(Graphviz 的图描述语言),json(结构化数据),html(交互式可视化页面),默认还会在终端里打印一个 ASCCI 缩略图。我试下来,日常分析用json做二次加工最顺手,给团队看架构图用html最直观,嵌入文档或做 CI 差异对比用dot配 Graphviz 渲染最灵活。
但命令参数只是表象,我更想讲清楚graphify链路里的四个阶段,因为理解了链路才知道参数该怎么调。
3.1 符号解析阶段:AST 和语言感知
这一步其实在scan里已经完成了,但graphify需要基于符号表做二次关联。底层的核心是把源代码解析成抽象语法树(AST),然后遍历 AST 提取四类信息:定义(函数、类、变量、接口)、引用(调用、导入、继承、实现)、文件归属、模块边界。
这里有第一个需要理解的关键点:AST 解析是不感知项目语义的。它只能告诉你a.ts里import { B } from './b',不能告诉你b.ts里的B是类还是接口、是默认导出还是具名导出。codegraph对外看起来能区分"类继承"和"接口实现",是因为它额外做了一层符号解析,把 import 的符号名和定义文件中的符号声明对齐,这层对齐做得好不好,直接决定图谱的准确率。
我之前在一个 TypeScript 项目上跑,发现一个模块被标记为"纯依赖"(没有出边),排查了半天才发现是项目的路径别名@/没有被解析,所有import { xx } from '@/core'全部变成了"孤儿引用"。后来在配置里补上了别名映射才恢复正常。这说明graphify的准确率受限于它对工程配置的理解,别把它当成魔法。
3.2 引用链接阶段:导入别名、重导出与同文件引用
链接阶段是把"引用名"解析为"定义节点"的关键环节。这里有三个容易踩的场景,逐一说明。
场景一:导入别名。源码里写import { UserService as US } from './services/user',图上的调用者符号是US,但定义的符号是UserService。graphify需要做别名记忆,才能把调用边正确连到UserService节点上。这个机制做得好,图上的节点永远显示规范名,你查UserService能查到所有来自US的调用,查US也能定位到定义。
场景二:重导出。很多项目会有一个index.ts把所有模块统一导出,graphify如果不做"重导出追踪",你会看到图上有一个index.ts节点被大量依赖,但它内部的每条边的语义都被打了埋伏。实际上codegraph会把index.ts解析成"转发节点",你展开它才能看到真正的 provider 模块。这个处理对洋葱架构、六边形架构的项目非常重要。
场景三:同文件引用。两个符号在同一个文件里互相调用,如果不小心处理,会生成大量自环边或者重复边。deduplicate_edges配置就是干这个的,建议保持true,否则图上全是噪声。
最后graphify会做一次传递闭包的裁剪。你给它一个"焦点节点",它默认只输出这个节点指定深度范围内的子图。比如--focus src/core --depth 3输出的只有src/core相关的三层以内的依赖路径。这个能力特别适合"改动一个模块会不会波及 XX"这种问题,不用先导出全量图再用外部工具过滤。
3.3 输出阶段:分层、聚类和布局
输出阶段对非技术背景的读者来说最容易忽略,但对实际使用体验影响极大。graphify输出图之前会做一个分层聚合:把同一模块内部的细节聚合成一个复合节点,把跨模块的调用聚合成模块级粗边,然后根据依赖深度做拓扑分层(先按层分配,层内再按模块区域分配),这样渲染出来的图天然有结构感。
我最初直接跑全量输出的时候,图出来完全没法看,一团乱麻。后来才意识到,问题不在布局算法,而是我让图承载了过多的粒度信息。正确做法是先看模块级粗图,确定可疑区域,再对焦点区域做函数级细图,分粒度看,图上呈现的信息密度才合理。
3.4 实际跑一个最小例子的输出
拿一个小的 Node.js 项目跑一下,结构大概是这样的:
src/ index.ts config/ index.ts services/ userService.ts logService.ts models/ user.ts执行:
codegraph graphify --format json --focus src/services/userService.ts --depth 2JSON 输出里,核心结构大概是:
{ "nodes": [ {"id": "src/services/userService.ts", "type": "file"}, {"id": "src/models/user.ts", "type": "file"}, {"id": "src/config/index.ts", "type": "file"} ], "edges": [ {"from": "src/services/userService.ts", "to": "src/models/user.ts", "type": "import"}, {"from": "src/services/userService.ts", "to": "src/config/index.ts", "type": "import"} ] }这个输出看着简单,实际上已经是三层处理(AST 解析、别名链接、传递裁剪)之后的结果。把它交给脚本做进一步分析,比如和上次发布版本的图谱做 diff,可以定位"这次重构到底新增了多少跨模块依赖",这是 IDE 完全给不了的能力。
4. 查询语法与进阶用法:把图谱当成数据库来用
讲到这,读者应该能理解我不把codegraph当作"画图工具"的原因了。它真正的高级用法建立在查询能力上,也就是把图当数据库查。我用的版本提供一个query子命令,语法本身有点像 Cypher 的简化版。
4.1 基础查询:找依赖和依赖感
# 查某个模块被哪些模块直接依赖 codegraph query "MATCH (a)-[:imports]->(b) WHERE b.path = 'src/core/moduleA.ts' RETURN a.path" # 查某个模块直接依赖了哪些模块 codegraph query "MATCH (a)-[:imports]->(b) WHERE a.path = 'src/core/moduleA.ts' RETURN b.path" # 查两个模块之间是否存在间接依赖路径 codegraph query "MATCH p = (a)-[:imports*1..5]->(b) WHERE a.name = 'serviceA' AND b.name = 'serviceB' RETURN p"这三个查询是我日常用得最多的。第一个回答"谁依赖我",第二个回答"我依赖谁",第三个回答"改 A 会不会间接影响 B"。[:imports*1..5]表示 1 到 5 层间接依赖,这个参数决定了分析深度,设太大会查出大量路径,设太小会漏掉关键链路。
4.2 环检测与核心枢纽识别
再进阶一点,直接查询环。一个健康的架构应该尽量减少模块间的循环依赖,而图查询能直接把环挖出来:
# 检测 3 层以内的循环依赖 codegraph query "MATCH p = (a)-[:imports*1..3]->(a) RETURN a.path, p LIMIT 20"执行结果会把每条循环路径列出来。我实践下来的经验是,90% 的循环都可以靠"把公共类型抽到独立模块"解决,但如果查出来后能在图上确认路径关系,改动时心里有底得多。
核心枢纽识别也有现成玩法:查入度最高的模块。入度越高,说明被越多模块依赖,一旦变更波及面越大,这类模块应该尽可能稳定:
codegraph query "MATCH (a)<-[:imports]-(b) WHERE a.type = 'file' RETURN a.path, count(b) AS refs ORDER BY refs DESC LIMIT 15"查输出结果做成一个小表格,往架构评审文档里一贴,比任何口头讲"这个地方是核心"都有说服力。
4.3 与 CI 集成:让图数据替你把关
查询能力还可以封装进 CI 脚本。我现在的做法是每次 MR 合并前跑一次增量扫描,然后对比新旧两版图谱,自动检查两条规则:
- 有没有新增跨层依赖(比如 domain 层突然 import 了 infrastructure 层的类);
- 有没有新增循环路径。
脚本大致思路:
# 在 CI 中跑增量扫描 codegraph scan --incremental # 生成 JSON 格式图谱 codegraph graphify --format json --output /tmp/graph_new.json # 对比基线 codegraph diff --baseline /tmp/graph_base.json --current /tmp/graph_new.json --rule layered --fail-on-violationcodegraph diff是版本对比子命令,我认为这是整个工具最被低估的功能。它不只在架构治理上有用,对代码评审的辅助也很大,看到 diff 里新增的跨模块边,比 review 十遍 PR 代码都直观。
5. 真实项目中的完整排查链:一次循环依赖问题的定位全过程
讲完命令,说一个实战案例。上周我们团队在推进支付服务重构,目标是把原本混合在一起的订单、支付、退款三个领域拆开。重构过程中用的就是codegraph把"隐藏的循环依赖"从代码库里捞出来。
5.1 现象:构建出现循环依赖警告
重构进行到一半,TypeScript 编译器开始报循环依赖相关警告,大概意思是orderService和paymentService之间存在互相 import。这种警告在大型项目里很常见,我以前通常选择忽略,因为只要业务逻辑不炸,循环也不会立刻出问题。但这轮重构本来就是冲着模块独立去的,留着循环就是打脸,所以我决定查清楚。
5.2 用图检索定位路径
先查这两个服务之间的直接和间接依赖:
codegraph graphify --format json --focus src/services/orderService.ts --depth 2输出 JSON 里,我看到orderService.ts的依赖边确实有src/services/paymentService.ts,而继续查paymentService.ts的出边,又指向orderService.ts。直接依赖环坐实了。
但真正有意思的是,这个环不是空手来的——继续把深度调到 3,我发现循环路径经过了一个叫src/shared/types/orderStatus.ts的文件。也就是说表面上看起来是两个服务互相依赖,实际上是因为两边都引用了一个"不该放共享层"的类型定义。
5.3 根因分析与修复
根因变得很清晰:orderStatus.ts里的枚举定义被paymentService引用,而paymentService又因为业务需要回掉到orderService的查询方法,于是产生一条长循环。修复方案是:把orderStatus类型拆成领域层的order包和payment包各自持有,共享层只保留纯语言类型(比如string别名),不承接任何跨领域的业务枚举。
改完之后重跑扫描,graphify输出里这个循环路径消失了,编译器警告也没了。整个过程大概两小时,其中有四十分钟花在确认循环路径的具体形状上。如果用传统方式对着 IDE 看,光理清orderService -> paymentService -> orderStatus -> orderService这条链路就得折腾一下午,更别说还要确认这中间没有其他隐藏节点。
5.4 这个案例给我们的三个提示
第一,循环依赖有时候不是"两个模块互相引用"这种一眼能看破的形状,而是通过共享类型、工具类绕了一个大圈。图查询的优势在于能自动算出传递闭包,把所有中间节点暴露出来。
第二,依赖关系是有"权值"概念可参考的。两个服务之间存在 import 边不一定是坏事,但如果间接依赖路径长度超过 3,这条路径上的代码耦合往往已经严重到需要治理了,建议对这种路径逐一检查。
第三,重构完成后记得把新图谱存档。下次有人再改到相关模块,可以直接对比 diff 看依赖是否按预期收敛,而不是靠口头约定维护架构边界。
6. 避坑与调优实录:大仓库、统计分析、别名配置和动态语言的那些事
最后这部分是纯粹的踩坑总结,覆盖几个我在实践中反复遇到的问题。这些都是官方文档基本不会写的边角内容,但恰恰是决定工具能否落地的关键。
6.1 大仓库扫描的内存和耗时调优
仓库一大,扫描和图谱生成都会变慢,甚至直接把内存打满。我手上有个仓库源码量接近百万行,第一次scan跑了 15 分钟,graphify --format html生成交互页面内存占用飙到 4GB。后来做了一系列调整:
- 把
max_file_size_kb从 512 调低到 256,跳过几个超大的自动生成文件。注意这个操作会导致部分文件失去节点,但如果是完全不读不写的"僵尸文件"(比如旧版本生成的 API 定义),跳过它们反而让图谱更干净。 - 打开
graphify的--prune-isolated参数,把没有出入边的孤立节点全部裁剪掉。这类节点通常是测试辅助文件或临时脚本,保留在图上没有分析价值。 - 生成 HTML 时加
--max-edges 20000,超出上限的边不参与渲染,防止前端卡死。这不会影响 JSON 输出,只影响可视化页面。
调完之后,扫描时间降到 8 分钟左右,HTML 渲染内存降到 1.5GB 左右,体验可接受。但必须承认,百万行以上仓库的图谱分析本身就不是轻量活,如果团队有强需求,建议上专用的静态分析服务器,而不是全部在本地跑。
6.2 从图谱里量化统计死代码
codegraph还能做一些很实用的统计推断。我分析一个项目时发现约 12% 的模块节点入度为 0(没有被任何其他模块 import),再获取这些文件最近一次 Git 修改时间,发现大部分已经超过半年没有更新。把这些信息整理成一个"疑似死代码名单"发给负责人,比靠代码 review 一个个问有效得多。
具体做法:
codegraph query "MATCH (a) WHERE a.type = 'file' AND NOT EXISTS { (b)-[:imports]->(a) } RETURN a.path ORDER BY a.path"配合 git 查最后修改时间:
git log --pretty=format:"%h %ad %s" --date=short -- <path>这套组合拳用下来,每次都能定位出一批可以删的僵尸代码。删完之后图谱的节点数下降,整体复杂度肉眼可见地降低,架构图也清爽很多。
6.3 路径别名配置的注意事项
前面提过路径别名会导致解析失败,这里展开讲。TypeScript 项目普遍配置paths:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }codegraph默认读取tsconfig.json来自动处理这类别名。但有几个例外:
而如果项目的别名同时被 webpack 和 tsconfig 维护,且两处不一致(比如 webpack 里多配了一个@utils指向src/shared/utils,tsconfig 里没有),解析器就会漏掉一部分引用。解决办法是检查tsconfig.json的paths是否覆盖所有构建工具里使用的别名,并保证主别名体系只有一个事实来源。
另外像 monorepo 场景,子包之间的互相引用往往写成@company/pkg-a的形式,codegraph需要把@company/*映射到packages/*目录。可以在配置里补充:
scan: workspace: enabled: true packages: - "packages/*"6.4 动态语言的天然劣势与应对
对 Python、JavaScript 这类动态语言的解析,codegraph的准确率会打折扣,原因是很多导入和调用发生在运行时才能确定。比如 Python 的importlib.import_module(module_name)、JS 的require(variable),AST 解析阶段根本不可能静态算出目标节点。
遇到这种情况,我的应对策略是把动态加载的边界显式化:在这些入口文件加上注释标记或者配置 alias,让codegraph知道"plugins/*"会作为插件目录被动态加载。这样至少能保证图上有入口节点和插件目录之间的依赖关系,不至于完全丢失。
如果是重度动态语言项目,还是建议把它当辅助参考工具使用,不要依赖单一的静态分析结果,配合运行时的 heap snapshot 或调用链追踪才算完整。codegraph能解决 80% 的静态结构问题,剩下的 20% 需要结合运行时行为来判断。
6.5 从 SQLite 直接导出自定义数据
最后分享一个小技巧。codegraph的存储层是 SQLite,这意味着你可以绕过命令,直接用 SQL 查任何自定义数据。我经常干的一件事是查"某个目录下所有文件的出边数量":
sqlite3 .codegraph/storage/codegraph.db \ "SELECT file_path, COUNT(edge_id) FROM nodes LEFT JOIN edges ON node_id = from_node WHERE file_path LIKE 'src/services%' GROUP BY file_path ORDER BY COUNT(edge_id) DESC;"虽然codegraph query已经提供了很高层的图遍历能力,但遇到它没暴露的聚合统计需求,直接上手 SQLite 仍然是最快速的手段。这个做法还有一个额外的好处:数据可以无缝接入我们已有的 BI 报表系统,让架构度量指标自动出现在每周的工程周报里。
7. 我个人的使用心得
用codegraph这半年多,我最深的体会是:静态分析工具的价值不在于算法多高明,而在于它能不能把一个模糊的架构问题变成一个可检索、可对比、可追踪的具体数据问题。以前做架构评审,两个人争论"这个模块到底有没有被别的地方依赖",谁也说服不了谁;现在直接跑一条查询,答案就在那里,不需要解释太多。
如果你正准备在团队里尝试,我的建议是不要一上来就追求生成一张完美的全量架构图,大概率你会像我一样被乱麻图劝退。先从一个小模块开始,跑一条focus查询,把它直接或间接引用的范围摸透,让团队感受到"查依赖从半天变成了三秒",工具自然会被接受。等大家习惯了依赖数据的存在,再慢慢把死代码清理、CI 依赖检查、架构边界校验这些流程加进来,治理效果会明显得多。