graphify 增量数据接入与监听指南:用 /graphify add 抓取 URL 与 --watch 守护知识图谱
本篇是 graphify 项目(说明文档)面向 Agent / 助手(如 Kiro、Claude Code、Cursor 等)的实操参考文档解读,聚焦两个不属于默认构建流程的扩展能力:通过 /graphify add <url> 把外部 URL(推文、论文、视频、PDF、网页等)抓取入库,以及通过 --watch 在后台监听目录、让代码变更自动触发图谱重建。读完本文,你将掌握两套命令的完整参数与执行语义、底层抓取管线与"代码变更免 LLM 重建 / 文档变更标记需语义重提"的分流策略,并能在多 Agent 写代码的工作流里用 --watch 保持图谱常新。
原始参考文档位于 tools/skillgen/expected/graphify__skills__kiro__references__add-watch.md,与 graphify/skills/kiro/references/add-watch.md 内容一致,是 graphify 为不同编码 Agent 平台(Kiro、Claude、Codex、Gemini 等)生成的 skill 参考页之一(生成器见 tools/skillgen),主 skill 定义可参见 graphify/skill-kiro.md。
一、背景:为什么需要"加料"与"守护"两条路径
graphify 的默认构建流程是:把 raw/ 语料目录中的代码、文档、论文、图片等一次性抽取成可查询的知识图谱(产出 graph.json、GRAPH_REPORT.md、graph.html 等,见 docs/how-it-works.md)。但真实工作流是持续演进的:
- 用户丢来一个链接(博客、推文、arXiv 论文、一段视频或一份 PDF),希望它进入语料并被后续查询命中——对应
/graphify add <url>; - 多 Agent 波次并发写代码,每次提交都希望图谱自动跟上,但又不希望为无关文件的读事件反复重建——对应
--watch。
add 与 watch 都不是默认构建的一部分,因此参考文档要求:只有当用户运行了 /graphify add <url> 或传入了 --watch 时才加载本参考。它们共享同一套产物目录与增量合并逻辑,属于图谱的"持续喂养"层。
二、/graphify add:抓取 URL 并入语料
2.1 调用方式与"正确解释器"约定
文档给出的执行片段以 $(cat graphify-out/.graphify_python) 开头。该文件是 graphify skill 在安装时写入的一个解释器探针(见 graphify/skill-kiro.md):把实际装有 graphify 的 Python 可执行文件路径保存在 graphify-out/.graphify_python 中,后续所有 bash 块都通过 $(cat graphify-out/.graphify_python) 引用它,从而避免 python3 指向了未安装 graphify 的解释器。
$(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 |
对应的函数签名在 graphify/ingest.py:
def ingest(url: str, target_dir: Path, author: str | None = None, contributor: str | None = None) -> Path:
"""Fetch a URL and save it into target_dir as a graphify-ready file.
Returns the path of the saved file."""
ingest 会先 mkdir(parents=True, exist_ok=True) 确保目标目录存在,然后返回保存文件的路径。CLI 层面等价命令是 graphify add <url> [--author Name] [--contributor Name] [--dir ./raw](见 graphify/cli.py),可直接在 shell 中使用。
2.2 错误处理契约:两种异常不能静默吞掉
文档强调:命令出错时必须把原因告知用户,不能静默继续。源码把失败明确分成两类(graphify/ingest.py):
ValueError:URL 校验失败。ingest在抓取前先调用validate_url(url),任何非法输入都会以ValueError(f"ingest: {exc}")形式抛出。典型场景见 graphify/security.py:只允许http/httpsscheme,拒绝file://、ftp://、data:等;拒绝已知云元数据主机名;解析 DNS 后拒绝回环、私网、保留网段(防止 SSRF)。RuntimeError:抓取阶段失败。urllib.error.HTTPError(非 2xx 状态)、urllib.error.URLError(DNS/连接失败)、OSError(超限)都会转成RuntimeError(f"ingest: failed to fetch ...")抛出。
因此参考片段里两个 except 分支分别捕获这两类错误并 sys.exit(1)。真正部署在 skill 中时,还应当在调用 ingest 前先检查 graphify-out/.graphify_python 是否存在——若用户删除了 graphify-out/,应先重新解析解释器再继续(参考各 skill 文件中 ".graphify_python 缺失时重建" 的步骤)。
2.3 抓取成功后的收尾:自动跑 --update
ingest 只是把文件保存进语料目录,尚未产生任何图谱节点。文档明确要求:保存成功后,自动对 ./raw 执行 --update 管线,把新文件合并进现有图谱(该管线最终落到 graphify/watch.py 的 _rebuild_code 全量/增量重建)。CLI 提示语 Run /graphify --update in your AI assistant to update the graph.(graphify/cli.py)即是给 Agent 的指令锚点。
2.4 支持抓取的 URL 类型:自动探测分发
参考文档列出的类型并非手写分支,而是由 _detect_url_type 依据 URL 特征自动判别(graphify/ingest.py):
| URL 特征 | 判定类型 | 实际处理 | 落盘格式 |
|---|---|---|---|
twitter.com / x.com |
tweet | 通过 publish.twitter.com/oembed 拉取(x.com 会先归一化为 twitter.com),解析 author_name 与正文;oEmbed 失败则存 URL 存根 |
.md,frontmatter 含 type: tweet、source_url、captured_at 等 |
arxiv.org |
arxiv | 正则提取 \d{4}\.\d{4,5} 论文号,转成 export.arxiv.org/abs/<id> 抓摘要、标题、作者 |
arxiv_<id>.md(小数点替换为下划线),frontmatter 含 type: paper、arxiv_id、paper_authors |
github.com |
github | 目前未特化(源码中只是先判出类型,未进入专门抓取分支) | 走通用网页路径 |
youtube.com / youtu.be |
youtube | 交给 graphify/transcribe.py 的 download_audio,用 yt-dlp 只下音频流(bestaudio[ext=m4a]/bestaudio/best),文件名基于 URL 的 SHA-1 前 12 位,如 yt_<hash>.m4a,已下载过则直接命中缓存 |
音频文件;下一次运行转写为 .txt |
路径以 .pdf 结尾 |
二进制直下 | .pdf |
|
.png/.jpg/.jpeg/.webp/.gif |
image | 二进制直下,扩展名缺失时回退 .jpg |
原图;Claude vision 在下一次运行抽取 |
| 以上皆非 | webpage | 抓 HTML → 提取 <title> → 先剥离 <script>/<style> 再经 markdownify(heading_style="ATX", bullets="-", strip=["img"])转 Markdown;未装 markdownify 时退化为基本标签剥离并截断 8000 字符;正文截断至 12000 字符 |
.md,frontmatter 含 type: webpage、title |
所有文本类内容都带有 YAML frontmatter,且经由 _yaml_str 安全转义(graphify/ingest.py),防止抓取到的标题/正文通过未转义的控制字符(\t、\0、Unicode U+2028/U+2029 等)逃逸出双引号标量、注入兄弟 YAML 键。文件命名由 _safe_filename 把 netloc+path 清洗为安全文件名并截断到 80 字符;重名文件通过 _<counter> 后缀避免覆盖(最多尝试到 999)。
需要注意两个可选依赖:
- 视频类需要
pip install 'graphifyy[video]'(含 yt-dlp 与 faster-whisper,见 graphify/transcribe.py 的 ImportError 提示;转写模型可用环境变量GRAPHIFY_WHISPER_MODEL覆盖,默认base); - 通用网页若想获得干净的 Markdown 而非降级文本,需要
markdownify。
2.5 底层安全栅栏:add 不是裸爬虫
ingest 的每一次网络访问都经过 graphify/security.py 的 SSRF 防护体系,这也是"把任意 URL 交给 Agent 执行"时的关键安全边界:
validate_url(security.py):scheme 白名单 + 云元数据主机黑名单 + DNS 解析后对每个地址做私网/保留网段拦截;_SSRFGuardedHTTP(S)Connection(security.py):连接前只解析一次 DNS 并校验 IP,校验与建连用同一个 IP,杜绝 DNS-rebind 的 TOCTOU 竞态;_NoFileRedirectHandler(security.py):每个重定向目标都会重新validate_url,防开放重定向把http://引到file://或内网;safe_fetch(security.py):流式读取并硬性限制响应体积(超过即抛OSError中止),超时默认 30s;文本抓取走safe_fetch_text(默认 15s 超时,UTF-8 容错解码)。
三、--watch:后台监听目录,按需自动重建
3.1 启动方式与参数
$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3
INPUT_PATH 换成要监听的目录。模块入口解析逻辑在 graphify/watch.py:路径缺省为当前目录 .,--debounce 缺省 3.0 秒。CLI 等价入口是 graphify watch [path](graphify/cli.py),路径不存在时报错退出,缺少 watchdog 依赖时给出 pip install watchdog 的安装提示。
启动后终端会打印三行状态(watch.py):正在监听哪个目录、代码变更会自动重建而文档/图片变更需 /graphify --update、当前 debounce 值。按 Ctrl+C 停止。
3.2 双通道分流:AST 快通道与 LLM 慢通道
watch 的核心是按变更文件类型分流(决策逻辑见 _batch_triggers_rebuild 与 _batch_needs_llm_flag,watch.py):
| 变更内容 | 立即行为 | 产物 | 是否需要 LLM |
|---|---|---|---|
纯代码文件(.py/.ts/.go 等 AST 可抽取扩展名),或任何被删除的文件 |
立即重跑 AST 抽取 + 重建 +(必要时)聚类 | graph.json、GRAPH_REPORT.md 自动更新,缺失/过期的 graph.html 也会被重生成(_reconcile_graph_html) |
否 |
| 文档 / 论文 / 图片等非代码文件仍在磁盘上 | 写入 graphify-out/needs_update 标记文件并打印提示 |
等待用户手动运行 /graphify --update |
是(LLM 语义重抽取) |
代码文件走 _rebuild_code(watch.py),内部链条是:detect() 扫描 → 对变更文件做增量 extract() → 与既有 graph.json 做 reconcile(保留未变更文件的历史节点、驱逐已删除源的陈旧节点)→ cluster() 社区检测 → report.generate() → 原子写回。即使检测到拓扑无变化,也会智能地"不动产物"或只补渲染缺失的 HTML(watch.py)。
非代码文件之所以要手动触发,是因为文档/论文/图片需要 LLM 语义抽取(视频转写后的 .txt、视觉图片理解同理)——AST 快通道无法生成这些语义节点。文档要求:标记文件存在时打印 [graphify check-update] Pending non-code changes ... 并提示运行 /graphify --update(见 watch.py 的 check_update)。
3.3 Debounce:把"一波并发写"折叠成一次重建
默认 3 秒的防抖(watch.py 的 --debounce 默认值)语义是:等到文件活动停止满 N 秒才触发。监听循环每 0.5s 检查一次 time.monotonic() - last_trigger >= debounce(watch.py),因此一个 Agent 波次内并发的几十次写入只会聚合成一个批次触发一次重建,而不是每写一个文件就重建一遍。watch() 还会加载一次 .graphifyignore 模式缓存,并用 _is_read_only_event 过滤掉 opened/closed_no_write 这类纯读事件,避免 watcher 自己读文件触发死循环(watch.py)。
被监听扩展名集合来自 CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS(watch.py,常量定义在 graphify/detect.py),点开头路径、graphify 自身输出目录 graphify-out 内的事件会被排除。
3.4 并发安全与可靠性设计(源码级细节)
watch 之所以适合多 Agent 并行场景,底层有几层防护(这些不在参考文档中列出,但决定了它的生产可用性):
- 每仓库重建锁:
_rebuild_lock(watch.py)基于fcntl.flock,非阻塞获取,持锁进程把 PID 写入graphify-out/.rebuild.lock;未抢到锁的增量提交先把自己的变更集写入graphify-out/.pending_changes队列,由持锁进程在重建前后排空合并,避免变更集被静默丢弃; - 原子写入:新图先写
.graph.tmp.json再replace到graph.json,防止中途崩溃留下截断文件; - 收缩防护:
_check_shrink(watch.py)拒绝"节点数异常减少"的写入(除非显式删除或--force),防止半截的语义抽取 chunk 静默清空图谱; - 平台差异:macOS 上使用轮询观察器(PollingObserver)以避免 FSEvents 漏掉编辑器的快速保存;Windows 无 fcntl 时锁自动降级为 no-op。
3.5 Agentic 工作流的推荐用法
参考文档给出的落地建议:
在多 Agent 工作流中,在后台终端运行
--watch。Agent 波次之间的代码改动会被自动拾取;如果 Agent 还写了文档或笔记,则在这些波次之后需要手动执行一次/graphify --update。
即:--watch 负责代码主线的"免打扰自动更新",/graphify --update 负责需要 LLM 的语义材料的"按需补齐"。两者共享同一套增量合并(_reconcile_existing_graph,watch.py),所以交替使用不会互相破坏既有节点。
四、工作目录约定小结
两类能力都围绕以下目录布局运转(输出目录名默认 graphify-out,可用环境变量 GRAPHIFY_OUT 覆盖,见 graphify/paths.py):
| 路径 | 角色 |
|---|---|
raw/(或 ./raw) |
/graphify add 默认落盘目录,也是默认语料根目录 |
graphify-out/.graphify_python |
解释器探针文件,add/watch 命令都通过它选择 Python |
graphify-out/graph.json |
图谱数据,watch 重建的目标产物 |
graphify-out/GRAPH_REPORT.md |
人读报告,随代码重建自动刷新 |
graphify-out/needs_update |
非代码变更待处理的标记文件(LLM 语义重抽取信号) |
graphify-out/.rebuild.lock / .pending_changes |
并发重建锁与待办变更队列(内部机制) |
五、验证与测试入口
若想了解该行为在仓库中的验证方式,相关测试集中在 tests/test_watch.py、tests/test_incremental.py、tests/test_incremental_mtime_collision.py、tests/test_office_incremental.py 等文件中;add 的抓取分派可在 graphify/ingest.py 直接阅读,转写链路见 graphify/transcribe.py。
六、一句话总结
/graphify add <url> 把外部世界(推文、论文、视频、PDF、网页)安全地变成语料文件并立即触发 --update 并入图谱;--watch 则让代码库的持续演进"零 LLM 成本"地自动反映到 graph.json 中,而对文档/图片这类需要语义理解的材料,用 needs_update 标记把决策权交还给 /graphify --update。两者相加,就构成了一条"抓取即入库、改动即重建、语义材料按需补齐"的图谱持续喂养闭环。
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