阅读时间:14 minutes

您正在阅读有关将pgvector与LangChain4j集成的文章。页面底部,JBake 它建议您查看“相关文章”。您点击。这是一篇关于…​配置的文章。 来自 Kitty Terminal。它们之间唯一的共同点是什么?它们是在不到…​发布的 相隔十五天。这不是建议。这是伪装成日历的 社论。

场景:星期二 5月12日,17:30

我正在重读我的一篇文章 — 那个关于 Knowledge Graph 作为代码库理解工具的 内容密集:节点、边、社区、PlantUML、入职。一篇文章 15分钟阅读技巧,能够调动`graphify-gradle`, plantuml-gradle, 以及图的拓扑概念。

页面底部,相关文章向我推荐:

  1. 关于 Firebase 联系表单 (0113) 的文章

  2. 关于 Eager/Lazy 机制的文章 (0108)

  3. 关于 Gradle 脚本 → 插件迁移的文章 (0102)

  4. 关于Ollama Pro订阅累积的文章 (0120)

这些文章中的三篇与知识图谱没有任何关系。 他们在这里是因为他们是最近发布的 — 时间邻近,不是 语义邻域

我在看模板`post.thyme`</think>

<!-- Articles connexes (liens internes SEO) -->
<th:block th:each="post,postStat : ${published_posts}">
    <th:block th:if="${!post.uri.equals(content.uri) and postStat.index lt 4}">
        ...
    </th:block>
</th:block>

`published_posts`这是一个按时间顺序的列表。`postStat.index lt 4`拿走这些 前四个不是当前帖子的。就这样。没有逻辑。 相似性的。没有内容的概念。只是一个循环`for`伪装成 推荐。

这不是JBake的错误。这是一切的默认行为。 静态网站生成器:文章列表是平铺的,按日期排序。 模板尽其所能地使用它所拥有的。

但这不是接受它的理由。

诊断: 模板无法识别的三种关系类型

我的文章语料库 — 23篇文章,4个月 — 具有丰富的结构 时间顺序模板完全覆盖:

  1. 显式引用 : 我使用`xref:`在我的文章中大量使用。

文章 0122 引用 0106(knowledge graph)和 0116(compartimentage) 认识论的)。这些链接是编辑性的 hard linking — 我有 故意决定连接这些概念。

  1. 共享标签 : 每篇文章都有`:jbake-tags:`. 文章关于

Graphify + PlantUML (0105) 有`gradle, graphify, plantuml, knowledge-graph`. 关于 Knowledge Graph (0106) 的文章有 knowledge-graph, graphify, plantuml. 七个标签中有三个相同 — 43% 的重叠。

  1. 共现命名实体 : « pgvector », « RAG », « embedding

它们一起出现在四篇不同的文章中。« LangChain4j », Ollama », « Gradle 插件 » 在六个其他中。这些不是标签。 已声明 — 它们是语料库中只有NLP才能发现的*patterns émergents* 可以检测到。

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center

title 文章之间的关系有三层
package "层 1 — 显式引用" #CCFFCC {
    [Article A] as A1
    [Article B] as B1
    A1 -down-> B1 : xref:
    note right of A1
        Liens durs
        Déterministes
        Déjà dans graph.json
    end note
}

package "层 2 — 共享标签" #FFFFCC {
    [Article A] as A2
    [Article C] as C2
    A2 ..> C2 : tag_cooccurrence
    note right of A2
        Poids = tags communs / tags totaux
        Déterministes
        À extraire via graphify
    end note
}

package "第3层 — 新兴本体" #FFCCCC {
    [Corpus A] as A3
    [Corpus B] as B3
    A3 ..> B3 : entity_overlap
    note right of A3
        Clusters NLP
        Co-occurrences d'entités nommées
        TF-IDF / cosine similarity
    end note
}

@enduml

今天,我的网站没有使用 aucune 这三层中的任何一层。它使用 零层:在 Java 列表中的插入顺序。

解决方案:Gradle 流水线,而非热调用 LLM

第一个诱惑会是在 bake 时调用一个 LLM:« Pour 这篇文章,在语料库中找到三篇最相似的文章。 不要那样做。

每次一个LLM调用`./gradlew bake`花费时间、金钱,并 在你的构建中引入了非确定性。LLM 的响应可能 在两个构建之间切换而内容未改变。您的 CI 变得 不可重复,您的测试变得不稳定。

解决方案是确定的:一个预先计算图的 Gradle 管道 相似度并将其存储在`graph.json`. 模板读取结果 — 它永不触发计算。

架构:Bakery 导入 Graphify,而不是 Engine

这是最重要的建筑点。诱惑将是 在…​中布线合作`engine/build.gradle.kts`: 引擎 应用 graphify 用于扫描,bakery 用于烘焙,而 engine 在其之间搭建桥梁 两者

这是一种反模式。引擎是一个*消费终端*——它应用 对于插件,它不实现业务逻辑。规则是: 举证责任在于专有插件。

正确的架构 — Bakery 导入 Graphify
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #FFF3CD

title Bakery 导入 Graphify — Engine 一无所知
package "N3 — 发动机" #FFCCCC {
    [engine/build.gradle.kts] as ENG
    note right of ENG
        plugins { bakery }
        Zéro connaissance de graphify
        4 tâches, ~150 lignes
    end note
}

package "N2 — 面包店" #CCFFCC {
    [bakery-plugin] as BAK
    note right of BAK
        plugins { graphify } ← import interne
        Lit graph.json
        Résout articles connexes
        Expose le modèle à JBake
    end note
}

package "N0 — 图形化" #CCE5FF {
    [graphify-plugin] as GRF
    note right of GRF
        Scan workspace → graph.json
        Enrichi avec :
        - xref: edges
        - co-occurrences de tags
        - entités nommées (NLP)
    end note
}

ENG -down-> BAK : "插件 { bakery }"
BAK -down-> GRF : "插件 { graphify } ← N2→N0 确定"
@enduml

DAG 合约得到遵守:bakery (N2) 导入 graphify (N0), N2 > N0, 没有违规。Engine (N3) 导入 bakery (N2),N3 > N2,OK。

Engine 没有 aucune 行引用代码。graph.json, relatedPosts, 或任何其他相似性的概念。他应用了 bakery,就此而已。拉 协作在面包店内部。

流水线:扫描 → 图谱 → 模板

这是完整的三步流程:

  1. 扫描 (图形化) :`graphify-plugin`扫描工作区。在其

目前的形式,它已经能够检测到`xref:`之间`.adoc`和 展示在`graph.json`像一些边的类型`reference`.
我们对其进行丰富,以用于编辑内容: * 解析 JBake 元数据`:jbake-tags:`, :jbake-description:) 每个的`.adoc`博客的 * 计算标签的共现 → 边`tag_cooccurrence`带重量 * 通过TF-IDF从描述中提取命名实体 → edges`entity_overlap` * 注入一个章节`blog_articles`在该`graph.json`存在的 保持所有反引号代码跨度(…​)原样不动 — — 不要修改反引号内容、间距或位置。 此文本可能是较大句子的片段 — — 请在不请求更多上下文的情况下翻译该片段。 仅输出翻译后的文本 — — 不要包含解释、注释、介绍、替代方案或选项。

+

// Extrait de l'enrichissement graphify pour le blog
fun enrichBlogSection(graphJson: File, blogDir: File): GraphJson {
    val articles = blogDir.listFiles { f -> f.extension == "adoc" }
        .map { parseJbakeMetadata(it) }

    val nodes = articles.map { ArticleNode(it.slug, it.title, it.tags) }
    val edges = mutableListOf<GraphEdge>()

    // Couche 1 : xref (déjà fait par scanWorkspace)

    // Couche 2 : co-occurrences de tags
    for (a in articles) {
        for (b in articles) {
            if (a.slug == b.slug) continue
            val common = a.tags.intersect(b.tags)
            if (common.isNotEmpty()) {
                edges.add(GraphEdge(
                    source = a.slug,
                    target = b.slug,
                    type = "tag_cooccurrence",
                    weight = common.size.toDouble() / (a.tags.size + b.tags.size)
                ))
            }
        }
    }

    return graphJson.copy(
        blogArticles = BlogSection(nodes, edges)
    )
}
  1. Bake (bakery) : 此时的`./gradlew bake`, `BakeryPlugin`床

`graph.json`并解决每个帖子的相关文章。

// BakeryPlugin — résolution des articles connexes
fun resolveRelatedPosts(
    currentSlug: String,
    graph: GraphJson,
    maxResults: Int = 4
): List<RelatedPost> {
    val edges = graph.blogArticles.edges
        .filter { it.source == currentSlug || it.target == currentSlug }

    return edges
        .sortedByDescending { it.weight }
        .take(maxResults)
        .map { edge ->
            val relatedSlug = if (edge.source == currentSlug) edge.target else edge.source
            graph.blogArticles.nodes.first { it.slug == relatedSlug }
        }
}
  1. 模板post.thyme) : JBake 模型现在接收一个映射

结构化的,而不是一个平的时间顺序列表。

<!-- Articles connexes basés sur le Knowledge Graph -->
<th:block th:if="${relatedPosts != null and !relatedPosts.empty}">
    <section class="mt-5 pt-4 border-top">
        <h2 class="h4 mb-3">Articles connexes</h2>
        <th:block th:each="related : ${relatedPosts}">
            <div class="mb-2">
                <a th:href="${content.rootpath} + ${related.uri}"
                   th:text="${related.title}" class="fw-semibold"></a>
                <br/>
                <small class="text-muted">
                    <th:block th:each="reason,iterStat : ${related.reasons}">
                        <span class="badge bg-light text-dark"
                              th:text="${reason}"></span>
                    </th:block>
                </small>
            </div>
        </th:block>
    </section>
</th:block>

模板是一样的——它不知道 数据来自哪里。 只有 bakery 和 JBake 之间的合同发生了变化:published_posts(列表 chronological) 变成`relatedPosts`(图加权映射).

徽章“理由”(例如:xref 0122, tag:gradle, cluster:pgvector-rag) 向读者解释*为什么*这篇文章是相关的。这不是 只是透明度 — 这是关于拓扑的教学 您自己的内容。 (Empty)

后备 chronological

Si `graph.json`不存在(本地构建无预扫描,第一 部署,CI 尚未集成 graphify 扫描),模板 必须优雅降级:

fun resolveRelatedPosts(currentSlug: String, graph: GraphJson?): List<RelatedPost> {
    if (graph != null && graph.blogArticles != null) {
        return resolveFromGraph(currentSlug, graph)
    }
    // Fallback chronologique — même comportement qu'aujourd'hui
    logger.warn("[bakery] graph.json absent — fallback chronologique")
    return resolveFromChronology(currentSlug)
}

默认行为与当前行为相同。该 推荐引擎是一种*渐进式改进* —— 不是一 破坏性更改。

新兴本体论:当文章在互不认识的情况下聚集

最有趣的一层是第三层:新兴的本体论。 不被引用的文章,标签也不相同的,但 谁在不知情的情况下谈论同一件事

让我们举一个具体的例子。我的语料库中的三篇文章:

  1. 0119 — 基准测试 DGX Spark 与云订阅 LLM

  2. 0121 — Gradle插件 控制两个Ollama Pro实例

  3. 0122 — 效率比率 27倍 45倍 舰队 AI 专家

这三篇文章之间没有 aucun xref。 它们的标签没有 se 仅重叠到 20%('ollama'、'llm' 常见)。但一个轻量级的 NLP 在描述中显现出一个明显的簇:"成本","订阅", 云 », « GPU », « API 密钥 », « Ollama Pro », « 效率 », « 比率

它们形成一个本体论聚类: LLM自托管经济 vs cloud. 此集群在任何地方都未被声明。 它从语料库中出现。

@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #E8F8F5

title Cluster Ontologique Émergent — "LLM自托管经济"

package "检测到的聚类:经济-llm" #D5F5E3 {
    [0119 — Benchmark DGX Spark\nvs Cloud Abonnement] as A
    [0121 — Plugin Gradle Piloter\nDeux Instances Ollama Pro] as B
    [0122 — Ratio Efficacité\n27x 45x Flotte Experts] as C
}

A .. B : entity_overlap (0.73)
B .. C : entity_overlap (0.81)
A .. C : entity_overlap (0.68)

note bottom of A
    Aucun xref entre ces articles.
    Tags communs : "ollama", "llm".
    Cluster découvert par NLP :
    co-occurrences "成本", "GPU",
    "订阅", "Ollama Pro".
end note
@enduml

这个本体聚类成为一个复合边在`graph.json`</think>

{
  "source": "0119-benchmark-dgx-spark",
  "target": "cluster:economie-llm",
  "type": "entity_cluster",
  "weight": 0.73,
  "metadata": {
    "clusterLabel": "Économie LLM Self-Hosted vs Cloud",
    "commonEntities": ["coût", "GPU", "abonnement", "Ollama Pro", "ratio"],
    "articlesInCluster": [
      "0119-benchmark-dgx-spark",
      "0121-ollama-pro-deux-instances",
      "0122-ratio-efficacite-flotte-experts"
    ]
  }
}

NLP 故意保持轻量 — TF-IDF + 余弦相似度 在 描述。不需要 BERT,也不需要语言模型。 语料库包含23篇文章,相似度矩阵可以容纳于 50KB的JSON文件。

自然语言处理的力量并不来自于算法的复杂性, 但是*语料库的大小*和*描述的质量*。一个 `:jbake-description:`写得好的150个字符包含更多的 为 TF-IDF 发出信号,表示这是一篇完整的 3000 字文章。

DAG合同:谁对谁重要

这是建筑学科见效的点。 DAG N0→N3 定义在`engine/build.gradle.kts`给出一个简单的规则: 没有任何项目对一个更高级别的项目而言是重要的

消费者插件 已导入插件 消费者级别 导入的级别 有效?

bakery-gradle

graphify-gradle

N2

N0

✅ N2 > N0 — OK

引擎

bakery-gradle

N3

N2

✅ N3 > N2 — OK

引擎

graphify-gradle

N3

N0

✅ 技术 OK,但 概念上错误 — 合作是在 bakery 内部的

引擎应用面包店。面包店应用图形化。这就是全部。如果有一天 我想要添加向量存储代码库 (N1) 用于研究。 跨语料语义,面包房也导入它。引擎不变。

模式是:插件 N2 是其自身依赖的中心。 Engine N3 不是一个 hub — 它是一个应用 hubs 的终端。

我们得到的(以及我们没有得到的)

主要收益是编辑方面的,而不是技术方面的:

之前 之后

相关文章 = 4 篇最新帖子

相关文章 = 知识图谱中权重最高的前 4

读取器读取 RAG → 推荐 Kitty Terminal

读取器读取 RAG → 推荐 pgvector, 分块, 向量存储

对原因完全没有透明度

徽章`xref 0122`, tag:gradle, `cluster:rag`可见的

JBake标准,零努力,零价值

确定性、可重复、可测试的 Gradle 流水线

对读者没有学习曲线

读者发现了我的内容的 拓扑

我们*不*得到的:

  • 这不是一个“真实”的推荐引擎(没有协作

过滤,没有 A/B 测试,没有反馈循环)

  • 质量取决于JBake元数据的丰富程度 — 如果您的

描述为空,TF-IDF 没有看到任何内容

  • NLP 处于离线状态 — 新文章仅被聚类到

下一次扫描(很好:构建仍然保持确定性)

视角:博客作为公共知识图谱

此功能打开了一个更广阔的视角:假如`cheroliv.com` 它自己是否变成了一个*可导航的知识图谱*?

文章是节点。标签是社区。xref 是 边缘。读者不再阅读孤立的文章——他正在一个 知识图谱 其中当前条目是入口点。

想象一个主页,它显示的不是按时间顺序的列表。 最新的帖子,但是一个 语料库地图 : 本体聚类, 枢纽文章(边最多的),阅读路径 推荐的 (« 如果你喜欢 Graphify 的文章,请接着读 它在Knowledge Graph上作为理解工具"".

这不再是一个博客。这是一个 语义图谱。

这不是科幻。 这`graph.json`已经存在。 它仅缺少导航界面。

结论:文件系统知道模板所忽略的内容

我周二晚上设计的,是一种启发式的替换。 懒惰的(按时间顺序)通过对…​的忠实再现 我的语料库结构(知识图谱)。

模板`post.thyme`不变。变化的是我们 给他喂食。前:一个按…​排序的Java列表`date`. 之后: 一个来自三层分析的加权图 — xrefs, 共现 标签的,以及通过NLP产生的新兴本体。

循环已闭合。graphify 扫描工作区。Bakery 读取 图形并填充模板。读者看到推荐。 相关。而且引擎 — 指挥家 — 甚至不知道 这一切都存在。

这就是良好的架构。每个插件只做一件事。 消费者终端不需要知道怎么做。

相关文章