为什么我们对每一次 LLM 调用都做缓存——dossier 背后的成本账
操作员的反馈直接到位:「请求返回的 overview 要落库,避免每次请求,造成浪费」。我们审计了每一个 call_llm 调用点,补齐了那两个没缓存的,并在所有地方加上 refresh 参数。每个 KOL 的 LLM 花费降到了操作员自己有意识做出的选择。
原则
引发这次改造的审计
操作员反馈只有两句话:「请求返回的 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 工具、调度器)按需挑:
read_cached(...)——绝不调 LLM。miss 返回None。用于仅读缓存的场景,比如 watchlist 的品牌匹配度星级。get_or_generate(...)——cache-first。miss 或 过期时才调 LLM。regenerate(...)——一定调。对应 UI 的 Refresh 按钮 +?refresh=truequery。
把 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、按钮显式选择)这一档不变—— 那一档本身就是要有意识地点的。
常见问题
- 在这次改造前,有哪些功能没缓存?
- 两个: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——议价 / 合作洞察漂移更快。
继续阅读
品牌匹配度档案:从 Creator Overview 到 Deep Analysis 的三层 AI 达人尽调
三层 LLM 解读,给每位海外达人一份完整尽调档案:1 credit 的带评分 Creator Overview、5 credits 的 Deep Analysis 进阶层、附引用的联网 Background Research。全部缓存、支持多语言。
Audience Snapshot 正式上线:用抽样数据看清 TikTok 达人粉丝画像
KOLens 的 Audience Snapshot 今天上线:达人粉丝住在哪、说什么语言、有多活跃、关心哪些品类。统计抽样附 95% 置信区间 / 误差棒——出海卖家 TikTok 投放前的真实受众尽调。