收尾两批并行开发的文档回写与任务包

- 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 中相应引用改为留痕表述
This commit is contained in:
xsl
2026-08-14 21:07:20 +08:00
parent 2934954eb2
commit e511b6a66f
22 changed files with 1407 additions and 461 deletions
@@ -0,0 +1,65 @@
# 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)。
偏差说明:<如有写在这里,无则写"无">
```