知识图谱作为推荐引擎:我是如何杀死按时间顺序的「相关文章」的
发布于 12 May 2026
您正在阅读有关将pgvector与LangChain4j集成的文章。页面底部,JBake 它建议您查看“相关文章”。您点击。这是一篇关于…配置的文章。 来自 Kitty Terminal。它们之间唯一的共同点是什么?它们是在不到…发布的 相隔十五天。这不是建议。这是伪装成日历的 社论。
场景:星期二 5月12日,17:30
我正在重读我的一篇文章 — 那个关于 Knowledge Graph 作为代码库理解工具的 内容密集:节点、边、社区、PlantUML、入职。一篇文章 15分钟阅读技巧,能够调动`graphify-gradle`, plantuml-gradle, 以及图的拓扑概念。
页面底部,相关文章向我推荐:
-
关于 Firebase 联系表单 (0113) 的文章
-
关于 Eager/Lazy 机制的文章 (0108)
-
关于 Gradle 脚本 → 插件迁移的文章 (0102)
-
关于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个月 — 具有丰富的结构 时间顺序模板完全覆盖:
-
显式引用 : 我使用`xref:`在我的文章中大量使用。
文章 0122 引用 0106(knowledge graph)和 0116(compartimentage) 认识论的)。这些链接是编辑性的 hard linking — 我有 故意决定连接这些概念。
-
共享标签 : 每篇文章都有`:jbake-tags:`. 文章关于
Graphify + PlantUML (0105) 有`gradle, graphify, plantuml, knowledge-graph`. 关于 Knowledge Graph (0106) 的文章有 knowledge-graph, graphify, plantuml. 七个标签中有三个相同 — 43% 的重叠。
-
共现命名实体 : « 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 在其之间搭建桥梁 两者
这是一种反模式。引擎是一个*消费终端*——它应用 对于插件,它不实现业务逻辑。规则是: 举证责任在于专有插件。
@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,就此而已。拉 协作在面包店内部。
流水线:扫描 → 图谱 → 模板
这是完整的三步流程:
-
扫描 (图形化) :`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)
)
}
-
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 }
}
}
-
模板
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`(图加权映射).
|
徽章“理由”(例如: |
后备 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)
}
默认行为与当前行为相同。该 推荐引擎是一种*渐进式改进* —— 不是一 破坏性更改。
新兴本体论:当文章在互不认识的情况下聚集
最有趣的一层是第三层:新兴的本体论。 不被引用的文章,标签也不相同的,但 谁在不知情的情况下谈论同一件事
让我们举一个具体的例子。我的语料库中的三篇文章:
-
0119 — 基准测试 DGX Spark 与云订阅 LLM
-
0121 — Gradle插件 控制两个Ollama Pro实例
-
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`, |
JBake标准,零努力,零价值 |
确定性、可重复、可测试的 Gradle 流水线 |
对读者没有学习曲线 |
读者发现了我的内容的 拓扑 |
我们*不*得到的:
-
这不是一个“真实”的推荐引擎(没有协作
过滤,没有 A/B 测试,没有反馈循环)
-
质量取决于JBake元数据的丰富程度 — 如果您的
描述为空,TF-IDF 没有看到任何内容
-
NLP 处于离线状态 — 新文章仅被聚类到
下一次扫描(很好:构建仍然保持确定性)
视角:博客作为公共知识图谱
此功能打开了一个更广阔的视角:假如`cheroliv.com` 它自己是否变成了一个*可导航的知识图谱*?
文章是节点。标签是社区。xref 是 边缘。读者不再阅读孤立的文章——他正在一个 知识图谱 其中当前条目是入口点。
想象一个主页,它显示的不是按时间顺序的列表。 最新的帖子,但是一个 语料库地图 : 本体聚类, 枢纽文章(边最多的),阅读路径 推荐的 (« 如果你喜欢 Graphify 的文章,请接着读 它在Knowledge Graph上作为理解工具"".
这不再是一个博客。这是一个 语义图谱。
这不是科幻。 这`graph.json`已经存在。 它仅缺少导航界面。
结论:文件系统知道模板所忽略的内容
我周二晚上设计的,是一种启发式的替换。 懒惰的(按时间顺序)通过对…的忠实再现 我的语料库结构(知识图谱)。
模板`post.thyme`不变。变化的是我们 给他喂食。前:一个按…排序的Java列表`date`. 之后: 一个来自三层分析的加权图 — xrefs, 共现 标签的,以及通过NLP产生的新兴本体。
循环已闭合。graphify 扫描工作区。Bakery 读取 图形并填充模板。读者看到推荐。 相关。而且引擎 — 指挥家 — 甚至不知道 这一切都存在。
这就是良好的架构。每个插件只做一件事。 消费者终端不需要知道怎么做。
参考文献
-
文章 0105 — 将 Graphify 集成到 Gradle 工作流程
相关文章
31 May 2026
14 May 2026