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 系统中的两个核心任务:
- 文档向量化:把每个文本块转换为向量,存入向量数据库
- 查询向量化:把用户问题转换为向量,与文档向量进行相似度匹配
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, ...]
核心用途
- 文档向量化:
embed_documents([...])将文本转换为向量,写入向量数据库 - 查询向量化:
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”
距离度量方式(向量相似度计算规则)
- COSINE 余弦相似度【中文 Embedding 首选】
- 除此之外还有 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("您这个问题,在文档中没有答案")
这段代码完成了三件事:
- 准备文档
- 保存到 Milvus
- 根据用户问题检索相似文档
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_name、file_type、page、category 是有价值的。
11. 企业案例:建设知识库检索索引
14.1 案例需求
公司已经准备了知识库文件:
knowledge_base/
├── hr/
│ └── employee_handbook.txt
├── customer_service/
│ └── refund_policy.md
└── product/
└── product_manual.pdf
现在需要完成两个程序:
build_index.py:扫描文件、切分文档、写入 Milvus 数据库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,生成最终的知识库问答结果。

397

被折叠的 条评论
为什么被折叠?



