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

66 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)。
偏差说明:<如有写在这里,无则写"无">
```