知识库Agent(Python)
一、意义
你有没有过这样的经历:项目上线前夜,突然报了一个告警,你印象中之前有人写过一份处理手册,但死活找不到在哪。翻了半小时的飞书文档、翻了聊天记录、翻了内部Wiki,最后发现那份文档藏在某个三级目录下的一个不起眼的角落里…
再比如,新同事入职了,问你“咱们这个API的鉴权流程是什么?“,你说”文档里有”,但他可能花了一下午,都没找到那份写得最全的文档。
这就是传统文档管理的痛点——信息孤岛。
什么意思呢?就是说,你的知识明明存在,但它们散落在各个角落,像一座座孤岛,彼此之间没有桥梁相连。你知道答案就在某个地方,但就是够不着。所以,知识库的出现,本质上就是要解决一个问题:把“大海捞针”变成”秒级找到”。
二、核心流程
2.1、文档拆分
你上传了份100页的PDF技术手册,AI能直接读懂吗?答案是:不能,至少不能一口吞下去。
这就像你准备考试,你不会把一整本教材从头到尾读一遍就完事了吧?你会按章节、按知识点去拆分,做成一张张笔记卡片,对不对?
知识库做的也是同样的事情。它会自动把你上传的文档,按照章节、段落、甚至自定义的规则进行拆分。而且,它还会智能适配不同的文档格式——PDF有PDF的拆法,Markdown有Markdown的拆法。
这一步的关键在于:每个拆出来的片段,都要包含完整的语义信息。
什么意思?举个例子,假设文档里有一段话:"当API返回401错误码时,通常是因为Token过期。解决方案是重新调用/auth/refresh接口获取新Token。
如果拆分的时候,把"解决方案是重新调用/auth/refresh接口"和前面的"API返回401错误码“拆到了两个不同的片段里,那后续AI检索的时候,就可能只找到问题描述,却找不到解决方案。这就是”语义信息不完整”带来的问题。
2.2、文本向量化
拆分完成后,每个片段还是人类能读懂的文字。但问题是,AI不是用”读文字”的方式来理解内容的,它需要的是数学语言。这一步,就是调用一个叫 Embedding模型 的东西,把每个文本片段转化为一组高维向量。等等,“高维向量”是什么?别慌,咱们用一个通俗的类比来理解。
你可以把每个文本片段想象成一个人。每个人都有很多特征:身高、体重、年龄、爱好等等。如果我们用一组数字来描述一个人,比如[175,70,25,...],那这组数字就是这个人的”向量表示”。同样的道理,Embedding模型 会根据文本的语义,给每个片段生成一组数字(通常是几百到上千维的)。语义越相近的文本,它们的向量在数学空间中就越”靠近”。
举个例子:
·“Token过期了怎么办"和“鉴权失败的处理方法",虽然字面上长得不一样,但语义是相近的,所以它们的向量会很接近。
·"Token过期了怎么办"和"今天中午吃什么",语义完全不相关,向量就会离得很远。
这就是向量化的魔力——它让AI能理解”意思”,而不仅仅是匹配”关键词”。这也是为什么知识库比传统的关键词搜索强大得多的原因。传统搜索你搜”鉴权失败”,可能搜不到标题叫"Token过期处理"的文档。但基于向量的检索,就能把它们关联起来。
2.3、向量库存储
向量生成好了,总得有个地方存起来吧?这就是向量数据库的职责。知识库会把生成的向量,连同它的 元信息(比如:这段内容来自哪份文档、属于第几章、作者是谁、最后更新时间是什么),一起批量存入向量数据库中。
为什么要存元信息?两个原因:
1.方便溯源:当AI给你一个回答的时候,你可以看到”这个答案来自《故障处理手册》第3章”,这样你就能判断答案靠不靠谱,而不是盲目信任AI。
2.支持过滤:比如你只想搜索最近一个月更新的文档,或者只搜索某个特定作者写的内容,有了元信息就能轻松做到。
三、过程解析
3.1、提问前的数据准备
3.1.1、分片
想象一下,你有一本1000页的产品手册。如果有人问你”产品A的退货政策是什么",你不会把整本书递给他,而是会翻到退货政策那一页,把那一段内容指给他看。 分片做的就是这件事——提前把文档切成一段一段的小片段,每个片段聚焦一个具体的知识点。
常见的分片方式有:
·按固定字数切:比如每1000个字切一段
·按段落切:以自然段落为单位
·按章节/标题切:按文档本身的结构来
·按页码切:一页一个片段
有一个非常重要的原则:每个片段的语义要完整。
3.1.2、Embedding
片段切好了,但计算机不认识中文啊。你跟它说”退货政策”,它只看到一堆字符编码,完全不理解这几个字是什么意思。所以我们需要一种方式,把人类的语言翻译成计算机能理解的”数学语言”。这就是Embedding(向量化)的作用。
Embedding就是把一段文字变成一个向量的过程。意思相近的文字,转出来的向量也会很相近。
举个例子:
"小林写Python" → [0.80,0.20,-0.50]
"小林写Golang" → [0.78,0.22,-0.48]
“今天天气真好” → [0.10,10.85,0.30]
3.1.3、存储
向量生成好了,我们需要一个地方来存放它们。这个地方就是向量数据库(比如Milvus)。
向量数据库存的不只是向量,而是向量+原始文本,两个一起存。
为什么要存原始文本?因为向量只是用来做相似度计算的”索引”,最终我们要拿给大模型看的,还是原始的文字内容。
向量数据库中的每条数据大概长这样:
{
"content":"产品A支持7天无理由退货,拆封后的电子产品除外。",
"vector":[θ.12,0.34,-0.56,θ.78,...]
}
3.2、提问后的回答生成
3.2.1、召回
1. 先把用户的问题也通过Embedding模型转成向量(用的是同一个Embedding模型,这样才能保证在同一个“语义空间”里比较)
2. 拿这个问题向量,去向量数据库里找最相似的片段,挑出相似度最高的TopN个(比如10个)。
其中,向量相似度的计算方法有两种:余弦相似度和欧氏距离
3.2.1.1、余弦相似度
计算两个向量夹角的余弦值,结果在-1到1之间。夹角越小,余弦值越接近1,说明两个向量方向越一致,语义越相近。它只看方向,不看长度,所以特别适合文本语义匹配(因为我们关心的是”意思像不像”,不关心”文本长不长")
3.2.1.2、欧氏距离
就是两个点之间的直线距离,书距离越小说明越相似。它既看方向也看长度,适合需要考虑数值大小的场景
3.2.2、重排
召回拿到了10个片段,但这里面可能有些是“擦边球” —— 跟问题沾点边但不太相关。我们需要进一步精筛。这时候登场的是重排模型(Cross Encoder),就是选出三个关系最大的。
召回阶段是把问题和片段分别转成向量,然后算向量之间的距离(所以叫Bi-Encoder,双编码器)
重排阶段是把问题和片段拼在一起输入模型,让模型直接判断”这两段话到底有多相关”(所以叫Cross Encoder,交叉编码器)
3.2.3、生成
现在我们手里有了3个最相关的知识片段,最后一步就是把它们和用户的问题一起交给大模型,让它生成最终的回答。
最后发给大模型的内容就是这样的:
请根据以下参考资料回答用户的问题。
参考资料:
1.产品A支持7天无理由退货,拆封后的电子产品除外···
2.退货流程:登录官网→我的订单→申请退货..·
3.退货运费由买家承担,质量问题除外...
用户问题:产品A怎么退货?
四、代码实战
4.1、分片
IndexingResult 类用于存储和管理文件索引任务的结果信息,提供了一系列方法来记录任务状态、统计数据和错误信息。
# 存储索引任务结果
class IndexingResult:
"""索引结果类"""
def __init__(self):
self.success = False # 任务整体是否成功(默认失败)
self.directory_path = "" # 被索引的目录路径
self.total_files = 0 # 总文件数
self.success_count = 0 # 成功处理的文件数
self.fail_count = 0 # 处理失败的文件数
self.start_time: Optional[datetime] = None # 任务开始时间
self.end_time: Optional[datetime] = None # 任务结束时间
self.error_message = "" # 整体错误信息
self.failed_files: Dict[str, str] = {} # 失败文件:路径 → 错误原因
def increment_success_count(self):
"""增加成功计数"""
self.success_count += 1
def increment_fail_count(self):
"""增加失败计数"""
self.fail_count += 1
def add_failed_file(self, file_path: str, error: str):
"""添加失败文件"""
self.failed_files[file_path] = error
def get_duration_ms(self) -> int:
"""获取耗时(毫秒)"""
if self.start_time and self.end_time:
return int((self.end_time - self.start_time).total_seconds() * 1000)
return 0
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"success": self.success,
"directory_path": self.directory_path,
"total_files": self.total_files,
"success_count": self.success_count,
"fail_count": self.fail_count,
"duration_ms": self.get_duration_ms(),
"error_message": self.error_message,
"failed_files": self.failed_files,
}
VectorIndexService 类是一个用于处理文件索引的核心模块,主要功能包括目录扫描、文件读取、文本分割和向量存储。该服务采用模块化设计,便于集成到 RAG 系统中。
# 整个 RAG 系统(检索增强生成)里最核心的文件处理模块
class VectorIndexService:
"""向量索引服务 - 负责读取文件、生成向量、存储到 Milvus"""
def __init__(self):
"""初始化向量索引服务"""
self.upload_path = "./uploads" # 默认文件存放目录
logger.info("向量索引服务初始化完成")
def index_directory(self, directory_path: Optional[str] = None) -> IndexingResult:
"""
索引指定目录下的所有文件
Args:(是 Arguments(参数)的缩写)
directory_path: 目录路径(可选,默认使用配置的上传目录)
Returns:
IndexingResult: 索引结果
"""
result = IndexingResult() # 创建结果对象
result.start_time = datetime.now() # 记录开始时间
try:
# 使用指定目录或默认上传目录
target_path = directory_path if directory_path else self.upload_path
dir_path = Path(target_path).resolve() # 把路径变成标准路径(绝对路径)
if not dir_path.exists() or not dir_path.is_dir():
raise ValueError(f"目录不存在或不是有效目录: {target_path}")
result.directory_path = str(dir_path)
# 获取所有支持的文件
files = list(dir_path.glob("*.txt")) + list(dir_path.glob("*.md"))
if not files:
logger.warning(f"目录中没有找到支持的文件: {target_path}")
result.total_files = 0
result.success = True # 没文件不算失败
result.end_time = datetime.now()
return result
result.total_files = len(files)
logger.info(f"开始索引目录: {target_path}, 找到 {len(files)} 个文件")
# 遍历并索引每个文件
for file_path in files:
try:
self.index_single_file(str(file_path)) # 处理单个文件
result.increment_success_count()
logger.info(f"✓ 文件索引成功: {file_path.name}") # 成功:计数+1
except Exception as e:
# 失败:计数+1,记录原因
result.increment_fail_count()
result.add_failed_file(str(file_path), str(e))
logger.error(f"✗ 文件索引失败: {file_path.name}, 错误: {e}")
# 判断整个任务是否成功,失败数 = 0 → 任务成功;有失败文件 → 整个任务标记失败
result.success = result.fail_count == 0
result.end_time = datetime.now()
logger.info(
f"目录索引完成: 总数={result.total_files}, "
f"成功={result.success_count}, 失败={result.fail_count}"
)
return result
except Exception as e:
logger.error(f"索引目录失败: {e}")
result.success = False
result.error_message = str(e)
result.end_time = datetime.now()
return result
def index_single_file(self, file_path: str):
"""
索引单个文件 (使用新的 LangChain 分割器)
Args:
file_path: 文件路径
Raises:
ValueError: 文件不存在时抛出
RuntimeError: 索引失败时抛出
"""
path = Path(file_path).resolve()
if not path.exists() or not path.is_file():
raise ValueError(f"文件不存在: {file_path}")
logger.info(f"开始索引文件: {path}")
try:
# 1. 读取文件内容
content = path.read_text(encoding="utf-8")
logger.info(f"读取文件: {path}, 内容长度: {len(content)} 字符")
# 2. 删除该文件的旧数据(如果存在)
normalized_path = path.as_posix()
vector_store_manager.delete_by_source(normalized_path)
# 3. 使用新的文档分割器
documents = document_splitter_service.split_document(content, normalized_path)
logger.info(f"文档分割完成: {file_path} -> {len(documents)} 个分片")
# 4. 添加文档到向量存储
if documents:
vector_store_manager.add_documents(documents) # 向量入库
logger.info(f"文件索引完成: {file_path}, 共 {len(documents)} 个分片")
else:
logger.warning(f"文件内容为空或无法分割: {file_path}")
except Exception as e:
logger.error(f"索引文件失败: {file_path}, 错误: {e}")
raise RuntimeError(f"索引文件失败: {e}") from e
# 统一使用 biz collection
COLLECTION_NAME = "biz"
class VectorStoreManager:
"""向量存储管理器"""
def __init__(self):
"""初始化向量存储管理器"""
self.vector_store = None
self.collection_name = COLLECTION_NAME # 类似于关系数据库中的"表名"
self._initialize_vector_store()
# 连接 Milvus 数据库并创建 VectorStore 实例
def _initialize_vector_store(self):
"""初始化 Milvus VectorStore"""
try:
# 必须在 PyMilvus / langchain_milvus 访问 Collection 之前建立连接,
# 否则会出现 ConnectionNotExistException: should create connection first.
# (模块导入时就会执行此处,早于 FastAPI lifespan 中的 milvus_manager.connect)
_ = milvus_manager.connect() # 提前建立 PyMilvus 的基础连接;_ 表示忽略返回值(只需要连接的副作用)
connection_args = {
"host": config.milvus_host,
"port": config.milvus_port,
}
# 创建 LangChain Milvus VectorStore
# 使用 biz collection,字段映射:text_field -> content, vector_field -> vector
self.vector_store = Milvus(
embedding_function=vector_embedding_service,
collection_name=self.collection_name,
connection_args=connection_args,
auto_id=False, # 使用自定义 id
drop_old=False,
text_field="content", # 文本内容存储到 content 字段
vector_field="vector", # 向量存储到 vector 字段
primary_field="id", # 主键字段
metadata_field="metadata", # 元数据字段
)
logger.info(
f"VectorStore 初始化成功: {config.milvus_host}:{config.milvus_port}, "
f"collection: {self.collection_name}"
)
except Exception as e:
logger.error(f"VectorStore 初始化失败: {e}")
raise
def delete_by_source(self, file_path: str) -> int:
"""
按文件路径删除向量数据的核心方法,用于更新文件时清理旧数据
Args:
file_path: 文件路径
Returns:
int: 删除的文档数量
"""
try:
# 使用 milvus_manager 获取已连接的 collection,就是那个biz
# milvus_manager 是一个全局连接管理器
collection = milvus_manager.get_collection()
# metadata 是 JSON 字段,使用 JSON 路径查询语法
# _source 是文档的来源文件路径
# 生成 Milvus 的查询表达式,用于匹配要删除的数据,类似于SQL表达式
expr = f'metadata["_source"] == "{file_path}"'
result = collection.delete(expr) # 执行删除操作,删除所有匹配 expr 的记录
deleted_count = result.delete_count if hasattr(result, "delete_count") else 0
logger.info(f"删除文件旧数据: {file_path}, 删除数量: {deleted_count}")
return deleted_count
except Exception as e:
logger.warning(f"删除旧数据失败 (可能是首次索引): {e}")
return 0
def get_vector_store(self) -> Milvus:
"""
获取 VectorStore 实例
Returns:
Milvus: VectorStore 实例
"""
return self.vector_store
def similarity_search(self, query: str, k: int = 3) -> List[Document]:
"""
相似度搜索
Args:
query: 查询文本
k: 返回结果数量
Returns:
List[Document]: 相关文档列表
"""
try:
docs = self.vector_store.similarity_search(query, k=k)
logger.debug(f"相似度搜索完成: query='{query}', 结果数={len(docs)}")
return docs
except Exception as e:
logger.error(f"相似度搜索失败: {e}")
return []
class DocumentSplitterService:
"""文档分割服务 - 使用 LangChain 的分割器"""
def __init__(self):
"""初始化文档分割服务"""
self.chunk_size = config.chunk_max_size
self.chunk_overlap = config.chunk_overlap
# Markdown 标题分割器 (只按一级和二级标题分割,减少分片数)
self.markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "h1"),
("##", "h2"),
# 不再按三级标题分割,避免过度碎片化
],
strip_headers=False, # 保留标题在内容中
)
# 递归字符分割器 (用于二次分割,使用更大的chunk_size)
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=self.chunk_size * 2, # 加倍chunk_size,减少分片数
chunk_overlap=self.chunk_overlap,
length_function=len,
is_separator_regex=False,
)
logger.info(
f"文档分割服务初始化完成, chunk_size={self.chunk_size}, "
f"secondary_chunk_size={self.chunk_size * 2}, "
f"overlap={self.chunk_overlap}"
)
def split_markdown(self, content: str, file_path: str = "") -> List[Document]:
"""
分割 Markdown 文档 (两阶段分割 + 合并小片段)
Args:
content: Markdown 内容
file_path: 文件路径 (用于元数据)
Returns:
List[Document]: 文档分片列表
"""
if not content or not content.strip():
logger.warning(f"Markdown 文档内容为空: {file_path}")
return []
try:
# 第一阶段: 按标题分割
md_docs = self.markdown_splitter.split_text(content)
# 第二阶段: 按大小进一步分割
docs_after_split = self.text_splitter.split_documents(md_docs)
# 第三阶段: 合并太小的分片 (< 300字符)
final_docs = self._merge_small_chunks(docs_after_split, min_size=300)
# 添加文件路径元数据
for doc in final_docs:
doc.metadata["_source"] = file_path
doc.metadata["_extension"] = ".md"
doc.metadata["_file_name"] = Path(file_path).name
logger.info(f"Markdown 分割完成: {file_path} -> {len(final_docs)} 个分片")
return final_docs
except Exception as e:
logger.error(f"Markdown 分割失败: {file_path}, 错误: {e}")
raise
def split_text(self, content: str, file_path: str = "") -> List[Document]:
"""
分割普通文本文档
Args:
content: 文本内容
file_path: 文件路径 (用于元数据)
Returns:
List[Document]: 文档分片列表
"""
if not content or not content.strip():
logger.warning(f"文本文档内容为空: {file_path}")
return []
try:
# 直接使用递归字符分割器
# 1. 分割文本(返回字符串列表)
# 2. 为每个分片创建 Document 对象
docs = self.text_splitter.create_documents(
texts=[content],
metadatas=[
{
"_source": file_path,
"_extension": Path(file_path).suffix,
"_file_name": Path(file_path).name,
}
],
)
logger.info(f"文本分割完成: {file_path} -> {len(docs)} 个分片")
return docs
except Exception as e:
logger.error(f"文本分割失败: {file_path}, 错误: {e}")
raise
def split_document(self, content: str, file_path: str = "") -> List[Document]:
"""
智能分割文档 (根据文件类型选择分割器)
Args:
content: 文档内容
file_path: 文件路径
Returns:
List[Document]: 文档分片列表
"""
if file_path.endswith(".md"):
return self.split_markdown(content, file_path)
else:
return self.split_text(content, file_path)
def _merge_small_chunks(
self, documents: List[Document], min_size: int = 300
) -> List[Document]:
"""
合并太小的分片
Args:
documents: 文档列表
min_size: 最小分片大小 (字符数)
Returns:
List[Document]: 合并后的文档列表
"""
if not documents:
return []
merged_docs = [] # 存储合并后的文档
current_doc = None # 当前正在构建的文档
for doc in documents:
doc_size = len(doc.page_content)
if current_doc is None:
# 第一个文档
current_doc = doc
elif doc_size < min_size and len(current_doc.page_content) < self.chunk_size * 2:
# 当前文档太小且合并后不会太大,则合并
current_doc.page_content += "\n\n" + doc.page_content
# 保留主文档的元数据
else:
# 保存当前文档,开始新文档
merged_docs.append(current_doc)
current_doc = doc
# 添加最后一个文档
if current_doc is not None:
merged_docs.append(current_doc)
return merged_docs
4.2、Embedding
class DashScopeEmbeddings(Embeddings):
"""阿里云 DashScope Text Embedding (OpenAI 兼容模式)
实现 LangChain 标准 Embeddings 接口:
- embed_documents(texts: List[str]) → List[List[float]]: 批量嵌入文档
- embed_query(text: str) → List[float]: 嵌入单个查询
"""
def __init__(
self,
api_key: str,
model: str = "text-embedding-v4",
dimensions: int = 1024,
):
"""
初始化 DashScope Embeddings
Args:
api_key: DashScope API Key
model: 嵌入模型名称
dimensions: 向量维度
"""
if not api_key or api_key == "your-api-key-here":
raise ValueError("请设置环境变量 DASHSCOPE_API_KEY")
self.client = OpenAI(
api_key=api_key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
self.model = model
self.dimensions = dimensions
# 打印初始化信息
masked_key = self._mask_api_key(api_key)
logger.info(
f"DashScope Embeddings 初始化完成 - "
f"模型: {model}, 维度: {dimensions}, API Key: {masked_key}"
)
@staticmethod
def _mask_api_key(api_key: str) -> str:
"""掩码 API Key 用于日志"""
if len(api_key) > 8:
return f"{api_key[:8]}...{api_key[-4:]}"
return "***"
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""
批量嵌入文档列表 (LangChain 标准接口)
Args:
texts: 文本列表
Returns:
List[List[float]]: 嵌入向量列表
"""
if not texts:
return []
try:
logger.info(f"批量嵌入 {len(texts)} 个文档")
# 批量调用 API
response = self.client.embeddings.create(
model=self.model,
input=texts,
dimensions=self.dimensions,
encoding_format="float"
)
embeddings = [item.embedding for item in response.data]
logger.debug(f"批量嵌入完成, 维度: {len(embeddings[0])}")
return embeddings
except Exception as e:
logger.error(f"批量嵌入失败: {e}")
raise RuntimeError(f"批量嵌入失败: {e}") from e
def embed_query(self, text: str) -> List[float]:
"""
嵌入单个查询文本 (LangChain 标准接口)
Args:
text: 查询文本
Returns:
List[float]: 嵌入向量
"""
if not text or not text.strip():
raise ValueError("查询文本不能为空")
try:
logger.debug(f"嵌入查询, 长度: {len(text)} 字符")
response = self.client.embeddings.create(
model=self.model,
input=text,
dimensions=self.dimensions,
encoding_format="float"
)
embedding = response.data[0].embedding
logger.debug(f"查询嵌入完成, 维度: {len(embedding)}")
return embedding
except Exception as e:
logger.error(f"查询嵌入失败: {e}")
raise RuntimeError(f"查询嵌入失败: {e}") from e
4.3、存储
def add_documents(self, documents: List[Document]) -> List[str]:
"""
批量添加文档到向量存储(自动批量向量化)
Args:
documents: 文档列表
Returns:
List[str]: 文档 ID 列表
"""
try:
import time
import uuid
start_time = time.time()
# 为每个文档生成唯一 id(因为 auto_id=False)
ids = [str(uuid.uuid4()) for _ in documents]
# LangChain Milvus 的 add_documents 会自动调用 embedding_function
# 并进行批量处理,性能更好
result_ids = self.vector_store.add_documents(documents, ids=ids)
elapsed = time.time() - start_time
logger.info(
f"批量添加 {len(documents)} 个文档到 VectorStore 完成, "
f"耗时: {elapsed:.2f}秒, 平均: {elapsed/len(documents):.2f}秒/个"
)
return result_ids
except Exception as e:
logger.error(f"添加文档失败: {e}")
raise
4.4、召回
class SearchResult:
"""搜索结果类"""
def __init__(
self,
id: str,
content: str,
score: float,
metadata: Dict[str, Any],
):
self.id = id
self.content = content
self.score = score
self.metadata = metadata
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"content": self.content,
"score": self.score,
"metadata": self.metadata,
}
class VectorSearchService:
"""向量检索服务 - 负责从 Milvus 中搜索相似向量"""
def __init__(self):
"""初始化向量检索服务"""
logger.info("向量检索服务初始化完成")
def search_similar_documents(self, query: str, top_k: int = 3) -> List[SearchResult]:
"""
搜索相似文档
Args:
query: 查询文本
top_k: 返回最相似的K个结果
Returns:
List[SearchResult]: 搜索结果列表
Raises:
RuntimeError: 搜索失败时抛出
"""
try:
logger.info(f"开始搜索相似文档, 查询: {query}, topK: {top_k}")
# 1. 将查询文本向量化
query_vector = vector_embedding_service.embed_query(query)
logger.debug(f"查询向量生成成功, 维度: {len(query_vector)}")
# 2. 获取 collection,打开数据库表
collection: Collection = milvus_manager.get_collection()
# 3. 构建搜索参数
search_params = {
"metric_type": "L2", # 欧氏距离
"params": {"nprobe": 10},
}
# 4. 执行搜索
results = collection.search(
data=[query_vector],
anns_field="vector",
param=search_params,
limit=top_k,
output_fields=["id", "content", "metadata"],
)
# 5. 解析搜索结果
search_results = []
for hits in results:
for hit in hits:
result = SearchResult(
id=hit.entity.get("id"), # 取出数据库里的 id 字段
content=hit.entity.get("content"), # 取出文本片段内容
score=hit.distance, # L2 距离,越小越相似
metadata=hit.entity.get("metadata", {}), # 取出元数据(文件路径、来源等)
)
search_results.append(result)
logger.info(f"搜索完成, 找到 {len(search_results)} 个相似文档")
return search_results
except Exception as e:
logger.error(f"搜索相似文档失败: {e}")
raise RuntimeError(f"搜索失败: {e}") from e
更多推荐

所有评论(0)