“不绑卡、不租服务器,把知识库跑起来”这句话是对的。但“跑起来”和“稳定跑”之间,隔着七个具体的数字限制。它们不会在写代码时提醒你,只会在批量任务跑到一半时给你一个 400。

下面每一条都是我在这个项目里真实撞上的,附上绕过方式和代价。

1. Workers AI 单请求 60000 token

嵌入模型不是按“条”计费,而是按 token。一次把 200 个 chunk 塞进去,最可能的结果是请求被拒,而不是排队。

我的做法是先算预算再分批:用 tokenizer 估算每段文本的 token 数,累加到接近 5 万就切一批,留出 20% 余量给模型自己的模板开销。批内再设一个条数上限,防止“200 条极短文本”这种边界情况。

代价是:请求数变多,总耗时线性增长,并且必须在失败时能定位到具体批次——所以每一批都带 batch_index 落日志,重试只重跑失败批次。

2. KV 单个 value 上限 25MiB

大产物(全量索引 JSON、图谱快照)写不进 KV。这个限制最初表现为 413,后来发现不是请求体太大,而是 value 太大。

绕法有两种:

  • 拆成多个 key,用 manifest.json 记录分片顺序;
  • 干脆不进 KV,把产物写进仓库,由 Pages 直接托管。

我选了后者:图谱和规范判定的结果本来就需要公开访问,放进 KV 反而多一次鉴权。

3. D1 单语句绑定参数约 100 个

WHERE id IN (?, ?, ...) 这种写法在 D1 上有绑定参数上限。超过之后不是慢,而是直接报错。图谱的邻域查询一次要查几十个节点、每个节点再查两边边,很容易超。

处理方式是把一批 ID 切成多个 IN 子句,分批执行再在内存里合并:

-- 每一批最多 90 个占位符,留一点余量
SELECT id, src, dst, rel, weight
FROM kg_edges
WHERE src IN (?, ?, ...) OR dst IN (?, ?, ...);

注意 src IN 和 dst IN 是两组占位符,所以实际单批只能放 45 个节点。这个数字要写在代码常量里,而不是散落在调用处。

4. 免费账户不支持子域名独立 Zone 路由

想让 api.example.com 指向 Worker,最直觉的做法是配一条 Zone Route。但免费套餐里,子域名的路由权限并不完整,配置会静默失效或直接拒绝。

最后的方案是同源转发:在 Pages 项目里放一个 Function,把 /api/* 转发到 Worker。好处不止是绕开权限:

  • 门户和 API 同源,Access 的登录凭据可以直接带上,不会反复弹验证码;
  • 前端不用处理跨域和预检;
  • Cookie、CORS、鉴权都少一层心智负担。

代价是多了一跳转发,Worker 里拿到的客户端 IP 需要从转发头里取,而不是直接信任 CF-Connecting-IP。

5. 配了 ACCESS_AUD 之后,所有请求都 401

这是最容易踩、也最容易被误判为“鉴权失败”的一条。

Cloudflare Access 校验 JWT 时,需要 ACCESS_AUD。一旦你把它当成唯一凭据,那些走 Bearer Token 的机器调用(CI、脚本、MCP)就全部被拒。正确做法是双通道:

  1. 有 Cf-Access-Jwt-Assertion 头就校验 JWT;
  2. 没有就回退校验 Authorization: Bearer <secret>;
  3. 两者都没有才 401。

关键是第 1 步失败时要明确区分“没带 JWT”和“JWT 无效”:前者应该继续走第 2 步,后者应该直接拒绝。

6. Workers 域会被边缘拦截

在 Worker 内部回调 Pages 的接口时,用 *.workers.dev 域名有时会被边缘拦掉,返回 403 或直接超时。原因不在代码,在路由层。

解决办法是回调一律走 Pages 项目自己的域名。这条经验没有文档,只有日志:同一个请求换域名就通了。

顺带一提,本机访问 Cloudflare API 也存在“部分 IP 不可达”的情况,表现是单个请求卡两分钟才超时。我的处理是启动时并行探测多个可用 IP,之后复用连接,避免每个请求都重新握手。

7. 枢纽节点会让邻域查询爆炸

图谱里总有那么几个节点,连向所有文档:device、ios、tools。从它们出发做 2 跳邻域,结果会瞬间上千条,前端渲染卡死,查询也超时。

三条约束必须同时存在:

  • 节点总数上限(例如 200);
  • 每层扩展数上限(例如 50);
  • 禁止穿透枢纽节点:邻居数超过阈值的节点只返回自身,不再向下展开。

前两条控制结果规模,第三条控制深度。只做前两条的话,查询还是会先算完再截断,CPU 时间照烧。

还有一条:把重计算放到离线管线

规范判定曾经跑在请求里,结果经常因为单请求 CPU 时间限制失败。后来把它整体移到离线管线:构建时算好、写进产物、请求只读结果。

这是架构层面的取舍:牺牲了“实时性”,换来了稳定性和可预测的延迟。对一个个人知识库来说,这个交换非常划算。

总结

免费额度不是“没有成本”,而是把成本从账单转移到了工程细节:

限制 触发表现 绕法
60000 token 嵌入请求 400 预算分批 + 批次重试
25MiB / value KV 413 分片或改走 Pages
~100 绑定参数 D1 绑定报错 拆批 IN 查询
Zone 路由权限 配置无声失效 Pages Function 同源转发
ACCESS_AUD 机器调用全 401 JWT + Bearer 双通道
Workers 域 回调 403 走 Pages 域名
枢纽节点 邻域爆炸 限节点 + 禁穿透

这七条都不难,难的是它们只会在你忘了的时候出现。现在它们被写进了项目的约束清单,下次换项目还能直接复用。