在构建基于大语言模型(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:

  1. embed_documents(texts: List[str]):批量处理文本块,用于构建向量索引。

  2. 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 全托管★★★★★★★★★★希望免运维的企业级外网项目
pgvectorPostgreSQL 插件★★★☆☆★★★★★中轻量级项目,复用已有关系型数据库

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 系统出现“答非所问”或“召回失败”时,建议按照如下优先级排查:

  1. Embedding 模型版本一致性:建库时的 Embedding 模型与检索时的 Embedding 模型必须绝对统一。若模型不一致,向量分布空间完全不同,检索必然失败。

  2. 切片粒度(Chunk Size)调优

    • 切片过小:丢失上下文完整语义。建议适当调大 chunk_size

    • 切片过大:单块混合了多个主题,稀释了关键信息的向量权重。

  3. 重叠窗口(Chunk Overlap):必须保留 10%~20% 的 overlap,避免关键实体或句式在切分点断开。

  4. 不要硬编码相似度阈值:不同数据库(Milvus, Chroma)和不同距离度量方式(Cosine, L2, IP)输出的 score 含义与区间完全不同,建议结合具体业务评估集(Evaluation Harness)设定动态召回策略。

小结

本文探讨了 RAG 系统中处理“语义理解”与“高速检索”的核心技术:

  • Embedding:负责将文本转化为捕捉了深层语义的高维数字向量。

  • 向量数据库(Milvus):负责高效存储向量及元数据,并在千万级规模下实现毫秒级余弦相似度匹配。

在下一篇文章中,我们将把本章检索出的相关文本片段,组装为上下文(Context)输入给 DeepSeek,完成最终“基于企业私有知识库的精准问答系统”开发!

更多推荐