# 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)。 偏差说明:<如有写在这里,无则写"无"> ```