thời gian đọc: 14 minutes

Bạn đang đọc một bài viết về việc tích hợp pgvector với LangChain4j. Ở cuối trang, JBake gợi ý cho bạn « Articles connexes ». Bạn nhấp vào. Đó là một bài viết về…​ cấu hình của Kitty Terminal. Điểm chung duy nhất giữa hai cái là gì? Chúng được phát hành cách nhau chưa đầy cách nhau mười lăm ngày. Đây không phải là một khuyến nghị. Đó là một lịch trình được ngụy trang dưới dạng biên tập.

Khung cảnh: Thứ Ba ngày 12 tháng 5, 17h30

Tôi đang đọc lại một trong những bài viết của mình — bài về Knowledge Graph như một công cụ để hiểu codebase. Nội dung rất dày đặc: nœuds, arêtes, communautés, PlantUML, onboarding. Một bài viết kỹ thuật đọc 15 phút giúp tập trung`graphify-gradle`, plantuml-gradle, và các khái niệm về tô pô đồ thị.

Ở cuối trang, mục « Articles connexes » đề xuất cho tôi:

  1. Một bài viết về Firebase Contact Form (0113)

  2. Một bài viết về cơ chế Eager/Lazy (0108)

  3. Một bài viết về việc di chuyển từ Gradle script → plugin (0102)

  4. Một bài viết về việc cộng dồn các gói đăng ký Ollama Pro (0120)

Ba trong bốn bài viết này không hề liên quan đến Knowledge Graph. Chúng ở đây vì chúng là những mục được xuất bản cuối cùng — sự gần gũi về thời gian, không phải vùng lân cận ngữ nghĩa.

Tôi đang xem template`post.thyme`Vui lòng cung cấp văn bản cần dịch.

<!-- 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`là một danh sách theo thứ tự thời gian.`postStat.index lt 4`lấy các bốn bài đầu tiên không phải là bài hiện tại. Thế thôi. Không có logic nào cả độ tương đồng. Không có khái niệm về nội dung. Chỉ là một vòng lặp`for`cải trang thành khuyến nghị.

Đây không phải là lỗi của JBake. Đó là hành vi mặc định của tất cả trình tạo trang tĩnh: danh sách các bài đăng ở dạng phẳng và được sắp xếp theo ngày. Template làm những gì nó có thể với những gì nó có.

Nhưng đó không phải là lý do để chấp nhận nó.

Chẩn đoán: Ba Loại Quan Hệ Mà Không Template Nào Nhìn Thấy

Bộ sưu tập các bài viết của tôi — 23 bài trong 4 tháng — có một cấu trúc phong phú mà mẫu chronologique ghi đè hoàn toàn:

  1. Tham chiếu tường minh: tôi sử dụng`xref:`nhiều trong các bài viết của tôi.

Điều 0122 tham chiếu đến 0106 (knowledge graph) và 0116 (compartimentage tri thức). Những liên kết này là hard linking biên tập — tôi đã cố tình quyết định kết nối các khái niệm này.

  1. Thẻ chia sẻ: mỗi bài viết có các`:jbake-tags:`. Bài viết về

Graphify + PlantUML (0105) a`gradle, graphify, plantuml, knowledge-graph`Vui lòng cung cấp nội dung cần dịch. Bài viết về Knowledge Graph (0106) có knowledge-graph, graphify, plantuml. Ba thẻ chung trên bảy — 43% mức độ trùng lặp.

  1. Các thực thể định danh cùng xuất hiện: « pgvector », « RAG », « embedding

xuất hiện cùng nhau trong bốn bài viết khác nhau. « LangChain4j », Ollama », « plugin Gradle » trong sáu mục khác. Đây không phải là các thẻ tag được tuyên bố — đó là những pattern émergents của corpus mà chỉ một NLP có thể phát hiện.

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center

title Ba lớp quan hệ giữa các bài viết
package "Lớp 1 — Tham chiếu tường minh" #CCFFCC {
    [Article A] as A1
    [Article B] as B1
    A1 -down-> B1 : xref:
    note right of A1
Liên kết cứng
Định mệnh
Đã có trong graph.json
    end note
}

package "Lớp 2 — Thẻ chia sẻ" #FFFFCC {
    [Article A] as A2
    [Article C] as C2
    A2 ..> C2 : tag_cooccurrence
    note right of A2
Trọng số = tag chung / tổng số tag
Định mệnh
Trích xuất thông qua graphify
    end note
}

package "Lớp 3 — Hệ thống bản thể học mới nổi" #FFCCCC {
    [Corpus A] as A3
    [Corpus B] as B3
    A3 ..> B3 : entity_overlap
    note right of A3
Clusters NLP
Đồng xuất hiện của các thực thể có tên
TF-IDF / cosine similarity
    end note
}

@enduml

Hôm nay, trang web của tôi không sử dụng bất kỳ lớp nào trong ba lớp này. Nó sử dụng lớp zero: thứ tự chèn trong một danh sách Java.

Giải pháp: Một Gradle Pipeline, không phải một lời gọi LLM trực tiếp

Cám dỗ đầu tiên sẽ là gọi một LLM tại thời điểm bake: « Pour bài viết này, hãy tìm ba bài viết tương tự nhất trong kho dữ liệu. Đừng làm vậy.

Một cuộc gọi LLM cho mỗi`./gradlew bake`tốn thời gian, tiền bạc, và giới thiệu tính không định hướng (non-determinism) vào bản build của bạn. Câu trả lời của LLM có thể thay đổi giữa hai bản build mà nội dung không thay đổi. CI của bạn trở thành không thể tái lập, các bài kiểm tra của bạn trở nên flaky.

Giải pháp này mang tính xác định: một pipeline Gradle tính toán trước đồ thị độ tương đồng và lưu trữ nó trong`graph.json`. Template đọc kết quả — nó không bao giờ kích hoạt một phép tính.

Kiến trúc: Bakery nhập Graphify, không phải Engine

Đây là điểm kiến trúc quan trọng nhất. Sự cám dỗ sẽ là thiết lập kết nối cộng tác trong`engine/build.gradle.kts`: công cụ áp dụng graphify cho việc scan, bakery cho việc bake, và engine đóng vai trò cầu nối giữa cả hai.

Đây là một anti-pattern. Engine là một terminal tiêu thụ — nó áp dụng các plugin, nó không triển khai logic nghiệp vụ. Quy tắc là: nghĩa vụ chứng minh thuộc về plugin độc quyền.

Kiến trúc chính xác — Bakery nhập Graphify

@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #FFF3CD

title Bakery nhập Graphify — Engine không biết gì
package "N3 — ENGINE" #FFCCCC {
    [engine/build.gradle.kts] as ENG
    note right of ENG
plugins { bakery }
Không có kiến thức về graphify
4 tác vụ, ~150 dòng
    end note
}

package "N2 — BAKERY" #CCFFCC {
    [bakery-plugin] as BAK
    note right of BAK
plugins { graphify } ← import nội bộ
Đọc graph.json
Giải quyết các bài viết liên quan
Công khai mô hình cho JBake
    end note
}

package "N0 — GRAPHIFY" #CCE5FF {
    [graphify-plugin] as GRF
    note right of GRF
Quét không gian làm việc → graph.json
Được làm giàu với:
- xref: edges
- sự đồng xuất hiện của các thẻ
- thực thể có tên (NLP)
    end note
}

ENG -down-> BAK : "plugins { bakery }"
BAK -down-> GRF : "plugins { graphify } ← N2→N0 OK"
@enduml

Hợp đồng DAG được tuân thủ: bakery (N2) import graphify (N0), N2 > N0, không có vi phạm. Engine (N3) import bakery (N2), N3 > N2, OK.

Engine không có dòng mã nào tham chiếu đến`graph.json`, relatedPosts, hoặc bất kỳ khái niệm tương đồng nào khác. Nó áp dụng bakery, điểm. La sự hợp tác là nội bộ trong bakery.

Pipeline: Scan → Graphe → Template

Đây là toàn bộ pipeline ba bước:

  1. Quét (graphify) :`graphify-plugin`quét workspace. Trong sa

hình thức hiện tại, nó đã phát hiện các`xref:`giữa`.adoc`và các trình bày trong`graph.json`như các cạnh kiểu`reference`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.
Chúng ta làm phong phú nó cho nội dung biên tập: Phân tích các siêu dữ liệu JBake (:jbake-tags:, :jbake-description:) của mỗi`.adoc`của blog * Tính toán các đồng xuất hiện của tag → edges`tag_cooccurrence`với trọng số * Trích xuất các thực thể có tên từ mô tả thông qua TF-IDF → edges`entity_overlap` * Chèn một phần`blog_articles`trong cái`graph.json`hiện có

// 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 (tiệm bánh) : vào thời điểm`./gradlew bake`, `BakeryPlugin`lit

`graph.json`và giải quyết các bài viết liên quan cho mỗi bài đăng.

// 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. Mẫu (post.thyme) : mô hình JBake hiện giờ nhận một map

được cấu trúc thay vì một danh sách trình tự thời gian phẳng.

<!-- 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>

Template là tương tự — nó không biết dữ liệu đến từ đâu. Chỉ có hợp đồng giữa bakery và JBake là thay đổi:`published_posts`(danh sách (theo trình tự thời gian) trở thành`relatedPosts`(bản đồ được trọng số bởi đồ thị).

Huy hiệu « lý do » (ví dụ:`xref 0122`, tag:gradle, cluster:pgvector-rag) giải thích cho người đọc tại sao bài viết này lại có liên quan. Đây không phải là chỉ là sự minh bạch — đó là sự hướng dẫn về cấu trúc topo của nội dung của riêng bạn. Please provide the text you would like me to translate.

Dự phòng theo trình tự thời gian

Si `graph.json`bị thiếu (build local mà không scan trước, lần đầu triển khai, CI chưa tích hợp quét graphify), template phải hạ cấp mượt mà:

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)
}

Hành vi mặc định giống với hành vi hiện tại. Le công cụ gợi ý là một sự cải tiến dần dần — không phải một thay đổi gây phá vỡ.

Hệ Thống Bản Thể Luận Mới Nổi: Khi Các Bài Viết Tự Nhóm Lại Mà Không Cần Biết Nhau

Lớp thú vị nhất là lớp thứ ba: ontology mới nổi. Những bài viết không trích dẫn lẫn nhau, không có cùng các thẻ tag, nhưng mà nói về cùng một điều mà không hề biết.

Hãy lấy một ví dụ cụ thể. Ba bài viết trong tập hợp dữ liệu của tôi:

  1. 0119 — Benchmark DGX Spark với Gói đăng ký Cloud LLM

  2. 0121 — Plugin Gradle Điều khiển Hai Instance Ollama Pro

  3. 0122 — Tỷ lệ Hiệu quả 27x 45x Đội ngũ Chuyên gia AI

Ba bài viết này không có xref nào giữa chúng. Các thẻ của chúng không chỉ giao thoa khoảng 20% (« ollama », « llm » chung). Nhưng một NLP nhẹ về các mô tả cho thấy một cụm rõ rệt: « coût », « abonnement », cloud », « GPU », « API key », « Ollama Pro », « hiệu quả », « tỷ lệ ».

Chúng tạo thành một cụm bản thể học: nền kinh tế của LLM self-hosted đối với cloud. Cluster này không được khai báo ở bất cứ đâu. Nó xuất hiện từ corpus.

@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #E8F8F5

title Cluster Ontologique Émergent — "Kinh tế LLM Self-Hosted"

package "Cụm đã được phát hiện : Kinh tế-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 "chi phí", "GPU",
    "gói đăng ký", "Ollama Pro".
end note
@enduml

Cụm bản thể luận này trở thành một edge composite trong`graph.json`Vui lòng cung cấp văn bản cần dịch.

{
  "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 được thiết kế tối giản — TF-IDF + cosine similarity trên các mô tả. Không cần BERT, không cần mô hình ngôn ngữ. Bộ ngữ liệu gồm 23 bài viết, ma trận tương đồng nằm gọn trong một tệp JSON 50 Ko.

Sức mạnh của NLP không đến từ sự tinh vi của thuật toán, mà là về kích thước của corpus và chất lượng của các mô tả. Một `:jbake-description:`được viết tốt dài 150 ký tự sẽ chứa nhiều hơn tín hiệu cho TF-IDF rằng một bài viết toàn bộ dài 3000 từ.

Hợp đồng DAG: Ai nhập ai

Đây là điểm mà kỷ luật kiến trúc phát huy tác dụng. DAG N0→N3 được định nghĩa trong`engine/build.gradle.kts`đưa ra một quy tắc đơn giản: không có dự án nào nhập một dự án cấp cao hơn.

Plugin tiêu thụ Plugin đã được nhập Cấp độ người tiêu dùng Cấp độ đã nhập Hợp lệ ?

bakery-gradle

graphify-gradle

N2

N0

✅ N2 > N0 — OK

công cụ

bakery-gradle

N3

N2

✅ N3 > N2 — OK

công cụ

graphify-gradle

N3

N0

✅ Kỹ thuật OK, nhưng sai về mặt khái niệm — sự cộng tác là nội bộ trong `bakery

Engine applique bakery. Bakery applique graphify. Thế thôi. Nếu một ngày nào đó tôi muốn thêm vector store codebase (N1) để tìm kiếm ngữ nghĩa cross-corpus, bakery cũng nhập nó. Engine không thay đổi.

Pattern là: plugin N2 là hub cho các phụ thuộc của chính nó. Engine N3 không phải là một hub — nó là một terminal thực thi các hub.

Những Gì Chúng Ta Đạt Được (Và Những Gì Chúng Ta Không Đạt Được)

Lợi ích chính là về mặt biên tập, không phải về kỹ thuật:

Trước Sau

Bài viết liên quan = 4 bài đăng mới nhất

Các bài viết liên quan = top 4 theo trọng số trong Knowledge Graph

Trình đọc đọc RAG → đề xuất Kitty Terminal

Người đọc tìm hiểu về RAG → khuyến nghị pgvector, chunking, vector store

Không có chút minh bạch nào về lý do tại sao

Huy hiệu`xref 0122`, tag:gradle, `cluster:rag`hiển thị

Chuẩn JBake, không nỗ lực, không giá trị

Pipeline Gradle mang tính xác định, có thể tái lập và có thể kiểm thử

Không có đường cong học tập cho người đọc

Người đọc khám phá ra cấu trúc nội dung của tôi

Những gì chúng ta không nhận được:

  • Đây không phải là một công cụ gợi ý "thực thụ" (không có lọc cộng tác

lọc, không có A/B testing, không có feedback loop)

  • Chất lượng phụ thuộc vào độ phong phú của siêu dữ liệu JBake — nếu các

mô tả trống, TF-IDF không thấy gì

  • NLP đang ngoại tuyến — các bài viết mới chỉ được phân cụm vào lúc

lần quét tiếp theo (đúng vậy: build vẫn đảm bảo tính deterministic)

Góc nhìn: Blog như một Knowledge Graph Công khai

Tính năng này mở ra một triển vọng rộng lớn hơn: điều gì sẽ xảy ra nếu`cheroliv.com` trở thành một knowledge graph navigable?

Các bài viết là các nút. Các thẻ là các cộng đồng. Các xref là các cạnh. Người đọc không còn đọc một bài viết riêng lẻ — họ đang điều hướng trong một đồ thị tri thức mà bài viết hiện tại là điểm nhập.

Hãy tưởng tượng một trang chủ hiển thị không phải là danh sách theo trình tự thời gian các bài đăng gần đây, mà là một bản đồ kho ngữ liệu: các cụm bản thể học, các bài viết trụ cột (những bài có nhiều cạnh nhất), đường dẫn đọc được đề xuất (« Nếu bạn thích bài viết về Graphify, hãy đọc tiếp cái về Knowledge Graph như một công cụ thấu hiểu »).

Đây không còn là một blog nữa. Đó là một atlas ngữ nghĩa.

Và đây không phải là khoa học viễn tưởng. Le`graph.json`đã tồn tại. Nó chỉ còn thiếu giao diện điều hướng.

Kết luận: Hệ thống Tệp biết những gì Template không biết

Điều tôi đã thiết kế vào tối thứ Ba này là sự thay thế cho một heuristic lười biếng (thứ tự thời gian) bằng một sự mô phỏng trung thực của cấu trúc corpus của tôi (Knowledge Graph).

Mẫu template`post.thyme`không thay đổi. Điều thay đổi là những gì chúng ta cho nó ăn. Trước đó: một danh sách Java được sắp xếp theo`date`. Sau đó: một đồ thị có trọng số được tạo ra từ ba lớp phân tích — xrefs, `co-occurrences các thẻ tag, và ontology mới nổi bằng NLP.

Vòng lặp đã khép kín. graphify quét workspace. Bakery đọc đồ thị và cung cấp dữ liệu cho template. Người đọc nhìn thấy các đề xuất phù hợp. Và engine — nhạc trưởng — thậm chí không biết rằng tất cả những điều đó đều tồn tại.

Đó chính là kiến trúc đúng đắn. Mỗi plugin thực hiện một việc. Và thiết bị đầu cuối của người tiêu dùng không cần phải biết làm thế nào.

Bài viết liên quan