Building a Lightweight Knowledge Index for Blogs (Part 6): AI Summarization and Editable Semantic Layer Design
Article Summary
针对博客摘要生成的痛点,文章提出基于大语言模型的离线预计算方案。通过构建summary-source.json统一数据源,提取文章结构信息并控制输入长度,再调用本地Qwen3模型生成结构化摘要存入summary-index.json。该设计实现摘要内容与WordPress解耦,既保证AI生成的准确性,又保留可编辑性,使摘要成为可复用的语义数据层。最终在WordPress前端以独立卡片形式展示,提升长文阅读转化率,为后续语义搜索和知识图谱构建奠定基础。
Qwen3-14B · 2026-06-24

1. Why add a summary to your blog?

After the first five articles in this series, I have implemented the "Series Article Cards," "Related Articles," and "You Might Want" features after the main body of the blog posts. Combined with several "maps" and "content structure hints" for each article, my blog content has actually established connections both vertically and horizontally: vertically, it's the structural link from the knowledge map to specific articles, and horizontally, it's the thematic connection between articles.

This means readers can not only browse related articles along thematic threads, but also quickly locate a specific article using the overall map, or return to the overall map from a specific article. In this way, readers can freely switch between the overall knowledge framework and the content of individual articles, gaining a clearer understanding of the blog's thematic structure and content flow.

However, there is still one shortcoming – each article still lacks a separate "abstract" section.

Actually, I had considered this issue before, but I initially thought that the title, table of contents, and the content of the first chapter were enough for readers to understand the main points of the article, and could be directly replaced by the abstract.

However, with the increase in the number and length of blog posts (especially since I write quite a few long articles), I worry that many impatient visitors might simply give up reading if they see an article that is too long. For these readers, if the titles and table of contents are not intuitive enough, making it difficult to quickly determine whether a long article is worth reading in depth, it can easily lead to reader loss—especially in this era of short videos where people swipe away if they are not interested.

Therefore, I believe it is necessary to introduce a separate content layer called "Summary" into the blog system: on the one hand, it can help readers quickly understand the main idea of the article and thus decide whether to continue reading in depth; on the other hand, it also provides a foundation for the semantic system within the blog, enabling the core information of each article to exist in a structured, editable, and reconstructable manner.

Having clarified the importance of summaries for reader experience and blog systems, the next question is: how do we truly implement summaries? Should they be entirely handwritten? Or automatically generated using readily available plugins leveraging cloud-based large language models? Or perhaps semi-automatically generated using locally built scripts with custom large language models? And how should the generated summaries be stored to ensure they are editable, reconstructable, and easily usable in subsequent applications such as knowledge maps, recommendation systems, or search functions?

To address these issues, Chapter 2 will start with existing implementation methods and outline two main paths for summary generation: traditional rule-based or statistical summarization techniques, and generative summarization schemes based on large language models that have gradually become mainstream in the last two years. By comparing the basic principles and applicable scenarios of these two approaches, a holistic understanding of "how summaries are generated" will be established first.

2. Existing blog summary solutions

Before we begin implementation, let’s take a look at some common summarization solutions in the blogosphere.

Early blog summaries were very simple: they were written manually by the author. Many blogging systems provided dedicated summary fields where authors could fill in a concise description of the article's content. The advantage of this was obvious: the summary content was entirely under the author's control, and therefore usually the most accurate. However, the disadvantage was equally clear—as the number of articles increased, the maintenance cost became increasingly high. For a blog that had accumulated hundreds or even thousands of articles, writing a separate summary for each article was clearly not an easy task.

With the development of blogging systems, automatic summarization has gradually become popular. For example, WordPress's built-in Excerpt feature can automatically extract a portion of text from the article content as a summary. Some plugins further enhance this process, such as extracting the first paragraph, truncating a fixed number of words, or using rule-based algorithms to filter out a few key sentences.

Strictly speaking, this type of solution is more like "content truncation" than "content understanding." While it can solve automation problems, it struggles to truly capture the core ideas of an article. For lengthy technical articles with multiple logical layers, the summaries generated in this way often only reflect the beginning and may not accurately capture the main points of the entire article.

In the last two or three years, with the development of large language models, AI summarization has gradually become a new mainstream solution. The biggest difference between AI and traditional summarization is that AI no longer simply extracts parts of an article; instead, it understands the entire article and then reorganizes the language to generate a new summary. Therefore, it typically achieves better results in terms of both readability and content summarization ability.

For technical blogs like mine, articles are generally long, and much of the content has strong dependencies between parts. Manually maintaining summaries is too costly, while traditional automatic summarization struggles to accurately capture the main idea. Therefore, AI summarization solutions based on large language models naturally become the most worthwhile direction to explore. However, even among AI summarization solutions, there are significant differences between different implementations.

Most blog AI summarization features currently on the market exist as plugins. Users only need to configure the API key, and the plugin handles the rest automatically. The biggest advantage of this approach is its simplicity and convenience, but at the same time, the summarization process is often encapsulated within the plugin. Users typically have little insight into how the article is processed, what content the model actually sees, what prompts are used, and how the final result is stored.

More importantly, these plugins are often designed simply to generate a summary for readers, focusing primarily on "quickly summarizing the article's content." For me, this isn't sufficient. Many of my articles are quite long, with strong logical connections between sections. If the article is truncated before being fed into the model, or if the summary generation strategy doesn't match the article type, the final result may deviate from the article's true message.

Meanwhile, for blogs building "lightweight knowledge indexes," summaries are more than just text displayed to readers. In my view, they are also a reusable data resource. Future semantic search and other AI functions may rely on this summary data. Therefore, rather than simply obtaining a summary result, I'm more concerned with how the summary is generated, whether it can be easily modified after generation, what data was used in the generation process, whether the final result meets my requirements, and whether it can be saved in the format I need.

Therefore, rather than choosing a ready-made black-box solution, I prefer to start from the entire generation process and build a fully controllable summarization system.

3. Design Ideas for the Blog Summary System

3.1 Overall Architecture Design

After deciding to add an article summary feature to the blog, I didn't rush to figure out how to write the prompts, nor did I focus on the large model itself from the beginning. Because for the entire system, the large model is just one part of it.

The real priority is ensuring that summary generation becomes a sustainable, long-term maintainable system. Generating summaries for a single article is quite simple: send the article content to a large model and save the returned results.

But when the target becomes the entire blog, the situation is completely different, for example:

  • The blog has accumulated hundreds of articles;
  • More content will continue to be added.
  • Abstract generation requires certain computing resources;
  • The model may be changed or the prompt may be adjusted in the future;
  • The WordPress front end needs to read data quickly, rather than calling AI in real time.

These factors dictate that summary generation must employ an offline pre-computation mode, rather than a real-time generation mode.

Therefore, I ultimately adopted a design approach similar to the previous "lightweight knowledge index":

WordPress original content ↓ summary-source.json ↓ summary-index.json

The entire process is divided into two stages: the first stage is responsible for extracting the data needed to generate the summary from WordPress and organizing it into a unified format. summary-source.jsonThe second stage involves using this data to generate a summary from a large model, which is then output directly to the WordPress front-end. summary-index.json.

From the results, this seems to be an extra step compared to directly generating the final summary file, but the advantage of doing so is that different stages are only responsible for their respective tasks: the data extraction stage is responsible for preparing the input; the AI generation stage is responsible for generating the summary; and the WordPress front-end is responsible for displaying the results. The three are independent of each other and are not coupled.

For example, when the model needs to be changed in the future, only the summary file needs to be regenerated; when the summary generation strategy needs to be adjusted, there is no need to modify the WordPress front-end code; and if the article content changes, only the corresponding data source needs to be rebuilt, and the data flow of the entire system always remains stable.

In fact, this is an approach I've been increasingly inclined to adopt in my blog feature design lately: first build a stable data layer, and then implement specific functions based on that data layer.

While this approach increases the initial design work, it makes subsequent tasks such as expanding functionality, adjusting strategies, and maintaining the system much easier.

3.2 summary-source.json: The data source for summary generation

Once the overall architecture was determined, the first problem to solve was: what kind of data should be provided to the large model? Theoretically, the content of posts exported from WordPress could be directly sent to the large model for processing, but after actual testing, I found that this was not an ideal solution.

On the one hand, WordPress raw data contains a large amount of information irrelevant to summary generation, such as author, publication date, category, tags, and comment count. While this data is meaningful to the blog system, it offers little help in generating post summaries. On the other hand, directly feeding all the data into a large model not only increases the context length but also raises the model's understanding cost, ultimately affecting generation efficiency and stability.

Therefore, I decided to first build a data source file specifically for large models before generating the summary:summary-source.jsonThis file is not intended for use by WordPress, but rather serves as the input layer for the summary generation stage. In other words, it is essentially a pre-processed and filtered dataset of articles.

For each article, I ultimately retained the following core fields:

{ "id": 14310, "title": "Emby Video Showcase Website Transformation Based on Cloudflare: From Public Network Publishing to Access Experience Optimization", "url": "https://blog.xxx.com/technology/xxxxx/", "headings": [ "1 Background", "2 Solution Design", "3 Access Experience Optimization" ], "content": "Article Body..." }

in,id Used to subsequently establish a mapping between the post and the WordPress post;title Used to provide article topic information to large models;headings Used to help the model understand the article structure; and content This provides the actual source of the content. Among these fields, I believe the headings and section structure are particularly important.

Human readers typically browse the title and table of contents before deciding whether to delve into the main text. Large models exhibit a similar characteristic. When a model has prior knowledge of the article's titles and chapter structure, it often finds it easier to understand the overall organization of the article, thereby generating more accurate summaries.

For example, with a Cloudflare tutorial, even if the model hasn't read the entire text, just seeing the titles and chapter names is enough to roughly determine whether the article discusses caching rules, security protection, or Tunnel deployment. This structural information actually helps the model quickly build an overall understanding of the article's content.

In addition, I controlled the length of the main text, because the goal of summary generation is not to make the model reread the entire article, but to enable it to quickly understand the core content. If tens of thousands of words of text are fed into the model, it will not only significantly increase the generation time, but may also cause the model to focus on too many details, thus affecting the overall convergence of the summary.

Therefore, in building summary-source.json In this process, I only retain the cleaned main text and appropriately limit the input length in the subsequent generation stage. This preserves the main information of the article while keeping the generation time within an acceptable range.

The final form summary-source.json It can be understood as a "summary generation data source" specifically prepared for large models. It does not handle display functions or participate in front-end logic, but is responsible for converting the originally complex WordPress content into structured input suitable for AI processing.

With this preprocessing layer, the subsequent abstract generation stage no longer deals with messy raw article data, but with a unified, stable, and controllable input format. This is also an important foundation for the long-term maintenance and continuous expansion of the entire abstracting system.

3.3 summary-index.json: For direct consumption by WordPress

summary-source.json After preparation is complete, the second phase of the abstracting system begins. In this phase, the system reads... summary-source.json The data is used to generate article summaries from a locally deployed large model, and the final results are then written to... summary-index.json.

Compared to the previous AI input layer summary-source.json different,summary-index.json It's now part of the data layer that faces the WordPress front end. It no longer cares about the complete content of the post, nor does it need to retain the various contextual information used to generate the summary; instead, it only saves the data required for the final display.

by"”Transformation of the Emby video showcase website based on Cloudflare: From public network publishing to optimized access experience“Taking this article as an example, the generated data structure is as follows:

{ "14310": { "version": "v1", "source_version": "v1", "title": "Transformation of Emby Movie Showcase Website Based on Cloudflare: From Public Network Publishing to Access Experience Optimization", "summary": "Addressing the challenges of public network display of home media servers, this article achieves stable publishing without a public IP address using Cloudflare Tunnel and optimizes the Emby system in multiple dimensions. Core issues include the risk of exposure to home networks, cumbersome login processes, and resource loading delays. Solutions include simplified access with Guest accounts, accelerated static resource caching with Cloudflare, and optimized first-screen experience on the Worker startup page. Ultimately, this lowers the access threshold, speeds up page response, and creates a stable movie showcase website, providing a feasible solution for personal collection display and laying the foundation for subsequent static architecture evolution.", "model": "qwen3:14b", "updated": "2026-06-18 16:13:33" } }

As can be seen, compared to summary-source.jsonThe data structure here has become very simple: the entire file uses the article ID as the index key, and each article corresponds to an independent data object. One direct benefit of this design is extremely high query efficiency.

When a WordPress post page is opened, you only need to obtain the Post ID of the current post and then look up the corresponding record directly in the JSON:

Current article ID ↓ summary-index.json ↓ Read summary ↓ Page display

The entire process does not require accessing a database, calling AI services, or performing any real-time calculations.

For visitors, the summary content is almost indistinguishable from the regular article content; and for the server, the overhead of reading a local JSON file is negligible.


Apart fromPost IDIn addition to the summary content, I also retained several auxiliary fields. Among them,version and source_version This is used to record data structure versions. When the JSON format is adjusted or the digest generation strategy is modified in the future, the version number can be used to quickly determine the compatibility between different data.model The field is used to record the large model used during summary generation. My system currently uses a locally deployed Qwen3-14B, so the corresponding record is:

{ "model": "qwen3:14b" }

This field isn't strictly necessary for the front-end display, but I kept it anyway. On one hand, it helps me track the source of the abstract when I change models in the future; on the other hand, it allows me to directly display the abstract generation information on the article page, for example:

Article Summary...... Qwen3-14B · 2026-06-018

This not only clarifies the source of the abstract but also facilitates subsequent quality comparisons and system maintenance.

The last updated The field records the time the summary was generated. While this may not be very meaningful to the average reader, it helps system maintenance determine whether the summary needs to be regenerated and whether the currently cached data has expired.


From the perspective of system responsibility divisionsummary-index.json The positioning is actually very clear:

  • It is not AI input data;
  • It is not responsible for generating the summary;
  • It does not participate in any complex calculations;

Its sole purpose is to provide WordPress with a digest index that can be read quickly.

This design follows the same logic as the lightweight knowledge indexing system described earlier: all computations are performed offline, while the front-end only handles retrieving the results. The biggest advantage of this approach is that the computational resources consumed in summary generation are completely decoupled from user access. Regardless of whether the number of articles increases to hundreds or thousands in the future, the front-end display logic will remain unchanged, and access performance will not be affected.

For a long-running personal blog, this pre-computed, low-consumption design pattern is often more stable and easier to maintain than a real-time generation solution.

4. Project Implementation

4.1 Data source construction: build_summary_source.py

After completing the structural design in the previous chapters, the next step is to solve a practical problem: how to extract post content from WordPress and generate a uniform format. summary-source.json Data source.

While summary generation ultimately relies on a large model, the model itself cannot directly read content from the WordPress database. Therefore, before calling the model, the article must first be converted into structured data.

Therefore, I wrote build_summary_source.py The script is responsible for extracting post information from WordPress and generating the data source files needed for subsequent processes.

The complete script code is as follows:

import requests
import re
import html
import json
import os
from urllib.parse import urlparse

BASE_URL = "http://127.0.0.1:50443"


# -------------------------
# 获取 WordPress 文章
# -------------------------
def fetch_posts(base_url):
    posts = []
    page = 1

    while True:
        url = f"{base_url}/wp-json/wp/v2/posts?page={page}&per_page=100"

        resp = requests.get(url)

        if resp.status_code != 200:
            break

        data = resp.json()
        posts.extend(data)

        total_pages = int(resp.headers.get("X-WP-TotalPages", 1))

        print(f"Fetched page {page}/{total_pages}")

        if page >= total_pages:
            break

        page += 1

    return posts


# -------------------------
# URL -> Path
# -------------------------
def extract_path(url: str) -> str:
    if not url:
        return ""

    return urlparse(url).path


# -------------------------
# 提取 headings
# -------------------------
def extract_headings(html_content):
    headings = re.findall(
        r"<h[1-6][^>]*>(.*?)</h[1-6]>",
        html_content,
        flags=re.IGNORECASE | re.DOTALL
    )

    result = []

    for h in headings:
        h = re.sub(r"<[^>]+>", "", h)
        h = html.unescape(h).strip()

        if h:
            result.append(h)

    return result


# -------------------------
# HTML 清洗
# -------------------------
def clean_html(raw_html):
    text = raw_html or ""

    # code
    text = re.sub(
        r"<pre.*?>.*?</pre>",
        " ",
        text,
        flags=re.DOTALL | re.IGNORECASE
    )

    text = re.sub(
        r"<code>.*?</code>",
        " ",
        text,
        flags=re.DOTALL | re.IGNORECASE
    )

    # script/style
    text = re.sub(
        r"<script.*?>.*?</script>",
        " ",
        text,
        flags=re.DOTALL | re.IGNORECASE
    )

    text = re.sub(
        r"<style.*?>.*?</style>",
        " ",
        text,
        flags=re.DOTALL | re.IGNORECASE
    )

    # figure
    text = re.sub(
        r"<figure.*?>.*?</figure>",
        " ",
        text,
        flags=re.DOTALL | re.IGNORECASE
    )

    # img
    text = re.sub(
        r"<img[^>]*>",
        " ",
        text,
        flags=re.IGNORECASE
    )

    # shortcode
    text = re.sub(r"\[/?[^\]]+\]", " ", text)

    # headings -> markdown
    for level in range(1, 7):
        text = re.sub(
            rf"<h{level}[^>]*>",
            "\n\n# ",
            text,
            flags=re.IGNORECASE
        )

        text = re.sub(
            rf"</h{level}>",
            "\n\n",
            text,
            flags=re.IGNORECASE
        )

    # paragraph
    text = re.sub(
        r"</?p[^>]*>",
        "\n",
        text,
        flags=re.IGNORECASE
    )

    # br
    text = re.sub(
        r"<br\s*/?>",
        "\n",
        text,
        flags=re.IGNORECASE
    )

    # ul / ol
    text = re.sub(
        r"</?(ul|ol)[^>]*>",
        "\n",
        text,
        flags=re.IGNORECASE
    )

    # li
    text = re.sub(
        r"<li[^>]*>",
        "\n- ",
        text,
        flags=re.IGNORECASE
    )

    text = re.sub(
        r"</li>",
        "",
        text,
        flags=re.IGNORECASE
    )

    # 删除剩余 HTML
    text = re.sub(r"<[^>]+>", " ", text)

    text = html.unescape(text)

    # 删除 URL
    text = re.sub(r"https?://\S+", " ", text)

    # 特殊符号
    text = re.sub(r"[丨||•·■◆►▶●]+", " ", text)

    # 压缩空格
    text = re.sub(r"[ \t]+", " ", text)

    # 压缩空行
    text = re.sub(r"\n{3,}", "\n\n", text)

    return text.strip()


# -------------------------
# 构建正文
# -------------------------
def build_content(clean_text):
    return clean_text.strip()


# -------------------------
# 构建 summary-source
# -------------------------
def build_summary_source(posts):
    result = []

    for post in posts:
        raw_html = post.get("content", {}).get("rendered", "")

        title = html.unescape(
            post.get("title", {}).get("rendered", "")
        )

        post_id = str(post.get("id", ""))

        url = extract_path(post.get("link", ""))

        headings = extract_headings(raw_html)

        clean_text = clean_html(raw_html)

        article = {
            "id": post_id,
            "title": title,
            "url": url,
            "headings": headings,
            "content_length": len(clean_text),
            "content": build_content(clean_text)
        }

        result.append(article)

    return result


# -------------------------
# 输出 JSON
# -------------------------
def write_json(data, filename="summary-source.json"):
    base_dir = os.path.dirname(os.path.abspath(__file__))
    output_path = os.path.join(base_dir, filename)

    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(
            data,
            f,
            ensure_ascii=False,
            indent=2
        )

    print(f"\nSaved: {output_path}")


# -------------------------
# Main
# -------------------------
if __name__ == "__main__":
    posts = fetch_posts(BASE_URL)

    print(f"\nFetched posts: {len(posts)}")

    summary_source = build_summary_source(posts)

    print(
        f"Built summary source: "
        f"{len(summary_source)} articles"
    )

    write_json(summary_source)

    if summary_source:
        print("\nSample:\n")
        print(
            json.dumps(
                summary_source[0],
                ensure_ascii=False,
                indent=2
            )[:2000]
        )

Functionally, this script mainly performs three tasks: obtaining basic article information; extracting article structure content; and outputting a JSON file in a uniform format.

The most crucial aspect isn't data acquisition itself, but rather the standardization of the data format. WordPress stores content for page display, while subsequent processing requires data for program consumption. Therefore, during the export process, information such as post titles, body text, and section structure needs to be reorganized into a unified data structure.

After generation summary-source.json In this way, each post contains information such as the post ID, title, URL, body content, and chapter titles mentioned in section 3.2. This transforms the post content, originally scattered throughout WordPress, into a single, structured data source file that is easy to process later.

At this point, the system has a structured data source that is independent of the WordPress runtime environment. Subsequent operations, such as summary generation, keyword extraction, and other AI processing, can be performed directly based on this file without needing to access WordPress again.

4.2 Summary generation: build_summary_index.py

summary-source.json After generation, the system actually possesses structured data for all articles. However, this data is still just raw content and cannot be directly used for front-end display.

The next problem to solve is: how to generate a summary for each article that encapsulates its core content, and how to solidify the result—this is precisely... build_summary_index.py Their responsibilities.

With the previous build_summary_source.py Unlike other scripts, this one introduces a large model into the processing: it reads the data generated in the previous step. summary-source.jsonThe system generates summaries for each article by calling the locally deployed Qwen3 model, and then saves the results to... /wp-content/themes/theme_directory/cache/summary-index.json middle.

The complete script code is as follows:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import os
import re
import time
from datetime import datetime

import requests

# 使用你已经验证可用的模型
MODEL = "qwen3:14b"

# 输出路径根据自己实际情况修改,我是以博客正在使用的argon主题为例
OUTPUT_FILE = "/docker/wordpress/html/wp-content/themes/argon-theme-master/cache/summary-index.json"
SOURCE_FILE = "summary-source.json"

# API地址需要根据实际情况修改
OLLAMA_URL = "http://127.0.0.1:11434/api/generate"

# 控制输入长度,避免超长文章拖慢 summary
MAX_CONTENT_LENGTH = 10000

# summary 长度限制
MIN_LEN = 80
MAX_LEN = 300

# 重试控制
MAX_RETRY = 2
REGEN_RETRY = 3

# -------------------------
# 排除“结构型页面 / 地图页”
# -------------------------
EXCLUDE_SLUGS = {
    "me",
    "map",
    "cloudflaremap",
    "aimap",
    "singingmap",
    "roadmap",
}


def is_excluded(article):
    url = article.get("url", "")

    for slug in EXCLUDE_SLUGS:
        if f"/{slug}/" in url:
            return True

    return False


# -------------------------
# 读取 source
# -------------------------
def load_source():
    base_dir = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(base_dir, SOURCE_FILE)

    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


# -------------------------
# 加载 cache
# -------------------------
def load_cache():
    if not os.path.exists(OUTPUT_FILE):
        return {}

    with open(OUTPUT_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


# -------------------------
# 原子写入
# -------------------------
def save_cache(cache):
    # 按 id 从大到小排序
    sorted_cache = dict(
        sorted(
            cache.items(),
            key=lambda x: int(x[0]),
            reverse=True
        )
    )

    tmp_path = OUTPUT_FILE + ".tmp"

    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(
            sorted_cache,
            f,
            ensure_ascii=False,
            indent=2
        )

    os.replace(tmp_path, OUTPUT_FILE)


# -------------------------
# 摘要清洗
# -------------------------
def clean_summary(text):
    text = text.strip()
    text = re.sub(r"\s+", " ", text)
    text = text.replace("\n", " ")

    return text.strip()


# -------------------------
# Prompt
# -------------------------
def build_prompt(article):
    title = article.get("title", "")
    headings = "\n".join(article.get("headings", []))
    content = article.get("content", "")[:MAX_CONTENT_LENGTH]

    prompt = f"""
你是一名中文技术博客编辑。

任务:
阅读文章内容,生成一段面向读者的摘要。

摘要要求:
- 单段输出
- 中文摘要
- 优先控制在120~180字
- 如果原文较短,可低于120字
- 禁止出现“本文”“文章”“作者”“介绍”
- 必须覆盖:问题、方案、结果或收获
- 可以适当体现技术背景或应用场景
- 如果属于系列内容或体系化主题,可简要体现其关联方向
- 如果只是单篇实践内容,则重点突出实际问题与解决经验
- 不要简单复述章节标题
- 不要分析写作过程
- 不要虚构原文未提及的背景、效果或结论
- 不要将实验性探索描述为成熟方案
- 禁止分点

文章标题:
{title}

章节结构:
{headings}

正文:
{content}

只输出摘要:
""".strip()

    return prompt


# -------------------------
# 调用 Ollama
# -------------------------
def call_llm(prompt):
    payload = {
        "model": MODEL,
        "prompt": prompt,
        "stream": False
    }

    resp = requests.post(
        OLLAMA_URL,
        json=payload,
        timeout=120
    )

    resp.raise_for_status()

    return clean_summary(
        resp.json()["response"]
    )


# -------------------------
# 修复生成
# -------------------------
def regenerate(last_summary):
    prompt = f"""
以下摘要长度或质量不符合要求:

{last_summary}

请重新生成。

要求:
- 单段中文
- 控制在150~250字
- 覆盖问题、方案、结果或收获
- 可以体现技术背景或应用场景
- 不要简单复述标题
- 不要解释生成过程
- 不要分析写作过程
- 不要虚构原文未提及的信息
- 不要将实验性内容描述为成熟方案
- 不要出现“本文”“文章”“作者”
- 禁止分点

直接输出摘要:
""".strip()

    return call_llm(prompt)


# -------------------------
# 校验
# -------------------------
def validate_summary(text):
    length = len(text)
    return MIN_LEN <= length <= MAX_LEN


# -------------------------
# cache entry
# -------------------------
def build_cache_entry(article, summary):
    return {
        "version": "v1",
        "source_version": "v1",
        "title": article.get("title", ""),
        "summary": summary,
        "model": MODEL,
        "updated": datetime.now().strftime(
            "%Y-%m-%d %H:%M:%S"
        )
    }


# -------------------------
# 主逻辑
# -------------------------
def build_cache():
    source = load_source()
    cache = load_cache()

    source = [
        article
        for article in source
        if not is_excluded(article)
    ]

    total = len(source)

    print(f"Total articles (after filter): {total}")

    saved_count = 0
    skipped_count = 0
    failed_count = 0

    for idx, article in enumerate(source, 1):
        post_id = str(article.get("id"))

        if post_id in cache:
            skipped_count += 1
            continue

        print(f"[{idx}/{total}] Processing {post_id}...")

        try:
            prompt = build_prompt(article)
            summary = None

            for _ in range(MAX_RETRY):
                summary = call_llm(prompt)

                if validate_summary(summary):
                    break

            if not validate_summary(summary):
                success = False

                for _ in range(REGEN_RETRY):
                    summary = regenerate(summary)

                    if validate_summary(summary):
                        success = True
                        break

                if not success:
                    print(
                        f"Skip {post_id}: "
                        f"invalid summary length ({len(summary)})"
                    )

                    failed_count += 1
                    continue

            cache[post_id] = build_cache_entry(
                article,
                summary
            )

            save_cache(cache)

            saved_count += 1

            print(f"Saved {post_id}")

            time.sleep(0.3)

        except Exception as e:
            failed_count += 1

            print(f"Failed {post_id}: {e}")

            time.sleep(1)

    print("\nFinished")
    print(f"Saved: {saved_count}")
    print(f"Skipped(existing): {skipped_count}")
    print(f"Failed: {failed_count}")


# -------------------------
# main
# -------------------------
if __name__ == "__main__":
    build_cache()

From an implementation perspective, the core task of this script is not actually calling the model, but rather how to make the model stably produce the required output.

In theory, you can simply submit the entire article to a large model and ask it to generate a summary. However, in practice, the output of the large model is not always consistent: sometimes the summary is too short, sometimes it is too long, and there may be explanatory text, analysis, or even additional supplementary content.

Therefore, in the script, I did not simply send the article content to the model, but added a Prompt constraint: First, the length of the main text is controlled, and only the first part of the content is kept as input to avoid excessively long articles occupying too much context window; at the same time, the Prompt explicitly requires that the summary must cover the core problem, technical solution and final result, and limits the length range of the summary, thereby improving the consistency of the output.

Even so, the local model still occasionally generates results that do not meet the requirements. To address this issue, a length validation mechanism was added to the script: when the length of the generated summary exceeds the preset range, the regeneration process is automatically triggered; if the requirements still cannot be met after multiple attempts, the current article is skipped and processed again on the next execution.

While this design increases computational overhead slightly, the benefit is that the quality of the entire abstract library remains relatively stable, without the final result being contaminated by individual abnormal outputs.

Another important design feature is incremental updates. As the number of blog posts grows, regenerating all summaries on every run would not only waste computing resources but also significantly increase processing time. Therefore, the script prioritizes reading existing summaries. summary-index.jsonArticles for which summaries have already been generated are skipped, and only new content is processed. This means that the first run may need to process all articles, while subsequent runs often only need to process the most recently added articles.

The final generated summary-index.json In this system, each article saves a summary and related metadata, such as model name and update time. This data is permanently saved after generation, so subsequent visits to the blog do not require re-accessing the model; the results can be retrieved directly.

At this point, the most computationally intensive part of the summary system is complete. WordPress's next task is simply to read the corresponding summary based on the post ID and display it on the page; no further AI inference is involved.

4.3 WordPress Summary Display Implementation

summary-index.json Once generation is complete, the most complex part of the entire summarization system is essentially finished. At this point, the large model has completed all the summarization work, and WordPress no longer needs to call any AI services or perform any additional calculations. For the front-end, it simply faces a plain JSON file.

Therefore, the final step becomes quite simple: when a user opens an article page, how to retrieve the current article ID from... summary-index.json Find the corresponding summary and display it.

To achieve this goal, I chose to add a custom PHP function to my WordPress theme using the code snippets plugin (or functions.php) to read the summary index file before the post body is output and insert the corresponding content into the page.

The implementation code is as follows:

add_filter('the_content', 'insert_ai_summary_before_content', 5);

function insert_ai_summary_before_content(content) {

    if (!is_single()) {
        returncontent;
    }

    static summary_index = null;

    if (summary_index === null) {
        file = get_theme_file_path('/cache/summary-index.json');

        if (!file_exists(file)) {
            return content;
        }json = file_get_contents(file);summary_index = json_decode(json, true);

        if (!is_array(summary_index)) {
            return content;
        }
    }post_id = get_the_ID();

    if (!isset(summary_index[post_id])) {
        return content;
    }item = summary_index[post_id];

    summary =item['summary'] ?? '';
    model =item['model'] ?? '';
    updated =item['updated'] ?? '';

    date = substr(updated, 0, 10);

    // 模型显示优化
    model_map = [
        'qwen3:8b' => 'Qwen3-8B',
        'qwen3:14b' => 'Qwen3-14B',
    ];model_label = model_map[model] ?? model;html = '';

    // 外层容器(增强结构隔离)
    html .= '<div class="article-summary-wrapper">';

    // 上分隔线html .= '<div class="article-summary-divider"></div>';

    // 内容主体
    html .= '<div class="article-summary">';html .= '<div class="article-summary-title">文章摘要</div>';

    html .= '<div class="article-summary-content">'
        . nl2br(esc_html(summary)) .
    '</div>';

    html .= '<div class="article-summary-meta">'
        . esc_html(model_label . ' · ' . date) .
    '</div>';html .= '</div>';

    // 下分隔线
    html .= '<div class="article-summary-divider"></div>';html .= '</div>';

    return html .content;
}

The overall implementation logic is not complicated: First, the code reads the data located at...Theme directory /cache/In summary-index.json The file is processed and parsed into an array structure. Since the summary data was generated offline, only a simple file read is needed here, without any model calls or network requests. Then, the corresponding record is searched for in the JSON data using the ID of the current WordPress post as the index key. Finally, if a match is found, the saved summary content, model information, and generation time are extracted; otherwise, the original post content is returned without affecting normal access.

The advantage of doing this is that even if the summary system stops running in the future or some articles have not yet generated summaries, it will not affect the normal display logic of the blog itself.

From a system perspective, WordPress's role here is very singular: it doesn't generate the summary, but only displays it. All complex calculations are done offline; the front-end access is merely a single JSON query. For the server, this overhead is negligible.

Of course, simply displaying a summary is not enough. If you output plain text directly, it can easily blend in with the main content, and readers may not even realize at first glance that it is an independent article overview.

Therefore, in the actual implementation, I added a separate style to the summary area, making it appear as an information card.

The corresponding CSS is as follows:

.article-summary { padding: 18px 22px; background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.08); border-radius: 10px; /* 👉 Added: Overall reading rhythm */ line-height: 1.65; } /* ========================== Outer spacing (Summary vs TOC) ========================= */ .article-summary-wrapper { margin: 30px 0 40px; display: flow-root; } /* Increase the spacing between TOC and summary */ #toc_container { margin: 40px 0 !important; } /* ========================= Internal Structure Rhythm (Key Optimization) ======================== */ .article-summary-title { font-weight: 600; margin-bottom: 12px; } .article-summary-content { margin-bottom: 14px; line-height: 1.7; } .article-summary-meta { margin-top: 10px; font-size: 12px; opacity: 0.7; } /* ========================= Night Mode ========================== */ body.dark .article-summary, [data-theme="dark"] .article-summary { background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 3px 12px rgba(0,0,0,0.28); } /* Maintain consistent pacing in night mode (only fine-tuning readability) */ body.dark .article-summary-title, [data-theme="dark"] .article-summary-title { opacity: 0.9; } body.dark .article-summary-meta, [data-theme="dark"] .article-summary-meta { opacity: 0.65; }

The final result is that the abstract is located between the article title and the table of contents; it maintains a clear visual separation from the main text; it remains consistent with the overall theme; and it is compatible with both light and dark modes.

Compared to inserting the abstract directly into the main text, this card-style presentation allows readers to quickly grasp the general content of the article before delving into the main text. This is especially important for longer technical articles.

Readers can often judge within a few tens of seconds whether an article meets their needs, and thus decide whether to continue reading or jump to other content.

The final summary display on the page looks like this:

image.png

At this point, the data flow of the entire summary system has formed a complete closed loop: from article content extraction and summary generation to the final page display, all work is completed locally, without relying on third-party online services, and without needing to call large models in real time when users access the system.

5 Conclusion

This article completes the semantic layer construction of the "lightweight knowledge index" system: through summary-source.json Structure the original WordPress posts and then... summary-index.json By solidifying the AI-generated results, the blog content now possesses a semantic expression independent of the original article for the first time.

The focus at this stage is not simply to add an article summary function, but to establish a stable data transformation process: transforming the original content into reusable semantic units and reducing the complexity of front-end operation through offline generation.

Currently, this semantic layer is mainly used for article summary display, helping readers quickly understand the article content. However, from a system design perspective, the summary itself is not the ultimate goal; more importantly, it establishes a unified semantic data foundation for the blog. In other words, this article completes the transformation from "article content" to "semantic expression," giving blog content, for the first time, the ability to provide semantic descriptions that can be further processed.

When this semantic data is no longer just used for display, but begins to participate in the association and organization between articles, the information structure of the blog will enter the next stage: from a simple content index, it will gradually evolve into a content space based on semantic relationships.

The next article will continue to build upon this semantic foundation and explore how to utilize existing article and summary data to construct a higher-level content organization mechanism.

📚 系列文章:为博客构建“轻量级知识索引”(6 / 6)

📌 Content Structure Hints:
This content belongs to "AI Learning MapThis is part of the document; you can view the full content path here: AI Learning Map .
View related categories · 3 matches
Share this article
All blog content is original; please indicate the source when reprinting! The blog's RSS address is:https://blog.tangwudi.com/feed, welcome to subscribe; if necessary, you can joinTelegram GroupDiscuss the problem together.
No Comments

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠(ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ°Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Emoticons
Emoji
Little Dinosaur
flower!
Previous
       

👋 Welcome to "Invincible Personal Blog"“

This section will focus on long-term exploration in the following areas:

🧱 Building Personal Digital Infrastructure and Blog Systems
☁️ Cloudflare and Network Architecture Practices
🧠 Exploring AI and Knowledge Systems
🛡️ Network security and access optimization
🎵 Music and Sound Cognition
👁️ Cognitive Perspective and Worldview