实战:从 AI 语义搜索到 AI 内容流水线,静态博客如何持续进化(续篇)
几个月前,我写过一篇《实战:基于 Cloudflare Vectorize 与 Gemini 构建全自动 AI 语义搜索》,当时解决的问题很明确:让一个静态博客具备语义检索能力,并把用户搜不到的问题沉淀为 Content Gap。
那套架构跑起来以后,我很快发现:搜索只是内容生命周期的最后一公里。
一篇文章从 Markdown 写完到真正被读者发现,中间还要经过摘要、翻译、相关推荐、内部链接、图片优化、搜索索引、SEO、部署和质量检查。如果这些环节仍然依赖人工逐项处理,AI 搜索再智能,也只是给传统发布流程外挂了一个新入口。
所以这次升级的重点,不是继续往页面上增加 AI 按钮,而是把整个博客改造成一条可重复运行的内容工程流水线:
作者仍然只负责写作和最终审阅;机器负责生成派生内容、建立索引、补齐分发信息,并验证发布结果。
本文是上一篇 AI 搜索文章的续篇,主要复盘这套系统从“一个 Worker + 一个向量库”,演进到“内容控制面 + 搜索数据面 + 静态降级面 + 质量门禁”的过程。
一、架构变化:搜索功能变成了内容平台的一部分
上一篇文章中的核心链路很短:
| |
现在的系统多了三个重要部分:
- 内容控制面:GitHub Actions 自动加工文章,并把结果写回仓库。
- 静态降级面:Worker、Vectorize 或外部模型不可用时,Pagefind 和 PWA 仍能提供基本能力。
- 质量门禁:Lighthouse、链接检查、Hugo 构建和部署保留策略持续验证结果。
为了避免把构建期和运行时链路挤在一张图里,下面拆成两个视角。
内容生成与回写
内容流水线以 Git Push 为入口,由 GitHub Actions 依次完成文章加工,并将生成结果回写至 Git 仓库:
%%{init: {"flowchart": {"nodeSpacing": 10, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
AUTHOR["作者
Markdown + 图片"] --> GIT["Git Push"]
GIT --> CONTENT["内容加工
摘要 / TL;DR / 推荐 / 交叉链接"]
CONTENT --> DELIVERY["媒体与多语言
Alt / WebP / OG / 英文翻译"]
DELIVERY -->|提交生成内容| REPO["Git Repository"]%%{init: {"flowchart": {"nodeSpacing": 10, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
AUTHOR["作者
Markdown + 图片"] --> GIT["Git Push"]
GIT --> CONTENT["内容加工
摘要 / TL;DR / 推荐 / 交叉链接"]
CONTENT --> DELIVERY["媒体与多语言
Alt / WebP / OG / 英文翻译"]
DELIVERY -->|提交生成内容| REPO["Git Repository"]%%{init: {"flowchart": {"nodeSpacing": 10, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
AUTHOR["作者
Markdown + 图片"] --> GIT["Git Push"]
GIT --> CONTENT["内容加工
摘要 / TL;DR / 推荐 / 交叉链接"]
CONTENT --> DELIVERY["媒体与多语言
Alt / WebP / OG / 英文翻译"]
DELIVERY -->|提交生成内容| REPO["Git Repository"]%%{init: {"flowchart": {"nodeSpacing": 10, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
AUTHOR["作者
Markdown + 图片"] --> GIT["Git Push"]
GIT --> CONTENT["内容加工
摘要 / TL;DR / 推荐 / 交叉链接"]
CONTENT --> DELIVERY["媒体与多语言
Alt / WebP / OG / 英文翻译"]
DELIVERY -->|提交生成内容| REPO["Git Repository"]发布、搜索与质量检查
以 Git 仓库为事实来源,发布链路进一步连接静态站点构建、AI 语义搜索与独立质量门禁:
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
REPO["Git Repository"] --> CI["构建、索引与质量门禁
Hugo / Pagefind / Vector Sync
Lighthouse / Link Check"]
CI --> PAGES["Cloudflare Pages"]
PAGES --> STATIC["静态访问
Pagefind / Service Worker"]
PAGES -.-> SEARCH["AI 语义搜索
Worker / Workers AI
Vectorize / D1"]%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
REPO["Git Repository"] --> CI["构建、索引与质量门禁
Hugo / Pagefind / Vector Sync
Lighthouse / Link Check"]
CI --> PAGES["Cloudflare Pages"]
PAGES --> STATIC["静态访问
Pagefind / Service Worker"]
PAGES -.-> SEARCH["AI 语义搜索
Worker / Workers AI
Vectorize / D1"]%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
REPO["Git Repository"] --> CI["构建、索引与质量门禁
Hugo / Pagefind / Vector Sync
Lighthouse / Link Check"]
CI --> PAGES["Cloudflare Pages"]
PAGES --> STATIC["静态访问
Pagefind / Service Worker"]
PAGES -.-> SEARCH["AI 语义搜索
Worker / Workers AI
Vectorize / D1"]%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
REPO["Git Repository"] --> CI["构建、索引与质量门禁
Hugo / Pagefind / Vector Sync
Lighthouse / Link Check"]
CI --> PAGES["Cloudflare Pages"]
PAGES --> STATIC["静态访问
Pagefind / Service Worker"]
PAGES -.-> SEARCH["AI 语义搜索
Worker / Workers AI
Vectorize / D1"]实际运行时,一次文章提交会触发多条相互独立的 GitHub Actions 工作流:

这些工作流分别负责内容加工、质量检查、搜索引擎通知与部署治理。职责拆分后,单条链路的失败不会掩盖其他环节的执行状态,也更便于独立重试和排查。
这里最关键的变化,是把不同职责分开:
- Worker 负责运行时检索,不负责文章生成。
- GitHub Actions 负责构建期内容加工,不参与用户请求。
- Pagefind 和 Service Worker 提供独立于 AI API 的降级能力。
- Git 仓库继续保存所有可审阅的内容状态。
这样即使某个 AI 服务临时不可用,博客仍然是一个可以正常阅读和搜索的静态站点。
二、搜索层的演进:从 Gemini 单路径到可切换 Embedding
上一篇文章使用 Gemini text-embedding-004 生成 768 维向量。当前实现把默认 Embedding 路径切换到了 Cloudflare Workers AI:
| |
Gemini 路径没有被删除,而是保留为可切换的备选实现。这样做不是为了追求“模型越多越好”,而是为了把模型选择从业务代码中抽离出来。
真正需要严格保证的是下面这条约束:
写入 Vectorize 的文档向量,与查询时生成的 Query 向量,必须使用相同模型、维度、Pooling 和归一化方式。
只要其中一个参数不一致,即使接口都返回成功,检索质量也会悄悄失效。这类问题比直接报错更危险,因为系统看起来仍然“能搜”,只是结果越来越不相关。
删除文章也要同步删除向量
早期同步脚本只做 Upsert。文章被删除或改名后,旧向量仍可能留在 Vectorize 中,最终出现“搜索结果能看到,但打开是 404”的幽灵文章。
现在的 Workflow 会先通过 Git diff 找出删除或重命名的文章 slug,再调用 Vectorize delete_by_ids:
| |
这一步看似只是清理数据,实际解决的是搜索索引与内容事实来源之间的一致性问题:
- Markdown 仓库仍然是 Source of Truth。
- Vectorize 只是可重建的索引层。
- 索引不能保留仓库里已经不存在的事实。
阈值从后端判断扩展到前端可调
Worker 目前使用 0.55 判断一次搜索是否真正命中,并把结果写入 D1:
| |
前端则提供默认值为 0.6 的滑块,让读者自行调整展示阈值。
这两个阈值职责不同:
- Worker 阈值决定这次查询是否记为 Content Gap。
- 前端阈值决定哪些候选结果展示给当前读者。
这种拆分比把一个固定分数同时用于分析和展示更灵活,但也意味着阈值需要持续根据真实查询校准,而不能把 0.55 当成适用于所有模型的通用常数。
三、十步流水线:一次 Push 如何加工一篇文章
当 content/** 或 static/image/** 发生变化时,GitHub Actions 会执行一条十步流水线:
| 步骤 | 处理内容 | 主要输出 |
|---|---|---|
| 1 | 同步 Embedding | Vectorize 索引 |
| 2 | 生成中文摘要 | ai_summary |
| 3 | 生成三条 TL;DR | ai_tldr |
| 4 | 识别文章系列 | series_part 等字段 |
| 5 | 计算语义相关推荐 | ai_related |
| 6 | 选择正文首图 | images / OG Image |
| 7 | 注入内部交叉链接 | Markdown 链接 |
| 8 | 生成图片 Alt Text | 无障碍与图片 SEO 文本 |
| 9 | 转换 WebP | 图片压缩副本 |
| 10 | 中译英 | index.en.md |
%%{init: {"flowchart": {"nodeSpacing": 8, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
PUSH["提交与索引
文章 Push · 1. 同步向量"]
PUSH --> CONTENT["内容结构化
2. 摘要 · 3. TL;DR
4. 系列 · 5. 相关推荐"]
CONTENT --> ENRICH["内容增强与翻译
6. OG 图 · 7. 交叉链接
8. Alt Text · 9. WebP · 10. 中译英"]
ENRICH --> COMMIT["提交生成内容"]%%{init: {"flowchart": {"nodeSpacing": 8, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
PUSH["提交与索引
文章 Push · 1. 同步向量"]
PUSH --> CONTENT["内容结构化
2. 摘要 · 3. TL;DR
4. 系列 · 5. 相关推荐"]
CONTENT --> ENRICH["内容增强与翻译
6. OG 图 · 7. 交叉链接
8. Alt Text · 9. WebP · 10. 中译英"]
ENRICH --> COMMIT["提交生成内容"]%%{init: {"flowchart": {"nodeSpacing": 8, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
PUSH["提交与索引
文章 Push · 1. 同步向量"]
PUSH --> CONTENT["内容结构化
2. 摘要 · 3. TL;DR
4. 系列 · 5. 相关推荐"]
CONTENT --> ENRICH["内容增强与翻译
6. OG 图 · 7. 交叉链接
8. Alt Text · 9. WebP · 10. 中译英"]
ENRICH --> COMMIT["提交生成内容"]%%{init: {"flowchart": {"nodeSpacing": 8, "rankSpacing": 14, "useMaxWidth": false}, "themeVariables": {"fontSize": "16px"}}}%%
flowchart TD
PUSH["提交与索引
文章 Push · 1. 同步向量"]
PUSH --> CONTENT["内容结构化
2. 摘要 · 3. TL;DR
4. 系列 · 5. 相关推荐"]
CONTENT --> ENRICH["内容增强与翻译
6. OG 图 · 7. 交叉链接
8. Alt Text · 9. WebP · 10. 中译英"]
ENRICH --> COMMIT["提交生成内容"]为什么把生成结果写回 Git
另一种方案是构建时临时生成所有内容,不写回仓库。它更“干净”,但有一个明显问题:摘要、翻译和内部链接只存在于构建产物中,作者无法像审阅普通代码一样审阅它们。
当前方案选择把结果写回 Markdown:
- 生成内容可以进入 Git diff。
- 错误翻译和错误链接可以人工修正。
- 每次修改都有提交记录。
- Hugo 构建不依赖运行时调用 LLM。
代价也很直接:CI 获得了修改内容仓库的能力,因此必须控制重复运行和并发写入。
幂等比“自动化”更重要
摘要、TL;DR 和翻译脚本都会记录正文 hash。正文未变化时直接跳过,避免每次 Push 都重新调用模型。
相关推荐会对分数做两位小数舍入,并在新旧数据一致时跳过写入,避免向量检索的细微浮动制造无意义 diff。
AI Workflow 自己生成的提交包含 [skip ai-sync],防止再次触发自己;如果运行期间用户又 Push 了新提交,脚本会尝试 rebase 后再 Push,最多重试三次。
这套机制解决的不是性能问题,而是自动写回系统最容易出现的两个故障:
- 工作流递归触发,形成无限提交。
- 多次并发运行互相覆盖内容。
四、AI 不只用于生成,还用于组织内容
增加摘要和翻译很容易被理解,但这次改造中更重要的部分,是让已有文章开始形成结构。
TL;DR 与系列导航
ai_tldr 会在文章顶部渲染三条核心结论,让读者在进入长文前快速判断是否值得继续读。
系列识别则不依赖 LLM,而是根据标题中的 Part、序号等规则生成:
| |
这里刻意使用确定性规则,而不是让模型判断一切。能通过稳定规则解决的问题,不应该额外引入模型的不确定性。
相关推荐从 Tag 匹配升级为语义匹配
传统博客的相关文章通常依赖 Tag。问题是 Tag 很容易漏标,而且两个主题接近的文章不一定共享完全相同的标签。
现在的 ai-related-rebuild.py 会用文章标题查询现有 Worker,排除文章自身后,把 Top-K 结果写入 ai_related。
这相当于复用了同一套向量索引:
| |
同一个检索能力,既服务用户,也服务内容组织。
自动交叉链接不是全自动乱加链接
交叉链接分成两个阶段:
- LLM 为每篇文章提取 1 到 3 个有辨识度的 Anchor。
- 确定性脚本在其他文章中找到首次提及,并注入内部链接。
脚本会跳过代码块、已有链接、标题和 HTML,每篇文章最多增加 5 条链接。
这个上限很重要。内部链接的目标是帮助读者补充上下文,而不是把正文变成 SEO 链接农场。
五、双语系统:翻译只是第一步
生成 index.en.md 之后,英文版还需要解决发现、跳转和搜索结果映射问题。
当前实现增加了四层处理:
- Hugo 为中文和英文生成独立 URL。
- 页面输出
hreflang和x-default。 - 首页根据浏览器语言做一次自动跳转,并尊重用户的手动选择。
- 页脚提供显式语言切换链接。
搜索层还有一个额外问题:Worker 返回的 Metadata 不一定是当前页面语言。
因此英文页面在构建时生成一张 slug → English title / URL 映射表。收到 Worker 结果后,用稳定的 slug 替换展示标题和链接:
| |
这是一种实用的兼容层,但不是最终形态。更完整的设计应该在向量索引中显式保存语言字段,甚至为不同语言使用独立 namespace,避免同一 slug 的多语言文档互相覆盖。
六、AI 服务不可用时,博客仍然要能搜索
系统加入的最重要能力之一,其实不是 AI,而是 Pagefind。
AI 搜索依赖 Worker、Embedding 模型和 Vectorize。任何一层异常,都可能让搜索入口失效。Pagefind 则在 Hugo 构建后扫描静态 HTML,生成纯前端全文索引:
| |
两种搜索承担不同任务:
| 能力 | AI 语义搜索 | Pagefind 全文搜索 |
|---|---|---|
| 擅长 | 语义相似、概念关联 | 精确词、标题和正文匹配 |
| 运行依赖 | Worker + Embedding + Vectorize | 浏览器中的静态索引 |
| 网络故障影响 | 可能不可用 | 已加载索引后仍可工作 |
| 成本 | 有 API 和边缘计算调用 | 构建期成本 |
页面不会把两者伪装成同一种搜索,而是明确告诉读者:AI 搜索是首选,全文搜索是独立兜底。
flowchart TD
USER["User query"] --> AISEARCH["AI semantic search"]
AISEARCH -->|Available| RESULTS["Semantic results"]
AISEARCH -->|Unavailable or no useful match| PAGEFIND["Pagefind full-text search"]
PAGEFIND --> STATIC["Static index results"]
USER --> ARTICLE["Previously visited article"]
ARTICLE --> SW["Service Worker cache"]
SW -->|Offline| CACHED["Cached HTML and assets"]flowchart TD
USER["User query"] --> AISEARCH["AI semantic search"]
AISEARCH -->|Available| RESULTS["Semantic results"]
AISEARCH -->|Unavailable or no useful match| PAGEFIND["Pagefind full-text search"]
PAGEFIND --> STATIC["Static index results"]
USER --> ARTICLE["Previously visited article"]
ARTICLE --> SW["Service Worker cache"]
SW -->|Offline| CACHED["Cached HTML and assets"]flowchart TD
USER["User query"] --> AISEARCH["AI semantic search"]
AISEARCH -->|Available| RESULTS["Semantic results"]
AISEARCH -->|Unavailable or no useful match| PAGEFIND["Pagefind full-text search"]
PAGEFIND --> STATIC["Static index results"]
USER --> ARTICLE["Previously visited article"]
ARTICLE --> SW["Service Worker cache"]
SW -->|Offline| CACHED["Cached HTML and assets"]flowchart TD
USER["User query"] --> AISEARCH["AI semantic search"]
AISEARCH -->|Available| RESULTS["Semantic results"]
AISEARCH -->|Unavailable or no useful match| PAGEFIND["Pagefind full-text search"]
PAGEFIND --> STATIC["Static index results"]
USER --> ARTICLE["Previously visited article"]
ARTICLE --> SW["Service Worker cache"]
SW -->|Offline| CACHED["Cached HTML and assets"]PWA Service Worker 又补了一层离线能力:
- HTML 使用 stale-while-revalidate。
- CSS、JavaScript 和图片使用 cache-first。
- Worker API、Cloudflare Analytics 等动态请求不缓存。
这里的设计原则是:缓存内容,不缓存动态判断。
七、从“能发布”到“可持续维护”
功能变多以后,另一个风险随之出现:页面能构建,不代表体验没有退化。
因此项目增加了几类质量检查。
Lighthouse CI
每次影响渲染的 Push 都会检查中文首页、英文首页、AI 搜索页和代表性文章。
当前门槛是:
- Performance ≥ 0.85
- Accessibility ≥ 0.90
- Best Practices ≥ 0.85
- SEO ≥ 0.90
这些门槛目前采用 warning,而不是硬性阻断。原因是 Lighthouse 本身存在环境波动,现阶段更适合作为趋势监控和回归提示。
详细报告会作为 GitHub Actions Artifact 保留 7 天,同时上传临时在线报告。
链接检查与搜索引擎通知
Lychee 每周扫描 Markdown 和主要 Layout 中的链接。发现失效链接后自动生成 Issue,而不是等读者反馈。
普通内容 Push 后,IndexNow Workflow 会提取本次变化的中英文 URL,主动通知支持 IndexNow 的搜索引擎。AI 流水线带 [skip ai-sync] 的回写提交则会跳过,避免重复触发。
这两条链路分别解决:
- 旧内容是否仍然可访问。
- 新内容是否能尽快被发现。
图片与元数据
流水线还会补齐一组容易被忽略但长期影响体验的细节:
- 从正文首图生成 Open Graph 图片。
- 没有正文图片时使用全站默认封面。
- 使用视觉模型补充弱 Alt Text。
- 将 PNG/JPG 转成 WebP,并保留原图作为兼容回退。
- 输出 JSON-LD Publisher 信息。
- 通过 Cloudflare Web Analytics 观察访问情况。
这些能力单独看都不复杂,但它们决定了一篇文章在社交分享、搜索结果、屏幕阅读器和移动网络中的真实表现。
八、踩坑与取舍
1. 不要把当前浮动按钮写成完整 AI 问答
文章页的浮动入口会把当前文章 slug 作为 ctx 参数传给 Worker,但 Worker 目前尚未消费这个参数,也没有调用生成模型组织最终答案。
它当前更准确的定位是:
带文章入口上下文的全站语义检索 UI,而不是基于本文内容直接回答问题的完整 RAG Agent。
如果后续要升级为真正的文章问答,需要增加 Chunk 级索引、上下文拼装、引用来源和生成答案等能力。
2. 自动生成不等于自动正确
翻译、摘要、Anchor 和 Alt Text 都可能出错。把结果写回 Git 的目的,就是让自动生成内容继续接受代码审阅式的检查。
在技术博客里,模型最容易犯的不是语法错误,而是把“可能”“计划”“当前实现”翻译成已经完成的事实。
3. 构建流水线越长,权限边界越重要
AI Workflow 可以修改仓库,Worker 可以访问 Vectorize、D1 和 Workers AI。这些都不是普通的前端插件,而是具备写权限或资源调用权限的系统主体。
生产化时至少需要继续收紧:
- GitHub Token 和 Cloudflare Token 的权限范围。
- Worker 的 CORS Allowed Origin。
- 搜索接口的限流和滥用防护。
- 自动提交发生冲突时的人工审阅入口。
4. 静态优先不能只是一句口号
如果首页渲染依赖 Worker、文章打开依赖数据库、搜索依赖生成模型,那么它实际上已经不再是一个可靠的静态博客。
当前系统坚持的边界是:
- 正文阅读永远不依赖 AI 服务。
- AI 搜索失败时有 Pagefind。
- 网络离线时可以读取访问过的页面。
- 所有 AI 生成结果在部署前落成普通 Markdown 或静态资源。
AI 是增强层,而不是站点存活的前提。
九、下一步
这套系统已经从单点 AI 搜索扩展成了一条内容工程流水线,但仍有几个明确的下一步:
- 为向量索引增加语言字段或 namespace,彻底解决多语言文档覆盖问题。
- 让 Worker 真正消费文章
ctx,实现 Chunk 级引用和有来源的答案生成。 - 为搜索接口增加限流、来源校验和更完整的可观测性。
- 把 Mermaid、翻译和内部链接检查加入自动验收,而不只依赖 Hugo 构建成功。
- 将 AI 生成内容的 diff 摘要作为明确的人工 Review Gate。
总结
上一篇文章解决的是“如何让静态博客具备 AI 语义搜索”。这次演进解决的是另一个问题:
当文章数量、语言和自动化能力持续增长时,如何让内容从写作到发布、发现、检索和维护形成稳定闭环。
最终形成的不是一个“AI 功能很多”的博客,而是一套职责相对清晰的工程系统:
- Git 是内容事实来源。
- GitHub Actions 是内容控制面。
- Cloudflare Worker、Workers AI、Vectorize 和 D1 是搜索数据面。
- Pagefind 和 PWA 是静态降级面。
- Lighthouse、Lychee 和 Hugo Build 是质量门禁。
真正有价值的不是让 AI 替作者写完所有内容,而是让机器承担重复、可验证、可回滚的加工工作,让作者把注意力留给选题、判断和最终审阅。
🤖 AI 相关推荐 按语义相似度
想跟进更新? RSS