首页
/ graphify 查询子系统实战:受限查询扩展、BFS/DFS 遍历、path/explain 与工作记忆反馈闭环

graphify 查询子系统实战:受限查询扩展、BFS/DFS 遍历、path/explain 与工作记忆反馈闭环

2026-09-06 13:02:18作者:余洋婵Anita

本篇以 graphify 技能包中的查询参考文档 query.md 为主体,系统讲解针对已建知识图谱的三类查询流程——query 子图遍历、path 最短路径、explain 单节点解释,并完整继承原文档的"受限查询扩展"(Step 0)、token 预算、save-result 反馈闭环与 reflect 工作记忆机制。结合 cli.pyserve.pyingest.pyreflect.py 的源码实现,你将掌握:从图谱词表安全扩展查询词、BFS 与 DFS 两种遍历模式的适用场景与参数、查询结果的记忆化回写方式,以及每次会话开始前如何刷新并读取经验教训文档 LESSONS.md

参考文档的定位与加载时机

query.md 是 graphify 多平台技能包(Claude Code、Amp、Codex 等)中的一份"按需加载"参考文档。文档开篇明确了它的触发条件:

当用户对已有图提问,或运行 /graphify path/graphify explain 时加载本文档。核心技能中的 query stub 会把完整的遍历流程指向这里。这些流程在 graphify query CLI 可用时优先使用它,否则回退到内联 NetworkX 遍历。

也就是说,它不是独立的命令行工具文档,而是 Agent 执行"图问答"任务时的标准作业程序(SOP):先判断用哪种遍历模式,再做查询词扩展,然后跑遍历,最后把答案写回图。所有命令都通过 $(cat graphify-out/.graphify_python) 定位构建时记录的解释器路径,保证内联脚本与建图环境一致。

两种遍历模式:先按问题类型选模式

原文档给出了一张选择表,这是整个查询流程的第一步决策:

模式 标志 适用问题
BFS(默认) (无) "X 连接了什么?"——需要宽泛上下文,先取最近邻
DFS --dfs "X 如何到达 Y?"——追踪某条具体链路或依赖路径

从源码结构看,两种模式的实现位于 serve.py 的 _bfs_dfs,二者共享一个关键设计:hub 抑制阈值。阈值取全图度分布的 p99 并向下取 50 为底(hub_threshold = max(50, p99)),非种子的、度数达到阈值的节点不会被继续展开——防止遍历从枢纽节点(如巨型 __init__.py 或基类文件)一路炸开成整图;而种子节点本身若是 hub 也照常展开。CLI 的 query 命令固定以 depth=2 调用 _query_graph_text,而技能文档里的内联回退脚本则用 BFS 走 3 层、DFS 限深 6 层,两者是同一思路在不同入口下的参数化版本。

前置检查:确认图谱存在

任何查询之前先验证图文件存在,失败就停下来提示用户先执行 /graphify <path> 建图:

$(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)
"

这个检查在 CLI 侧也有对应物:cli.py 的 query 分支 会解析 --graph 参数(默认 _default_graph_path()),校验文件存在且后缀为 .json,并对超尺寸图谱做 _enforce_graph_size_cap_or_exit 守卫。

Step 0 — 受限查询扩展(遍历前必做)

这是原文档中最有工程价值的一段,它直接针对 graphify 查询匹配器的能力边界设计。

问题背景:graphify 的 query CLI 通过大小写折叠子串匹配 + IDF 加权来找节点——二进制内部没有词干提取、没有同义词、没有跨语言匹配(内联回退脚本的匹配方式与之一致)。当用户提问用的语言或领域词汇与图谱标签不一致时(用户说"обработчик" / 图里叫 handler;用户说 "authentication" / 图里叫 Guardian),字面匹配返回 0 命中,答案就退化成噪音。

解法:先拿图谱自己的词表来扩展查询,而不是凭空造词。 分三步:

1. 从节点标签提取词表

$(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')
"

细节值得注意:正则 [A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+ 是典型的驼峰拆分器(FooBarServiceFoo/Bar/Service),词长限制在 3~30 个字符——这与内联遍历脚本里 len(t) >= 3 的词元阈值一致,注释里还特别提到该阈值是为了保留 api/jwt/ios 这类短标识符。

2. 从词表中选择扩展词元(硬性约束)

读取 graphify-out/.vocab.txt 后,针对用户问题最多选 12 个语义匹配的词元,并遵守四条硬约束:

  • 只能选词表里真实存在的词元,绝不编造
  • 若某个查询概念在词表里没有对应词元,跳过它——不要用训练记忆里的近义词顶替;
  • 一个都匹配不上,输出空列表并明确告知用户"该语料对此问题没有相关词汇",不要伪造一次检索
  • 跨语言翻译示例:俄语 "аутентификация" → 在词表中找 authcredentialtokensecurity前提是它们真的在词表里
  • 词形归并:"handlers" 映射到 handler(若存在);"todos" 映射到 todo(若存在)。

3. 显式打印扩展结果,保证可审计

Query expanded to (from graph vocab, N tokens): [token1, token2, ...]

列表为空就直说并停止,不进入遍历。这一步把"查询扩展"从黑盒变成了对用户可见、可核查的中间产物。

Step 1 — 遍历:CLI 优先,内联回退保底

把选中的词元用空格拼成扩展查询串,用它作为下面的 QUESTION不是原始问题;原始问题只在最后 save-result 时保留)。

优先使用 CLI(已安装时)

graphify query "QUESTION"
# 或:graphify query "QUESTION" --dfs --budget 3000

对照 cli.py 中 query 命令的完整参数解析,实际可用的参数比文档示例更全:

参数 默认值 说明
--dfs 切换为 DFS 遍历(默认 BFS)
--budget N 2000 输出 token 预算(约 4 字符/token 截断)
--context C 上下文过滤(可多次),限制遍历所在范围
--graph PATH graphify-out/graph.json 指定图谱文件

源码里还有几个值得了解的行为:

  1. 刻意保持无向图。与 path/explain 强制 directed=True 不同,query 加载图谱时不建 DiGraph——BFS/DFS 必须同时探索种子节点的 caller 与 callee 两侧才能形成有用上下文;若强制有向,没有出边的种子节点 G.neighbors() 会静默丢掉所有 caller 侧结果。边的方向改由每条 link 上的 _src/_tgt 标记在渲染时还原,遍历不受限。
  2. 每次查询都记账querylog.log_query 会记录 kind、问题、模式、深度、预算与耗时(cli.py#L1177-L1186),_touch_query_stamp 更新查询时间戳,供增量更新时判断图是否被使用过。
  3. 输出头会标明来自哪张图_query_graph_text 在结果头部打印 Graph: <path> (N nodes)——这是针对"在父项目目录里查询、却静默命中 vendored 子项目的图"这类事故(#2789)的防护:输出格式完全正确但语料答错了,而输出里看不出差异;标出图路径和节点数后,"355 nodes vs 3178 nodes"这种第一眼可疑之处立刻可见。

匹配打分:CLI 到底如何找种子节点

文档说匹配器是"大小写折叠子串 + IDF",serve.py 的源码给出了完整细节:

  • 查询词先经 _query_terms 处理:分词、中文分词、去标点,再过滤掉英/德/法/西/葡/意多语言停用词("how does it work"、"wie funktioniert das" 这类纯虚词问题也能有内容词兜底);
  • IDF 权重:log(1 + N/(1+df)),高频词(errorexception)权重低,稀有标识符(FooBarService)权重高;
  • 分层匹配加分:整串精确匹配 +1000、前缀匹配 +100、子串匹配 +1、源文件路径命中 +0.5_find_node_tiers 的四级阶梯);
  • 大图谱上用字符三元组(trigram)倒排索引做候选预过滤,避免全图扫描;
  • 关系意图词(callsuses 等)会被从"逐词元种子保证"中剔除(#2507),防止动词碰巧命中某标识符而占据一个 BFS 种子席位。

内联回退(CLI 不可用时)

加载 graphify-out/graph.json 后用 NetworkX 原地遍历。原文档给出的完整脚本流程是:

  1. 找出标签与扩展词元最匹配的 1~3 个节点(按命中词元数打分取前 3);
  2. 从每个起始节点执行对应模式遍历——BFS 按层扩展 3 层;DFS 用栈深探、限深 6 防止走完整图;
  3. 读取子图的节点标签、边关系、置信度标签、源码位置;
  4. 只用图里有的内容作答,引用具体事实时引用 source_location
  5. 图信息不足就明说,不幻觉边

输出是 token 预算感知的:节点按词元重合度排序,总字符数超过 BUDGET * 4(默认 2000,即 --budget N 指定的值)就截断并提示 ... (truncated at ~N token budget - use --budget N for more)。脚本中需要替换三个占位符:QUESTION(扩展后的查询串)、MODEbfs/dfs)、BUDGET(token 预算)。脚本对 nx.MultiGraph 也做了兼容——同一节点对可能携带多条平行边(如既有 references 又有 calls),取边数据时用 next(iter(_raw.values()), {}) 处理。

save-result:把答案写回图,闭合反馈环

回答写完后,原文档要求把 Q&A 存回图,使未来的查询变聪明

$(cat graphify-out/.graphify_python) -m graphify save-result \
  --question "ORIGINAL_QUESTION" \
  --answer "ANSWER" \
  --type query \
  --nodes NODE1 NODE2

参数替换规则:ORIGINAL_QUESTION 是用户的原话ANSWER 是完整答案文本(其中要包含扩展词元痕迹,例如 "Expanded from original query via vocab: [tokens]. Then traversed...",这样下一次 --update 会把扩展历史提取为图节点),NODE1 NODE2 是你引用过的节点标签列表。

ingest.py 的 save_query_result 展示了落盘格式:文件写到 graphify-out/memory/(默认),文件名形如 query_YYYYMMDD_HHMMSS_<slug>.md,带 YAML frontmatter(typedatequestioncontributor: "graphify",可选 outcomecorrectionsource_nodes(最多 10 个)),正文是 # Q: ...## Answer## Outcome## Source Nodes 四段。docstring 点明了机制本质:"这些 markdown 会在下次 --update 时被 graphify 的提取器读入图——系统既从你添加的东西,也从你提问的东西变聪明。"

工作记忆:outcome 信号与 reflect 经验文档

save-result 后追加 --outcome 让未来会话从本次学习(--correction 配合纠正):

信号 含义
useful 被引用的节点很好地回答了问题(成为优先来源
dead_end 该问题/路径没走到头,下次别再重复推导
corrected 存下的答案是错的;--correction "正确答案" 记录正确内容

每次图工作开始时,先刷新并阅读经验:

graphify reflect --if-stale

然后读取 graphify-out/reflections/LESSONS.md。它列出优先来源(从这里开始查)、已知死胡同(跳过)、以及历史纠正

对照 cli.py 的 reflect 命令reflect.py 的 reflect(),几个默认值与行为值得记住:

  • --half-life-days 30:信号权重每 30 天减半——旧经验自动淡出;
  • --min-corroboration 2:至少 2 次独立的 useful 结果才会把某节点提升为"优先来源";
  • --if-stale:当 LESSONS.md 已经比所有输入都新时直接跳过(no-op)——典型场景是 git post-commit hook 刚刚刷新过它,会话启动时这次运行几乎零成本。若未安装 git hook,手动跑一次 reflect 同样保证经验是最新的;
  • 有图时,教训按社区(community)分组组织,且已不在图中的源节点会被丢弃
  • reflect 还会在 graph.json 旁尽力写一个 .graphify_learning.json 侧车文件(preferred/tentative/contested 状态 + 代码指纹),explain 的输出会合并展示这个经验覆盖层——而 graph.json 本身(持久化的结构事实)永不被它触碰。

/graphify path:两个概念之间的最短路径

优先使用 CLI:

graphify path "NODE_A" "NODE_B"

cli.py 中 path 命令的实现 比文档的简写多出一组方向控制参数,值得完整掌握:

graphify path "<source>" "<target>" [--graph path] [--directed | --undirected]
  • 默认有向(#2487):graph.json 里每条边都带方向真值(_src/_tgt 标记),所以默认尊重它;--undirected 才忽略方向,两者互斥;
  • 歧义防护:两个查询词解析到同一节点时直接报错退出(零跳路径几乎从不是用户想要的,#828);若第一名与第二名得分差不到 10%,打印 ambiguity 警告提示;
  • 确定性结果(#2074):等长路径中选哪条不再依赖进程内 hash 顺序——源码用排序后的物化图保证同一份 graph.json 每次跑出的路径完全一致;
  • 诚实的边标注:路径上每一跳报告的是实际存储的关系(平行边时全部列出),无关系时回退到诚实的 related,绝不伪造 calls
  • 有向模式下找不到路径会提示"用 --undirected 忽略方向再试"。

CLI 不可用时,用文档给出的内联脚本:对两端做词元命中打分找节点(find_node),找不到就退出;nx.shortest_path 求最短路径,逐跳打印 label --relation--> [confidence],并捕获 NetworkXNoPath/NodeNotFound

得到路径后,要用自然语言解释每一跳的含义与重要性,然后写回:

$(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_B

/graphify explain:单节点及其全部连接

优先使用 CLI:

graphify explain "NODE_NAME" [--graph path]

explain 命令的实现 的输出比文档内联脚本更丰富:

  • 分层节点定位:整串源路径精确 > 标签整串/前缀 > 子串,四级优先(_find_node_tiers);命中多个不同文件的同名节点时打印全部竞争者(文件路径 + 节点 id),要求你改用仓库相对路径或完整节点 id 重试——这是比"取第一个匹配"更安全的消歧策略;
  • 节点卡片IDSource(source_file + source_location)、Type(file_type)、Community(社区名)、Degree
  • Lesson 行:若该节点在 .graphify_learning.json 侧车中有经验记录,直接显示 Lesson: preferred source (start here) — N useful, score=...,代码变动过时还会追加 [code changed since — re-verify]——工作记忆在此直接兑现到查询体验上;
  • Connections(最多 20 条,按邻居度数降序):每条标明真实方向(基于 _src 标记,而非加载时的弧序)、关系、置信度、邻居文件。

CLI 不可用时,用文档内联脚本:找最佳匹配节点(无匹配则退出),打印 NODE(含 source/type/degree)与全部 CONNECTIONS--relation--> label [confidence] (source_file))。

然后写一段 3~5 句的解释:这个节点是什么、连接了什么、这些连接为何重要,用源码位置作为引用。最后写回:

$(cat graphify-out/.graphify_python) -m graphify save-result \
  --question "Explain NODE_NAME" --answer "ANSWER" \
  --type explain --nodes NODE_NAME

流程速查表

阶段 命令 / 动作 关键参数 落点
前置 检查 graphify-out/graph.json 不存在则先 /graphify <path>
会话开始 graphify reflect --if-stale graphify-out/reflections/LESSONS.md
扩展 生成 .vocab.txt,选 ≤12 个词元 只用词表内词元 打印 Query expanded to ...
遍历 graphify query "TOKENS" --dfs--budget N(默认 2000)、--context--graph 只用图中内容作答
路径 graphify path "A" "B" --directed(默认)/--undirected --type path_query 写回
解释 graphify explain "NODE" --graph --type explain 写回
回写 graphify save-result --outcome useful|dead_end|corrected--correction graphify-out/memory/*.md,下次 --update 入图

以上行为均有对应的测试保障,可进一步深入验证:test_query_cli.pytest_explain_cli.pytest_path_cli.pytest_query_induced_edges.py。整体来看,这份参考文档的价值不在单条命令,而在于它把"匹配器的能力边界 → 受限扩展 → 可审计遍历 → 结果记忆化 → 经验蒸馏"串成了一条可重复执行的确定性流水线——这正是 graphify 在无向量库前提下让知识图谱"越查越懂语料"的核心机制。

登录后查看全文
热门项目推荐
相关项目推荐