前文引用:通用分块器搞不定 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,撰稿时数据):
| 模型 | 参数量 | 向量维度 | 最大 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
- 参数规模可控:4B 参数,单张消费级显卡能跑,API 调用成本也低
- 长上下文:32768 token,远超 chunk 上限,留足空间
- 中文友好:阿里出品,中文语义理解经过专门优化。Jaspersoft 的字段名、参数名通常用中文或中英混合命名
- Retrieval/STS 双高:检索和语义相似度这两个最关键指标都在合理区间
- 开源可本地部署:数据不出企业内网
不用更大模型的原因
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"iftorch.cuda.is_available()else"cpu"model=SentenceTransformer(model_path,device=device)GPU 环境下默认启用 FP16 半精度,显存占用约减半:
ifdevice=="cuda"anduse_fp16:model=model.half()# FP16RTX 4060(8G 显存)下 4B 模型用 FP16 稳定运行,8B 模型即使开 FP16 也会爆显存。
文本构造:chunk → embedding 输入
不能直接把 chunk 的human_description丢给模型——那样丢失了类型标签、上下文和元数据。build_text_for_embedding()负责将 chunk 拼接为富含信号的文本:
defbuild_text_for_embedding(chunk:dict)->str:parts=[f"[ChunkType:{chunk.get('chunk_type','unknown')}]",chunk.get('human_description',''),]context=chunk.get('context','')ifcontext:parts.append(f"Context:{context}")raw_xml=chunk.get('raw_xml','')ifraw_xml:parts.append(f"XML:{raw_xml[:500]}")# 前 500 字符meta=chunk.get('metadata',{})ifmeta:if'field_names'inmeta:parts.append(f"Fields:{', '.join(meta['field_names'])}")if'parameter_names'inmeta:parts.append(f"Parameters:{', '.join(meta['parameter_names'])}")if'report_name'inmeta:parts.append(f"Report:{meta['report_name']}")if'band_name'inmeta:parts.append(f"Band:{meta['band_name']}")if'element_kind'inmeta:parts.append(f"Element:{meta['element_kind']}")if'query_language'inmeta:parts.append(f"QueryLang:{meta['query_language']}")return"\n".join(parts)设计要点:
[ChunkType: fields]前缀让模型感知 chunk 的领域类型,检索"字段定义怎么写"时更容易命中 fields 类型的 chunkraw_xml[:500]截取前 500 字符——既保留 XML 结构信号,又不让长 XML 淹没语义- 元数据按优先级选择性拼接,避免无意义的键值对填充
批量向量化
使用SentenceTransformer而非 LangChain 的HuggingFaceEmbeddings——直接控制设备、精度、归一化:
fromsentence_transformersimportSentenceTransformer 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_size16# 完整参数.venv\Scripts\python.exe embed_chunks.py chunks.json\--output_dir./embeddings\--model_path./models/Qwen3-Embedding-4B\--batch_size16# 禁用归一化或 FP16(调试用).venv\Scripts\python.exe embed_chunks.py chunks.json--no_normalize--no_fp16batch_size根据显存调整。项目默认配置为 64(config.py中BATCH_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 元素。
下一篇讲这些向量最终的去处:向量数据库的选型与构建。