首页
/ graphify 语料扩展实战:/graphify add 拉取 URL 入图与 --watch 文件夹自动重建

graphify 语料扩展实战:/graphify add 拉取 URL 入图与 --watch 文件夹自动重建

2026-09-06 16:04:39作者:段琳惟

本篇基于 graphify 的 Kiro 技能参考文档 add-watch.md,完整讲解 graphify 两个非默认构建流程——/graphify add <url>(把 URL 拉取进语料库并更新图谱)与 --watch(后台监听文件夹、文件变化时自动更新图谱)——的标准命令、参数替换规则、六类 URL 的自动识别与落地行为,以及监听重建背后的事件过滤、防抖机制与并发锁实现,帮助你在 Agent 协作的长周期开发中保持知识图谱与代码库实时同步。

两个非默认流程的定位

graphify 的核心技能 skill-kiro.md 定义了默认构建流水线(检测、AST/语义抽取、建图、聚类、报告、导出)。add--watch 都不属于这条默认链路:技能文档明确写道,当用户执行 /graphify add <url> 或传入 --watch 时才加载参考文档 references/add-watch.md。二者解决的是同一类问题——让图谱持续吸收新内容

  • /graphify add:把外部 URL(视频、推文、论文、PDF、图片、网页)抓下来放进语料目录,再走增量更新合并进现有图谱;
  • --watch:起一个后台监听进程,代码文件变化时免 LLM 自动重建,文档类变化则打上需要语义重抽的标记。

两个流程都依赖一个前置约定:graphify-out/.graphify_python 文件记录着当前项目可用的 Python 解释器路径,后续命令统一用 $(cat graphify-out/.graphify_python) 前缀调用,确保与首次构建用的是同一解释器。该文件由技能 Step 1(检测/安装阶段)写入,若被误删需先按技能文档中的 Interpreter guard 重新解析。

/graphify add:抓取 URL 并并入语料库

标准命令

参考文档给出的完整命令如下(执行于项目根目录,与 graphify-out/ 同级):

$(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:内容作者(用户提供了才填),写入产物 frontmatter 的 author 字段;
  • CONTRIBUTOR:把内容加入语料库的人,写入 contributor 字段,团队共用语料库时用于追溯是谁加的料。

错误处理是硬性要求:若命令以非零码退出(ValueError 通常是 URL 校验失败,RuntimeError 是抓取/网络失败),必须把错误告诉用户,不能静默继续。保存成功后,自动对 ./raw 触发 --update 增量流水线,把新文件合并进既有图谱——增量流程(只重抽变更文件、代码变更跳过语义抽取省 token)详见姊妹参考文档 update.md

六类 URL 的自动识别与落地行为

文档声明 URL 类型是自动检测的,不需要用户指定。对照实现 graphify/ingest.py 中的 _detect_url_type(),检测顺序与文档一一对应:

URL 类型 识别条件 落地行为
YouTube / 任意视频 youtube.comyoutu.be 通过 yt-dlp 下载音频到 ./raw下次运行时转写为 .txt;需要 pip install 'graphifyy[video]'
Twitter/X twitter.comx.com 走 oEmbed 接口抓取,存为 .md,含推文正文与作者
arXiv arxiv.org 抓摘要页,存摘要 + 元数据为 .md
PDF 路径以 .pdf 结尾 直接下载为 .pdf
图片 路径以 .png/.jpg/.webp 结尾 直接下载,下次构建时由宿主 Agent 的视觉能力(Claude vision)提取
任意网页 兜底 经 HTML 转 Markdown 后存 .md

几个值得注意的实现细节(均出自 ingest.py):

  • 推文抓取_fetch_tweet,L103-L133):先把 x.com 规范化回 twitter.com,请求 publish.twitter.com/oembed 端点;若 oEmbed 失败则降级为 URL 占位存根,而不是整体报错。产物带 YAML frontmatter(source_urltype: tweetauthorcaptured_atcontributor)。
  • arXiv 抓取_fetch_arxiv,L165-L207):用正则 \d{4}\.\d{4,5} 从 URL 中提取 arXiv 编号,改走 export.arxiv.org/abs/ 页面抽取标题、作者、摘要;URL 中识别不出编号时退回普通网页路径。文件名固定为 arxiv_<id>.md
  • 网页转换_fetch_webpage,L136-L162):先正则剥离 <script>/<style> 防止脚本内容泄漏,再优先用 markdownify 转 ATX 风格 Markdown,缺失该依赖时退化为裸标签剥离并截断到 8000 字符;正文统一截断到 12000 字符再落盘。
  • 防覆盖(L259-L265):同名文件已存在时自动追加 _1_2… 后缀(上限 1000),避免同一 URL 抓两次互相覆盖。
  • frontmatter 注入防护_yaml_str,L13-L52):所有写进 YAML 双引号标量的抓取内容(页面标题、作者名)都经过逐字符转义,覆盖 \"、换行、\t\0 以及 Unicode 行分隔符 U+2028/U+2029——恶意页面的标题不可能借此逃逸引号注入 YAML 键。
  • URL 安全校验ingest() 入口先调用 security.pyvalidate_url 再做任何网络请求,抓取统一走 safe_fetch / safe_fetch_text
  • 源码支持范围略宽于文档:图片后缀实际还接受 .jpeg.gif(L79),另外含 github.com 的 URL 会被分类为 github 类型——但仓库内 GitHub 仓库的克隆流程由技能的 Step 0(github-and-merge.md)负责,ingest() 本身对 github 类型会走通用网页路径。

CLI 等价入口

不经过 Agent 技能时,也可以直接用命令行子命令,实现在 cli.py

graphify add <url> [--author Name] [--contributor Name] [--dir ./raw]

CLI 版本抓取成功后会打印 Saved to <path> 并提示 Run /graphify --update in your AI assistant to update the graph.——与技能版“自动触发 --update”的区别在于 CLI 把更新动作交还给人/AI 助手执行,语义一致。

--watch:后台监听与自动重建

标准命令

$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3

INPUT_PATH 替换为要监听的文件夹。该命令直接执行 graphify/watch.py 模块的 __main__ 入口(L2240-L2247),其 argparse 定义了两个参数:位置参数 path(默认 .)与浮点参数 --debounce(默认 3.0 秒)。启动后会打印监听目标、行为说明与防抖时长,按 Ctrl+C 停止

两类变化,两种处理

行为取决于变化文件的类型(判定逻辑在 _batch_triggers_rebuild / _batch_needs_llm_flagwatch.py):

  • 仅代码文件变化(.py、.ts、.go 等): 立即重跑 AST 抽取 + 重建 + 聚类,不需要 LLMgraphify-out/graph.jsongraphify-out/GRAPH_REPORT.md 自动更新。删除任意受监听文件同样触发重建——因为驱逐节点只需要“文件已从磁盘消失”这一事实,同样不需要 LLM(源码注释引用了 #2580 的修复:纯文档删除批次此前会卡在标记文件后面,直到下一次代码事件)。
  • 文档、论文或图片变化: 写入 graphify-out/needs_update 标记文件并打印通知,提示运行 /graphify --update 完成 LLM 语义重抽取。check_update()(L2077-L2089)是配套的只读检查入口:标记存在时打印提示,且永远返回成功,方便挂进 cron 而不会误报警;它只负责“提醒”,不清除标记。

防抖(debounce)机制

默认 3 秒。实现上(watch(),L2149-L2237):watchdog 事件到达时只记录最后事件时间戳并累积变更文件集合,主循环每 0.5 秒醒来检查一次,当“距上次事件 ≥ debounce”才把整批变更取出处理。目的正如文档所说:一波并行的 Agent 写入不会每个文件都触发一次重建,而是合并成一次批处理。这与“在 Agent 工作流中跑 watch”的场景直接对应——下一节展开。

监听重建的实现纵深

参考文档只描述了表面行为,watch.py 源码(约 2200 行)揭示了它为何能在多进程、多提交的高压场景下保持图谱一致:

事件过滤:只认真实写事件

  • 只读事件剔除_is_read_only_event,L2121-L2136):Linux inotify 会把“打开/关闭未写”(openedclosed_no_write)也报成事件——包括监听器自己的重建读树、编辑器与 Agent 的读文件。不过滤的话监听器会自我触发死循环、空耗 CPU。只有创建、修改、移动、删除、写完关闭才算变化;macOS 的轮询观察者天然不发这些事件,过滤为无操作。
  • .graphifyignore 启动时一次性加载(L2171-L2181,注释引用 gh-928):处理器在观察者线程上运行、对 OS 投递的每个事件都会被调用(Time Machine 写入、Spotlight 索引等),若逐事件解析忽略规则,繁忙卷会打满一个 CPU 核;宽泛模式(node_modules/.venv/)的用户因此获益最大。
  • 其余过滤:目录事件忽略;扩展名白名单为代码/文档/论文/图片四类后缀的并集(_WATCHED_EXTENSIONS,L278);任一路径分量以 . 开头(隐藏目录)或位于 graphify-out/ 内的事件直接丢弃。
  • macOS 用轮询观察者(L2212):PollingObserver 替代 FSEvents,规避某些编辑器快速保存被 FSEvents 漏掉的问题。

重建核心 _rebuild_code:锁、排队与防缩水

_rebuild_code()(L1274 起)是“无 LLM 重建”的完整实现,几个关键设计:

  • 每仓库 flock 锁_rebuild_lock,L161-L218):多个仓库的 post-commit 钩子可能并发重建,fcntl.flock 保证同一时刻只有一个进程写图;锁文件 .rebuild.lock 内写入持有者 PID 供外部轮询,释放时删除。拿不到锁的增量调用不会丢工作——先把变更路径追加进 .pending_changes 队列(_queue_pending,L22-L41),拿到锁的进程在重建前后各 drain 一次并合并(_drain_pending + _merge_changed_paths),重建结束还会补最多 20 轮“晚到批次”排空,避免提交风暴下的活锁。Windows 无 fcntl 时降级为直接放行。
  • 增量 vs 全量:传入变更列表时只重抽“变更且仍存在”的文件,未变更文件的节点从既有 graph.json 保留;被删除的路径单独追踪用于驱逐陈旧节点。不传列表则全量重抽(watch 主循环与 post-checkout 钩子走此路径)。
  • 解析上下文注入(L1558-L1639,注释引用 #2406/#2437/#2438):增量重建只解析变更文件,跨文件解析器看不到未变更文件里的被调用方,会导致“变更→未变更”的 calls 边全部消失。实现上把既有图中未重抽文件的 AST 节点(含 _callable 标记)与 contains/method 边作为只读上下文交给 extract(),专门用于扩展解析索引,不解析、不修改、不输出。
  • 防缩水守卫_check_shrink,L1080-L1150):新图节点数少于旧图时拒绝覆写并打印警告,--force 可绕过。但“合理缩水”会计入豁免:每个丢失节点都归属于本次重抽/删除的源文件时放行,失败源(failed_sources)的节点缺失永远豁免不了——守卫防的正是“抽取失败导致的大面积静默丢节点”。
  • 写前备份 + 原子替换:覆写 graph.json 前先 backup_if_protected,候选图写入 .graph.tmp.jsonreplace,中途崩溃不会留下半截 JSON。既有图不可读(超大小上限或解析失败)时 fail-closed 拒绝覆写——“读失败”不等于“没变化”。
  • 无变化快路径:拓扑对比(节点/边/超边规范化后逐字节比 JSON)与报告对比都相同时,完全不动盘,只打印 No code-graph changes detected; outputs left untouched.graph.html 缺失或被标记 stale 时仍会从 graph.json 里已有的社区信息独立重建(_reconcile_graph_html,L1192-L1271)。
  • 社区标签保鲜:重建后用成员签名(community_member_sigs)校验既有标签,社区构成变了就丢弃旧名、按枢纽节点(最高度数成员)确定性重命名,并在 stderr 提示可运行 graphify label 用 LLM 刷新命名(L1904-L1947)。
  • 清单同步:每次重建用 save_manifest 刷新 manifest.json(AST 侧),让后续 --update 能正确判断哪些文件已抽取;本批抽取失败的文件不盖时间戳、旧哈希清空(L1653-L1658,注释引用 #2543),下次运行重新排队。
  • needs_update 标记清除:成功重建后删除该标记(L2058-L2061),所以纯代码变化不会留下过期标记。
  • HTML 可视化联动:若 graphify-out/ 下已有 *-callflow.html,重建成功后自动重新生成(按存在即 opt-in,L2041-L2056)。

语义文档不重复 AST 快扫

一个容易踩坑的点(L1407-L1482,注释引用 #1915/#2333):已有语义(LLM)节点的文档不会再被 AST 快扫——否则每次重建都会在保留的语义节点之上又造一层标题节点,图谱体积膨胀数倍。这类文档保留在语料集合中参与“删除证据”判断与缩水会计,但其语义层是唯一定位,增量重建也不得把它列入重抽目标,否则会清掉语义节点。

Agent 工作流中的推荐用法

参考文档给出两条实战建议:

  1. 后台终端跑 watch--watch 放在一个独立终端常驻,Agent 一波波(wave)改代码时,波与波之间监听器自动拾取变化并重建;由于防抖合并,一波内的批量写入只产生一次重建。
  2. 文档/笔记变化需要手动收尾:如果 Agent 同时写文档或笔记,这些变化只会留下 needs_update 标记,那些波结束后需要手动跑一次 /graphify --update 完成语义重抽。

配合 post-commit 钩子(见 hooks.md)时,钩子进程拿不到重建锁也不会丢提交——变更集进 .pending_changes 队列,由持锁者合并重建(watch.py 顶部注释引用的 #1059 即此场景)。

依赖安装与验证

对照 pyproject.toml 的可选依赖,两条流程的额外依赖分别是:

  • 视频类 URL 抓取:pip install 'graphifyy[video]'faster-whisper + yt-dlp,需 Python ≥ 3.11)——PDF 与网页 Markdown 转换则依赖 pdf 额外包里的 pypdf + markdownify
  • 监听:pip install 'graphifyy[watch]'watchdog);watch() 在缺少 watchdog 时会抛出明确的 ImportError: watchdog not installed. Run: pip install watchdog

行为可用仓库内测试核对:tests/test_watch.py 覆盖了变更批次判定(纯文档删除触发重建但跳过 LLM 标记、纯代码变更触发重建)、needs_update 标记的创建/幂等/check_update 只提醒不清除、flock 锁的 PID 写入与释放、删除文件的节点驱逐、重命名源文件的驱逐、既有图合并保留超边与语义边等;URL 抓取路径的测试见 tests/test_ingest.py

小结

/graphify add--watch 是 graphify 把知识图谱从“一次性快照”变成“活体语料”的两个杠杆:前者用六类 URL 自动识别 + 安全抓取 + frontmatter 注入防护,把外部内容规范化地并入 ./raw 再走 --update 增量流水线;后者用 watchdog 事件过滤(只读事件剔除、忽略规则预加载)、防抖批处理、flock + 待处理队列、增量重建 + 解析上下文注入与防缩水守卫,在零 LLM 成本下让代码图谱随每次文件写盘自动收敛到最新。两者共同的边界也很清晰:语义类内容(文档、论文、图片、视频转写)永远需要一次显式的 /graphify --update,watch 只负责及时地替你提醒。

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