| name | rag-client-migration-pattern |
| description | 当 Cloudflare Pages/Workers Free 计划的 10ms CPU 限制阻塞服务端 AI 调用时,将 RAG 计算从 Pages Function 迁移到客户端浏览器的通用方案。包含三层递增架构(客户端→QMD BM25→讯飞+Vectorize)、跨架构 Docker 构建、Vectorize API 陷阱。 |
客户端 RAG 迁移模式
触发条件
- Cloudflare Pages/Workers Free 计划遇到 10ms CPU 时间限制
- 服务端 Workers AI embedding / reranker / Vectorize 触发 503/1102
- 不想升级 Workers Paid($5/月)
- 静态站点(GitHub Pages / Docker)也需要 RAG 能力
核心洞察
Cloudflare 10ms 限制的是 CPU 时间,不是 wall-clock。env.R2.get().body 流式返回是纯 I/O,不计 CPU。Pages Function 当"静态文件网关"完全合规。同样,fetch() 到外部 API 也是 I/O 等待,不计 CPU——这是绕过 Workers AI 限制的关键。
三层递增 RAG 架构(最终方案)
Layer 1: 浏览器 (rag-client.js) 0ms 关键词+近邻图 全部环境
→ 质量不足时
Layer 2: QMD BM25 (HP Docker) ~50ms SQLite FTS5 仅 HP 在线
→ 跨语言/语义时
Layer 3: 讯飞 + Vectorize ~300ms 语义搜索 CF Pages
→ 兜底
Pages Function: Phase 1 关键词 + Phase 2 Reranker
| Layer | 方案 | 延迟 | 成本 | 维护方式 |
|---|
| 1 | 客户端 rag-client.js (IndexedDB) | 0ms | $0 | 零运维,build.sh 自动 |
| 2 | QMD BM25 (HP Docker) | ~50ms | $0 (电费) | cron rsync docs/ → HP 增量索引 |
| 3 | 讯飞 xop3qwen8bembedding + Vectorize | ~300ms | $0 (API 免费) | cron 每天 03:30 全量重建 ~29min (并发 36 docs/s) |
| 兜底 | Pages Function rag-query.js | ~1s | $0 (Free 额度) | 自动 |
Layer 1 vs Layer 2 BM25 对比
| 维度 | 客户端 rag-client.js | QMD BM25 |
|---|
| 搜索算法 | 简单词频打分(tokenize + 计数) | SQLite FTS5 BM25(工业级全文检索) |
| 排序质量 | 词频分,标题 3x 正文 1x | TF-IDF 加权,BM25 算法 |
| 延迟 | 0ms(本地 IndexedDB) | ~50ms(HTTP 到 HP) |
| 离线可用 | ✅ 首次加载后可离线 | ❌ 需连 HP |
| 维护 | 零运维 | HP Docker 需维护 |
两者不冲突,Layer 1 覆盖 80% 场景,Layer 2 在需要更高质量关键词时使用。
方案 A:客户端 RAG(Layer 1,已上线 v1.3.3)
数据流
浏览器 rag-client.js
→ IndexedDB 缓存 search_index (61K docs) + neighbor_graph (57K nodes)
→ 关键词搜索 + 近邻图扩展 → 融合排序 → top 5
→ 注入 LLM → ai-proxy → MiMo API
降级路径: ragClient 不可用 → fetch(/rag-query) → 空结果静默降级
三环境最终状态(验证通过 v1.3.3)
| 能力 | Docker (8002) | GitHub Pages | CF Pages |
|---|
| 客户端关键词搜索 | ✅ | ✅ | ✅ |
| 近邻图扩展 | ✅ (注入) | ✅ (GHA) | ✅ (R2) |
| Reranker | ❌ nginx 兜底 | ❌ 无服务器 | ⚠️ Free 503 |
| AI Chat 面板 | ✅ | ✅ | ✅ |
方案 B:QMD BM25(Layer 2,HP Docker 备选)
部署
HP (192.168.0.111, x86_64, 47GB RAM, 12核)
→ Docker: qmd-server (port 8005, --restart unless-stopped)
→ Tunnel: qmd.jinguo.tech → HP:8005
→ 挂载: /root/.cache/qmd (索引) + /opt/wiki-book/docs (文档)
→ 索引: 14,473 篇文档, BM25 就绪
QMD 关键命令
npm install -g @tobilu/qmd
qmd collection add /path/to/docs --name wiki-book
qmd embed
qmd search "查询" --json
qmd query "查询"
qmd mcp --http --port 8005 --host 0.0.0.0
网络限制处理
export HF_ENDPOINT=https://hf-mirror.com
qmd embed
npm pack @tobilu/qmd
rsync tobilu-qmd-*.tgz hp:/tmp/
ssh hp "npm install -g /tmp/tobilu-qmd-*.tgz"
方案 C:讯飞 API + Vectorize(Layer 3,语义搜索)
为什么可行
Workers AI env.AI.run(): 计 CPU 时间 → Free 10ms 超限 → 503
HTTP fetch(讯飞 API): I/O 等待,不计 CPU ✅
Vectorize .query(): 纯 I/O,不计 CPU ✅
配置
| 项 | 值 |
|---|
| API | https://maas-api.cn-huabei-1.xf-yun.com/v2/embeddings |
| 模型 | xop3qwen8bembedding(8B 参数) |
| 维度 | 1024(Vectorize 上限 1536,模型支持 32-4096,1024 是交集) |
| 认证 | Bearer token,格式 key:secret |
| 向量库 | wiki-book-embeddings-v2(1024 dim, cosine) |
| 批量脚本 | scripts/build-vectorize-xunfei.py |
Python 全流程测试
import requests
r = requests.post('https://maas-api.cn-huabei-1.xf-yun.com/v2/embeddings',
headers={'Authorization': 'Bearer key:secret'},
json={'model': 'xop3qwen8bembedding', 'input': ['查询文本'], 'dimensions': 1024})
emb = r.json()['data'][0]['embedding']
r2 = requests.post(
f'https://api.cloudflare.com/client/v4/accounts/{ACCOUNT}/vectorize/v2/indexes/{INDEX}/query',
headers={'Authorization': f'Bearer {CF_TOKEN}'},
json={'vector': emb, 'topK': 10, 'return_metadata': True, 'return_values': False})
批量构建
scripts/build-vectorize-xunfei.py 流程:
- 读取
site/search/search_index.json
- 每批 20 篇(API_BATCH_SIZE),调讯飞 API 生成 1024 维向量
- ThreadPoolExecutor 并发 10 路(CONCURRENCY),匹配 Xunfei QPS=20 限制
- 每 100 条批量插入 Vectorize
- 63K 文档 ≈ 3,150 次 API 调用
并发优化(ThreadPoolExecutor)
旧版串行处理 63K 文档需要 ~188 分钟(9 docs/s)。利用讯飞 API 允许 20 并发、20 QPS 的限制,改为 ThreadPoolExecutor:
10 个 worker × 20 篇/请求 = 200 篇/wave
每请求 ~0.5s → 实际 ~20 QPS
63K 文档 → ~15 分钟(实测 29 分钟,含网络开销)
提速 6.5x
关键代码模式:
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(get_embeddings, batch, key): batch_i for batch_i in ...}
while futures:
for f in as_completed(futures.keys()):
batch_i = futures.pop(f)
embeddings = f.result()
futures[executor.submit(...)] = next_batch_i
break
并发参数说明
| 参数 | 值 | 依据 |
|---|
API_BATCH_SIZE | 20 | 每请求处理 20 篇文档 |
CONCURRENCY | 10 | 线程池大小,留余量(API 限 20) |
INSERT_BATCH_SIZE | 100 | Vectorize 单次插入上限 |
| 实际 QPS | ~20 | 10 并发 × 每请求 ~0.5s = 20 QPS |
从 Cloudflare Pages Functions 调用
全流程测试:
|---|-----------|-----|
| 本质 | 向量数据库 | 对象存储(类似 S3) |
| 存什么 | 向量([0.1, ...]) | 文件(JSON/图片) |
| 怎么用 | .query(向量, topK) → 最近邻 | .get(key) → 文件 |
| 能搜索吗 | ✅ 余弦相似度 | ❌ 只能按 key 读 |
| 修改 | ❌ 只能插/删 | ✅ 可覆盖 |
跨架构 Docker 构建(native addon)
问题
Mac ARM 上 npm install 的 native addon(better-sqlite3, node-llama-cpp 等)编译为 ARM .node 文件。推送到 x86_64 服务器时报 invalid ELF header 或 Error loading shared library。
推荐:目标机 Docker commit(最可靠)
docker run -d --name builder node:22-slim sleep 120
docker exec builder npm install -g @tobilu/qmd
docker commit builder qmd-server
docker rm builder
docker run -d --name qmd --restart unless-stopped -p 8005:8005 qmd-server
解法二:node:22-alpine + 预装 + COPY + npm rebuild(可自动化)
docker run -d --name builder node:22-slim sleep 120
docker exec builder npm install -g @tobilu/qmd
docker commit builder qmd-server
docker rm builder
解法二:预装 + COPY + npm rebuild(可自动化)
mkdir -p qmd-pkg && cd qmd-pkg
npm install @tobilu/qmd
FROM node:22-slim
COPY qmd-pkg/ /opt/qmd-pkg/
RUN npm rebuild better-sqlite3
RUN ln -sf /opt/qmd-pkg/node_modules/@tobilu/qmd/bin/qmd /usr/local/bin/qmd
解法三:本地 buildx + amd64(不推荐)
docker buildx build --platform linux/amd64 -t qmd-server --load .
npm install 仍使用本地 ARM Node.js,下载 ARM 二进制。需要 QEMU 模拟 + 容器内 npm rebuild。
Vectorize API 已知陷阱
参数命名:snake_case 不是 camelCase
{"return_metadata": true, "return_values": false}
{"returnMetadata": true, "returnValues": false}
维度兼容性
| 限制 | 值 |
|---|
| Vectorize 最大维度 | 1536 |
| 讯飞 xop3qwen8bembedding 支持 | 32, 64, 128, 256, 512, 768, 1024, 2048, 4096 |
| 推荐 | 1024(两边都支持且够用) |
日常维护(每日 cron 流程)
03:00 wiki-book-sync → 同步 wiki → mkdocs build → 近邻图 → 上传 R2
03:05 sync-docs-to-hp → rsync docs/ → HP,QMD 自动增量索引(几秒)
03:30 sync-vectorize → 全量重建 Vectorize(~29 min,10 并发 × 20 doc/批)
| 维护项 | 自动化 |
|---|
| Layer 1 客户端 RAG | ✅ build.sh 自动 |
| Layer 2 QMD BM25 (HP) | ✅ cron rsync + QMD 增量索引 |
| Layer 3 讯飞 + Vectorize | ✅ cron 每天 03:30 |
| 服务器兜底 Phase 2 | ✅ 部署时自动 |
实施翻车记录
1. CSR 矩阵维度不匹配
现象:axis 0 index 63011 exceeds matrix dimension 57389。近邻图构建时 doc_vectors 只有 57K 条目(有 >=2 词项的文档),但行索引最大 63K。
修复:build_csr_matrix 接受 n_docs = len(valid_docs) 参数。
2. JS brace 不匹配(rag-client.js)
现象:SyntaxError: Unexpected token ')'。if (!self._graph) 块缺少闭合 }。
根因:patch 上下文偏移 + 多层嵌套。
预防:增删代码块时逐层检查 brace 匹配,在 } 后加注释标记 // closes for / // closes if。
3. for 循环后中间变量未赋值
现象:fetch 返回 200,数据正确解析,但 self._graph 仍为 null。
根因:单路径赋值改为 fallback URL 循环后,漏了循环外 self._graph = graphData || {}。
检查清单:循环前声明 → 循环内赋值 → ✅ 循环外赋给目标字段。
4. Docker 文件权限(nginx 403)
现象:docker cp 后浏览器加载 JS 返回 403。
根因:docker cp 保留源权限 600,nginx worker 用户无法读取。
修复:docker exec <container> chmod 644 <file>。
5. 跨架构 native addon(invalid ELF header)
现象:ARM Mac 构建的 Docker 镜像在 x86_64 服务器上报 Error: invalid ELF header。
根因:better-sqlite3.node 等 native addon 编译为 ARM。buildx --platform linux/amd64 不自动重编译。
修复:目标机上 npm rebuild 或 docker commit。
7. Pages Secret 空值陷阱
现象:env.XUNFEI_API_KEY 在 Pages Function 中长度为 0。
根因:npx wrangler pages secret put 的交互式输入可能截断或保存空值。
修复:通过管道传入,避免交互式输入:
echo "actual-secret-value" | npx wrangler pages secret put SECRET_NAME --project-name=xxx
验证方式:在 Pages Function 中打印 key.length 确认非零。
8. wrangler.toml index_name 变更不生效
现象:修改 wrangler.toml 中 [[vectorize]] 的 index_name 后重新部署,Pages Function 仍查询旧索引。
根因:Pages 项目缓存了首次部署时的绑定配置。
解决:改用 HTTP API 直接查询 Vectorize,或在 Dashboard 中手动编辑 Functions 绑定的索引名。
9. Free 计划 503 模式
单次: ✅ 200 | 连 3 次: ❌ 503 1102 | 等 2-4s: ✅ 恢复
客户端 RAG 优先策略导致服务器 /rag-query 在三环境均未被实际调用。Reranker 在 Free 下间歇可用但不稳定。
10. 讯飞 API 网关 HMAC 退化
从 Cloudflare Pages Functions 调用讯飞 v2/embeddings 时,虽然传入标准 Bearer token,讯飞网关可能返回:
HMAC signature cannot be verified: enforced header 'host' not used for signature creation
根因:讯飞 API 网关对 Cloudflare IP 段将 Bearer 认证降级为 HMAC 签名验证。本地(中国 IP)调用正常,Cloudflare(美国 IP)触发退化。
修复:确保 XUNFEI_API_KEY Pages secret 正确存储(非空)。如果仍失败,需改用 HMAC 签名(app_id + api_secret + date + 签名算法)或通过 HP 中转。
用户偏好
- 部署前确认:不允许自动部署到生产域名
- 测试验证:Playwright E2E 测试(navigate → click → verify),不接受 curl/grep 替代
- Docker over systemd:服务必须容器化(
--restart unless-stopped),不接受宿主机直接安装。理由:系统重启后 systemd 可能失效,Docker 生命周期管理更可靠
- 成本驱动决策:优先找 Free 方案($0),其次才考虑付费升级。讯飞免费 API → Workers Paid $5/月 是备选
- 子代理独立 config:delegation 段 model/provider 独立于主模型
关联文件
| 文件 | 用途 |
|---|
references/original-server-side-architecture.md | 原始服务端 Phase 1+2+3 方案全文存档 |
references/zero-trust-ssh.md | Cloudflare Zero Trust SSH 配置步骤(HP 外网访问) |