· 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 做的事很少:
- 看长度(最多 200 字)和是不是在套系统提示。这种问题直接拒绝,不调用模型。
- 访客才在 D1 里记今天这个 IP 和全站已经问了几次。超了就返回「今天的额度用完了,明天再来」,同样不调用模型。站长本人不记这两笔。
- 用嵌入模型把问题变成向量,到 Vectorize 里找出最接近的几段。
- 把这几段和人设交给文本模型,流式把字传回页面。优先请求 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 绑定重写索引的那条路。