大模型 RAG 实战:一文搞懂 Embedding 语义向量与向量数据库(Milvus 落地)
在构建基于大语言模型(LLM)的企业级知识库(RAG 系统)时,很多开发者面临的第一道难关就是:如何让 AI 准确定位到企业文档中的关键信息?
上一阶段,我们通常已经完成了 TXT、Markdown、PDF 等格式文档的清洗与切块。本文我们将继续完成 RAG 架构中最核心的一步:将文本转化为数字向量(Embedding),并使用向量数据库实现高效的语义检索。
1. 为什么传统关键词搜索不够用?
在大模型项目中,用户提问的方式与企业文档中的原话往往存在巨大差异。
例如:
-
用户提问:“迟到半小时要扣多少钱?”
-
文档原话:“员工考勤规范:未按规定考勤者,根据情节轻重给予警告或相应绩效扣减。”
这两句话在文字层面上几乎没有重合的关键词,但其表达的深层语义是高度一致的。
如果采用传统的正则匹配或关键词搜索(如 ElasticSearch 的 BM25),很容易漏检或召回无关内容。而 Embedding(文本嵌入) 的作用,就是将自然语言转换成一组包含了语义特征的高维浮点数数组(即向量)。
核心逻辑:语义越接近的文本,其在向量空间中的几何距离越近。
[文本 A: "迟到半小时要扣多少钱"] ----(Embedding 模型)----> [0.12, -0.35, 0.78, ...]
↓ (计算空间余弦距离/相似度)
[文本 B: "未按规定考勤给予绩效扣减"] ----(Embedding 模型)----> [0.11, -0.32, 0.75, ...]
2. Embedding 模型 vs. 大语言模型(LLM)
很多人容易混淆 Embedding 模型与 DeepSeek、GPT-4 这类生成式聊天模型。简单来说,它们在 RAG 系统中分工极其明确:
| 特性 | 大语言模型 (LLM, 如 DeepSeek-R1) | Embedding 模型 (如 text-embedding-v4 / BGE) |
| 主要作用 | 理解复杂指令、逻辑推理、总结摘要、生成文本 | 将文本映射为连续的高维语义向量 |
| 输出格式 | 自然语言文本 (String) | 高维浮点数列表 (List[float]) |
| 在 RAG 中 | 充当“大脑”:结合检索出的上下文回答问题 | 充当“导航”:精准查找相关的知识片段 |
协同工作流程图

3. Embedding 底层原理拆解:向量与维度究竟是什么?
当你看到一段 Embedding 输出为 [-0.0182, 0.8601, 0.0053, ...] 时,这些数字到底代表什么?
什么是向量维度?
-
核心定义:向量维度就是数组里浮点数的个数。例如
[0.12, -0.35, 0.78, 0.09]就是一个 4 维向量。 -
主流模型维度参考:
-
bge-small-zh-v1.5:512 维 -
bge-base-zh/ 通义千问text-embedding-v4:768 维 -
OpenAI
text-embedding-3-small/ada-002:1536 维 -
OpenAI
text-embedding-3-large:3072 维
-
维度代表什么含义?
我们可以把高维空间中的每一个维度,理解为模型自动学习到的某种语义特征权重(如:行业、情绪、动作、时态等)。
-
数值含义:
-
正数值大:文本在该维度上的特征极强(如:运动领域)。
-
数值接近 0(如 ±0.01):文本与该特征几乎无关。由于模型维度通常高达上千,一段具体文本只会激活少数几个维度,因此绝大多数维度都会呈现为接近 0 的数值。
-
向量相似度匹配算法
在向量数据库中,比对两段文本是否相似,最主流的方法是计算余弦相似度(Cosine Similarity):

-
夹角接近 0°(余弦值接近 1):语义高度相似。
-
夹角接近 90°(余弦值接近 0):语义毫无关联。
4. 实战一:在 LangChain 中使用 Embedding
LangChain 提供了标准化的统一接口,核心包含两个 API:
-
embed_documents(texts: List[str]):批量处理文本块,用于构建向量索引。 -
embed_query(text: str):处理单条用户提问,用于语义检索。
批量文本向量化代码示例
这里的embedding模型选择的是阿里官网中的千问模型,需要去其官网配置,首次可能需要充值一点余额,和大模型API KEY配置一样。
Python
from langchain_community.embeddings import DashScopeEmbeddings
# 1. 准备一段原始文本(直接写死,不依赖3.2.2节的文本块)
document_text = """
××销售有限公司员工守则:
公司要求全体员工遵守职业行为规范,包括准时上下班、客户接待礼仪、办公环境维护、信息保密义务、安全生产责任制度等。
违反规定将根据情节轻重给予警告、记过、停职直至解除劳动合同的处理。
"""
# 2. 配置 DashScope 的向量模型
embeddings = DashScopeEmbeddings(
model="text-embedding-v4", # 通义千问最新的向量模型
dashscope_api_key="你自己的key"
)
# 3. 对文档文本进行向量化
doc_embedding = embeddings.embed_documents([document_text])[0]
print(f"文档向量维度: {len(doc_embedding)}")
print(f"前 10 个向量值: {doc_embedding[:10]}")
# 4. 示例:将一个查询请求转换为向量
query = "公司的迟到早退处罚规则是什么?"
query_embedding = embeddings.embed_query(query)
print(f"查询向量维度: {len(query_embedding)}")
print(f"前 10 个向量值: {query_embedding[:10]}")
输出结果:
文档向量维度: 1024
前 10 个向量值: [0.0031694183126091957, -0.06250526756048203, 0.014166963286697865, -0.038556888699531555, -0.0008119997219182551, 0.026986829936504364, 0.08926758170127869, 0.10800719261169434, -0.010821674019098282, 0.04768721014261246]
查询向量维度: 1024
前 10 个向量值: [-0.025414617732167244, -0.06204322353005409, -0.010523679666221142, -0.03479717671871185, -0.01292567141354084, -0.011615494266152382, 0.08869757503271103, 0.06412823498249054, -0.056943394243717194, 0.02696428820490837]
进程已结束,退出代码为 0
注意:如果不方便调用云端 API,也可以使用 HuggingFace 上的开源中文本地模型(如
BAAI/bge-small-zh-v1.5)。
5. 向量数据库选型指南
有了向量,我们需要一个能够持久化存储浮点数组并支持毫秒级近邻搜索(ANN)的专门数据库。
主流向量数据库选型参考:
| 数据库 | 部署方式 | 性能/扩展性 | 易用性 | 适用场景 |
| Milvus | 开源自建 / 托管 | ★★★★★ | ★★★★☆ | 企业级大规模 RAG(支持亿级向量,分布式架构) |
| Chroma | 轻量本地自建 | ★★★☆☆ | ★★★★★ | 开发者 MVP 原型 / 本地轻量应用 |
| Pinecone | 云原生 SaaS 全托管 | ★★★★★ | ★★★★★ | 希望免运维的企业级外网项目 |
| pgvector | PostgreSQL 插件 | ★★★☆☆ | ★★★★★ | 中轻量级项目,复用已有关系型数据库 |
6. 实战二:基于 Milvus + LangChain 构建企业检索索引
下面我们模拟一个真实的企业级场景:将公司 HR 手册与客服退款政策导入开源向量数据库 Milvus,并实现基于元数据过滤的精准检索。
1) 环境搭建与索引建模
在 Milvus 中,针对中文向量检索,推荐的索引配置如下:
-
metric_type:"COSINE"(首选余弦相似度) -
index_type:"HNSW"(基于分层导航小世界图结构,检索速度极快) -
params:{"M": 16, "efConstruction": 128}
2)项目结构
units/
├── knowledge_base/
│ ├── hr/
│ │ └── employee_handbook.txt
│ ├── customer_service/
│ │ └── refund_policy.md
│ └── product/
│ └── product_manual.pdf
├── build_index.py
├── search_knowledge.py
├── embedding_factory.py
├── document_loader.py
笔者撰写该文章时的项目结构如图所示:

3)上传资料
①、employee_handbook.txt中内容为:
员工考勤制度
工作时间为周一至周五,每天 9:00 至 18:00。
员工每月可以申请两次补卡。超过两次后,需要部门负责人审批。
员工年假制度
正式员工每年享有 5 天带薪年假。
工作满三年后,每年享有 10 天带薪年假。
②、refund_policy.md中内容为:
# 退款规则
## 未发货订单
订单未发货时,用户可以直接申请退款。
## 已发货订单
订单已经发货时,需要等待商品送达后申请退货退款。
## 退款到账时间
审核通过后,退款通常在 1 至 3 个工作日内原路返回。
③product_manual.pdf为一个PDF格式的文件,由于我无法上传,读者可以自己让大模型生成一些相关的数据放到一个PDF文档里面,或者私信笔者要一下PDF文件。
4)封装 Embedding 模型
`text-embedding-v4`是通义实验室基于Qwen3训练的多语言文本统一向量模型,能将文本转换为数值向量,在文本检索、聚类、分类等任务上相较V3版本性能提升15%至40%。它支持从64到2048维的自定义向量维度,并覆盖超过100种主流语言。
可以去阿里官网申请API KEY.
Python(embedding_factory.py)
from langchain_community.embeddings import DashScopeEmbeddings
def get_embedding_model() -> DashScopeEmbeddings:
embedding_model = DashScopeEmbeddings(
model="text-embedding-v4",
dashscope_api_key="你的KEY"
)
return embedding_model
5)加载和切分文档
Python(document_loader.py)
from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf"}
def load_file(file_path: Path, base_dir: Path) -> list[Document]:
suffix = file_path.suffix.lower()
if suffix in {".txt", ".md"}:
loader = TextLoader(
file_path=str(file_path),
encoding="utf-8",
)
elif suffix == ".pdf":
loader = PyPDFLoader(str(file_path))
else:
return []
documents = loader.load()
category = file_path.parent.relative_to(base_dir).as_posix()
for document in documents:
document.metadata["file_name"] = file_path.name
document.metadata["file_type"] = suffix
document.metadata["category"] = category
return documents
def load_knowledge_base(base_dir: Path) -> list[Document]:
documents: list[Document] = []
for file_path in sorted(base_dir.rglob("*")):
if not file_path.is_file():
continue
if file_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
continue
loaded_documents = load_file(file_path, base_dir)
documents.extend(loaded_documents)
print(
f"已加载:{file_path},"
f"原始文档数量:{len(loaded_documents)}"
)
return documents
def split_documents(documents: list[Document]) -> list[Document]:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
add_start_index=True,
)
chunks = text_splitter.split_documents(documents)
for index, chunk in enumerate(chunks):
chunk.metadata["chunk_id"] = index
return chunks
sorted() 把生成器转为列表,并按路径字符串自然排序
6) 索引构建脚本
Python(build_index.py)
from pathlib import Path
from document_loader import load_knowledge_base, split_documents
from unitls.model_factory import get_milvus_client
KNOWLEDGE_BASE_DIR = Path("../knowledge_base")
COLLECTION_NAME = "company_knowledge"
def main() -> None:
documents = load_knowledge_base(KNOWLEDGE_BASE_DIR)
if not documents:
print("没有找到可处理的知识库文档。")
return
chunks = split_documents(documents)
vector_store=get_milvus_client(COLLECTION_NAME,True)
ids = [f"chunk-{chunk.metadata['chunk_id']}" for chunk in chunks]
vector_store.add_documents(documents=chunks, ids=ids)
print("\n===== 构建完成 =====")
print(f"原始文档数量:{len(documents)}")
print(f"文档块数量:{len(chunks)}")
print(f"集合名称:{COLLECTION_NAME}")
if __name__ == "__main__":
main()
7)检索知识库
Python(search_knowledge.py)
from utils.model_factory import get_milvus_client
COLLECTION_NAME = "company_knowledge"
def search(query: str, category: str | None = None) -> None:
vector_store=get_milvus_client(COLLECTION_NAME,False)
filter_str=""
if category is not None:
filter_str= f"category=='{category}'"
else:
filter_str="1==1"
results=vector_store.similarity_search_with_score(
query=query,
k=3,
expr=filter_str
)
print(f"\n用户问题:{query}")
print("检索结果:")
for index, (document, score) in enumerate(results, start=1):
print("-" * 60)
print(f"序号:{index}")
print(f"距离分数:{score}")
print(f"文件:{document.metadata.get('file_name')}")
print(f"分类:{document.metadata.get('category')}")
print(f"页码:{document.metadata.get('page', '无')}")
print(f"内容:{document.page_content}")
if __name__ == '__main__':
search("快递已经发出还能退款吗?")
search("每个月可以补卡几次?", category="hr")
然后运行该脚本,得到输出类似:
用户问题:快递已经发出还能退款吗?
检索结果:
------------------------------------------------------------
序号:1
距离分数:0.23
文件:refund_policy.md
分类:customer_service
页码:无
内容:订单已经发货时,需要等待商品送达后申请退货退款。
7. 高级检索技巧:相似度打分与元数据过滤(Metadata Filtering)
在实际业务场景中,我们经常遇到需要按部门、产品线、权限隔离查询的情况,这就需要用到元数据过滤。
带打分与表达式过滤的检索 (search_knowledge.py)
Python
import os
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_milvus import Milvus
embeddings = DashScopeEmbeddings(
model="text-embedding-v4",
dashscope_api_key=os.getenv("DASHSCOPE_API_KEY")
)
# 连接现有 Collection
vector_store = Milvus(
embedding_function=embeddings,
collection_name="enterprise_knowledge_base",
connection_args={"uri": "http://localhost:19530"}
)
query = "申请退款需要满足什么条件?"
# 使用 Milvus expr 表达式进行元数据精确过滤
# 仅在 customer_service 类的文档中检索 Top 2 结果
results_with_score = vector_store.similarity_search_with_score(
query=query,
k=2,
expr='category == "customer_service"' # 元数据过滤表达式
)
for doc, score in results_with_score:
print(f"【相似度得分 (Distance/Score)】: {score:.4f}")
print(f"【来源元数据】: {doc.metadata}")
print(f"【匹配内容片段】: {doc.page_content}\n" + "-"*50)
8. 避坑指南:检索效果不佳的排查清单
当 RAG 系统出现“答非所问”或“召回失败”时,建议按照如下优先级排查:
-
Embedding 模型版本一致性:建库时的 Embedding 模型与检索时的 Embedding 模型必须绝对统一。若模型不一致,向量分布空间完全不同,检索必然失败。
-
切片粒度(Chunk Size)调优:
-
切片过小:丢失上下文完整语义。建议适当调大
chunk_size。 -
切片过大:单块混合了多个主题,稀释了关键信息的向量权重。
-
-
重叠窗口(Chunk Overlap):必须保留 10%~20% 的 overlap,避免关键实体或句式在切分点断开。
-
不要硬编码相似度阈值:不同数据库(Milvus, Chroma)和不同距离度量方式(Cosine, L2, IP)输出的 score 含义与区间完全不同,建议结合具体业务评估集(Evaluation Harness)设定动态召回策略。
小结
本文探讨了 RAG 系统中处理“语义理解”与“高速检索”的核心技术:
-
Embedding:负责将文本转化为捕捉了深层语义的高维数字向量。
-
向量数据库(Milvus):负责高效存储向量及元数据,并在千万级规模下实现毫秒级余弦相似度匹配。
在下一篇文章中,我们将把本章检索出的相关文本片段,组装为上下文(Context)输入给 DeepSeek,完成最终“基于企业私有知识库的精准问答系统”开发!
更多推荐
所有评论(0)