KOLens
全部文章
·KOLens 团队AI工程成本缓存

为什么我们对每一次 LLM 调用都做缓存——dossier 背后的成本账

操作员的反馈直接到位:「请求返回的 overview 要落库,避免每次请求,造成浪费」。我们审计了每一个 call_llm 调用点,补齐了那两个没缓存的,并在所有地方加上 refresh 参数。每个 KOL 的 LLM 花费降到了操作员自己有意识做出的选择。

原则

每一次对同样输入可能被重复的 LLM 调用都必须缓存。 Refresh 按钮是唯一会重新向操作员计费的路径。

引发这次改造的审计

操作员反馈只有两句话:「请求返回的 overview 要落库,避免每次请求,造成浪费。 其他的所有文案如果是有调用 LLM 进行转换的,都要落盘」。 我们于是在 repo 里 grep call_llm + generateText。八个 LLM 驱动的功能。六个已经 缓存了。两个没有。

已缓存(不动)

  • kol_overview——DBKolOverviewSummary,30 天
  • kol_background——DBKolBackgroundResearch,30 天
  • kol_deep_overview——DBKolDeepOverview,7 天
  • kol_activity——DBKolActivitySummary,24 小时
  • translate——直接写 bio_zh / display_name_zh / category_zh 到 author 列;每个 author 每个字段只跑一次
  • 4 张 insight 卡——通过 insight_* 服务写入 DBSearchInsight

新增缓存

  • kol_outreach——新表 kol_outreach_drafts(migration 013),30 天, locale 关卡
  • kol_audience_insights——新表 kol_audience_insights(migration 013),30 天, locale 关卡 + snapshot hash 关卡

每个功能三种调用形态

每个被缓存的 LLM 功能都暴露同样的三个入口,调用层(HTTP 路由、MCP 工具、调度器)按需挑:

  1. read_cached(...)——绝不调 LLM。miss 返回 None。用于仅读缓存的场景,比如 watchlist 的品牌匹配度星级。
  2. get_or_generate(...)——cache-first。miss 或 过期时才调 LLM。
  3. regenerate(...)——一定调。对应 UI 的 Refresh 按钮 + ?refresh=true query。

把 locale 当成一等缓存 key 维度

2026 年 5 月给 dossier 加 zh-CN 时暴露出一个微妙的缓存 bug: zh-CN 用户来请求时被一行英文旧缓存命中了,要点 Refresh 才会 重生成。所以我们把 locale 写进缓存行的 JSON payload,_read_cached 现在把 locale 不匹配视作 miss。

向下兼容:上线前的旧行没有 locale 字段,默认按 "en" 处理。英文用户的旧缓存继续服务英文 请求——只有新增语言需要一次新鲜生成。

Snapshot hash 关卡(仅 audience insights)

audience insights 是异类:输入是操作员可以重新跑的抽样粉丝 快照。我们写入时把 snapshot dict 做 SHA-256;读取时如果调用方 给的 snapshot hash 跟缓存行不一致,自动失效——不需要手动 Refresh。

扣费在路由层,不在服务层

服务层不知道 credits 这回事。路由层先调 charge()(workspace 池 → 个人兜底),成功之后 才进 regenerate 路径。AI Gateway 在 debit 之后返回 503 时, 路由当即把 workspace 那一半退掉。

为什么拆开:缓存行只该在「操作员被计费 AND LLM 返回有效 内容」的双成立后才存在。两阶段提交在重试下会死锁;先扣后退 的模式更简单,也匹配 /api/scrape 已有的做法。

每个 workspace 实际的 LLM 花费

数量级估算,按每周打开 50 张 KOL dossier 的 workspace 计:

  • 改造前:50 次 dossier 打开 × 页面上 2 张 LLM 驱动的卡 = 约 100 次 LLM 调用 / 周
  • 改造后:第一周 50 次首生成,之后每周约 5-10 次刷新
  • 稳态下降 5-10 倍;第一周一样高,因为啥都没缓存

Deep Analysis(5 credits、按钮显式选择)这一档不变—— 那一档本身就是要有意识地点的。

准备好了?

立即试用 —— 注册即送 50 credits。

看一下效果——打开任意 KOL

常见问题

在这次改造前,有哪些功能没缓存?
两个:cold outreach 草稿(kolens-web/app/api/kols/[u]/outreach)和 audience-insight 解读(kolens-web/app/api/kols/[u]/audience/insights)。两个都活在 Vercel BFF 里,通过 generateText() 直连 AI Gateway——没有任何 DB 持久层。操作员第二次点 Generate 就要付第二次钱。
locale 关卡怎么实现的?
每个缓存表都把生成时的 locale 写在 JSON payload 里。read_cached 把这个字符串和调用方的 locale 对比;不一致按 cache miss 处理。在 locale-gate 上线前生成的旧行没有这个字段,默认按 'en' 处理——存量英文缓存继续给英文用户服务。
audience-snapshot 的 hash 关卡是怎么回事?
audience insights 有点特殊——输入是会变的抽样粉丝快照。写入时把 snapshot dict 做 SHA-256,读取时再校验。locale 一致 + snapshot 字节一致 → 命中缓存。locale 一致 + snapshot 不同(比如你重新采样了)→ 自动失效,不需要手动 refresh。
同一行缓存会被双扣费吗?
命中缓存的路径永远不会扣费。只有 regenerate / refresh 路径才扣。失败路径会退款(AI Gateway 503 在 debit 之后)——workspace 这边自动退;personal 这边需要操作员主动 claim,因为 Railway 写 Supabase 的链路有限制。
我更新品牌画像后,缓存行会自动失效吗?
说实话:还不会——这是 PR #255 和 #256 显式 out-of-scope 的部分。当前的做法是:改完品牌画像在 dossier 上点 Refresh。把品牌画像 hash 纳入缓存 key 是一个干净的后续。
30 天 TTL 的逻辑是什么?
Creator Overview:30d——主题稳定。Background Research:30d——身份事实漂移慢。Cold Outreach:30d——达人 profile 一周内不会变。Audience Insights:30d(hash 关卡处理了 snapshot 真变了的情况)。Activity Summary:24h——明确跟踪近期发布,窗口更短。Deep Analysis:7d——议价 / 合作洞察漂移更快。

继续阅读

为什么我们对每一次 LLM 调用都做缓存——dossier 背后的成本账 · KOLens | KOLens