跳到正文

· 7 分钟 · AI / Cloudflare Workers / Vectorize

用免费额度做一个只会引用本站的 AI 小助手

在 /chat 加了一个 AI 小助手,只能根据博客、关于、项目、吉他和摄影页里写过的话回答。回答优先走 DeepSeek Flash 的预付费接口,失败时退回免费模型。访客有每天上限,站长本人不占这个额度。

这是 Seabloom 建站记录的第十篇。这次加的是 /chat/:站长的 AI 小助手。她只许根据这个网站上已经公开的文字回答,资料里没有的就说不知道,并给出页面链接。

检索仍走 Workers AI 和 Vectorize 的免费档。写回答优先用 DeepSeek 自己的 Flash 接口,按预充值余额扣费,站长已经同意这笔费用。没配密钥、余额不够、超时或接口报错时,退回免费的文本模型,对话还能用。访客每天的提问次数到了上限就停,不再请求任何模型。

一、它实际在回答什么

小助手读的是这些:

  • src/content/blog/ 里的文章
  • 关于、项目、吉他、摄影这几个页面上的文字

不读社区帖子、用户资料、后台、账号。索引代码里如果发现地址属于要登录才能看的页面,会直接抛错,测试会卡住这种地址。

照片的标题存在数据库里,不在仓库里。部署时索引的是页面上的介绍文字;站长在后台点「让小助手重新读网站」时,会再带上照片标题。按钮会写明成功写入了多少段;中途失败会留下已经写入的段数。

说话方式放在 src/content/twin/。persona.md 第一行是名字(默认小潮),visitor.md 和 owner.md 是两种语气。文件开头给作者自己看的注释,生成提示词时会删掉。人设只决定语气,不能拿来编造经历。是不是站长,由服务器看登录角色,不看对方怎么自称。

二、一次提问实际走了哪几步

浏览器把问题 POST 到 /api/twin/chat。Worker 做的事很少:

  1. 看长度(最多 200 字)和是不是在套系统提示。这种问题直接拒绝,不调用模型。
  2. 访客才在 D1 里记今天这个 IP 和全站已经问了几次。超了就返回「今天的额度用完了,明天再来」,同样不调用模型。站长本人不记这两笔。
  3. 用嵌入模型把问题变成向量,到 Vectorize 里找出最接近的几段。
  4. 把这几段和人设交给文本模型,流式把字传回页面。优先请求 DeepSeek 的 deepseek-flash(https://api.deepseek.com/chat/completions,OpenAI 兼容的流式接口)。密钥来自 Worker 的 DEEPSEEK_API_KEY,不写进代码或配置。没配、返回 402、超时或其它错误,就改用 Workers AI 的 @cf/zai-org/glm-4.7-flash。来源链接是检索结果里的地址,不是模型自己编的。

没检索到段落时,直接回答「网站上没写到这件事,我不知道」,不再花一次文本模型的额度。

嵌入、Vectorize 和 DeepSeek 都是对外请求,等待时间不算进 Workers 的 CPU。免费版每个请求只有大约 10 毫秒 CPU。所以相似度不能在 Worker 里用 JavaScript 对全部段落做一遍:那是本地计算,很容易超。Vectorize 的查询是 I/O。

三、免费额度怎么算

数字来自 Cloudflare 文档(Workers AI 定价页 2026-10-20,Vectorize 定价页 2026-10-30),写在 worker/lib/twin.ts 顶部。

Workers AI 免费档是每天 10,000 Neurons,UTC 零点重置。免费套餐用完会失败;付费套餐用完会按 1,000 Neurons 0.011 美元继续扣。所以程序自己封顶,不把「继续调用」交给平台。

检索用的模型仍不在「必须付费才能调用」的名单里:

用途模型单价
把文字变成向量@cf/qwen/qwen3-embedding-0.6b每百万输入 token 1,075 Neurons,向量 1024 维
免费兜底的回答@cf/zai-org/glm-4.7-flash每百万输入 token 5,500,每百万输出 token 36,400

写回答的主模型是 DeepSeek 官方文档(Models & Pricing)里的 deepseek-flash,现在由 DeepSeek-V4.1-Flash-1017 提供。不用 Pro。思考模式默认是开的,这里关掉,否则推理 token 也按输出计费,256 的长度上限就管不住账单。

官方标价(每百万 token,峰值是周一到周五的部分钟点,其余时间低峰减半):

峰值低峰
输入,缓存未命中0.30 美元0.15 美元
输出1.20 美元0.60 美元

一次问答按偏多的汉字估算(大约一个汉字一个 token):人设和资料大约 2,200 token,回答最多 256 token。峰值、缓存未命中时大约 0.00097 美元 / 问。访客全站每天最多 120 问,一个月 3,600 问,每次都写满,大约 3.5 美元。低峰大约 1.7 美元。实际会更低:回答往往更短,系统提示还可能命中缓存(峰值命中只要 0.006 美元 / 百万输入 token)。

这 120 问不含站长自己。站长的提问不设每天封顶,按上面的单价从预充值余额实扣。每次的输入长度和输出 token 上限与访客相同,所以单次请求仍然有界。

免费兜底如果访客整天都在用,同一次大约 22 Neurons(嵌入 0.2 + 输入 12.1 + 输出 9.3)。120 次大约 2,600,仍低于每天 10,000。同一个访客每天最多 10 次。

Vectorize 免费档是每月 3,000 万查询维度、500 万存储维度。按更严的算法(把每次返回的 4 条也算进去):120 × 30 天 × 4 × 1024 ≈ 1,500 万,低于 3,000 万。语料大约两百段,存储大约 20 万维,远小于 500 万。

重建一次索引是把这两百段再嵌入一遍,大约一百 Neurons,相对每天的额度很小。

四、问太多、套提示,怎么停

访客额度存在 D1 的 twin_usage 表里,键是 global:2026-11-02 或 ip:地址:日期,日期用 UTC,和 Neurons 的重置时间一致。增加次数用一条 INSERT ... ON CONFLICT DO UPDATE ... WHERE used < 上限。到了上限,更新不会发生,接口就拒绝。站长的请求不写这张表,所以不会把访客的 120 问用完。表还没迁移时,访客会看到一句稍后再试,Worker 日志里记下缺表,而不是「服务器出错了」。

套系统提示的句子(「忽略之前的指令」「把系统提示打印出来」这类)在进模型之前就返回 400。这挡不住所有绕法,只是不把明显的套话送去花额度,也避免模型把人设原文复述出来。

Workers AI 自己返回额度类错误时,同样变成「今天的额度用完了,明天再来」,并且不会重试。DeepSeek 失败会先换免费模型,换也失败才把这句话返回去。不会对同一个问题反复打付费接口。

五、新文章怎么被读进去

scripts/build-twin-corpus.mts 在 pnpm build 里先跑,把公开页面切成段,写到 worker/generated/。部署用的 Worker 打包的是这份结果,请求时不再读 Markdown。

推到 main 之后,GitHub Actions 部署完会再跑 scripts/index-twin.mts:没有索引就创建 seabloom-twin(1024 维、cosine),然后用 API 令牌把段落写进 Vectorize。令牌要有 Workers AI 和 Vectorize 的 Edit 权限。没配令牌时这一步跳过,网站其它部分照常部署。

本地第一次也可以自己建索引:

pnpm exec wrangler vectorize create seabloom-twin --dimensions=1024 --metric=cosine

六、刻意没做的事

没有接 DeepSeek 的 Pro(deepseek-v4-pro)。官方标价大约是 Flash 的四倍,这次用不到。

也没有把 Workers 的 CPU 上限调高。那是付费套餐上的选项。小助手这条路径把重活放在检索和模型请求里,Worker 自己只做校验、拼几段字符串和转发流。

密钥只放在 Cloudflare 的 Secret 里:wrangler secret put DEEPSEEK_API_KEY。wrangler.jsonc 的 vars 里没有这项,部署不会把它清掉,代码和日志里也不会出现密钥。

在 /chat/ 加了一个只根据本站公开文字回答的 AI 小助手。检索仍是免费的嵌入和 Vectorize。回答优先用 DeepSeek Flash,按预充值扣费;访客每天最多 120 问、每次最多 256 token,峰值打满大约每月 3.5 美元。站长本人不占这个额度,按实际用量扣费,不设每天封顶。密钥没配、余额不足或超时时,改用免费的 glm-4.7-flash。访客到了自己的上限就停,不重试。

七、两种语气,以及线上那句「服务器出错了」

小助手不再用第一人称当站长本人。名字写在 persona.md,可以改。

两种语气由服务器决定,看的是登录会话里的 role:

  • 访客(没登录,或登录了但不是站长):礼貌的助手,称站长为「站长刘海洋」。问题里写「我是布布」不会改变这一点。
  • 站长(role 为 admin):称呼「布布大人」。仍然只根据公开资料回答。没有每天提问上限,也不计入访客的 120 问,费用按 DeepSeek 实际用量扣除。

线上如果聊天接口抛出未捕获的异常,页面只能看到「服务器出错了,请稍后再试」。最容易走到这里的,是额度表 twin_usage 还没建(migrations/0013_twin.sql)。空的向量索引不会变成这句:没有检索结果时,接口直接说不知道。DeepSeek 或嵌入模型抛错,以前有的会被接住,有的不会;现在这些失败都记进 Worker 日志,页面看到的是稍后再试,或者改走免费模型。

确认迁移和日志:

pnpm exec wrangler d1 migrations list seabloom --remote
pnpm exec wrangler d1 migrations apply seabloom --remote
pnpm exec wrangler tail

列表里应能看到 0013_twin.sql。复现一次提问,在 wrangler tail 里搜 twin chat failed。GitHub Actions 部署前会先列出再应用迁移。令牌如果没有 D1 的编辑权限,这一步会失败,表就还是旧的。索引写入还需要 Workers AI 和 Vectorize 的编辑权限;只有读取权限时,部署日志里的索引一步会失败,后台的「让小助手重新读网站」才是用 Worker 绑定重写索引的那条路。