graphify 图查询实战指南:query / path / explain 三命令驱动的知识图谱问答、路径追溯与节点解释
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
graphify 会把代码库连同文档、SQL 模式与配置解析成可查询的知识图谱(产物为graphify-out/graph.json),而本文面向的是图建好之后“怎么问”:围绕/graphify query、/graphify path、/graphify explain三条查询链路,讲解其双模式图遍历(BFS/DFS)、受约束的查询扩展(constrained query expansion)、答案写回(save-result)与工作记忆(reflect)机制。读完本文,你将掌握在 Claude Code、Cursor、Codex 及 opencode 等 Agent 场景下直接基于已建图回答“X 连接了什么”“X 如何到达 Y”“X 是什么”三类问题,并能用不超过 2000 token 的预算拿到可追溯、带源码位置引用的图证据。
本文对应的规范出处是 opencode 技能(skill)的 query 参考文档 graphify__skills__opencode__references__query.md,其模板源头是 tools/skillgen/fragments/references/query/default.md;生成这类技能文件的入口在 tools/skillgen/gen.py,而 opencode 技能的完整形态参见 graphify__skill-opencode.md。所有命令的底层实现都可回查 graphify/cli.py 与 graphify/serve.py。
适用范围:什么时候加载这份参考
当用户针对一张“已存在的图”提问,或显式运行/graphify path、/graphify explain时,就应加载本参考。核心的 query stub(在 tools/skillgen/fragments/query-stub/default.md)会把完整的遍历流程指向这份文档。整条链路遵循同一原则:
- 优先使用
graphify query/graphify path/graphify explainCLI(安装后可用); - CLI 不可用时,回退到内联 NetworkX 遍历:加载
graphify-out/graph.json,用networkx.readwrite.json_graph.node_link_graph还原成图对象后自行搜索。
无论走哪条路径,问题的答案都只应来自图本身的内容——节点标签、边的 relation/confidence、source_location等字段,禁止凭空“脑补”不存在的边或节点。
两种遍历模式:先想清楚你在问哪类问题
| 模式 | 参数 | 适用场景 |
|---|---|---|
| BFS(默认) | 无 | “X 连接了什么?”——广度优先,先看最近邻,适合获取全局上下文 |
| DFS | --dfs | “X 如何到达 Y?”——沿单条链/依赖路径深入追溯 |
BFS 适合“俯瞰”:从一个种子节点出发逐层展开,得到的是包含多级邻居的子图快照;DFS 适合“追线”:沿一条路径尽可能往下钻,直到命中目标或超出深度上限。在 graphify/cli.py 中,--dfs只是切换传给底层_query_graph_text的mode参数(见 graphify/serve.py),真正决定语义的是内部_dfs/_bfs两个遍历函数。
前置检查:确认图已存在
任何查询动作前,先检查图产物是否就位。技能运行环境会在graphify-out/下写入.graphify_python(记录应使用的 Python 解释器路径)与graph.json(图本体)。检查脚本如下:
$(cat graphify-out/.graphify_python) -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify <path> first to build the graph.') raise SystemExit(1) "如果检查失败,应停下并明确告知用户:先运行/graphify <path>建图,再做查询。
Step 0 —— 受约束的查询扩展(遍历前必做)
为什么必须先做这一步?因为 graphify 的queryCLI 对节点做的是“case-folded 子串 + IDF”匹配:二进制内部没有词干还原(stemming)、没有同义词、没有跨语言匹配,下面给出的内联回退脚本也遵循同样的匹配规则。从源码看,graphify/serve.py 的_compute_idf为查询词计算全图 IDF 权重并缓存在G.graph['_idf_cache']中,打分路径依赖的就是词项在标签文本上的重合度与 IDF 加权的组合。
因此,如果用户问题用的是与图标签不同的语言或领域词汇(例如用户说俄语 “обработчик”,图里却是 “handler”;用户说 “authentication”,图里却是类名 “Guardian”),字面匹配会返回 0 命中,答案就会退化成噪声。
解决办法是先对照图的真实词汇表做查询扩展,且绝不凭空发明 token:
1. 先从节点标签中抽取 token 词汇表:
$(cat graphify-out/.graphify_python) -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) vocab = set() for n in data['nodes']: for c in re.findall(r'[^\W\d_]+', n.get('label','') or '', re.UNICODE): parts = re.findall(r'[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+', c) or [c] for p in parts: t = p.lower() if 3 <= len(t) <= 30: vocab.add(t) Path('graphify-out/.vocab.txt').write_text('\n'.join(sorted(vocab)), encoding='utf-8') print(f'vocab: {len(vocab)} tokens') "脚本把每个标签先按“非字母数字”切分,再把每个词按 CamelCase 边界(如AuthService→Auth+Service)拆成小写 token,过滤掉 3 字符以下与 30 字符以上的噪音项(短 token 如api/jwt/ios会被保留,见注释中 #1392),最终写入graphify-out/.vocab.txt并打印词表规模。
2. 阅读graphify-out/.vocab.txt,针对用户问题从中挑选至多 12 个与查询意图语义匹配的 token。硬性约束:
- 只允许挑选词表文件中真实存在的 token,不得发明;
- 若某个查询概念在词表中没有合理对应 token,就跳过它——不能用训练记忆里的近似同义词顶替;
- 若没有任何词表 token 能匹配该问题,就输出空列表,并如实告知用户“该语料对这个问题没有相关词汇”,不要伪造一次搜索;
- 跨语言翻译:俄语 “аутентификация” → 仅当词表里存在时才查找
auth、credential、token、security; - 形态变化:“handlers” → 仅当存在时映射到
handler;“todos” → 仅当存在时映射到todo。
3. 在真正运行查询前,先把选中的 token 显式打印给用户看,使整个扩展过程可审计:
Query expanded to (from graph vocab, N tokens): [token1, token2, ...]若列表为空,就直说并停止——不要继续遍历。
这套“先看词表、后给证据”的流程,正是要让 Agent 的输出可被复核:用户能看到它基于哪些真实词汇去查,而不是靠大模型记忆中的近义词去瞎蒙。
Step 1 —— 遍历:CLI 优先,NetworkX 内联兜底
把上一步选出的 token 用空格连接成扩展后的查询串,作为下面的QUESTION(不要用用户的原始提问;原始问题仅保留给最后一步的save-result使用)。
CLI 可用时优先走 CLI:
graphify query "QUESTION" # or: graphify query "QUESTION" --dfs --budget 3000graphify query支持的参数(在 graphify/cli.py 中手工解析)包括:
| 参数 | 含义 | 默认值 |
|---|---|---|
QUESTION | 位置参数,扩展后的查询串 | 必填 |
--dfs | 切到 DFS 深度优先遍历 | BFS |
--budget N(或--budget=N) | 输出 token 预算,非整数会报错 | 2000 |
--context C(或--context=C) | 追加上下文过滤器,可多次传入,用于把遍历限制在某文件/目录范围 | 无 |
--graph path | 指定 graph.json 路径(默认取graphify-out/graph.json) | 默认路径 |
底层实现里,CLI 会把图故意按无向图加载(见 graphify/cli.py 的注释):BFS/DFS 需要同时探索种子节点的调用方(callers)与被调用方(callees),若强制DiGraph,G.neighbors()只会返回后继节点,会悄悄丢掉所有“无出边”种子的调用方结果。方向信息改为在每条边上用_src/_tgt标记保留,渲染时仍然正确。CLI 查询在本版本内部以depth=2驱动遍历并记录 querylog(见 graphify/cli.py)。
若 CLI 不可用,就加载graphify-out/graph.json内联执行遍历,按此流程:
- 找出 1–3 个标签与扩展 token 最匹配的起始节点;
- 从每个起始节点执行相应模式的遍历;
- 读取子图——节点标签、边关系、confidence 标签、源码位置;
- 只依据图内含有的内容作答,引用具体事实时给出
source_location; - 若图信息不足,明确说明,不要臆造边。
内联回退脚本完整如下(把QUESTION换成扩展后的查询串、MODE换成bfs/dfs、BUDGET换成 token 预算):
$(cat graphify-out/.graphify_python) -c " import sys, json from networkx.readwrite import json_graph import networkx as nx from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') question = 'QUESTION' mode = 'MODE' # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes scored = [] for nid, ndata in G.nodes(data=True): label = ndata.get('label', '').lower() score = sum(1 for t in terms if t in label) if score > 0: scored.append((score, nid)) scored.sort(reverse=True) start_nodes = [nid for _, nid in scored[:3]] if not start_nodes: print('No matching nodes found for query terms:', terms) sys.exit(0) subgraph_nodes = set() subgraph_edges = [] if mode == 'dfs': # DFS: follow one path as deep as possible before backtracking. # Depth-limited to 6 to avoid traversing the whole graph. visited = set() stack = [(n, 0) for n in reversed(start_nodes)] while stack: node, depth = stack.pop() if node in visited or depth > 6: continue visited.add(node) subgraph_nodes.add(node) for neighbor in G.neighbors(node): if neighbor not in visited: stack.append((neighbor, depth + 1)) subgraph_edges.append((node, neighbor)) else: # BFS: explore all neighbors layer by layer up to depth 3. frontier = set(start_nodes) subgraph_nodes = set(start_nodes) for _ in range(3): next_frontier = set() for n in frontier: for neighbor in G.neighbors(n): if neighbor not in subgraph_nodes: next_frontier.add(neighbor) subgraph_edges.append((n, neighbor)) subgraph_nodes.update(next_frontier) frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) token_budget = BUDGET # default 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output def relevance(nid): label = G.nodes[nid].get('label', '').lower() return sum(1 for t in terms if t in label) ranked_nodes = sorted(subgraph_nodes, key=relevance, reverse=True) lines = [f'Traversal: {mode.upper()} | Start: {[G.nodes[n].get(\"label\",n) for n in start_nodes]} | {len(subgraph_nodes)} nodes'] for nid in ranked_nodes: d = G.nodes[nid] lines.append(f' NODE {d.get(\"label\", nid)} [src={d.get(\"source_file\",\"\")} loc={d.get(\"source_location\",\"\")}]') for u, v in subgraph_edges: if u in subgraph_nodes and v in subgraph_nodes: _raw = G[u][v]; d = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw lines.append(f' EDGE {G.nodes[u].get(\"label\",u)} --{d.get(\"relation\",\"\")} [{d.get(\"confidence\",\"\")}]--> {G.nodes[v].get(\"label\",v)}') output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) "这段脚本的内核值得拆解:
- 种子选择:把问题按空白切词(长度 ≥ 3 才纳入),对每个节点统计其标签(小写化后)命中的词数,取命中数最高的至多 3 个节点作为起点——与词汇表脚本的阈值一致;
- DFS 分支:显式栈实现,深度上限 6,防止把整张图都走穿;先入栈的后出,保证从每个起始节点按序深入;
- BFS 分支:按层推进,最多扩 3 层,每层只登记“第一次见到”的邻居并记录产生它的那条边,避免环与重复;
- 预算感知输出:按“约 4 字符 ≈ 1 token”把字符预算设为
token_budget * 4,节点按相关度降序输出,超预算即截断并提示可换用--budget N; - 行格式统一:节点行带
src=与loc=,边行带--relation [confidence]-->,方便后续作答时逐条引用。
作答时只依据上面的子图输出。回答写完后,把结论写回图,让它改善后续查询。建议把扩展出的 token 也写进--answer正文(例如"Expanded from original query via vocab: [tokens]. Then traversed..."),这样下一次--update抽取时能把这段扩展历史当成一个图节点保留下来:
$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2把ORIGINAL_QUESTION换成用户的逐字原问题,ANSWER换成完整答案(含 token 扩展轨迹),NODE1 NODE2换成你引用过的节点标签列表。这形成闭环:下一次--update会把这段 Q&A 作为节点抽回图里。
工作记忆:让后续会话从本次会话学习
save-result支持追加--outcome,让未来会话借鉴这次的结论——修正场景可再加--correction "the right answer":
useful—— 所引用节点很好地回答了问题(这些节点会升级为preferred sources,即优先来源);dead_end—— 问题/路径没有导向任何有价值的结果,下次不必再推导一遍;corrected—— 已保存的答案是错的,--correction记录正确结论。
--outcome的三个取值在 CLI 层被严格限定(见 graphify/cli.py),非法值直接报错。调用链上save-result实际走的是graphify.ingest.save_query_result(导入于 graphify/cli.py),结果默认落入graphify-out/memory目录。
每次开始图工作前,先刷新并阅读经验教训:运行graphify reflect --if-stale(廉价、确定性、无 LLM;--if-stale会在LESSONS.md已经比所有输入新时变成空操作——例如 git hook 刚刷新过它),然后阅读graphify-out/reflections/LESSONS.md。它列出preferred sources(从这些开始)、known dead ends(跳过它们)以及过往corrections。自己运行reflect能保证即使没有安装 git hook,经验也是最新的;若已安装 post-commit hook,--if-stale让会话开始时的这次运行几乎零成本。
graphify reflect的完整参数与save-result一同定义在 graphify/cli.py:--memory-dir(默认graphify-out/memory)、--out(默认graphify-out/reflections/LESSONS.md)、--graph/--analysis/--labels(默认从 graph.json 同目录推断)、--half-life-days(信号权重每 N 天减半,默认 30)、--min-corroboration(需要多少次独立 useful 才能把某节点提升为 preferred,默认 2)、--if-stale。真正的聚合逻辑在 graphify/reflect.py 的reflect()中,运行结束后会打印形如Reflected N memories (u useful, d dead ends, c corrected)的统计。
用于 /graphify path:找两个概念之间的最短路径
在图中求两个命名概念之间的最短路径。CLI 可用时优先:
graphify path "NODE_A" "NODE_B"CLI 层面graphify path接受--graph path以及--directed/--undirected两个互斥方向开关(实现见 graphify/cli.py)。默认按有向图处理(#2487):方向信息存在于每张 graph.json 中,尊重它;--undirected显式退化为忽略方向搜索。它还有两道防线值得了解:当两个查询都解析到同一节点时会报歧义(此时“最短路径”是 0 跳,几乎从不是调用方想要的,对应 bug #828);当头部与亚军得分差距小于 10% 时会打印模糊匹配警告。
CLI 不可用时,内联执行:
$(cat graphify-out/.graphify_python) -c " import json, sys import networkx as nx from networkx.readwrite import json_graph from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') a_term = 'NODE_A' b_term = 'NODE_B' def find_node(term): term = term.lower() scored = sorted( [(sum(1 for w in term.split() if w in G.nodes[n].get('label','').lower()), n) for n in G.nodes()], reverse=True ) return scored[0][1] if scored and scored[0][0] > 0 else None src = find_node(a_term) tgt = find_node(b_term) if not src or not tgt: print(f'Could not find nodes matching: {a_term!r} or {b_term!r}') sys.exit(0) try: path = nx.shortest_path(G, src, tgt) print(f'Shortest path ({len(path)-1} hops):') for i, nid in enumerate(path): label = G.nodes[nid].get('label', nid) if i < len(path) - 1: _raw = G[nid][path[i+1]]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw rel = edge.get('relation', '') conf = edge.get('confidence', '') print(f' {label} --{rel}--> [{conf}]') else: print(f' {label}') except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') "把NODE_A、NODE_B换成用户的真实概念名。然后用平实语言解释这条路径:每一跳(hop)意味着什么、为什么它有意义。
写完解释后同样写回:
$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_BCLI 版本在打印时比内联脚本更严格地遵循“只报真实存储的关系”(对应 #2074):同一对节点可能并行携带多条边(例如既有references又有calls),它会以多图方式加载并把所有 relation 用/连接如实呈现,只有存储的边完全没有 relation 时才诚实回退为 “related”。打印同时给出每跳方向的箭头(-->/<--),并输出总跳数(见 graphify/cli.py)。
用于 /graphify explain:解释单个节点及其连接
给出某个节点的平实语言解释——以及一切与它相连的内容。CLI 可用时优先:
graphify explain "NODE_NAME"CLI 不可用时,内联执行:
$(cat graphify-out/.graphify_python) -c " import json, sys import networkx as nx from networkx.readwrite import json_graph from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') term = 'NODE_NAME' term_lower = term.lower() # Find best matching node scored = sorted( [(sum(1 for w in term_lower.split() if w in G.nodes[n].get('label','').lower()), n) for n in G.nodes()], reverse=True ) if not scored or scored[0][0] == 0: print(f'No node matching {term!r}') sys.exit(0) nid = scored[0][1] data_n = G.nodes[nid] print(f'NODE: {data_n.get(\"label\", nid)}') print(f' source: {data_n.get(\"source_file\",\"unknown\")}') print(f' type: {data_n.get(\"file_type\",\"unknown\")}') print(f' degree: {G.degree(nid)}') print() print('CONNECTIONS:') for neighbor in G.neighbors(nid): _raw = G[nid][neighbor]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw nlabel = G.nodes[neighbor].get('label', neighbor) rel = edge.get('relation', '') conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') "把NODE_NAME换成用户问到的概念。然后写一段 3–5 句的解释:这个节点是什么、它连接了什么、为什么这些连接有意义,把源码位置当作引用证据使用。
写完解释后写回:
$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAMECLI 版graphify explain(见 graphify/cli.py)比内联脚本更严格:节点查找分“source 精确 / 精确 / 前缀 / 子串”多个层级,命中多文件同名节点时会打印全部候选并要求用仓库相对路径或完整节点 ID 重试,杜绝在两个等价匹配之间瞎猜(参见 graphify/serve.py 的分层匹配逻辑)。它还会输出节点的 ID、类型、社区名、度,并叠加“工作记忆覆盖层”——若graphify reflect已为某节点写下了preferred source/dead_end/corrected之类的经验标记,会显示为一行Lesson: ...,代码变更后还会打上[code changed since — re-verify]提醒复核。连接列表按邻居度从高到低排序、最多展示 20 条,且每条都带上该边真正的 call/import/reference发生点(在调用方文件中的那个位置),而非常规定义行(#BUG1),让解释可以直接指向“这段关系发生在哪一行代码”。
设计要点回顾
回顾整条链路,可以看到四个贯穿始终的设计原则:
- 字面匹配 → 必须受约束扩展:匹配器只做 case-folded 子串 + IDF 打分(IDF 实现在 graphify/serve.py),没有模糊语义;所以每次查询前都要对照
.vocab.txt选 token,宁可说“无相关词汇”也不伪造搜索。 - 答案只来自图:CLI 与内联脚本都只输出图里真实存在的节点、relation、confidence 与
source_location,信息不足就明说。 - 预算与深度受控:默认 2000 token 输出预算、BFS 3 层/DFS 深度 6(CLI 内部以 depth 2 驱动),保证长尾大图上答案不会失控膨胀。
- 问完就写回、开工先回顾:
save-result --outcome记录 useful/dead_end/corrected,reflect --if-stale在会话开始廉价地刷新LESSONS.md,让每次查询都成为下一次查询的“经验图谱”。
如果你想继续深入,可以在本仓库查看这些实现:命令分发与参数解析在 graphify/cli.py,查询打分与遍历在 graphify/serve.py,经验聚合在 graphify/reflect.py,技能生成流水线在 tools/skillgen/gen.py,而这份参考最初的面向所有平台的通用模板保存在 tools/skillgen/fragments/references/query/default.md。各平台(Claude、Codex、opencode、Kiro、Trae 等)落地后的同名文件则在 graphify/skills/opencode/references/query.md 这样的技能目录下。
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考