前文引用:通用分块器搞不定 JRXML:一个领域感知分块器的三层设计
分块之后,每一段文本需要转成一个向量,才能存进向量数据库做相似度检索。这个"文本 → 向量"的函数就是文本嵌入模型(Embedding Model)。

选错嵌入模型,后续分块策略再好、向量库再快,检索回来的也是不相关内容。这一篇讲嵌入模型的选择逻辑。

文本嵌入模型是什么

一个数学函数,输入一段文本,输出一个高维向量。核心特性:语义相似的文本,向量距离近;语义无关的文本,向量距离远。

比如"员工姓名"和"employee name"经同一个嵌入模型编码后,两个向量的余弦相似度应该接近 1。而"员工姓名"和"页面边距 20px"的相似度应该接近 0。

三种向量类型

稀疏向量

向量中大部分位置是 0,只有少数非零值。典型代表是传统的 TF-IDF 和 BM25。

  • 擅长:精确匹配——术语、编号、代码中的关键字
  • 短板:无法理解同义词。"员工"和"雇员"在稀疏向量看来是完全不同的维度

稠密向量

向量中几乎所有位置都有值。现代深度学习嵌入模型(BERT、Qwen-Embedding 等)输出的是稠密向量。

  • 擅长:语义理解——"出生日期"和"生日"虽然字面不同,但稠密向量能识别出语义相似
  • 短板:计算成本高,可解释性差

混合/多向量

同时生成稀疏和稠密向量,或者为一段文本生成多个向量来捕捉不同侧面。

  • 擅长:精确匹配 + 语义理解,两路并行检索然后加权融合
  • 短板:技术复杂,计算和存储成本都高。需要模型本身支持(如 BGE-M3)

对于 JRXML 场景,需求是"用户说’添加一个显示合计的文本框’,系统能找到包含 $F{total_amount} 表达式和 sum 计算的 band 片段"。这需要语义理解——用稠密向量。

上下文长度

嵌入模型有最大输入 token 数限制:

  • 短上下文模型(512 token):适合句子级、段落级文本。轻量,快。
  • 长上下文模型(8192+ token):适合长文档、复杂结构。但更长不等于更好——如果 chunk 本身控制在 500 字符以内,512 token 足够。

我们的 chunk 大小在 500-2000 字符,选择余地很大。但考虑到后续可能直接对整段 band XML 做 embedding(不做自然语言转换的兜底方案),长上下文模型更保险。

MTEB 排行榜解读

MTEB(Massive Text Embedding Benchmark)是目前评估嵌入模型最权威的基准。它覆盖 8 类任务:

任务类型衡量什么对本项目的意义
Retrieval从大量文档中找到相关文档最关键——直接影响 RAG 检索质量
STS判断两句语义相似度高——可以用来验证分块质量
Pair Classification判断两句是否同义中
Clustering将相似文档分组中
Classification文本分类低
Reranking对检索结果重排序中
Bitext Mining双语平行语料挖掘低
Instruction Reranking指令跟随的重排序低

重点关注 Retrieval 和 STS 两个维度的得分。

排行榜分析与模型选择

排名前列的模型(数据来源 MTEB Leaderboard,撰稿时数据):

模型参数量向量维度最大 TokenRetrievalSTS
harrier-oss-v1-27b27B537613107278.2779.99
Qwen3-Embedding-8B8B40963276870.8881.08
Qwen3-Embedding-4B4B25603276869.6080.86
llama-embed-nemotron-8b8B40963276868.6979.41

几个观察:

harrier-oss-v1-27b 排第一是有代价的。 27B 参数,5376 维向量,一次 embedding 的算力开销是 4B 模型的数十倍。对企业的批量处理场景——几百份 JRXML 模板、上千个 chunk——这个成本不现实。

Qwen3-Embedding-4B 的性价比突出。 4B 参数、2560 维向量,Retrieval 得分 69.60,仅比 8B 版低 1.28 分,但参数量减半、维度降低 37%。在内存受限和需要批处理的场景下,这个折损完全可接受。

STS(语义相似度)上 Qwen 系列表现最好。 Qwen3-Embedding-4B 的 STS 得分 80.86,和 8B 版(81.08)几乎没有区别。这意味着在判断"两段文本是否语义相同"这件事上,4B 和 8B 的能力相当。这对分块质量验证很有价值——可以用它来检测相邻 chunk 是否被错误切断。

为什么选 Qwen3-Embedding-4B

  1. 参数规模可控:4B 参数,单张消费级显卡能跑,API 调用成本也低
  2. 长上下文:32768 token,远超 chunk 上限,留足空间
  3. 中文友好:阿里出品,中文语义理解经过专门优化。Jaspersoft 的字段名、参数名通常用中文或中英混合命名
  4. Retrieval/STS 双高:检索和语义相似度这两个最关键指标都在合理区间
  5. 开源可本地部署:数据不出企业内网

不用更大模型的原因

8B 版本在多卡环境下能跑,但单卡显存紧张。实际测试中,4B 模型在 RTX 4060(8G 显存)上批量处理 chunk 稳定运行,8B 模型会爆显存。线上 API 调用 8B 成本也翻倍,而检索质量提升不到 2%。

极致场景下(万亿级知识库、检索精度要求 95%+),harrier-27B 或 Qwen3-8B 有价值。但我们的场景是几千个 chunk、几十份模板——够用就好。

相似度度量:余弦相似度

选完模型,还要选对距离度量函数。必须在创建向量索引时就设定,后续不能改。而且度量函数要与模型训练目标匹配。

度量原理适用场景
余弦相似度向量夹角,看重方向文本语义相似度首选
欧氏距离直线距离,对数值敏感图像检索、已做 L2 归一化的场景
内积向量点积,值与相似度正相关需要得分直接解释为"置信度"的场景

Qwen3-Embedding 系列官方文档明确:使用余弦相似度或点积计算文本相似度。选择余弦相似度。

实践:模型下载、文本构造、批量向量化

模型下载

项目中 down_embedding_model.py 负责下载,通过 config.py 统一管理模型名称、存储路径和镜像地址:

# config.py 中的模型配置(可通过 .env 覆盖)
EMBEDDING_MODEL_NAME = "Qwen/Qwen3-Embedding-4B"
EMBEDDING_MODEL_PATH = "models/Qwen3-Embedding-4B"
HF_ENDPOINT = "https://hf-mirror.com"  # 国内镜像

下载脚本使用 huggingface_hub.snapshot_download,支持断点续传。如果未安装依赖会自动补装:

.venv\Scripts\python.exe down_embedding_model.py

不直接用 huggingface-cli 或 modelscope 的原因:项目所有配置集中在 .env / config.py 管理,切换模型只需改一行配置,不需要修改下载命令。

GPU 与 FP16

默认情况下 PyTorch 跑在 CPU 上,4B 模型在 CPU 上的推理耗时会很长。确认 CUDA 可用:

python -c "import torch; print(torch.cuda.is_available())"

代码自动检测设备:

device = "cuda" if torch.cuda.is_available() else "cpu"
model = SentenceTransformer(model_path, device=device)

GPU 环境下默认启用 FP16 半精度,显存占用约减半:

if device == "cuda" and use_fp16:
    model = model.half()  # FP16

RTX 4060(8G 显存)下 4B 模型用 FP16 稳定运行,8B 模型即使开 FP16 也会爆显存。

文本构造:chunk → embedding 输入

不能直接把 chunk 的 human_description 丢给模型——那样丢失了类型标签、上下文和元数据。build_text_for_embedding() 负责将 chunk 拼接为富含信号的文本:

def build_text_for_embedding(chunk: dict) -> str:
    parts = [
        f"[ChunkType: {chunk.get('chunk_type', 'unknown')}]",
        chunk.get('human_description', ''),
    ]
    context = chunk.get('context', '')
    if context:
        parts.append(f"Context: {context}")

    raw_xml = chunk.get('raw_xml', '')
    if raw_xml:
        parts.append(f"XML: {raw_xml[:500]}")  # 前 500 字符

    meta = chunk.get('metadata', {})
    if meta:
        if 'field_names' in meta:
            parts.append(f"Fields: {', '.join(meta['field_names'])}")
        if 'parameter_names' in meta:
            parts.append(f"Parameters: {', '.join(meta['parameter_names'])}")
        if 'report_name' in meta:
            parts.append(f"Report: {meta['report_name']}")
        if 'band_name' in meta:
            parts.append(f"Band: {meta['band_name']}")
        if 'element_kind' in meta:
            parts.append(f"Element: {meta['element_kind']}")
        if 'query_language' in meta:
            parts.append(f"QueryLang: {meta['query_language']}")
    return "\n".join(parts)

设计要点:

  • [ChunkType: fields] 前缀让模型感知 chunk 的领域类型,检索"字段定义怎么写"时更容易命中 fields 类型的 chunk
  • raw_xml[:500] 截取前 500 字符——既保留 XML 结构信号,又不让长 XML 淹没语义
  • 元数据按优先级选择性拼接,避免无意义的键值对填充

批量向量化

使用 SentenceTransformer 而非 LangChain 的 HuggingFaceEmbeddings——直接控制设备、精度、归一化:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer(model_path, device=device)
embeddings = model.encode(
    texts,
    batch_size=batch_size,
    show_progress_bar=True,
    normalize_embeddings=True,   # L2 归一化,配合余弦相似度
    convert_to_numpy=True
)

命令行:

.venv\Scripts\python.exe embed_chunks.py chunks.json --batch_size 16

# 完整参数
.venv\Scripts\python.exe embed_chunks.py chunks.json \
    --output_dir ./embeddings \
    --model_path ./models/Qwen3-Embedding-4B \
    --batch_size 16

# 禁用归一化或 FP16(调试用)
.venv\Scripts\python.exe embed_chunks.py chunks.json --no_normalize --no_fp16

batch_size 根据显存调整。项目默认配置为 64(config.py 中 BATCH_SIZE),适合 24G 及以上显存。8G 显存建议降到 8-16——显存不足时模型推理不会报错但会回退到 CPU,速度骤降。不确定时先用默认值跑一次,观察 GPU 利用率再调整。

输出与质量检查

输出 5 个文件:

文件内容
embeddings.npyfloat32 向量矩阵,shape=(N, 2560)
chunk_id_map.jsonchunk_id 列表,与向量矩阵行号一一对应
chunk_type_map.jsonchunk_type 列表,用于按类型过滤检索
chunks.json原始 chunk 数据(含 human_description 和 raw_xml)
embeddings.pkl完整 pickle(chunks + embeddings + texts + 归一化标记)

向量化完成后自动输出质量报告:

📊 质量检查:
   NaN values: 0
   Norms: min=0.9998, max=1.0000, mean=1.0000

📈 Chunk 类型分布:
   band_detail: 45
   fields: 12
   parameters: 8
   ...

NaN 检测确保没有无效向量。L2 归一化后范数应全部接近 1.0,偏差超过 0.01 说明模型输出异常。Chunk 类型分布帮助快速判断知识库的数据构成——如果某个类型的 chunk 数量异常少,说明分块器可能漏掉了对应领域的 JRXML 元素。

下一篇讲这些向量最终的去处:向量数据库的选型与构建。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐