前文引用:通用分块器搞不定 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 指令跟随的重排序

重点关注 RetrievalSTS 两个维度的得分。

排行榜分析与模型选择

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

模型 参数量 向量维度 最大 Token Retrieval STS
harrier-oss-v1-27b 27B 5376 131072 78.27 79.99
Qwen3-Embedding-8B 8B 4096 32768 70.88 81.08
Qwen3-Embedding-4B 4B 2560 32768 69.60 80.86
llama-embed-nemotron-8b 8B 4096 32768 68.69 79.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-climodelscope 的原因:项目所有配置集中在 .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.pyBATCH_SIZE),适合 24G 及以上显存。8G 显存建议降到 8-16——显存不足时模型推理不会报错但会回退到 CPU,速度骤降。不确定时先用默认值跑一次,观察 GPU 利用率再调整。

输出与质量检查

输出 5 个文件:

文件 内容
embeddings.npy float32 向量矩阵,shape=(N, 2560)
chunk_id_map.json chunk_id 列表,与向量矩阵行号一一对应
chunk_type_map.json chunk_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

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐