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

6.7 KiB
Raw Permalink Blame History

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 已示例)

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)。

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