给 Hexo 博客加一个 AI 助手:MiniMax + Cloudflare Workers 实战
我的博客已经有项目分类、公开简历和本地搜索,但读者仍然要自己翻文章。于是我做了一个“问博客”助手:输入问题,它先从博客中找相关文章,再把少量公开片段交给 MiniMax 生成回答,并附上来源链接。
这篇记录从前端检索、Cloudflare Worker、MiniMax-M3 接入到安全部署的完整过程,也会说明它为什么还不算完整的向量 RAG,以及我准备怎样用最小成本升级。
先说结论:静态博客也能做 AI 问答
Hexo 部署到 GitHub Pages 后只有静态文件,不能在页面里直接调用大模型:
- API Key 一旦写进前端,就等于公开;
- 浏览器直连模型接口会遇到跨域和滥用问题;
- 把整站文章都塞给模型,既慢又浪费 Token;
- 公网接口如果没有限流,额度很容易被刷掉。
最后采用的链路很短:
1 | 读者提问 |
这个版本已经有“检索后再生成”的核心思路,但检索还是关键词和中文二元词组匹配,没有 Embedding、向量库和重排。因此更准确的叫法是:检索增强问答 v0,不是我最终想做的完整语义 RAG。
一、前端:复用 Hexo 已有的搜索索引
博客已经安装 hexo-generator-searchdb,每次构建都会生成 /search.xml。里面有文章标题、链接和正文,因此第一版完全没必要再造一套索引。
1. 用 NexT 的扩展点加载脚本
在主题配置中指定自定义文件:
1 | custom_file_path: |
body-end.njk 只负责注入脚本和服务地址:
1 | <script |
如果 endpoint 为空,脚本直接退出,按钮也不会显示。这样 Worker 还没部署好时,博客不会出现一个必定报错的入口。
2. 在浏览器中完成轻量召回
前端读取 search.xml,去掉 HTML 后,对问题做两种切分:
- 英文、数字按普通词项匹配;
- 中文按二元词组匹配,例如“数据归集”会拆出“数据、据归、归集”。
每篇文章按标题命中和正文命中计分,最后只保留 Top 4:
1 | const matches = posts |
为了控制请求体,每段正文最多取 1400 个字符。前端只发送:
1 | { |
第一版把检索放在浏览器有三个好处:零新增依赖、零额外存储、文章更新后随 Hexo 构建自动生效。
二、Worker:把它当成安全边界
Cloudflare Worker 不只是“隐藏 Key 的转发器”,它还承担输入校验、数据脱敏、超时和限流。
1. 只接受博客域名
1 | const allowedOrigin = env.ALLOWED_ORIGIN; |
CORS 能阻止普通网页跨站调用,但它不是防爬虫方案,因为脚本可以伪造 Origin。所以后面还必须有限流。
2. 在服务端再次限制输入
客户端校验可以被绕过,Worker 仍要检查:
- 问题不能为空,最多 300 字;
- 最多接收 4 段上下文;
- 每段正文最多 1400 字;
- 总上下文最多 5600 字;
- 非 JSON、错误路径和错误方法立即拒绝。
发送给模型前,还会处理公开文章中可能混入的 URL、邮箱、IPv4、Token 形态字符串和控制字符。博客本来就是公开内容,但“公开”不代表要把所有原文无差别转交给第三方模型。
3. 给模型一个窄任务
系统提示词只做一件事:
1 | 只根据提供的公开博客片段回答; |
这里特意把文章片段视为不可信数据,是为了降低提示词注入风险。Worker 还设置了 30 秒超时、800 个输出 Token,并且不自动重试,避免一次前端点击产生多次计费。
三、通过 Anthropic 兼容接口调用 MiniMax-M3
本次使用:
1 | Base URL: https://api.minimaxi.com/anthropic |
请求体保持最小:
1 | const response = await fetch( |
接口文档的模型列表有时会落后于实际能力,因此最终判断不能只看名称列表。我用部署后的 Worker 做了一次真实调用,MiniMax-M3 返回 200 和正常中文答案,才继续接入博客。
四、Cloudflare 部署:密钥、限流和 Node 版本
1. Worker 配置
公开变量写进 wrangler.toml,密钥绝不能写:
1 | name = "zhlifeiy-blog-assistant" |
Worker 用 CF-Connecting-IP 作为当前匿名访客的限流键,同一来源每分钟最多 10 次。共享出口可能让多名用户共用限额,但对个人博客来说,这是不引入登录系统的最小保护。
2. 部署与 Secret
1 | npx wrangler login |
输入 Secret 时字符不显示是正常的。不要把 Key 放进命令参数、.env 后误提交,也不要发到聊天或工单。
我这里还碰到一个版本坑:常用的 Node 20.18.0 比新版 Wrangler 间接依赖要求的 20.18.1 少一个补丁版本。电脑已有 Node 22,所以直接用 Node 22 跑 Wrangler,没有升级全局版本,也没有影响仍依赖 Node 14 的老项目。
五、这次最有价值的四个排障点
1. OAuth 页面登录了,不等于授权完成
wrangler login 会启动本地回调服务器。浏览器进入 Cloudflare 后,还要及时点击 Authorize。如果停留太久,终端会报:
1 | Timed out waiting for authorization code, please try again. |
这不是账号错误。重新运行登录命令,并确认 NO_PROXY 包含 localhost,127.0.0.1 即可。
2. Secret 名称存在,不代表值可用
wrangler secret list 只能证明有一个叫 MINIMAX_API_KEY 的绑定,不能证明它是非空值。
我的 Worker 用这一段做运行时判断:
1 | if (!env.MINIMAX_API_KEY) { |
一个很实用的验证方法是发送格式合法但业务参数无效的请求:
- 返回
503:Worker 没读到 Secret; - 返回
400 Invalid input:Secret 已进入运行时,但请求在调用模型前被拦截; - 返回
200:完整链路正常; - 返回
502:已经调用上游,需要检查模型名、接口协议或账户额度。
不要只看 CLI 的 Success 就宣布完成,要验证运行时行为。
3. Key 一旦出现在公开位置,必须轮换
“只发给可信的人”不是密钥保护。聊天记录、截图、日志和终端历史都有可能长期保存。只要完整 Key 离开了密码管理器或 Secret 输入框,就应该:
- 立即删除或吊销旧 Key;
- 创建新 Key;
- 只写入 Cloudflare Secret;
- 扫描 Git 仓库确认没有残留;
- 再做线上调用。
4. GitHub Pages 推送成功后仍可能看到旧页面
hexo deploy 推送成功,只能说明发布仓库已更新。GitHub Pages 和 CDN 仍可能延迟几十秒。
我最后分别验证了:
1 | 首页 200 |
远端提交已经变化但页面没刷新时,等待后带查询参数复查即可,不要反复部署制造无意义提交。
六、测试:最少,但必须覆盖边界
Worker 使用 Node 内置测试,不增加测试框架:
1 | node --check worker.mjs |
目前覆盖三条最关键链路:
- 没有 Secret 时返回 503;
- 超出限流时返回 429,不调用 MiniMax;
- Anthropic 请求会脱敏 URL,并且只返回文本答案。
Hexo 侧则执行:
1 | npm run clean |
再检查首页是否包含 Worker 地址、助手脚本是否生成、站内链接是否有 404。对这个体量的个人博客,这些验证已经够用。
七、现在离真正的 RAG 还差什么
当前版本的检索依赖关键词,优点是简单、快、免费,缺点也很明确:
- 问法和原文用词不同,可能完全召回不到;
- 长文章只靠词频,相关段落不一定排在前面;
- 跨多篇文章综合回答时,Top 4 容易选偏;
- 没有语义重排,也没有可量化的召回指标。
下一版我不准备直接手搓 Vectorize、Embedding 管道和增量同步。对初学者而言,最短路径是 Cloudflare AI Search:
1 | sitemap.xml |
AI Search 可以直接连接网站,支持自动索引、混合搜索、元数据过滤和 Worker Binding。这样可以保留现有 MiniMax 生成层,只替换“浏览器关键词召回”这一段。
八、我的 RAG 升级计划
第 0 步:先做一份问题集
先准备 20 个真实问题,并记录期望命中的文章,例如:
| 问题类型 | 示例 | 期望来源 |
|---|---|---|
| 精确关键词 | Redis DB 为什么会导致园区切换失败? | Redis DB 排障文章 |
| 同义表达 | 怎样避免循环查库? | N+1 / 批量预加载文章 |
| 项目过滤 | 数据归集项目有哪些踩坑? | 数据归集分类文章 |
| 跨文综合 | 多园区改造涉及哪些服务? | 多篇多园区文章 |
先用当前版本跑一遍,记录 Top 4 是否包含正确文章。没有这份基线,换成向量检索后只能凭感觉说“好像更聪明了”。
第 1 步:创建 AI Search 实例
在 Cloudflare 控制台创建 AI Search,数据源选择网站:
1 | https://zhlifeiy.codes |
使用站点已有的 sitemap.xml,排除这些低价值页面:
1 | /tags/** |
模型先选 Smart Default。中文召回效果不够时,再测试 Qwen3 Embedding 或 BGE-M3;不要第一天就同时调切块、Embedding、Top K 和重排,否则根本不知道是哪项起作用。
第 2 步:Worker 改为服务端检索
给现有 Worker 增加 AI Search Binding,收到问题后:
- 调 AI Search 的 Search API;
- 获取 Top 5 片段;
- 保留标题、URL 和正文;
- 把片段交给现有 MiniMax 调用;
- 在响应中返回可点击来源。
前端不再下载整份 search.xml,移动端首屏也会更轻。
第 3 步:加入分类过滤和引用校验
利用博客现有的项目分类做元数据过滤:
1 | 水文 / 南网 / 南沙物联网 / 数据归集 |
如果用户明确问“水文项目”,检索前先限定分类,再做语义召回。答案中的来源链接必须来自实际召回结果,禁止模型自己编 URL。
第 4 步:指标不够再加重排
只有当测试集显示“正确文章进入 Top 10,但进不了 Top 3”时,才增加 Reranker。否则先调切块大小、重叠和过滤规则。
自己管理 Vectorize 更适合后续学习:当我需要自定义入库流程、版本控制、离线评测或更换 Embedding Provider 时再做。现在为了“拥有一个向量库”而增加同步脚本、维度管理和删除逻辑,不划算。
参考资料
- MiniMax Anthropic API 兼容
- Cloudflare Workers Rate Limiting
- Cloudflare AI Search
- AI Search 快速开始
- AI Search 元数据过滤
结语
这次最重要的不是给博客塞了一个聊天框,而是把边界划清楚了:
- Hexo 负责内容和索引;
- 浏览器负责交互;
- Worker 负责安全和调用编排;
- MiniMax 只根据公开片段生成答案;
- Secret 永远不进入前端和 Git;
- RAG 升级先做评测,再替换检索层。
第一版只有少量原生 JavaScript、一个 Worker 和三条测试,已经能稳定工作。下一步也不需要推倒重来:保留 UI、限流和 MiniMax,把 search.xml 关键词召回替换成 AI Search,就能完成从“能问”到“更懂语义”的升级。