graphify 跨仓库知识图谱实战:graphify clone 与 merge-graphs 从克隆到合并的完整链路
当你手头有多个服务仓库、多个 monorepo 子目录,甚至只是想基于 GitHub URL 直接对陌生项目做结构分析时,graphify 的 /graphify skill 提供了一条完整的"克隆 → 抽取 → 合并 → 查询"链路。本文以 graphify/skills/opencode/references/github-and-merge.md 这份官方参考文档为主线,逐步拆解 graphify clone、graphify merge-graphs 两条命令的用法与边界,并结合 graphify/cli.py、graphify/build.py 的源码实现说明合并过程中的节点前缀、社区 ID 偏移与跨仓库类型关联等底层机制,读完你可以独立完成跨仓库知识图谱的构建与检索。
何时加载这条链路
参考文档开宗明义:当用户传入一个或多个 https://github.com/... 形式的 URL,或者指定了多个本地子目录要合并成一张图时,才需要走这条 reference。它覆盖两类典型场景:
- 跨仓库(cross-repo)图谱:每个服务一个独立仓库,各跑一遍完整 pipeline 再合并;
- 多子目录(monorepo 或 multi-service 布局)图谱:同一个仓库内有若干独立可扫描的子目录。
两类场景最终都收敛到同一个产物:一份合并后的 graph.json,之后任何代码结构问题都直接对合并图执行 graphify query,无需重新抽取,也不受体积门限约束(原文档称之为 fast path)。
Step 0:用 graphify clone 克隆 GitHub 仓库
只有当输入是 GitHub URL 时才需要这一步。
单仓库
LOCAL_PATH=$(graphify clone <github-url> [--branch <branch>])
# Use LOCAL_PATH as the target for all subsequent steps
clone 子命令的完整用法形如:
Usage: graphify clone <github-url> [--branch <branch>] [--out <dir>]
命令成功时会在最后一行打印本地路径(源码中 print(local_path)),因此可以直接用 $(...) 捕获后作为后续 pipeline 的目标路径。
多仓库(cross-repo 图谱)
# Clone each repo, run the full pipeline on each, then merge
graphify clone <url1> # → ~/.graphify/repos/<owner1>/<repo1>
graphify clone <url2> # → ~/.graphify/repos/<owner2>/<repo2>
# Run /graphify on each local path to produce their graph.json files
# Then merge:
graphify merge-graphs \
~/.graphify/repos/<owner1>/<repo1>/graphify-out/graph.json \
~/.graphify/repos/<owner2>/<repo2>/graphify-out/graph.json \
--out graphify-out/cross-repo-graph.json
源码印证:clone 的四个关键行为
结合 graphify/cli.py 中 _clone_repo 的实现,文档中"clone into ~/.graphify/repos/<owner>/<repo> and reuses existing clones on repeat runs"这句话可以展开为四个确定行为:
- URL 归一化与校验:自动补全/剥离
.git后缀,再用正则从 URL 中提取owner/repo;不是 GitHub URL 会直接报错退出(error: not a recognised GitHub URL)。 - 浅克隆:首次克隆执行
git clone --depth 1,指定--branch时附加--branch <branch>参数,只取单个 commit,体积最小。 - 复用已有克隆:若目标目录(默认
~/.graphify/repos/<owner>/<repo>,可用--out覆盖)已存在,则改跑git -C <dest> pull(带 branch 时是pull origin -- <branch>),而不是重新克隆——这就是"重复运行复用已有克隆"的实现来源。 - 输出可脚本化:最后一行统一打印
Ready at: <dest>后返回dest,供 shell 变量捕获。
一个值得注意的细节:--branch 的值若以 - 开头会被判定为非法分支名并退出,这是对选项解析误判的防御。
多个本地子目录:用 CLI 的 extract 代替 skill pipeline
参考文档特别强调了一个容易踩的坑:skill pipeline 会把所有中间与最终产物写到当前工作目录下的 graphify-out/。如果对着每个子目录各跑一次 skill,后一次会覆盖(clobber)前一次的输出目录。
正确做法是直接调用 CLI,因为 graphify extract 会把 graphify-out/ 放在被扫描路径内部:
graphify extract ./core/ # → ./core/graphify-out/graph.json
graphify extract ./service/ # → ./service/graphify-out/graph.json
graphify extract ./platform/ # → ./platform/graphify-out/graph.json
# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set
# Then merge at the project root:
graphify merge-graphs \
./core/graphify-out/graph.json \
./service/graphify-out/graph.json \
./platform/graphify-out/graph.json \
--out graphify-out/graph.json
注意 --backend 参数:当需要对非代码内容(文档、笔记等)做 LLM 语义增强时,按你手头配置的 API key 选择对应后端;纯 AST 结构抽取则不需要。
merge-graphs 做了什么:前缀、标签与冲突消解
graphify merge-graphs 接收多个 graph.json 路径与一个 --out 输出路径。它的实现位于 graphify/cli.py,配合 graphify/build.py 中的两个工具函数,完成了五件关键的事。
1. 每个节点打上 repo 前缀与 repo 属性
prefix_graph_for_global(graphify/build.py#L2005-L2057)把输入图的每个节点 ID 重写为 <repo_tag>::<原 ID>:
- 显示用的
label保持不变; - 原 ID 存入
local_id属性,便于回溯; - 每个节点都会写入
repo属性——这正是参考文档所说"Each node in the merged graph carries arepoattribute so you can filter by origin"的落点,后续按来源仓库过滤、或删除某一仓库的所有节点(prune_repo_from_graph)都依赖这个属性; - 边的
_src/_tgt方向标记与超边(hyperedge)成员 ID 会同步重写到前缀形态,超边 ID 本身也加前缀,防止不同仓库同名超边冲突。
2. 仓库标签自动消歧
repo tag 的默认取值是 graphify-out 的父目录名。但 src/graphify-out 与 frontend/src/graphify-out 都会得到 tag src,两个同名 tag 会把无关实体悄悄合并成同一节点(源码注释中标注为 #1729)。distinct_repo_tags(graphify/build.py#L2060-L2085)的消解策略是:先检测冲突,冲突时把标签加宽为"父目录名_目录名"(如 frontend_src),仍重复则追加 -2、-3 索引后缀,保证任意两张输入图的标签唯一。标签冲突时 CLI 会在终端打印一行提示:note: repo dir names collide; using distinct tags: ...。
3. 社区 ID 偏移
每张输入图都从 0 开始编号自己的 community,若原样带入合并图,不同仓库的 community 0 会撞号,聚合社区视图会把不相干的社区融合成一个元节点(#3014)。merge 处理因此按输入顺序维护一个 community_offset:第一张图保持原 ID,其后每张图的 community 被平移到共享 ID 空间,原始编号保留在 local_community 属性中。
4. 混合图类型归一化
不同 extract 路径在不同时期写出的 graph.json,其 directed / multigraph 标志未必一致,而 nx.compose 要求所有输入图同型。实现里会把 DiGraph / MultiGraph / MultiDiGraph 一律归一为普通无向 Graph(合并出的跨仓库视图本来就以无向方式消费),避免 All graphs must be directed or undirected 崩溃(#1606)。tests/test_merge_graphs_cli.py 用一个"DiGraph + Graph + MultiGraph"三合一输入验证了这条归一化路径,并断言输出 directed 为 false、multigraph 为 false 且三张图的节点全部存活;同一测试文件还覆盖了"同名仓库目录不得折叠"(#1729)场景:
def test_merge_graphs_same_named_repo_dirs_do_not_collapse(tmp_path):
# #1729: two graphs under a same-named repo dir (src/graphify-out and
# frontend/src/graphify-out both → tag "src") share the `src::` prefix, ...
app_nodes = [n for n in data["nodes"] if n["id"].endswith("::app")]
assert len(app_nodes) == 2
5. 跨仓库同名类型自动连线
消息总线型架构里最有价值的"一跳"往往跨仓库:producer 在一个仓库引用 SyncProductUpsertToSearchEvent,consumer 在另一个仓库实现 IConsumer<SyncProductUpsertToSearchEvent>。由于所有节点 ID 都带仓库前缀,这两个同名类型声明在合并图里默认互不相连。
link_shared_type_declarations(graphify/cross_repo_types.py)在 compose 之后补上这层连线:把"namespace + 类型名"相同、且分属至少两个不同仓库的类型声明两两之间加一条 same_type_as 边(confidence=INFERRED、confidence_score=0.9、context=cross_repo)。两个设计取舍值得注意:
- 要求 namespace 完全相同:只凭短类名相同就连线会把无关类型误连(源码 docstring 提到在一对 .NET 服务上,namespace+name 匹配产生 7 对,全部是共享的事件契约,零误报);
- 只加边、不合并节点:两个仓库可能持有发生漂移(drift)的契约副本,合并节点会掩盖这种漂移,而连线既允许遍历跨越仓库边界,又让两侧各自保留自己的成员、文件与来源信息。
触发时 CLI 会打印 linked N type declaration(s) shared across repos。
此外,merge 过程还会对超边做"先收集、后重挂"的并集处理——nx.compose 合并图属性时会用 dict.update 覆盖,若放任不管只有最后一张输入图的超边能存活(#2484),因此实现里先收集每张图前缀化后的超边、compose 完成后再统一去重挂回,且同时写入顶层与 graph 内两个存储槽位,与 to_json 的双槽形态保持一致。最终结果经原子写入落到 --out 指定路径,并打印 Merged N graphs -> X nodes, Y edges。
合并之后:query 走 fast path
按参考文档的收尾描述,一旦 graphify-out/graph.json 存在,后续所有代码库问题都直接对合并图执行 graphify query——不再重新抽取,也不再有体积门限(size gate)拦截。也就是说,跨仓库构建的一次性成本换来的是后续持续的结构检索能力;当某个仓库更新后,重新 git pull 该仓库(clone 命令对已存在克隆会自动走 pull)、重新 extract、重新 merge-graphs 即可刷新合并图。
小结:一条可复制的跨仓库工作流
| 步骤 | 命令 | 产物/效果 |
|---|---|---|
| 克隆仓库 | graphify clone <url> [--branch b] |
~/.graphify/repos/<owner>/<repo>(已有克隆则 git pull) |
| 抽取(子目录) | graphify extract ./core/ [--backend ...] |
./core/graphify-out/graph.json(产物落在扫描路径内部,互不覆盖) |
| 合并 | graphify merge-graphs a/graph.json b/graph.json --out graphify-out/graph.json |
节点带 repo 属性与 <tag>:: 前缀,同名类型自动加 same_type_as 边 |
| 查询 | graphify query ... |
直接在合并图上执行,无重新抽取、无体积门限 |
这套链路的关键工程保障都来自源码层:distinct_repo_tags 防止同名目录静默合并无关节点,社区 ID 偏移防止跨仓库社区撞号,same_type_as 边让遍历能够跨越仓库边界。测试用例(如 tests/test_merge_graphs_cli.py)对这些边界场景均有回归覆盖,可以作为阅读实现时的最佳入口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00