Files
meida/docs/开发任务包/T8-向量与混合检索.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

84 lines
6.7 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.
# T8 · 知识库·向量与混合检索
> 对应任务:`T-P2-03`(向量化可重跑/模型可切换)、`T-P2-04`(检索、硬过滤零漏)、`T-P2-06`(无结果 query 记录)| 需求:`FR-KB-03/04`、架构 §8.3、`TD-08` | 分支:`task/t8-retrieval` | worktree:`/home/xsl/meida_backend-t8`
> 先读《README.md(协作规程)》,遵守全部纪律。
## 1. 目标
pgvector 上的混合检索管线骨架:**硬过滤 → 向量召回 → 关键词召回 → RRF 融合重排**,加 `kb_chunk` 写入通道(向量 + tsvector 双索引)与 `kb_gap` 知识缺口埋点。检索是 Agent(M4-b)和报告(M3)的共同前置。
## 2. 已就绪的契约(协调提交已提供,禁改 models/migrations)
### 表(迁移 `0005` 已建,ORM `KbChunk`/`KbAsset`/`KbGap` 已加,先读 models.py 核对)
- `kb_chunk`:`chunk_id` PK、`entity_id`、`chunk_type` varchar(32)、`text` text、`embedding` **vector(1024)**、`embedding_model` varchar(64)、`embedding_version` varchar(16)、`tsv` tsvector(GENERATED ALWAYS AS to_tsvector('simple', text) STORED——**写入时不要手工填 tsv**);HNSW 索引(cosine)+ GIN(tsv)
- `kb_asset`:`asset_id` PK、`entity_id`、`asset_type`、`cos_key` varchar(255)、`spec_check` JSONB、`tryon_ready` bool、`review_status`、`license`;Index(entity_id)、partial Index(tryon_ready)
- `kb_gap`:`gap_id` PK、`query_text`、`filter_ctx` JSONB、`hit_count` int、`first_seen_at`;Index(query_text)
### 配置(config.py 已加,.env.example 已示例)
```python
kb_vector_dimensions: int = 1024 # KB_VECTOR_DIMENSIONS
kb_embedding_model: str = "deterministic-v1" # KB_EMBEDDING_MODEL(当前默认=确定性假实现)
kb_embedding_device: str = "cpu" # KB_EMBEDDING_DEVICE
retrieval_top_k: int = 50 # RETRIEVAL_TOP_K
retrieval_rerank_weights: dict = {"vector":0.45,"keyword":0.2,"rule":0.25,"freshness":0.1} # RETRIEVAL_RERANK_WEIGHTS_JSON
```
⚠️ 上批经验:带 alias 的 Settings 字段在**测试里按字段名构造会被静默忽略**——测试用 `monkeypatch.setenv` + `get_settings.cache_clear()`。
### 重排公式(架构 §8.3 唯一口径)
`最终分 = 0.45×向量 + 0.2×关键词 + 0.25×规则权重 + 0.1×新鲜度`(权重可配,就是上面的 rerank_weights)。规则权重项本批的输入是 `rule_boost: dict[chunk_id, float]`(由调用方传入,规则引擎联动属后续任务,管线只认接口)。
### 分层位置
`packages/retrieval` 在 import-linter 第 4 层(与 rules 同层)。只准 import `packages.shared`/`packages.domain` 与已装依赖(pgvector 的 sqlalchemy 类型、numpy)。
## 3. 实现要点
允许改动:`packages/retrieval/**`、测试文件。**不建 HTTP 路由**(检索是内部能力,被 agent/report 的 service 调用)。
1. **`packages/retrieval/embedding.py`**:
- `EmbeddingProvider` Protocol:`async def embed(texts: list[str]) -> list[list[float]]`、属性 `model_name/version/dimensions`;
- `DeterministicEmbedder`:把文本 sha256 种子映射到**归一化**的 1024 维向量(同文本同向量、不同文本近似正交——用 seed 生成伪随机向量后 L2 归一化)。注释写明:这是测试/dev 的假实现,**真实 BGE-M3 ONNX 加载是后续任务**(配置里的 `kb_embedding_device` 是为它预留的);
- `build_embedding_provider(settings)` 工厂:按 `kb_embedding_model` 选择实现(当前只认 `deterministic-v1`,其它值抛 `AppError(CONFIG_ERROR)`)。
2. **`packages/retrieval/repo.py`**:
- `upsert_chunks(session, entity_id, chunks, embedding)`:写 kb_chunk(embedding 用 `pgvector.sqlalchemy.Vector`;`embedding_model/version` 必填——**没有版本号的向量不可重跑**,这是 T-P2-03 的验收);
- `hybrid_search(session, query_vec, query_text, *, entity_types, occasion, body_shape, rule_boost, top_k)`:按架构 §8.3 的 CTE 结构实现(可用 `sqlalchemy.text()` 原生 SQL + 绑定参数,或 SQLAlchemy core;`embedding <=> :query_vec` 余弦距离、`ts_rank`、RRF:`1/(60+rank_vec) + 1/(60+rank_kw)`,再叠加权重与新鲜度 `(now-updated_at)` 归一化衰减);
- **硬过滤是第一公民**:`e.status='published'` + entity_types + occasion(`attrs->'occasions' ? :occasion`)+ 禁忌排除(`NOT (attrs->'avoid_body_shapes' ? :body_shape)`)——**硬过滤零漏**是 T-P2-04 红线,集成测试必须证明"禁忌实体即使向量最近也绝不出现";
- `record_gap(session, query_text, filter_ctx)`:无结果时 upsert kb_gap(同 query_text + filter_ctx 则 `hit_count += 1`)。
3. **`packages/retrieval/service.py`**:`RetrievalService.search(query_text, *, user_ctx) -> SearchOutcome`(编排 embed → hybrid_search →(结果为空时)record_gap);`SearchOutcome` 含 `results: list[ScoredChunk]`(chunk_id/entity_id/text/vec_score/kw_score/final_score)与 `degraded: bool`(无结果走了 gap 记录)。
## 4. 测试要求
- 单测 `tests/unit/test_embedding.py`:确定性(同文本同向量)、维度、L2 归一化、工厂的未知模型名拒绝。
- 单测 `tests/unit/test_rerank.py`:RRF 融合 + 权重 + 新鲜度的数学正确性(构造已知分数断言最终序)。
- 集成 `tests/integration/test_hybrid_retrieval.py`(新文件,用 DeterministicEmbedder,**不需要真实模型**):
1. 建 3 个实体(1 个 published 带禁忌属性、1 个 draft、1 个 published 正常)+ 各挂 chunk → 检索命中正常实体、**draft 不出现、禁忌实体零出现**(T-P2-04 硬过滤零漏);
2. 关键词命中路径(text 里放英文关键词,tsv 'simple' 分词);
3. 无结果 query → kb_gap 出现一行、再查同 query → hit_count=2(T-P2-06);
4. `tryon_ready=true` 的 kb_asset 可按 entity 查询(顺手验证 partial 索引可用)。
## 5. 禁改清单
通用禁改之外:`packages/kb/**`、`packages/rules/**`、`packages/identity/**`、所有路由文件。
## 6. 验收标准(T-P2-03/04/06)
1. 向量写入带 `embedding_model/version`,换模型名即视为不同版本(可重跑判据);
2. 硬过滤零漏(集成断言禁忌/draft 实体绝不出现在结果);
3. RRF + 权重重排数学正确(单测);
4. 无结果 query 全量落 kb_gap 且可聚合排序(hit_count);
5. 全部检查绿(mypy 范围同 T6 列表 + `packages/retrieval`)。
## 7. 提交信息
```
feat(retrieval): hybrid search over pgvector with hard filters and gap tracking
DeterministicEmbedder(可换 BGE-M3 的接口位)+ kb_chunk 双索引写入
+ 硬过滤→向量→关键词→RRF 重排管线 + kb_gap 缺口埋点(T-P2-03/04/06,TD-08)。
偏差说明:<如有写在这里,无则写"无">
```