"""嵌入提供方:接口位 + 确定性假实现(架构 §8.3,TD-08). 当前只有 ``deterministic-v1``:以文本 sha256 为种子生成伪随机向量后 L2 归一化。 它是**测试与 dev 的假实现** —— 真实 BGE-M3 ONNX 加载是后续任务,配置里的 ``kb_embedding_device`` 就是为它预留的。接口先定死,届时换模型只改工厂, 检索管线(repo/service)不动。 """ from __future__ import annotations import hashlib from typing import Protocol, runtime_checkable import numpy as np from packages.shared.config import Settings from packages.shared.errors import AppError, ErrorCode #: 唯一被承认的假实现模型名(与 Settings.kb_embedding_model 的默认值对应)。 DETERMINISTIC_MODEL_NAME = "deterministic-v1" @runtime_checkable class EmbeddingProvider(Protocol): """嵌入模型的最小接口。 ``model_name`` + ``version`` 会随向量逐行写进 kb_chunk(T-P2-03): 没有版本号的向量不可重跑 —— 换模型即换版本,这是向量化可重跑的判据。 """ @property def model_name(self) -> str: ... @property def version(self) -> str: ... @property def dimensions(self) -> int: ... async def embed(self, texts: list[str]) -> list[list[float]]: """批量嵌入。返回顺序与输入一一对应。""" ... class DeterministicEmbedder: """确定性假嵌入:同文本同向量,不同文本近似正交。 sha256(text) 做种子的标准高斯伪随机向量再 L2 归一化。1024 维空间里 随机向量两两夹角趋于 90°(|dot| ≈ 1/√1024 ≈ 0.03),这让"按余弦距离 排序"的管线在没有真模型时也能被测试驱动:同文本距离 0,异文本距离 ~1。 """ def __init__(self, *, dimensions: int = 1024, version: str = "v1") -> None: if dimensions <= 0: raise ValueError(f"dimensions 必须为正数, 得到 {dimensions}") self._dimensions = dimensions self._version = version @property def model_name(self) -> str: return DETERMINISTIC_MODEL_NAME @property def version(self) -> str: return self._version @property def dimensions(self) -> int: return self._dimensions def _vector(self, text: str) -> np.ndarray: # 只取 sha256 前 16 字节做种子:PCG64 的种子空间足够,且避免跨平台 # int 大小差异。同一 numpy 版本内逐位确定(假实现不承诺跨大版本稳定, # 真正的可重跑保证来自 kb_chunk.embedding_model/version 落库)。 seed = int.from_bytes(hashlib.sha256(text.encode("utf-8")).digest()[:16], "big") vec = np.random.default_rng(seed).standard_normal(self._dimensions) norm = float(np.linalg.norm(vec)) # 标准正态向量的范数恒 > 0,这里只防御浮点极端值,不做业务分支。 return vec / norm if norm > 0.0 else vec async def embed(self, texts: list[str]) -> list[list[float]]: return [self._vector(t).tolist() for t in texts] def build_embedding_provider(settings: Settings) -> EmbeddingProvider: """按 ``settings.kb_embedding_model`` 选择实现。 当前只认 ``deterministic-v1``:出现其它名字说明配置与代码版本不匹配 (例如提前把 KB_EMBEDDING_MODEL 改成 BGE-M3 但 ONNX 加载任务还没合入), 直接拒绝比静默回退到假向量更能暴露问题。 """ if settings.kb_embedding_model == DETERMINISTIC_MODEL_NAME: return DeterministicEmbedder(dimensions=settings.kb_vector_dimensions) raise AppError( ErrorCode.CONFIG_ERROR, message=f"未知的嵌入模型: {settings.kb_embedding_model}", hint="当前仅支持 deterministic-v1,真实 BGE-M3 属后续任务", data={"model": settings.kb_embedding_model}, )