graphify 查询子系统实战:受限查询扩展、BFS/DFS 遍历、path/explain 与工作记忆反馈闭环
本篇以 graphify 技能包中的查询参考文档 query.md 为主体,系统讲解针对已建知识图谱的三类查询流程——query 子图遍历、path 最短路径、explain 单节点解释,并完整继承原文档的"受限查询扩展"(Step 0)、token 预算、save-result 反馈闭环与 reflect 工作记忆机制。结合 cli.py、serve.py、ingest.py、reflect.py 的源码实现,你将掌握:从图谱词表安全扩展查询词、BFS 与 DFS 两种遍历模式的适用场景与参数、查询结果的记忆化回写方式,以及每次会话开始前如何刷新并读取经验教训文档 LESSONS.md。
参考文档的定位与加载时机
query.md 是 graphify 多平台技能包(Claude Code、Amp、Codex 等)中的一份"按需加载"参考文档。文档开篇明确了它的触发条件:
当用户对已有图提问,或运行
/graphify path、/graphify explain时加载本文档。核心技能中的 query stub 会把完整的遍历流程指向这里。这些流程在graphify queryCLI 可用时优先使用它,否则回退到内联 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]+ 是典型的驼峰拆分器(FooBarService → Foo/Bar/Service),词长限制在 3~30 个字符——这与内联遍历脚本里 len(t) >= 3 的词元阈值一致,注释里还特别提到该阈值是为了保留 api/jwt/ios 这类短标识符。
2. 从词表中选择扩展词元(硬性约束)
读取 graphify-out/.vocab.txt 后,针对用户问题最多选 12 个语义匹配的词元,并遵守四条硬约束:
- 只能选词表里真实存在的词元,绝不编造;
- 若某个查询概念在词表里没有对应词元,跳过它——不要用训练记忆里的近义词顶替;
- 若一个都匹配不上,输出空列表并明确告知用户"该语料对此问题没有相关词汇",不要伪造一次检索;
- 跨语言翻译示例:俄语 "аутентификация" → 在词表中找
auth、credential、token、security,前提是它们真的在词表里; - 词形归并:"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 |
指定图谱文件 |
源码里还有几个值得了解的行为:
- 刻意保持无向图。与
path/explain强制directed=True不同,query加载图谱时不建 DiGraph——BFS/DFS 必须同时探索种子节点的 caller 与 callee 两侧才能形成有用上下文;若强制有向,没有出边的种子节点G.neighbors()会静默丢掉所有 caller 侧结果。边的方向改由每条 link 上的_src/_tgt标记在渲染时还原,遍历不受限。 - 每次查询都记账。
querylog.log_query会记录 kind、问题、模式、深度、预算与耗时(cli.py#L1177-L1186),_touch_query_stamp更新查询时间戳,供增量更新时判断图是否被使用过。 - 输出头会标明来自哪张图。
_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)),高频词(error、exception)权重低,稀有标识符(FooBarService)权重高; - 分层匹配加分:整串精确匹配
+1000、前缀匹配+100、子串匹配+1、源文件路径命中+0.5(_find_node_tiers的四级阶梯); - 大图谱上用字符三元组(trigram)倒排索引做候选预过滤,避免全图扫描;
- 关系意图词(
calls、uses等)会被从"逐词元种子保证"中剔除(#2507),防止动词碰巧命中某标识符而占据一个 BFS 种子席位。
内联回退(CLI 不可用时)
加载 graphify-out/graph.json 后用 NetworkX 原地遍历。原文档给出的完整脚本流程是:
- 找出标签与扩展词元最匹配的 1~3 个节点(按命中词元数打分取前 3);
- 从每个起始节点执行对应模式遍历——BFS 按层扩展 3 层;DFS 用栈深探、限深 6 防止走完整图;
- 读取子图的节点标签、边关系、置信度标签、源码位置;
- 只用图里有的内容作答,引用具体事实时引用
source_location; - 图信息不足就明说,不幻觉边。
输出是 token 预算感知的:节点按词元重合度排序,总字符数超过 BUDGET * 4(默认 2000,即 --budget N 指定的值)就截断并提示 ... (truncated at ~N token budget - use --budget N for more)。脚本中需要替换三个占位符:QUESTION(扩展后的查询串)、MODE(bfs/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(type、date、question、contributor: "graphify",可选 outcome、correction、source_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 重试——这是比"取第一个匹配"更安全的消歧策略; - 节点卡片:
ID、Source(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.py、test_explain_cli.py、test_path_cli.py、test_query_induced_edges.py。整体来看,这份参考文档的价值不在单条命令,而在于它把"匹配器的能力边界 → 受限扩展 → 可审计遍历 → 结果记忆化 → 经验蒸馏"串成了一条可重复执行的确定性流水线——这正是 graphify 在无向量库前提下让知识图谱"越查越懂语料"的核心机制。
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 StartedRust0623
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