---
title: 从零搭建博客RAG知识库问答——基于Embedding语义检索的实践
date: 2026-06-11 16:00:00
categories:
- 全栈实战
---

# 一句话总结

> 在纯静态 Hexo 博客上零后端服务搭建 RAG 知识库问答系统：用 Embedding API 做向量化 + Cloudflare Worker 做代理 + IndexedDB 做缓存，实现用户自然语言提问即可检索 1500+ 篇博客内容并生成回答。

---

# 问题背景

## 我在做什么

我有一个基于 Hexo + Butterfly 主题的个人技术博客 [kukudelin.top](https://kukudelin.top)，已经积累了 1500+ 篇 Markdown 文档。我希望在博客页面上加一个问答窗口，让访客可以用自然语言提问，系统自动从所有博客文章中检索相关内容并给出回答——也就是给静态博客加一个 RAG（检索增强生成）能力。

## 遇到了什么问题

**核心矛盾**：Hexo 是一个纯静态博客框架，没有后端服务。传统的 RAG 方案需要起一个后端来做向量存储和检索，但我——

1. 不想单独启动一个后端服务
2. 不想维护数据库
3. 不想花钱租服务器

我想让整个系统"无需部署任何服务"就能跑起来。

---

# AI 分析过程

## AI 的定位思路

整个对话是一条逐步探索的路径，AI 引导我完成了架构选型：

1. **第一步：可行性分析** — AI 分析了项目结构，确认知识库问答在技术上完全可行
2. **第二步：架构拍板** — 确定"前端本地向量搜索 + Cloudflare Worker 代理 LLM API"的架构，完全避免自建后端
3. **第三步：建模选型** — Embedding 模型选了 `Qwen/Qwen3-Embedding-4B`，对话模型选了 `deepseek-ai/DeepSeek-V4-Flash`，都通过硅基流动 API 调用
4. **第四步：分块策略** — 从固定字符分块改为按 Markdown 标题段落智能分段
5. **第五步：CORS 地狱** — 从本地 localhost 到线上 kukudelin.top，CORS 一路踩坑
6. **第六步：前端时序问题** — 15MB 向量文件加载 + IndexedDB 缓存 + Edge 浏览器兼容性

## 关键发现

1. **Cloudflare Worker 可以做 API 代理**，隐藏 API Key，同时处理 CORS
2. **Embedding 向量可以预计算、打包成二进制文件**，前端直接 fetch，不需要向量数据库
3. **IndexedDB 在 Edge 的跟踪防护下会被阻止**，需要 fallback 到内存存储
4. **CSS 选择器名和 JS ID 不一致**（`toggle` vs `trigger`）会导致功能完全失效
5. **CORS 白名单不能只写 `localhost`**，线上域名也需要加进去

## 涉及的技术概念

- **RAG (Retrieval-Augmented Generation)** — 先检索相关文档，再把文档内容塞进 prompt 让 LLM 回答
- **Embedding 向量** — 把文本转换成高维向量（2560 维），语义相近的文本向量距离也相近
- **余弦相似度** — 用两个向量的夹角余弦值衡量它们的相似程度
- **Cloudflare Worker** — 在 CDN 边缘节点运行的 Serverless 函数，用于代理 API 请求
- **IndexedDB** — 浏览器端的结构化存储，可以存大量向量数据避免重复下载
- **CORS (跨域资源共享)** — 浏览器安全策略，Worker 需要返回正确的 `Access-Control-Allow-Origin` 头

---

# AI 解决方案

## 最终方案

整体架构分四层：

```
用户提问 → 前端本地向量搜索（找相关文档）
         → Cloudflare Worker（代理 /api/chat，拼接 system prompt + 上下文）
         → 硅基流动 LLM API（生成回答）
         → 前端展示 + 参考来源
```

**关键设计决策**：

1. **向量不存数据库** — `build_kb.js` 脚本预计算所有文章的 embedding，`pack_vectors.js` 打包成 `embeddings.bin`（二进制）+ `index.json`（元数据），直接放在 `source/kb/` 下，Hexo 构建时自动拷贝到 `public/`
2. **前端本地搜索** — `kb-chat.js` 从 `public/kb/` 下载向量文件，用户提问时调用 Worker 的 `/api/embed` 获取问题向量，然后在前端用余弦相似度遍历所有文档向量找 Top 5
3. **Worker 只做代理** — 隐藏 API Key，转发 `/api/chat` 和 `/api/embed` 请求到硅基流动

## 核心代码片段

```js
// 前端本地向量搜索
function cosineSimilarity(a, b) {
  let dot = 0, na = 0, nb = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    na += a[i] * a[i];
    nb += b[i] * b[i];
  }
  return dot / (Math.sqrt(na) * Math.sqrt(nb));
}

// 遍历所有预计算向量找 Top 5
const scored = index.map((item, i) => ({
  ...item,
  score: cosineSimilarity(queryVec, getVector(i))
})).sort((a, b) => b.score - a.score).slice(0, 5);
```

## 为什么这样做能解决问题

**核心思路是"计算前置，检索后置"**：

- Embedding 计算是最耗资源的步骤（每篇文章需要调一次 API），但它是**离线**的——可以在构建时一次性完成
- 检索是**在线**的，但只需要做向量点积运算，前端 JavaScript 完全能胜任 1500 × 2560 维的计算（约几十毫秒）
- LLM 调用通过 Worker 代理，前端无需知道 API Key

这套架构的本质是把"重计算"从在线挪到离线，把"轻计算"留在前端。

---

# 本次知识萃取

## 显性知识

- **Embedding 模型选型**：中文场景优先选 Qwen 系列，`Qwen3-Embedding-4B` 输出 2560 维，比 `BAAI/bge-large-zh-v1.5` 的 1024 维语义更丰富
- **Markdown 分块策略**：不要固定字符数切分，应该按 `##` 标题段落自然分段，保持语义完整性
- **Cloudflare Worker 部署**：`npx wrangler deploy` 一键部署，配置写在 `wrangler.toml`
- **CORS 三件套**：`Access-Control-Allow-Origin` + `Access-Control-Allow-Methods` + `Access-Control-Allow-Headers`，OPTIONS 预检要单独处理

## 隐性知识

- **Edge 跟踪防护会静默阻止 IndexedDB**，没有任何可见报错，只能通过 `try-catch` 发现并 fallback
- **`fetch` 大文件（15MB）超时**后 Promise 可能永远 pending，必须加 `AbortController`
- **CSS 类名和 JS ID 必须精确匹配**，`toggle` 和 `trigger` 一字之差功能全废
- **PowerShell 不支持 `&&`**，构建脚本要用 `;` 串联命令
- **npm 全局安装可能触发 EPERM**，需要管理员权限或换用 `npx`
- **硅基流动 API 的 401 和 500 错误**要区分处理：401 是 token 问题，500 可能是内容审核不通过

## 关联知识

- **RAG → LangChain / LlamaIndex** — 成熟的 RAG 框架，本项目是它们的"极简实现"
- **前端向量搜索 → Transformers.js** — 可以在浏览器里直接跑模型，但本项目文件太大不适用
- **Cloudflare Worker → Cloudflare Pages / Vercel Edge Functions** — 同类 Serverless 平台
- **Embedding → 向量数据库 (Milvus / Pinecone / Qdrant)** — 工业级方案，本项目用纯文件替代

---

# 安全思考

- **API Key 泄露风险**：Key 存在 Worker 环境变量中，前端不可见。但 Worker 代码本身在 Cloudflare 控制台可见，需要确保控制台权限安全
- **CORS 白名单**：已经配置了 `localhost:4000` 和 `kukudelin.top`，避免被其他域名滥用
- **速率限制**：Worker 代码中配置了 `rateLimit`，防止恶意刷 API 导致费用暴涨
- **无 DoS 风险**：所有重计算（embedding）在构建时完成，Worker 只做轻量转发
- **内容审核**：硅基流动 API 自带内容审核，敏感内容会返回 500，已做好 fallback 跳过处理

---

# 延伸学习路线

```
Node.js 脚本（build_kb.js）
    ↓
Embedding 向量原理（余弦相似度、KNN）
    ↓
RAG 架构（检索 + 生成的 pipeline）
    ↓
Cloudflare Workers（Serverless 边缘计算）
    ↓
IndexedDB + 浏览器存储（前端缓存策略）
    ↓
CORS 安全模型（跨域资源共享）
    ↓
LLM API 调用与 Prompt Engineering
    ↓
从 RAG → Agent 演进（工具调用、多轮推理）
```

---

# 面试视角

## 可能会怎么问

- **"你们项目的 RAG 是怎么实现的？"** — 考察架构理解和选型理由
- **"为什么不用向量数据库？"** — 考察工程权衡能力
- **"Embedding 模型怎么选的？"** — 考察模型评估能力
- **"CORS 为什么会出问题？怎么解决的？"** — 考察浏览器安全模型理解
- **"15MB 的向量文件前端加载不会慢吗？"** — 考察性能优化意识
- **"API Key 怎么保护的？"** — 考察安全意识

## 你可以怎么答

回答框架：**先讲架构全景（离线 Embedding + 前端搜索 + Worker 代理），再解释为什么这样选（零成本、零运维、纯静态），最后补充踩过的坑（IndexedDB、CORS、大文件加载）**。

对于"为什么不加重向量数据库"这个问题，核心回答逻辑是：1500 篇文档 × 2560 维 = 约 15MB，浏览器遍历一遍不到 50ms，完全不需要额外数据库。量上去了（10 万+ 文档）才需要引入向量数据库。

---

# 我的收获

这次从零搭建博客 RAG 知识库问答，最大的收获不是技术本身，而是一种**"离线计算 + 在线检索"的架构思维**。

以前总觉得 AI 功能 = 后端服务 + 数据库 + GPU，这次证明了即使是纯静态博客，也能通过巧妙的计算前移实现完整的 RAG 能力。

另外印象很深的是调试过程——CORS 白名单、CSS 类名匹配、IndexedDB 兼容性、PowerShell 语法差异……每一个都是踩一脚才知道的坑。**技术的"最后一公里"往往不是算法，而是这些琐碎的工程量细节**。

最后，自己博客右下角出现一个能回答"我的 Docker 博客讲了什么"的聊天窗口时，那种满足感是无法替代的。
