graphify 实战:`/graphify add` 的 URL 摄取与 `--watch` 的文件夹自动重建
导读
Graphify 把"把外部内容加进知识图谱"和"让图谱跟随代码仓库演进"做成了两条自动化通道:/graphify add <url> 负责抓取网页、推文、论文、PDF、图片甚至视频并把它们落盘到语料目录,而 --watch 则在后台上监控一个文件夹,在代码变更时无需 LLM 即可自动重建 graph.json 与 GRAPH_REPORT.md,遇到文档/论文/图片变更时再提示你执行一次 /graphify --update 做语义级重提取。本文基于项目内 VSCode 等各平台 Agent 共用的技能参考文档 graphify/skills/vscode/references/add-watch.md 展开,并结合 graphify/ingest.py、graphify/watch.py 等源码说明其底层实现。读完你将掌握:两种能力的触发场景与确切命令、6 类 URL 的自动识别与落盘规则、错误处理与"摄取后自动更新"的完整闭环,以及如何把 --watch 接入多 Agent 波次式开发工作流。
该参考文档的加载条件写得很明确:仅当用户执行了
/graphify add <url>或显式传入--watch时才加载它——这两个能力都不属于默认构建流程,是图构建之外的增量扩展通道。它在agents、amp、claude、codex、kiro、opencode、vscode、windows等各平台技能目录下都有同名副本,本文以vscode目录为例讲解,其他平台完全一致。
一、/graphify add:把一个 URL 变成语料并立即入图
/graphify add 的目标是:抓取一个 URL → 保存为 graphify 可消费的文件 → 随后把新文件合并进既有图谱。整个动作由两段组成——先调用 ingest() 完成抓取与落盘,成功后自动对 ./raw 跑一遍 --update 流水线。
1.1 摄取调用的标准写法
参考文档给出的调用脚本如下(其中的 $(cat graphify-out/.graphify_python) 用于读取构建时记录在 graphify-out/.graphify_python 中的 Python 解释器路径,确保与当初构建图谱的解释器一致,避免 PATH 漂移):
$(cat graphify-out/.graphify_python) -c "
import sys
from graphify.ingest import ingest
from pathlib import Path
try:
out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR')
print(f'Saved to {out}')
except ValueError as e:
print(f'error: {e}', file=sys.stderr)
sys.exit(1)
except RuntimeError as e:
print(f'error: {e}', file=sys.stderr)
sys.exit(1)
"
需要替换的三个占位符:
| 占位符 | 替换内容 |
|---|---|
URL |
用户给出的真实链接 |
AUTHOR |
用户提供姓名时填写,否则保持占位(语义上为空) |
CONTRIBUTOR |
团队图谱场景下的贡献者名,规则同 AUTHOR |
这两个元数据会被写进落盘文件头部的 YAML frontmatter,其中 contributor 字段的取值逻辑在 graphify/ingest.py 的各抓取函数中统一为 contributor or author or 'unknown'——也就是说即使调用方只传了 author,它也会被继承到 contributor,保证图谱节点永远带有可溯源的人名信息。
1.2 错误处理契约:绝不静默继续
脚本对 ingest() 抛出的异常做了分类处理,这一点值得强调,它是 skill 的硬性行为契约:
ValueError:URL 校验失败。在 graphify/ingest.py 中,ingest()会先调用graphify.security的validate_url()(配合safe_fetch系列函数做安全抓取),校验不通过即以ValueError抛出——这可以拦截私有 IP、非法 scheme 等不安全目标。RuntimeError:抓取阶段失败。ingest()会把urllib.error.HTTPError / URLError / OSError统一包装成RuntimeError("ingest: failed to fetch ...")再抛出。
无论哪种失败,参考文档都要求:把错误原样转告用户,解释清楚哪里出了问题,然后用 sys.exit(1) 退出,而不是假装成功继续往下走。只有 print(f'Saved to {out}') 正常输出、保存成功之后,才允许自动触发 --update 流水线把新文件 merge 进既有图谱。
1.3 六类 URL 的自动识别与落盘规则
ingest() 会根据 URL 形态自动分流,不要求用户声明类型。类型判定集中在 _detect_url_type()(见 graphify/ingest.py),规则与最终处理如下:
| URL 类型 | 判定依据 | 抓取与落盘方式 | 后续管线 |
|---|---|---|---|
| YouTube / 任意视频 | 域名含 youtube.com/youtu.be |
通过 yt-dlp 下载纯音频流(默认存为 yt_<url_hash>.<ext>,.m4a/.opus 等),见 download_audio()(graphify/transcribe.py) |
下次运行时由 faster-whisper 转写成 .txt 文本 |
| Twitter / X | 域名含 twitter.com/x.com |
走 oEmbed API(publish.twitter.com/oembed),剥离 HTML 后连同作者名存为 .md,见 _fetch_tweet() |
直接可被 Markdown 提取 |
| arXiv | 域名含 arxiv.org |
从摘要页 API 提取标题、作者列表与 Abstract,存为带 frontmatter 的 .md,见 _fetch_arxiv() |
直接可被 Markdown 提取 |
路径以 .pdf 结尾 |
直接以二进制方式下载为 .pdf |
下一次 /graphify --update 时经语义管线切片提取 |
|
| 图片 | 路径以 .png/.jpg/.jpeg/.webp/.gif 结尾 |
直接下载原图 | 下一次运行时由 Claude vision 做视觉提取 |
| 任意网页 | 以上皆非 | 将 HTML 清洗为 Markdown 后存为 .md,见 _fetch_webpage() |
直接可被 Markdown 提取 |
几点值得注意的源码细节:
- 网页转 Markdown 的实现:graphify/ingest.py 中的
_html_to_markdown()会先用正则剥掉所有<script>/<style>内容(防止页面脚本文本泄漏进正文),然后优先使用markdownify转换,缺少该依赖时退化为"去标签 + 压缩空白"的基础清洗,正文上限 8000 字符。 - 视频下载需要可选的
videoextra:依赖声明在 pyproject.toml 的[project.optional-dependencies]中,即video = ["faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9"]。参考文档明确指出需要pip install 'graphifyy[video]'(注意包名是双 y 的graphifyy),transcribe.py在缺依赖时抛出的ImportError提示信息也与之一致。 - 音频文件名稳定可缓存:
download_audio()用 URL 的 SHA-1 前 12 位命名(yt_<hash>.<ext>),二次抓取同一视频时直接命中缓存返回,不重复下载。 - 落盘防覆盖:Markdown 类抓取产物若与既有文件重名,
ingest()会用_1、_2…… 递增后缀(上限 999)自动改名,绝不静默覆盖已有语料。 - 文件名安全化:
_safe_filename()会把 URL 的 netloc+path 中非[\w\-]的字符替换为下划线并截断到 80 字符,避免 URL 中的特殊字符污染文件系统路径。
1.4 命令行等价入口
ingest.py 自带 __main__,因此在终端里也能以同样的方式单发抓取:
python graphify/ingest.py <URL> [target_dir] [--author NAME] [--contributor NAME]
target_dir 缺省为 ./raw,与 /graphify add 的落盘位置保持一致。--contributor 用于团队图谱场景;输出会以 Ready for graphify: <path> 提示文件已经可以进入提取管线。
二、摄取成功之后:把新文件合并进既有图谱
/graphify add 的成功路径并不会停留在"文件已保存"。参考文档要求:保存成功后,自动对 ./raw 运行一次 --update 流水线,把新落盘的文件增量合并进现有 graph.json,而不是等到用户下次手动触发。
这条流水线的完整分步脚本记录在同目录的技能参考 graphify/skills/vscode/references/update.md 中,核心机制是"增量重提取":
detect_incremental()对比上次 manifest,找出 new/changed/deleted 文件,并把结果写入graphify-out/.graphify_incremental.json;- 将增量结果改写成
.graphify_detect.json,供后续 AST 提取与语义提取步骤读取; - 分流判断:若变更全部是代码文件(
.py/.ts/.js/.go/.rs/.java/.cpp/.c/...),则只跑 AST 提取(无需 LLM、零 token 消耗);一旦混入 doc/paper/image/video,才进入完整语义子代理管线; - 用
build_merge()把新提取结果与graph.json合并——它直接读图而不走 NetworkX 往返,从而保留calls/implements/imports等有向边的方向语义(directed参数需与初始构建一致); - 用
graph_diff()展示新旧图差异摘要,并把本次状态写回 manifest,让下一次--update从今天的状态开始 diff。
简言之:代码变更走零成本 AST 通道,非代码变更走 LLM 语义通道,二者共用同一个 merge 骨架。这个分层思想在下一节 --watch 中会再次出现。
三、--watch:后台监听文件夹,让图谱自动跟随演进
--watch 是"实时增量"的另一面:在后台启动一个文件夹监听器,当文件发生变化时自动更新图谱。它与 /graphify add 互为补充——前者管"外部新内容进图",后者管"仓库内部日常演进"。
3.1 启动方式
$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3
把 INPUT_PATH 换成要监听的目录(默认 .)。--debounce 的单位是秒,默认 3。按 Ctrl+C 即可停止。
从 CLI 层面看,graphify watch 子命令(graphify/cli.py)以及 graphify watch.py 的 __main__(graphify/watch.py)都支持该调用;--debounce 是 float 类型,允许小数。watch 能力依赖可选 extra:watch = ["watchdog"](pyproject.toml),文件系统事件正是由 watchdog 提供的。
3.2 核心行为:按变更类型走双通道
监听器对"发生了什么变化"极其敏感,处理策略由变更类型决定,这同时也是整个 watch.py 设计的灵魂:
- 只改了代码文件(
.py、.ts、.go等一切 AST 可解析扩展名):立即重跑 AST 提取 + 重建 + 社区聚类,全程不需要 LLM。graph.json与GRAPH_REPORT.md会被自动更新(源码中的重建路径还会顺带 reconcile 缺失或过期的graph.html可视化)。 - 改了文档、论文或图片:此时写入一个
graphify-out/needs_update标志文件,并打印通知,提示运行/graphify --update——因为这类文件的语义级重提取需要 LLM。
也就是说,watch 把"零成本可自动"与"需花钱需人工确认"的两类变更物理隔离了。needs_update 标志的实现见 graphify/watch.py 的 _notify_only():它创建标志文件并写入 "1";check_update()(同一文件 L2083-L2090)则检查该标志并打印 Pending non-code changes in ...。graphify check-update <path> 子命令暴露了这一检查能力(graphify/cli.py),适合被 CI 或外部脚本轮询。
值得补充的判定细节(来自源码注释,graphify/watch.py):
- 删除任何被监听文件也触发立即重建:因为"驱逐"(从图中移除已删源文件的节点/边)同样不需要 LLM,全语料 reconcile 会直接依据磁盘存在性把对应记录清掉——否则纯文档删除会被错误地挂到
needs_update后面,直到下一个代码事件或手动 update 才被处理。 - "文件被读"不算变更:只有创建、修改、移动、删除和"写入后关闭"才算。这样当 Agent 正在阅读语料文件时,watcher 不会把读操作误判为变更而陷入自我触发的死循环。
3.3 debounce:抵御"一波并行写入"
参考文档对 debounce 的解释是:等待文件活动完全停止后才触发,避免一波并行 Agent 写入让每次写文件都触发一次重建。默认 3 秒,实践中通常无需修改。
源码层面的循环实现印证了这一点(graphify/watch.py):
- watcher 把一段时间内积累的变更收集进一个
changed集合,记录最后触发时间last_trigger; - 主循环每
0.5s醒来一次,只有当存在 pending 变更、且距last_trigger已超过debounce秒时,才把整个批次合并成一次重建(batch = list(changed))。
这保证了一次 git pull、一次多文件格式保存或一波 Agent 并发写操作,最终只产生一次重建而不是 N 次。--debounce 0 可以用于调试时需要"立即响应"的场景。
3.4 增量重建的可靠性设计(源码纵深)
代码通道的自动重建远比"检测到变化就全量重跑"精细。watch.py 内的增量 rebuild(_rebuild_code,graphify/watch.py)具备一套面向并发与失败场景的保护机制,值得了解,便于排查线上问题:
- 按变更集增量提取:调用方(如 git post-commit hook)提供
changed_paths时,只对这批文件重跑 AST 提取;未变更文件的 AST 节点与所有 semantic/LLM 层节点从既有graph.json中保留并合并,删除的路径则从保留集合中驱逐。_reconcile_existing_graph()负责新旧合并与逐源驱逐。 - 并发安全:
_rebuild_lock()基于fcntl.flock实现仓库级 advisory lock,进程被杀时内核自动释放锁,无需清理陈旧锁文件;拿不到锁的调用方(例如并发的多仓库 post-commit hook)会先把变更写入graphify-out/.pending_changes排队(_queue_pending),持锁进程在重建前、后各 drain 一次,把队列内容并入自己的变更集,避免变更集被静默丢弃。重建时锁文件.rebuild.lock内记录持锁进程 PID,供外部发布脚本轮询。 - 失败防护(fail-closed):
_check_shrink()是防"塌缩"守卫——若新图节点数骤降且丢失无法归因于被重提取/被删除的文件(例如上一轮语义分块文件缺失导致的批量丢失),重建会拒绝覆盖并提示Pass --force to override,而不是默默写坏一个几千节点的图。 - 排除规则持久化:初次
extract时的--exclude/.gitignore设置会被写入graphify-out/.graphify_build.json(_write_build_config),后续 watch/hook 重建自动重新应用,避免"当初排除的路径被无声重新纳入"。 - hook 环境的健壮性:detached git hook 可能继承一个已被删除的临时工作目录,此时相对路径全部失效;通过
GRAPHIFY_REPO_ROOT环境变量可以指定仓库根以便重建前chdir回去(_stabilize_rebuild_cwd)。另有GRAPHIFY_REBUILD_MEMORY_LIMIT_MB可对重建进程做 best-effort 的内存上限与nice(10)降优先级(_apply_resource_limits)。 - 目录名可配置:所有产物目录名统一由 graphify/paths.py 的
GRAPHIFY_OUT决定(默认"graphify-out",可用同名环境变量覆盖,支持相对名或绝对路径),watcher 的needs_update标志、锁文件等全部随之迁移。
这些机制共同保证了:一个跑在后台的 watcher,即使面对多 Agent 并发写、进程被杀、部分提取失败等恶劣情况,也只会产出一致、完整、不塌缩的图谱。
四、Agentic 工作流:把 --watch 变成 Agent 的"后台队友"
参考文档专门给出了面向多 Agent 编程工作流的建议,这也是 --watch 最有价值的应用场景:
在后台终端里运行
--watch。Agent 波次之间产生的代码变更会被自动拾取并重建图谱;如果 Agent 同时也在写文档或笔记,那么这些波次结束后需要你手动执行一次/graphify --update。
推荐的落地姿势:
- 启动前:先做一次完整的首次构建(生成
graph.json、GRAPH_REPORT.md、graphify-out/.graphify_python)。 - 启动 watcher:开一个独立后台终端,执行上文
python -m graphify.watch INPUT_PATH --debounce 3。 - 派出 Agent 波次:每个 Agent 波次改完代码后,watcher 会在 debounce 静默期自动完成 AST 重建——你随时拿到的
graph.json/GRAPH_REPORT.md都是"刚刚的代码"对应的图谱,查询时不需要再担心图过期。 - 文档波次后收尾:当某波 Agent 大量写文档/笔记/图片时,终端会打印
needs_update通知,此时跑一次/graphify --update,让 LLM 语义提取把这些文档节点并进图谱。
这套组合的逻辑闭环在于:代码进图零成本、可全自动;文档进图成本高、需要人在场确认——watcher 用 needs_update 标志把"高成本动作"推迟到你有空的时候,而不是悄悄烧 token。
五、依赖与产物速查
把 add / watch 两条通道跑通所需的可选依赖,全部声明在 pyproject.toml 的 [project.optional-dependencies]:
| 能力 | 对应 extra | 关键依赖 |
|---|---|---|
| 文件夹监听 | watch |
watchdog |
| 视频 URL / 转写 | video |
yt-dlp、faster-whisper(Python ≥ 3.11) |
| PDF / 网页正文清洗 | pdf |
pypdf、markdownify |
产物约定:一切中间态都落在 graphify-out/(可被 GRAPHIFY_OUT 环境变量整体迁移),其中 needs_update 是"待语义更新"标志,.graphify_incremental.json/.graphify_detect.json/.graphify_extract.json 是 --update 流水线各阶段的交接文件,graph.json 与 GRAPH_REPORT.md 是最终图谱与人类可读报告。
六、快速检查清单
/graphify add的脚本把ValueError(URL 校验失败)与RuntimeError(抓取失败)都转译为带明确消息的非零退出——任何错误都要转告用户,不要静默继续;- 视频 URL 需要先
pip install 'graphifyy[video]',否则transcribe.py会提示缺 faster-whisper / yt-dlp; - 代码文件变更走 AST 通道,
graph.json/GRAPH_REPORT.md立即更新,无需 LLM; - 文档/论文/图片变更只写
needs_update标志并打印通知,需要你执行/graphify --update; --debounce(默认 3s)把一波并行写入合并为一次重建,避免每写一文件就重建一次;- 纯代码场景请始终让 watcher 跑在后台终端;文档密集的 Agent 波次结束后记得手动补一次
/graphify --update。
更细的增量 merge 分步命令见同技能下的 update.md,watch 的并发与驱逐逻辑测试覆盖在 tests/test_watch.py 与 tests/test_watch_manifest_location.py,URL 摄取的输入输出样例可参考 tests/test_ingest.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 StartedRust0630
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
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