1. codegraph 到底在解决什么问题
1.1 代码也是一种地图
后端项目维护到第18个月的时候,你会明显感受到一个变化:代码量还在涨,但脑子里的“地图”开始失灵。以前改一个函数,能凭印象说出哪几个地方调过它;现在同样的函数,可能散落在七八个模块里,有的还是通过消息队列间接触发的。这时候靠搜索关键字、靠翻调用栈来定位影响面,代价已经高到让人烦躁了。codegraph 这个工具,核心思路就是把代码里隐藏的结构关系变成一张可以检索的地图——通过静态分析扫描源码,提取模块、类、函数之间的依赖与调用,再输出成结构化的关系数据或者可交互的可视化页面。它不需要真正运行你的程序,也不污染生产环境,扫描完数据落盘,随时可以拿出来分析。
这套能力听起来像IDE自带的功能,但真的把项目里几千个文件全量铺开看的时候,IDE的调用层次窗口往往不够用,而且IDE只活在开发环境里,CI、评审、重构前的影响面评估都需要一份可复用的关系数据。codegraph 解决的就是这个距离感:把代码关系变成工程流程里人人可用的一份资产。适合什么人?接盘老项目的新人、准备做大规模重构的技术负责人、还有那些想在代码审查里加上“依赖规则”的团队,都能从这里找到切入点。
1.2 一个真实的痛点场景
说一个我自己踩过的坑。当时要改造订单服务的 status 字段,从整型改成枚举类型,原以为很简单,用全局搜索搜了一下 status 出现在六十七个文件里。问题是这六十七处里有大量同名字段、和订单状态无关的局部变量,真正需要跟着改的调用点可能只有二十几处。如果按搜索结果的列表挨个看,漏掉哪一处,上线之后就是埋雷。
后面我用 codegraph 跑了一遍订单模块的调用关系,直接查询OrderService.update_status这个节点,所有上游调用方被聚成一张子图。列表清清楚楚:api 层两个接口、一个消息消费者、还有两个内部定时任务在调用它。随后逐个确认,把真正要改的点用一个脚本追踪到底,整个影响面分析从“猜”变成了“核对清单”。这就是代码图谱在重构场景里的价值:它能让你在动手之前先看见改动会波及到哪些地方。
1.3 整体设计:scan 和 graphify 两级拆解
codegraph 的实际工作流程被拆成了两级。第一级叫 scan,负责扫描源码,产出结构化的数据文件,例如 graph.json;第二级叫 graphify,负责把这份数据渲染成可交互的可视化页面。最初我也试过一条命令直接出图,但很快就发现两条命令拆开更合理。
scan 的结果是纯粹的关系数据,它不依赖任何前端渲染逻辑,也不夹带展示选项。这份数据可以被长期保存、被脚本读取、被 CI 拿来对比依赖变化,甚至可以写进测试断言,作为架构规则的执行依据。graphify 则专注于把 graph.json 变成人能看的页面,支持过滤、折叠、高亮,这些选项如果都塞进 scan,会让扫描命令变得很重,很难扩展。数据层和展示层一旦解耦,后面加新功能就顺手很多——比如我在 graphify 里加过“按包聚合”的视图,完全不需要改动扫描逻辑。
| 子命令 | 输入 | 输出 | 核心职责 |
|---|---|---|---|
codegraph scan | 源码目录 | graph.json | 解析语法树,抽取节点与依赖边 |
codegraph graphify | graph.json | HTML 页面 | 渲染关系图,支持过滤与高亮 |
2. codegraph 的核心原理:从 AST 到关系图
2.1 让机器“读”代码:AST 静态分析
要扫描代码,第一步是让机器“读懂”代码结构。这里用到的基础设施是 AST,抽象语法树。你可以把它理解成代码的骨架标本:源码先被编译器或解释器拆成一棵结构化树,每个节点代表一个语法元素,比如FunctionDef节点表示一个函数定义,Call节点表示一次函数调用。在 Python 里,标准库的ast模块就能做这件事,不需要引入额外重量级依赖。
以 Python 为例,一个最简单的调用关系追踪器可以这样写:
import ast class CallTracker(ast.NodeVisitor): def __init__(self): self.current_scope = None self.edges = [] def visit_FunctionDef(self, node): old_scope = self.current_scope self.current_scope = node.name self.generic_visit(node) self.current_scope = old_scope def visit_Call(self, node): if self.current_scope and isinstance(node.func, ast.Name): self.edges.append((self.current_scope, node.func.id)) self.generic_visit(node)代码的逻辑并不复杂:遇到函数定义时把它当作新的作用域,遇到函数调用时就把“当前函数 -> 被调用的函数”记录成一条边。真实场景里还需要区分类方法、模块导入、构造函数调用,但整体的遍历思路是一致的。AST 的优点是稳定,代码怎么运行没关系,只要语法合法,它就能稳定产出结构关系。
2.2 节点、边和关系模型
有了 AST 遍历的基础,接下来就看怎么定义关系模型。codegraph 里的节点主要有三类:模块、类、函数。模块对应源码文件,类对应类定义,函数对应函数或方法。边的类型也相对固定:导入、调用、继承、包含。用图的角度看,这就是一张带类型的有向图。
| 节点类型 | 说明 | 唯一标识示例 |
|---|---|---|
| Module | 源码文件或包 | order_service.repository.order_repository |
| Class | 类定义 | order_service.repository.order_repository.OrderRepository |
| Function | 函数或方法 | ...OrderRepository.update_status |
| 边类型 | 含义 | 典型示例 |
|---|---|---|
import | 模块之间导入 | OrderService -> DbHelper |
calls | 函数调用关系 | update_status -> OrderRepository.update |
inherits | 类的继承关系 | OrderService -> BaseService |
contains | 包含关系 | OrderService -> update_status |
节点和边都需要带上尽可能多的元数据:文件路径、行号、调用的具体位置。这些信息在后续做影响面分析时非常关键。如果只保留“谁调谁”的名称,缺失行号和文件路径,那么当图谱上出现问题时你还是要回到源码里重新定位,分析效率会大幅下降。
唯一标识的命名建议采用完整路径风格,例如模块名.类名.函数名。不要只用函数名做节点 ID,因为不同类里的同名方法太常见了。这个方法在扫描几十个文件时问题不大,但扫描整个项目时,ID 一旦冲突,图谱就会乱成一锅粥。
2.3 循环依赖与同名函数的处理
图结构里很容易出现环。A 调用 B,B 又调用 C,C 在某个条件下又调用 A,这在业务代码里极其常见。处理环在遍历的时候就要做防护,最简单的方案是维护一个访问集合,每次展开边之前先判断目标节点是否访问过,必要时限制 BFS 深度。否则一个环就能导致遍历程序无限膨胀,把 graph.json 撑爆。
同名函数的问题更隐蔽。比如两个不同类里都有parse方法,如果直接用parse做节点 ID,两个类的方法会被合并成一个节点,整张图谱的结构就失真了。处理方法是坚持用“完整路径”作为唯一标识。扫描阶段在记录函数名时,就带上模块名和类名,宁可 ID 长一些,也不要让两个真实不相关的节点被粘在一起。
还有一类常见问题是动态调用抓不到。比如用getattr(obj, method_name)()这种方式构造的调用,AST 里看到的是一个字符串变量,不是直接的函数名,静态分析天然抓不到。这个限制不要硬扛,合理的做法是扫描时提供“手工补充边”的机制,或者通过配置告诉 codegraph 某个动态入口等价于调用某几个函数。工具可以识别出大约七八成的直接调用关系,剩下的动态调度逻辑需要人工补位,这个预期要提前告诉团队。
2.4 graphify:图谱化渲染层
graphify 不只是把节点和边照搬上页面。直接画一张全量节点图,几千个节点和上万条边堆在一起,最后就是一团乱麻。我在实践里常用的做法是:先做聚合,再做过滤,最后才是渲染。
聚合的意思是先把函数折叠进类,再把类折叠进模块,图上默认显示模块层级的依赖关系。用户想深入看某一个模块,再双击展开到类和函数层级。过滤是指可以根据调用深度、文件路径前缀、边类型来决定哪些节点显示、哪些边隐藏。比如只看calls不看import,或者只显示order_service包下的节点,上游和下游的依赖通过“展开到第 N 层”的方式渐进式引入。
渲染底层可以用 D3.js 的力导向图,也可以用 Graphviz。我的经验是:节点在两三千这个量级时 D3.js 力导向图表现还不错,交互也灵活;如果单纯出架构图、不想让用户拖来拖去,Graphviz 的 dot 布局更规整。graphify 当前默认输出静态 HTML,把数据以 JSON 形式内联到页面里,借助浏览器本地渲染。这样部署也简单,丢到任何静态服务器上都能跑,不依赖后台服务。
3. codegraph 怎么使用:从安装到出图的完整链路
3.1 安装与环境准备
在 Python 3.10 以上的环境里,安装 codegraph 走 pip 就行。建议先用虚拟环境隔离,避免污染全局 Python:
python -m venv .venv source .venv/bin/activate pip install codegraph codegraph --help如果你的机器上没有提前装 graphviz,就不需要额外装,因为默认的可视化方案是 HTML 页面的 D3.js 渲染,Graphviz 只是可选的导出路径。验证安装成功后,命令行会出现scan、graphify、query三个子命令。scan是扫描,graphify是出图,query是后续做深度查询用的小工具,可以基于 graph.json 查上下游、查环。
如果你更喜欢源码方式运行,从仓库 clone 下来之后执行pip install -e .也能达到一样的效果。这个方式适合要改代码的人,毕竟工具还在持续迭代,本地开发环境跑源码版本调试起来更方便。
3.2 用 codegraph scan 导出结构化的 graph.json
扫描命令是常用的第一步。我用过很多次的完整命令长这样:
codegraph scan \ --source ./src \ --output graph.json \ --language python \ --ignore "tests,docs,venv" \ --workers 4参数含义很直接:--source指定源码根目录,--output指定输出文件,--language告诉解析器用哪套语法规则,--ignore忽略掉不需要分析的目录,--workers控制并发解析的进程数。多进程在这类纯计算任务里收益很明显,尤其是多个文件之间没有共享状态,可以放心并行。
扫描完成后生成的 graph.json 结构大致如下:
{ "project": "order-service", "language": "python", "nodes": [ { "id": "order_service.service.order_service.OrderService", "type": "class", "file": "src/order_service/service/order_service.py", "lineno": 10 } ], "edges": [ { "source": "order_service.service.order_service.OrderService.update_status", "target": "order_service.repository.order_repository.OrderRepository.update", "type": "calls", "line": 25 } ] }这份 JSON 就是后续所有分析的基础。建议把它纳入版本管理,每次扫描生成新版本后可以和旧版本做 diff,快速发现新增的依赖、消失的调用、或者突然冒出来的循环依赖。在实际项目中,我把 graph.json 的 diff 作为每次重构合并请求里的一个检查项,效果比口头强调“注意依赖关系”要实在得多。
3.3 用 codegraph graphify 生成可视化页面
拿到 graph.json 之后,出图的命令也很短:
codegraph graphify \ --input graph.json \ --output ./site/index.html \ --title "订单中心依赖图谱"执行完会生成一个静态 HTML 页面,里面内嵌了图谱数据和渲染脚本。打开方式有两种,如果节点数少,几百个节点以内,直接双击 HTML 文件就能看;如果节点多,强烈建议起一个本地静态服务,因为在file://协议下浏览器对资源加载限制更多,交互流畅度也会打折扣:
python -m http.server 8080 --directory site然后浏览器访问http://localhost:8080就能看到可交互的图谱。页面上支持搜索节点、高亮上下游、按边类型过滤、折叠和展开模块。graphify 侧的这些交互能力,就是前面说“数据和展示解耦”的收益。想调整视图时,不需要重新扫描源码,改一下渲染参数再跑 graphify 就行。
3.4 实战:对一个 Flask 项目做影响面分析
拿一个实际的订单模块举例。项目结构大致是:
src/order_service/ api/ order_api.py service/ order_service.py repository/ order_repository.py models/ order.py需求还是那个需求:把OrderService.update_status的 status 参数从整型改成枚举。我先跑了一次 scan,再用 graphify 出图,在页面里搜索update_status。搜索结果会高亮这个节点,同时显示它的上游调用方和下游依赖。
从图上能直观看到,调用它的一共五条边:OrderAPI.create、OrderAPI.cancel、OrderConsumer.handle、OrderScheduler.expire、NotifyService.order_changed。下游则是OrderRepository.update和EventPublisher.publish。这样影响面就非常清晰了。真正动手改代码前,我可以把五个上游调用点逐个打开源码确认,而不是靠记忆力或搜索关键字硬猜。
这个流程最大的价值是省时间。以前这种影响面分析至少得半天,现在从扫描到出图十到二十分钟足够,而且结果还能存档,评审的时候直接把图谱截图贴进文档就行。
4. 常见问题与排查技巧实录
4.1 扫出来是空图?先查这三件事
codegraph 扫描结果为空,是新手最容易遇到的问题。我遇到的情况基本归为三类:
第一,--source路径指错了。检查一下路径到底是相对路径还是绝对路径,扫描工具不会帮你探测“你觉得应该在哪”,它只按路径找文件。第二,源码里有语法错误。AST 解析碰到语法过不了的文件,默认策略是跳过还是中断?我用的是跳过并告警,但如果你项目里恰好多文件都有语法错误,结果就可能是空图。排查时可以先用python -m py_compile <file>检查一下文件能否正常编译。第三,项目里大量使用了动态调用。前面说过,getattr、eval、exec这类动态调度代码,静态分析根本追踪不到函数名,这不是 codegraph 的缺陷,而是静态分析的天然边界。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 节点数为 0 | 路径错误 | 用绝对路径重新扫描或先ls确认目录 |
| 大量文件告警 | 语法错误 | 找到报错文件,用py_compile逐个检查 |
| 边数远少于预期 | 动态调用居多 | 使用手工补充边配置,或只在扫描结果中标记 |
4.2 图谱一团乱麻,怎么读
graphify 默认展示全部节点时,确实会产生“毛线团”效果。几千个节点同时铺在一个力导向图里,谁都看不清。我的习惯是:从不过度依赖一张全量图,而是把视图切到模块层,先看架构总览。
graphify 支持按包聚合时,可以先只看import边和模块节点,把架构的依赖方向搞清楚。然后再针对重点模块,选择展开到函数层,只看这个模块内部的调用细节。还有一种更实用的视角是把搜索功能当成入口:输入一个函数名,只看它的上下游,忽略全局图。
所以遇到“图谱太乱”的问题,不是工具不行,而是用法还没切过来。乱的不是图,是你让它显示了太多东西。过滤选项值得多试几轮,先看大方向,再钻小细节,比试图一眼看完整张图画风要轻松得多。
4.3 大项目扫描太慢,怎么优化
扫描速度决定工具能不能用进日常流程。我第一次拿它扫一个十万行左右的 Python 仓库,全量扫描大概花了五十多秒,能接受但不够爽快。后来打开增量缓存之后,二次扫描基本能压进十秒以内。
这里的关键是--cache参数,它会把解析结果以文件哈希为索引缓存下来。文件没改动,就直接复用旧的解析结果,不需要重新跑 AST。配合--workers多进程参数,扫描速度的提升非常明显。
内存方面也要心里有数。节点数和边数的增长不是线性的,关系密集的模块很容易出现“边爆炸”。我在实践里遇到过一个网关模块,几百个函数互相调用,边数直接冲到几万条。这时候 graph.json 的体积会变得很大,内存占用也随之上升。处理办法是让扫描阶段支持并行的同时,避免一次把所有文件内容都读进内存,改成一文件一释放的方式。如果你要扫更大的项目,建议先按模块拆分扫描,再在数据层做合并,而不是试图一次全量解析。
4.4 Windows 下的编码坑和本地打开问题
Windows 环境主要有两个坑。第一个是编码,Python 在 Windows 上默认编码不一定是 UTF-8,源码文件如果带中文注释,读取时可能直接崩或者产生乱码节点名。解决方式是在运行 scan 前设置环境变量:
set PYTHONIOENCODING=utf-8更稳妥的做法是扫描器内部读取文件时,统一用encoding='utf-8',读取失败再降级到gbk。第二个坑是 graphify 生成的 HTML 文件在 Windows 下直接双击打开,部分浏览器会限制 JavaScript 读取本地 JSON,导致图渲染不出来。处理方式同样是别双击,用python -m http.server起一个本地服务再访问。这不是代码缺陷,而是浏览器对本地文件的安全限制,绕开它就好。
5. 从个人工具到工程实践
5.1 在 CI 里用 graph.json 做自动化检查
代码图谱最实用的落地场景之一,是把它变成 CI 里的架构规则检查。graph.json 是结构化数据,意味着可以在测试里对它做断言。比如团队规定核心领域层不能反向依赖基础设施层,这条规则很难通过代码 review 强制,但用图谱数据就很好查。
我用 pytest 写过类似这样的检查:
def test_no_cycle_in_core_module(): graph = load_graph("graph.json") core_nodes = [n for n in graph["nodes"] if n["id"].startswith("order_service.core")] cycles = find_cycles_in_subgraph(graph["edges"], core_nodes) assert not cycles, f"core module contains cycles: {cycles}"这个测试跑得快、规则明确、失败信息直接指向具体环路径。把它焊在 CI 里以后,任何一次改动引入循环依赖都会在合并请求阶段被拦住,而不是等到线上出问题再回头查。
5.2 用关系数据做“坏味道”检查
除了依赖方向,图谱数据还可以用来做基础的质量分析。入度高的节点说明被大量调用,往往意味着这个模块承担了过重的公共职责;出度高的函数说明它依赖了太多外部能力,潜在的扇出过高。这些指标用脚本算一遍,就能自动产出“疑似需要拆分”的候选清单。
列出扇出最高的几个函数,逐个看过去,大概率能发现一些开始膨胀的代码。这比人肉在源码里找要快得多。实际操作里,我会把扇出超过 20 的函数和入度超过 50 的节点单独导出成一个报告,放进每周的代码评审会材料里。团队成员看到的是具体函数名和调用关系图,而不是抽象的说教,讨论起来效率高很多。
5.3 从 Python 扩展到多语言和团队生态
codegraph 的核心结构其实不绑定某种语言。只要把 AST 解析器换成对应语言的语法树实现,后面的关系模型、可视化层、CI 检查逻辑都能复用。JavaScript 生态可以用 Babel 或 acorn 做解析,Java 可以用 Eclipse JDT 那套抽象语法树,C# 里也有 Roslyn。多语言支持最麻烦的不是解析本身,而是命名空间的差异和调用形式的差异,需要花精力适配。
当团队里已经有了一份稳定的 graph.json,后续可以做很多延伸。比如生成模块级别的架构文档、在 IDE 里做调用的反向跳转、在评审工具里自动标注“本次改动影响的模块范围”。这些功能都是建立在“关系数据可用”这个前提上的。所以我的建议是:先别想着一步到位做全套平台,先把扫描流程跑起来,把 graph.json 当作团队的一项资产维护好,之后的能力都是水到渠成的事。
这套工具用到现在,我最深的感受是:拿到一个新项目或者准备动老代码之前,先花十分钟把依赖图谱跑出来看一眼,比自己埋头翻代码高效得多。它不是银弹,动态调用、泛型擦除、宏展开这些场景依然需要人工介入,但那些繁琐的“谁调用了谁”的问题,终于不用再靠脑子硬记了。