Files
meida/docs/开发任务包/T14-检索缓存与失效.md
T
xsl e511b6a66f 收尾两批并行开发的文档回写与任务包
- Sprint 执行计划 v1.1:§1.1 补第一批交付实据;T9 SPIKE 结论回写 Sprint 1 表
  (T-P0-16 通过 2.8ms;T-P0-12 有条件;T-P0-10 原型 6/6 + 真实照片重验前置);
  Sprint 2 表标注 T-P2-01~06 交付;新增 §9 两批交付记录
- 开发任务拆解 v1.5:交付回写 + 新风险登记(RTMPose 域差异、BiRefNet 异步化)
- 新增 开发任务包/:三批多 agent 并行的协作规程与 14 份任务契约
- 新增 前置开通清单-云资源与资质.md
- 清理已用毕的评审报告(文档评审 2026-08-13、项目整体评审 2026-08-14),
  README 中相应引用改为留痕表述
2026-08-14 21:07:20 +08:00

5.1 KiB
Raw Blame History

T14 · 检索结果缓存与发布失效

对应任务:T-P2-10 | 需求:发布后检索缓存自然失效(Sprint 2 验收行)| 分支:task/t14-retrieval-cache | worktree:/home/xsl/meida_backend-t14 先读《README.md(协作规程)》,遵守全部纪律。

1. 目标

给 RetrievalService.search 加进程内结果缓存(TTL + 容量上限 + 代数失效),把协调提交里的 invalidate_cache() 桩变成真实现。缓存解决的是"同一 query 高频重复检索"(Agent 多轮、黄金集回归);失效保证知识发布后不吐旧结果。

2. 已就绪的契约(协调提交 9c18021 已提供)

  • 配置(config.py 已加,.env.example 已示例):retrieval_cache_enabled=True、retrieval_cache_ttl_seconds=60、retrieval_cache_max_entries=512。
  • 钩子已存在:packages/retrieval/service.py 模块级 invalidate_cache()(当前只是 repo.INVALIDATION_VERSION += 1)与 packages/retrieval/repo.py 的 INVALIDATION_VERSION = 1——运营后台(T12)已经在调它。你实现真缓存,不改钩子签名。
  • RetrievalService.__init__(session, *, embedding, weights, top_k) 与 from_settings(session, settings)——缓存参数经 from_settings 读 Settings 注入(不改 __init__ 既有签名,加带默认值的 keyword-only 参数)。

3. 实现要点

允许改动:packages/retrieval/cache.py(新建)、packages/retrieval/service.py(缓存接线)、测试文件。不改 repo.py 的既有函数与 INVALIDATION_VERSION 语义。

  1. packages/retrieval/cache.py(纯逻辑,重点单测):
    • CacheKey:(version, query_text, body_shape, occasion, entity_types, rule_boost_hash, top_k)——全部影响结果的输入都必须进键,缺一个就会串结果(rule_boost 是 dict[int,float],排序后序列化再哈希);
    • TtlCache:容量上限用简单 LRU(OrderedDict 移动末尾 + 超限 popitem(last=False));TTL 用惰性过期(读时比对时间戳,过期视作 miss);线程安全不需要(asyncio 单循环,无 await 竞态窗口——注释写明这个前提,别加锁);
    • 指标:hits/misses/evictions 计数器(Prometheus 可后续接,先暴露属性)+ stats() 方法。
  2. service.py 接线:
    • RetrievalService 增加 cache: TtlCache | None(None = 禁用,行为与现在完全一致);
    • search():cache 非 None 且非 degraded 路径——命中直接返回缓存 SearchOutcome;未命中照常检索后只在有结果时写缓存(空结果不缓存:record_gap 每次都要跑,否则缺口计数被缓存吞掉);
    • from_settings 按 Settings 装配缓存;模块级 invalidate_cache():把 service 模块持有的共享缓存实例(或 repo.INVALIDATION_VERSION,二选一,选实现更干净的那个)作废——调用方(admin 发布编排)不需要知道细节。
  3. 正确性红线(写进 cache.py docstring):缓存的是 SearchOutcome(frozen dataclass,不可变)——绝不缓存可变对象引用;命中路径不得复用 session(缓存命中时 DB 零交互)。

4. 测试要求

  • 单测 tests/unit/test_retrieval_cache.py(fake repo/embedding,不连库):
    1. 同参数二连查:第二次零 DB 调用(fake 计数断言)、结果相等;
    2. 任一键分量变化(query/occasion/body_shape/rule_boost/top_k)→ miss;
    3. TTL 过期 → miss(注入 clock);
    4. LRU 容量超限 → 最旧被逐、计数正确;
    5. 空结果不缓存(degraded 路径每次都触发 gap 记录 fake 调用);
    6. invalidate_cache() 后同参数 → miss(代数失效);
    7. cache=None → 行为与现在一致。
  • 集成 tests/integration/test_retrieval_cache.py(新文件):
    1. seed 实体 + chunk → search 命中 → 发布新实体(直接调 kb service 或 SQL 置 published)+ invalidate_cache() → 再 search 能看到新实体(这是 T-P2-10 的验收语义);
    2. 不失效等 TTL 过期(TTL 配 1s,sleep 1.1s)→ 新实体同样可见(双保险)。

5. 禁改清单

通用禁改之外:packages/retrieval/repo.py 的既有函数、packages/{kb,rules}/**、apps/**、所有已有测试文件。T12 会调 invalidate_cache()——签名不变是你与它的契约。

6. 验收标准(T-P2-10)

  1. 同参重复检索零 DB 交互(单测断言);
  2. 发布后新内容立即可见(invalidate 路径)且 TTL 到期自然可见(双路径集成测试);
  3. 影响结果的输入全量进缓存键(rule_boost 哈希);
  4. 空结果不缓存(gap 埋点不被吞);
  5. 全部检查绿(mypy 范围照 T6 文档同口径 + packages/retrieval)。

7. 提交信息

feat(retrieval): TTL+LRU result cache with generation-based invalidation

TtlCache(全输入进键/惰性过期/容量逐出/命中零 DB)+ search 接线
(空结果不缓存,保住 gap 埋点)+ invalidate_cache 真实现(T-P2-10)。

偏差说明:<如有写在这里,无则写"无">