从文本到向量:中文Embedding与向量数据库实战指南

1. 本章目标

文档块 -> 向量 -> 向量数据库 -> 相似度检索

学完本章后,你应该能够:

  • 理解 Embedding 的作用
  • 使用中文 Embedding 模型
  • 把文本转换成向量
  • 理解向量数据库的作用
  • 使用 Milvus 保存文档块
  • 根据用户问题检索相关文档
  • 使用元数据过滤检索结果
  • 完成企业知识库检索案例

2. 为什么需要 Embedding

在大模型项目中,用户的问题和企业文档通常不是完全一样的文字。

问题:传统关键词搜索无法理解语义相似性

例如用户问:

快递已经发出还能退款吗?

文档中写的是:

订单已经发货时,需要等待商品送达后申请退货退款。

这两句话没有相同的关键词,但意思非常接近。

解决方案:Embedding 将文本转换成向量(一组数字)

文本 -> Embedding 模型 -> 向量

语义越接近的文本,向量距离越近:

"未发货可以退款吗?" -> [0.12, -0.08, 0.31, ...]
"订单没发货能退吗?" -> [0.11, -0.07, 0.29, ...]

这样就能通过比较向量相似度找到相关文档。

3. Embedding 和大语言模型的区别

Embedding 模型和聊天模型(如 DeepSeek)功能不同:

类型作用输出
聊天模型生成回答、总结、改写、推理文本
Embedding 模型把文本转换成向量数字列表

使用场景

  • 聊天模型:model.invoke("请解释什么是 RAG")
  • Embedding 模型:embedding_model.embed_query("退款多久到账")

在知识库项目中配合使用

Embedding 模型:负责找资料
聊天模型:负责根据资料回答

本章重点学习"找资料"的部分。

4. 选择中文 Embedding 模型

文档切分后得到文本块,需要转换为计算机能处理的数学形式——向量。

向量化的作用:将自然语言映射到高维向量空间,语义相近的文本位置接近。

例如:"人工智能"与"机器学习"的向量接近,与"烹饪技巧"的向量差异大。

RAG 系统中的两个核心任务

  1. 文档向量化:把每个文本块转换为向量,存入向量数据库
  2. 查询向量化:把用户问题转换为向量,与文档向量进行相似度匹配

LangChain 的 Embedding 接口

  • embed_documents(texts: List[str]):批量处理文档向量化(建索引)
  • embed_query(text: str):对单个查询文本向量化(检索)

课堂案例使用

text-embedding-v4

备选模型:huggingface 的 bge-small-zh-v1.5

5. 案例一:把文本转换成向量

安装依赖

pip install dashscope

基本使用

from langchain_community.embeddings import DashScopeEmbeddings

# 1. 准备文档文本
document_text = """
××销售有限公司员工守则:
公司要求全体员工遵守职业行为规范,包括准时上下班、客户接待礼仪、办公环境维护、信息保密义务、安全生产责任制度等。
违反规定将根据情节轻重给予警告、记过、停职直至解除劳动合同的处理。
"""

# 2. 配置向量模型
embeddings = DashScopeEmbeddings(
    model="text-embedding-v4",
    dashscope_api_key="你的API密钥"
)

# 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.003169, -0.062505, 0.014166, -0.038556, -0.000811, ...]
查询向量维度: 1024
前10个向量值: [-0.025414, -0.062043, -0.010523, -0.034797, -0.012925, ...]

核心用途

  1. 文档向量化embed_documents([...]) 将文本转换为向量,写入向量数据库
  2. 查询向量化embed_query(text) 将用户问题转换为向量,用于相似度检索

注意:通义千问的 text-embedding-v4 在中文语义理解方面表现突出,适合处理中文文档。

6. 案例二:批量生成文本向量

创建 02_embedding_documents.py

在model_factory.py 中补充一个新的方法,用于获取向量模型:

def get_embedding_model():
	embeddings_model = DashScopeEmbeddings(
		model="qwen3.7-text-embedding",  # 通义千问最新的向量模型
		dashscope_api_key="sk-ws-H.EHXIYPR.kY1M.MEUCIBCVmczsf029b7xFzMhkgA1AZiRzY2FOiaqb8XtbBfgXAiEAkDehjjapBf32DNaWKjYtKxnNMF83mdXq4LDZxGGWALs"
	)
	return embeddings_model
from utils.model_factory import get_embedding_model

embed_model=get_embedding_model()

documents=[
	"订单未发货时,用户可以直接申请退款。",
    "订单已经发货时,需要等待商品送达后申请退货退款。",
    "审核通过后,退款通常在 1 至 3 个工作日内原路返回。",
]
# 获取一个二维的数组
results=embed_model.embed_documents(documents)
print(results)

运行:

python 02_embedding_documents.py

常用方法有两个:

方法作用
embed_query()处理用户查询
embed_documents()处理文档列表

虽然它们最后都是生成向量,但在检索场景中建议按语义区分使用。

7. 什么是向量数据库

Embedding 只能把文本转换成向量。

如果文档有成千上万段,还需要一个地方保存这些向量,并支持快速检索。

这就是**向量数据库**的作用。

向量数据库通常保存三类数据:

数据说明
文本内容文档块正文
向量文本对应的数字表示
元数据文件名、页码、分类等信息

检索流程如下:

用户问题
  |
  v
Embedding 模型生成查询向量
  |
  v
向量数据库查找相似向量
  |
  v
返回相关文档块

8.1 常见的向量数据库

1 商用向量数据库

Pinecone /ˈpaɪnˌkoʊn/ [ 派恩扣恩 ] 松果的意思,

是一款全球知名的云原生商用向量数据库,主要面向向量检索与语义检索场景。它的核心特点是完全托管、开箱即用,用户无须关注节点、集群或索引配置,可快速搭建高性能的 RAG 系统。Pinecone 的可扩展性强,能提供企业级 SLA(Service Level Agreement,服务水平协议),广泛应用于大型商业系统中。

:::color2
RAG 就是知识库的意思,大模型的背包(额外的数据)

:::

2. 云厂商托管的向量数据库

:::color2
云厂商,一般指的就是提供云服务的公司,比如最有名的Amazon【亚马逊】,国内最大的阿里云。

Azure–> 微软

信创:不要使用国外的任何技术

:::

1)Amazon OpenSearch Service(Vector Search)

2)Azure AI Search(Vector Search)

3)阿里云向量检索服务(Vector Retrieval Service)

4)火山引擎 VikingDB 向量数据库

3 开源免费的向量数据库

1)Milvus Milvus 是当下很流行的开源向量数据库之一,具备存储、索引、高性能检索、分布式扩容等完整功能。它的架构专为大规模向量场景设计,社区活跃,生态成熟,在开源领域是最接近企业级商用能力的方案。本书后续将介绍基于 Milvus 的 LangChain 集成方式。

:::color2
milvus : 老鹰 鸢 类似的产物

:::

2)Chroma**** Chroma 是一款轻量级的 AI 原生向量数据库,特点是简单、易集成,适合开发者在本地构建 RAG 原型。它注重与 Python 和 LLM 工具链的快速集成,适合作为中小规模 RAG 系统的知识存储工具。

:::color2
源自希腊语 chrôma = 颜色

:::

3)FAISS FAISS 是由 Meta 开源的向量相似度检索库,提供了高性能的 ANN 算法实现,如 IVF、HNSW、PQ 等。它更像一个向量检索引擎,而不是完整的数据库,通常作为底层组件嵌入自建系统或其他向量数据库(如 Milvus、Vespa 等)中。

4 通过扩展方式加入向量能力的传统数据库

1)PostgreSQL + pgvector pgvector 是 PostgreSQL 数据库的向量扩展,为数据库引入了 vector 数据类型和向量相似度检索能力。

2)OceanBase(向量索引支持) OceanBase 是一款金融级分布式数据库,支持向量数据类型,可用于构建具备语义检索能力的企业级应用。

3)Elasticsearch(k-NN 向量检索)

上述向量数据库的对比情况如图 3-10 所示。

类别产品托管 / 自建性能扩展性易用性典型规模适用场景
商用向量数据库Pinecone托管★★★★★★★★★★★★★★★百万~亿个级企业 RAG、推荐系统
云厂商托管的向量数据库Amazon OpenSearch Service托管★★★★☆★★★★☆★★★★☆百万个级云上检索 + RAG
云厂商托管的向量数据库Azure AI Search托管★★★★☆★★★★☆★★★★☆百万个级企业级搜索 + 混合检索
云厂商托管的向量数据库阿里云向量检索服务托管★★★★☆★★★★☆★★★★☆百万~千万个级企业知识库、内部应用
云厂商托管的向量数据库火山引擎 VikingDB 向量数据库托管★★★★☆★★★★★★★★★☆千万个级高并发内容 / 推荐场景
开源免费的向量数据库Milvus自建 / 托管★★★★★★★★★★★★★★☆百万~亿个级大规模企业 RAG
开源免费的向量数据库Chroma自建★★★☆☆★★★☆☆★★★★★中小规模开发者 RAG、本地应用
开源免费的向量数据库FAISS自建★★★★★★★★☆☆★★★☆☆百万个级自建 ANN 系统
通过扩展方式加入向量能力的传统数据库PostgreSQL + pgvector自建 / 托管★★★☆☆★★★☆☆★★★★★中等规模中轻量级企业 RAG
通过扩展方式加入向量能力的传统数据库OceanBase自建★★★★★★★★★★★★★★☆大规模金融级语义检索
通过扩展方式加入向量能力的传统数据库Elasticsearch自建 / 托管★★★★☆★★★☆☆★★★★★中大规模全文 + 向量混合检索
5 选择建议

1)企业级大规模 RAG 系统 对于数据量大、对性能要求高的生产环境,如果选择自建架构,Milvus 是首选。它采用分布式设计,有丰富的索引选项,能支撑亿个级向量进行高效检索。若希望降低运维负担,则 ****Pinecone 托管服务能提供开箱即用的体验。这两种方案在扩展性和稳定性方面都经过了长期验证。

2)云原生一站式方案 如果业务已经深度依赖特定的云平台,那么用该云厂商的向量检索服务,往往能获得更好的集成体验。在 AWS 生态中,Amazon OpenSearch Service 的向量检索功能可与现有服务无缝协作;Azure 用户可以选择 Azure AI Search;在国内云环境下,阿里云向量检索服务和火山引擎 VikingDB 向量数据库都经过了大规模实践验证,是较为可靠的选择

8.2 向量数据库的基本使用

接下来,以 Milvus 为例,完成从文档处理到持久化存储的完整流程。

Milvis官方文档:https://milvus.io/docs/zh

安装

1、 环境准备与 Milvus 部署 首先,要确保 Milvus 服务能够正常运行。

安装详见附件。https://www.yuque.com/yxiansheng-njx6f/yqxuqk/fggco0db2f423w2o

2、 完整的数据处理与入库流程 接下来,在前面示例的基础上进行调整,将向量化后的数据存储到 Milvus 中。整个流程包含以下五个步骤。

(1)加载 PDF 文档:将原始文档映射为 LangChain 的 Document 对象。

(2)切分文档:将完整页面切分为易于向量化的语义片段。

(3)生成向量:使用通义千问的最新向量模型 text-embedding-v4,对切分后的文本片段进行向量化处理,将文本转换为向量表示。

(4)插入 Milvus Collection:通过 langchain_milvus 依赖库将向量写入向量数据库。

(5)语义检索验证:通过自然语言查询来检索相关文档片段。

为了保证示例可重复执行,在写入前会自动删除现有集合(Collection),避免数据冲突。具体代码如下: XX销售有限公司员工守则.pdf

使用
pip install pymilvus

连接 milvus 数据库:

最简单的一版,用于测试连接,顺便插入了一条数据

from langchain_community.vectorstores.milvus import Milvus
from langchain_core.documents import Document
from pymilvus import connections

from utils.model_factory import get_embedding_model

URI = "http://localhost:19530"
if not connections.has_connection("default"):
    connections.connect("default", uri=URI)

embeddings=get_embedding_model()
#构造向量库实例
vector_store = Milvus(
    embedding_function=embeddings,
    collection_name="employee_handbook",
    connection_args={"uri": URI},
    index_params={"index_type": "HNSW",
    "metric_type": "COSINE",
    "params": {"M": 16, "efConstruction": 128}},
    auto_id=True
)

documents=[Document(
	page_content="每天9点上班,12点下班,中午休息一个小时,下午5点下班",
	metadata={
		"source":"员工手册.md",
		"category":"考勤制度"
	}
)]

ids=vector_store.add_documents(documents)
print(ids)
print(f"数据已插入到 Milvus,共写入 {len(ids)} 条")

:::color2
[467896873732801739]

数据已插入到 Milvus,共写入 1 条

:::

第二版,读取数据并插入到表中,验证数据是否插入成功,查询问题

from pathlib import Path

# resolve 绝对路径
# resolve 也可以不添加,因为Path(__file__) 返回的也是当前的绝对路径
print(Path(__file__))
file_path=Path(__file__).resolve().parent / "files" / "XX销售有限公司员工守则.pdf"
print(file_path) # 打印一个对象,相当于打印这个对象的__str__ 或者 __repr__
print(isinstance(file_path,str))
print(type(file_path))  # <class 'pathlib.WindowsPath'>
print(type(file_path).__name__) #WindowsPath
import time
from pathlib import Path

from langchain_community.vectorstores import Milvus
from pymilvus import connections, utility, Collection
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import DashScopeEmbeddings


# 1. 加载 PDF 文档
file_path = Path(__file__).resolve().parent / "files" / "XX销售有限公司员工守则.pdf"
if not file_path.exists():
    raise FileNotFoundError(
        f"未找到 PDF 文件:{file_path}\n"
        "请确认已在当前目录下创建 files/XX销售有限公司员工守则.pdf"
    )

loader = PyPDFLoader(str(file_path))
docs = loader.load()

# 2. 文本切分
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "!", "?"]
)
split_docs = text_splitter.split_documents(docs)

print(f"文档切分完成,共 {len(split_docs)} 个文档块")

# 3. 初始化 Embedding 模型
embeddings = DashScopeEmbeddings(
    model="text-embedding-v4",
    dashscope_api_key="sk-dd80f56a3b254902966ee92780be4e51"
)

# 4. 强制删除旧collection
try:
    URI = "http://localhost:19530"
    if not connections.has_connection("default"):
        connections.connect("default", uri=URI)
    utility.drop_collection("employee_handbook")
    print("已删除旧的 collection: employee_handbook")
except Exception as e:
    print(f"删除 collection 时出错(可能本就不存在,忽略继续): {e}")

# 5. 构造向量库实例
vector_store = Milvus(
    embedding_function=embeddings,
    collection_name="employee_handbook",
    connection_args={"uri": "http://localhost:19530"},
    index_params={"index_type": "HNSW",
    "metric_type": "COSINE",
    "params": {"M": 16, "efConstruction": 128}},
    auto_id=True
)

# 6. 写入数据
ids = vector_store.add_documents(split_docs)

print(f"数据已插入到 Milvus,共写入 {len(ids)} 条")

# 7. 验证插入是否成功
try:
    collection = Collection("employee_handbook")
    collection.load()

    # 先 flush 确保数据持久化
    collection.flush()
    time.sleep(0.5)  # 等待数据同步

    num_entities = collection.num_entities
    print(f"✓ 验证:Collection 中包含 {num_entities} 条数据")

    if num_entities == len(split_docs):
        print(f"✓ 插入成功!数据条数匹配(期望 {len(split_docs)} 条,实际 {num_entities} 条)")
    elif num_entities > 0:
        print(f"⚠ 数据已插入,但数量不匹配(期望 {len(split_docs)} 条,实际 {num_entities} 条)")
    else:
        # 如果数量为0,但查询能成功,说明数据在内存中,也算成功
        print("⚠ Collection 数量为 0,但数据可能在内存中(将通过查询验证)")

except Exception as e:
    print(f"⚠ 验证时出错: {e}")

# 7. 查询示例
query = "公司对迟到、早退是如何处理的?"
results = vector_store.similarity_search(query, k=3)

print(f"\n查询:{query}")
print(f"找到 {len(results)} 条相关结果:\n")

if len(results) > 0:
    print("✓ 查询成功,数据可用!插入验证通过")
    for i, doc in enumerate(results, 1):
        print(f"--- 结果 {i} ---")
        print(doc.page_content[:200])
        print()
else:
    print("✗ 警告:查询没有返回结果,数据可能未正确插入")

:::danger
Milvus参数解读:

auto_id:True

自动生成主键 ID

  • True:Milvus 服务自动为每条向量生成唯一主键;

index_params={“index_type”: “HNSW”,

"metric_type": "COSINE",

"params": {"M": 16, "efConstruction": 128}}

“index_type”: “HNSW”

主流分层导航小世界图索引

✅ 优点:检索速度快,适合百万级以内向量;

❌ 缺点:构建索引消耗内存;

其他可选:

  • FLAT:暴力搜索,无索引,仅测试少量数据;
  • IVF_FLAT、IVF_SQ8:适合超大规模向量库。

👉 本地学习、中小型 RAG 项目首选 HNSW。

“metric_type”: “COSINE”

距离度量方式(向量相似度计算规则)

  1. COSINE 余弦相似度【中文 Embedding 首选】
  2. 除此之外还有 L2 (欧氏距离) 以及 IP (内积) 可选

“params”: {“M”: 16, “efConstruction”: 128}}

M=16 表示 每个节点最大连接邻居数量。 取值范围:4~64。M 越大,索引占用内存越高,检索精度小幅提升。默认 16 均衡通用

“efConstruction”: 128 构建索引时候选邻居数量。 取值:8 ~ 512;数值越大,索引构建越慢、占用资源越高,但是索引质量更好,搜索准确率更高。 日常调试 64~128 足够。

类比生活中的例子:

M 值代表 每个路口最多修几条连接线(邻居)

efConstruction 值代表:修路阶段,新建一个路口时,会放眼周边最多 128 个候选点,从中挑选最合适的邻居相连

  • M:地图上每个点能有几条路(静态结构,决定内存上限)
  • efConstruction:当初修地图时,用心程度(只在建库运行一次)

:::

Milvus 的操作相对简单,只需先构造 Milvus 对象,再调用 <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">add_documents</font> 方法写入数据。在每次写入前,都会强制删除同名的 Collection,以保证可重复运行,

从运行结果中可以看到,文档被切分成了六个文本块,并且全部成功插入 Milvus

在提出 “公司对迟到、早退是如何处理的?” 这一问题后,由于代码中设置了 <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">vector_store.similarity_search(query, k = 3)</font>,即 Top k 为 3,因此系统会检索出语义最接近的三个结果。输出结果使用 <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">doc.page_content[:200]</font> 进行了截断处理,因此每个结果只会显示 200 个字符。

8. 案例三:创建 Milvus 向量库

继续封装工具类:model_factory.py 中继续添加

:::color2
collection_name 和 is_delete 是必须传递的,剩余两个可以走默认值

:::

# 获取milvus对象   传参:uri 表名  是否删除表名
def get_milvus_client(collection_name,is_delete,uri="http://localhost:19530",connect_name="default"):
	# 得到milvus的客户端
	try:

		if not connections.has_connection(connect_name):
			connections.connect(connect_name, uri=uri)
		if is_delete:
			utility.drop_collection(collection_name)
			print(f"已删除旧的 collection{collection_name}")
	except Exception as e:
		print(e)

	vector_store = Milvus(
		embedding_function=get_embedding_model(),
		collection_name=collection_name,
		connection_args={"uri": uri},
		index_params={"index_type": "HNSW",
					  "metric_type": "COSINE",
					  "params": {"M": 16, "efConstruction": 128}},
		auto_id=True
	)

	return vector_store

将代码简化为:

from datetime import time
from pathlib import Path
from typing import Collection

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter


from utils.model_factory import  get_milvus_client

# 加载文档
# 1. 加载 PDF 文档
file_path = Path(__file__).resolve().parent / "files" / "XX销售有限公司员工守则.pdf"
if not file_path.exists():
    raise FileNotFoundError(
        f"未找到 PDF 文件:{file_path}\n"
        "请确认已在当前目录下创建 files/XX销售有限公司员工守则.pdf"
    )

loader = PyPDFLoader(str(file_path))
# 切割文档
docs=loader.load()
splitter=RecursiveCharacterTextSplitter(
    chunk_size=300,
    chunk_overlap=30,
    add_start_index=True
)
chunks=splitter.split_documents(docs)

# 保存文档数据


vector_store = get_milvus_client("employee_handbook",True)
ids=vector_store.add_documents(chunks)
print(f"插入了{len(ids)}条数据")
# 验证文档是否保存成功
try:
    collection = Collection("employee_handbook")
    collection.load()
    # 先 flush 确保数据持久化
    collection.flush()
    time.sleep(0.5)  # 等待数据同步
    num_entities=collection.num_entities
    if len(chunks) == num_entities:
        print(f"✓ 插入成功!数据条数匹配(期望 {len(chunks)} 条,实际 {num_entities} 条)")
    elif num_entities > 0:
        print(f"⚠ 数据已插入,但数量不匹配(期望 {len(chunks)} 条,实际 {num_entities} 条)")
    else:
        # 如果数量为0,但查询能成功,说明数据在内存中,也算成功
        print("⚠ Collection 数量为 0,但数据可能在内存中(将通过查询验证)")

except Exception as e:
    print(e)

# 通过一个问题查询文档 的内容
results=vector_store.similarity_search("今天我迟到了30分钟,公司怎么处罚?",k=3)
# results 的类型是 list[Document]
if len(results)>0:
    print("最接近你这个问题的答案是:")
    for i,result in enumerate(results,start=1):
        print(f"--- 结果 {i} ---")
        print(result.page_content[:100])
        print()
else:
    print("您这个问题,在文档中没有答案")

这段代码完成了三件事:

  1. 准备文档
  2. 保存到 Milvus
  3. 根据用户问题检索相似文档

k=3 表示返回最相似的 3 条文档。

9. 相似度分数

有时候只看检索结果不够,还想知道相似度大概是多少。

可以使用:

similarity_search_with_score()

创建 04_search_with_score.py

from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores.milvus import Milvus
from langchain_core.documents import Document
from pymilvus import connections
from pymilvus.orm import utility

embedding_model =  DashScopeEmbeddings(
    model="text-embedding-v4",
    dashscope_api_key="sk-dd80f56a3b254902966ee92780be4e51"
)

URI = "http://localhost:19530"
connections.connect("default", uri=URI)


# 5. 构造向量库实例
vector_store = Milvus(
    embedding_function=embedding_model,
    collection_name="employee_handbook",
    connection_args={"uri": "http://localhost:19530"},
    index_params={"index_type": "HNSW",
    "metric_type": "COSINE",
    "params": {"M": 16, "efConstruction": 128}},
    auto_id=True
)

results = vector_store.similarity_search_with_score(
    query="发货后还能退款吗?",
    k=3,
)

for document, score in results:
    print("-" * 50)
    print(f"分数:{score}")
    print(document.page_content)

运行:

python 04_search_with_score.py

Milvus 返回的 score 可以理解为距离,通常越小越相似。

不同向量数据库的分数含义可能不完全一样,所以项目中不要只凭一个固定数字做判断。

更稳妥的做法是结合:

  • Top K 结果
  • 文档内容是否相关
  • 业务测试集效果
  • 用户反馈

10. 使用元数据过滤

知识库中可能有多个业务分类。

例如:

  • 人事制度
  • 售后规则
  • 产品手册
  • 技术文档
假如知识库中的文档有多个组成,我只想要某一个文档的搜索结果,其他的我不要

创建 07_metadata_filter.py

:::color2
此处因为你使用的是 milvus 向量数据库,此处不要使用 filter ,而采用 expr 表达式,否则起不到过滤的作用

:::

from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores.milvus import Milvus
from pymilvus import connections

embedding_model =  DashScopeEmbeddings(
    model="text-embedding-v4",
    dashscope_api_key="sk-dd80f56a3b254902966ee92780be4e51"
)

URI = "http://localhost:19530"
connections.connect("default", uri=URI)


# 5. 构造向量库实例
vector_store = Milvus(
    embedding_function=embedding_model,
    collection_name="employee_handbook",
    connection_args={"uri": "http://localhost:19530"},
    index_params={"index_type": "HNSW",
    "metric_type": "COSINE",
    "params": {"M": 16, "efConstruction": 128}},
    auto_id=True
)

results = vector_store.similarity_search(
    query="可以申请退款吗?",
    k=2,
	filter={"category": "售后"},
)

for document in results:
    print("-" * 50)
    print(document.page_content)
    print(document.metadata)

运行:

python 07_metadata_filter.py

元数据过滤适合这些场景:

  • 只检索某个部门的资料
  • 只检索某个产品线的手册
  • 只检索某个文件来源
  • 只检索某个版本的文档

过滤条件依赖元数据,所以第六章保留 file_namefile_typepagecategory 是有价值的。

11. 企业案例:建设知识库检索索引

14.1 案例需求

公司已经准备了知识库文件:

knowledge_base/
├── hr/
│   └── employee_handbook.txt
├── customer_service/
│   └── refund_policy.md
└── product/
    └── product_manual.pdf

现在需要完成两个程序:

  1. build_index.py:扫描文件、切分文档、写入 Milvus 数据库
  2. search_knowledge.py:根据用户问题检索相关文档块

14.2 项目结构

chapter07/
├── 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

14.3 准备示例文档

knowledge_base/hr/employee_handbook.txt

员工考勤制度

工作时间为周一至周五,每天 9:00 至 18:00。
员工每月可以申请两次补卡。超过两次后,需要部门负责人审批。

员工年假制度

正式员工每年享有 5 天带薪年假。
工作满三年后,每年享有 10 天带薪年假。

knowledge_base/customer_service/refund_policy.md

# 退款规则

## 未发货订单

订单未发货时,用户可以直接申请退款。

## 已发货订单

订单已经发货时,需要等待商品送达后申请退货退款。

## 退款到账时间

审核通过后,退款通常在 1 至 3 个工作日内原路返回。

14.4 封装 Embedding 模型

创建 embedding_factory.py

:::color2
可以使用之前的封装,以前这个东西封装在了 model_factory.py

:::

from langchain_community.embeddings import DashScopeEmbeddings


def get_embedding_model() -> DashScopeEmbeddings:
    embedding_model =  DashScopeEmbeddings(
        model="text-embedding-v4",
        dashscope_api_key="sk-dd80f56a3b254902966ee92780be4e51"
    )
    return embedding_model

把 Embedding 初始化单独封装,避免建库和查询时写成两个不同模型。

14.5 加载和切分文档

创建 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
file_path.parent
获取文件所在文件夹路径(去掉文件名,只剩目录)
例:/data/a/b/1.jpg → /data/a/b
.relative_to(base_dir)
计算相对路径:从 base_dir 作为起点,得到子目录相对路径
要求:file_path.parent 必须是 base_dir 的子路径,否则抛出 ValueError

.as_posix()
把 Path 对象转为 POSIX 风格字符串
统一使用 / 分隔,Windows 环境也不会出现 \
输出:"a/b"



file1=Path("D:\\AI20260501\\pyworkspace\\donghu_langchain\\unit07\\knowledge_base\\customer_service\\refund_policy.md")
print(file1.parent)
# relative_to 计算相对路径的
print(file1.parent.relative_to("D:\\AI20260501\\pyworkspace\\donghu_langchain\\unit07\\knowledge_base\\").as_posix())

:::color1
sorted() 把生成器转为列表,并按路径字符串自然排序

:::

14.6 构建向量索引

创建 build_index.py

from pathlib import Path


from document_loader import load_knowledge_base, split_documents
from utils.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()

运行:

python build_index.py

14.7 检索知识库

创建 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")

运行:

python search_knowledge.py

输出示例:

用户问题:快递已经发出还能退款吗?
检索结果:
------------------------------------------------------------
序号:1
距离分数:0.23
文件:refund_policy.md
分类:customer_service
页码:无
内容:订单已经发货时,需要等待商品送达后申请退货退款。

如果能检索到语义相关的文档块,说明知识库索引已经可以工作。

13. 检索效果不好怎么办

如果检索结果不准,优先检查这些地方:

问题处理思路
文档块太短增大 chunk_size
文档块太长减小 chunk_size
关键上下文被切断增大 chunk_overlap
标题和正文分离调整分隔符或文档结构
查询表达和文档差异太大增加同义表达或优化文档内容
模型不适合中文更换中文 Embedding 模型

不要只看某一次检索结果。

可以准备一组测试问题:

test_queries = [
    "快递已经发出还能退款吗?",
    "退款几天到账?",
    "每个月能补卡几次?",
    "年假有几天?",
]

逐个观察 Top 3 文档是否相关。

这类小测试就是知识库项目最早期的评估 harness。

14. 常见问题

17.1 DeepSeek 可以做 Embedding 吗

本课程中 DeepSeek 主要用于聊天生成。

Embedding 这里使用本地中文模型,课堂学习使用更稳定,也不会消耗 API 额度。

后面生成最终回答时,仍然可以使用 DeepSeek。

17.2 为什么要使用同一个 Embedding 模型

建库时,文档块会被转换成向量。

查询时,用户问题也会被转换成向量。

如果两次使用的模型不同,向量空间可能不一致,检索效果会明显变差。

17.4 相似度分数应该设置多少阈值

不要一开始就写死阈值。

不同模型、不同向量库、不同距离算法的分数含义都可能不同。

建议先观察一批真实问题的检索结果,再决定是否需要阈值。

17.5 为什么第一次运行很慢

第一次运行会下载并加载 Embedding 模型。

模型下载完成后,后续运行通常会快很多。

15. 本章重点

本章需要重点掌握:

  • Embedding 是把文本转换成向量
  • 聊天模型负责生成文本,Embedding 模型负责语义检索
  • embed_query() 用于用户问题
  • embed_documents() 用于文档列表
  • 向量数据库保存文本、向量和元数据
  • similarity_search() 返回相似文档
  • similarity_search_with_score() 返回文档和距离分数
  • persist_directory 用于持久化向量库
  • 元数据过滤可以缩小检索范围

完整检索流程:

用户问题
  -> Embedding
  -> Chroma 相似度检索
  -> 相关文档块

下一章会把检索到的文档块交给 DeepSeek,生成最终的知识库问答结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值