1. 项目概述:LangChain与Milvus构建混合搜索RAG系统
最近在AI开发领域有个有趣的争论:OpenAI和LangChain在agent开发理念上产生了分歧。OpenAI主张通过高级抽象简化开发流程,而LangChain则认为需要更精细的控制来确保系统可靠性。这种理念差异其实反映了技术演进中的永恒话题——抽象与控制的平衡。
作为一名长期从事AI系统开发的工程师,我认为这两种观点都有其合理性。OpenAI代表的是未来发展方向,而LangChain提供的则是当下最实用的解决方案。特别是在构建企业知识库这类实际应用时,我们既需要面向未来的架构,又必须解决眼前的具体问题。
今天要分享的就是一个典型场景:如何用LangChain和Milvus构建一个同时支持全文检索和语义搜索的RAG(Retrieval-Augmented Generation)系统。这个方案完美解决了"精确匹配"与"语义理解"的取舍难题,让系统能根据查询内容自动选择最优的搜索策略。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术背景解析
2.1 全文检索与语义搜索的本质区别
全文检索和语义搜索是两种互补的搜索技术,它们的工作原理和适用场景截然不同:
全文检索 就像图书馆的卡片目录——它通过精确匹配关键词来查找文档。当你知道要找的确切术语时(比如"Python 3.9新特性"),全文检索能快速定位相关文档。它考虑的因素包括:
- 关键词出现频率(TF)
- 逆向文档频率(IDF)
- 词语位置和邻近关系
语义搜索 则更像一个理解你意图的助手。当你用描述性语言查询时(比如"编程语言最新版本功能"),它能找到语义相关的内容,即使文档中没有出现你使用的确切词语。这得益于现代嵌入模型(Embedding)将文本转换为高维向量,在向量空间中保持语义关系。
2.2 Milvus的创新设计:稀疏与密集向量的协同
传统上,Elasticsearch擅长全文检索,而向量数据库专攻语义搜索。但Milvus打破了这种界限,其核心创新是同时支持两种向量表示:
稀疏向量:
- 维度极高(数万到数百万)
- 大部分元素为零
- 每个非零元素对应一个词项的存在或权重
- 适合表示关键词出现情况
密集向量:
- 维度相对较低(通常几百到几千)
- 几乎所有元素都有值
- 通过神经网络训练获得,捕获语义信息
- 适合表示文本的"含义"
Milvus内部通过优化的索引结构和查询处理引擎,使这两种向量能够高效协同工作。例如,它可以:
- 并行执行两种搜索
- 对结果进行加权融合
- 根据查询类型自动调整搜索策略
这种设计让开发者不再需要纠结选择哪种搜索方式,而是可以"全都要",根据实际需求获得最佳的综合效果。
3. 系统实现详解
3.1 环境准备与Milvus部署
3.1.1 基础环境要求
在开始前,请确保你的开发环境满足以下要求:
- Python 3.8+(推荐3.10)
- Docker及Docker Compose
- 至少8GB内存(16GB更佳)
- OpenAI API访问权限
提示:如果是在本地开发环境运行,建议使用conda或venv创建隔离的Python环境,避免依赖冲突。
3.1.2 Milvus的安装与配置
Milvus提供了多种部署方式,对于开发和测试环境,使用Docker Compose是最简单的选择:
bash复制# 下载官方提供的docker-compose配置文件
wget https://github.com/milvus-io/milvus/releases/download/v2.2.8/milvus-standalone-docker-compose.yml -O docker-compose.yml
# 启动服务(-d表示后台运行)
docker-compose up -d
启动后,可以通过以下命令验证服务状态:
bash复制docker ps | grep milvus
正常情况应该看到3个容器在运行:milvus-standalone、etcd和minio。
3.1.3 Python依赖安装
系统实现需要以下Python包:
bash复制pip install --upgrade \
langchain \
langchain-core \
langchain-community \
langchain-text-splitters \
langchain-milvus \
langchain-openai \
bs4 \
python-dotenv
这些包分别提供了:
- LangChain核心功能
- Milvus集成支持
- OpenAI模型接入
- 文档处理工具
3.2 文档处理流程实现
3.2.1 文档加载器设计
LangChain提供了多种文档加载器,我们可以根据文件类型选择适当的加载方式:
python复制from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
def load_documents(directory: str) -> List[Document]:
"""支持多种格式的文档加载"""
documents = []
# 加载文本文件(.txt, .md等)
text_loader = DirectoryLoader(directory, glob="**/*.txt", loader_cls=TextLoader)
documents.extend(text_loader.load())
# 加载PDF文件
pdf_loader = DirectoryLoader(directory, glob="**/*.pdf", loader_cls=PyPDFLoader)
documents.extend(pdf_loader.load())
# 可扩展其他格式...
return documents
在实际应用中,你可能还需要处理Word、Excel等格式,可以添加相应的加载器。
3.2.2 文本分块策略
将长文档分割成适当大小的块是提高检索效果的关键。我们使用递归字符分割器:
python复制from langchain.text_splitter import RecursiveCharacterTextSplitter
def split_documents(documents: List[Document],
chunk_size: int = 1000,
chunk_overlap: int = 200) -> List[Document]:
"""文档分块处理"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=["\n\n", "\n", "。", "!", "?", ".", " ", ""]
)
return text_splitter.split_documents(documents)
参数选择建议:
- 中文文档:chunk_size=800-1200,chunk_overlap=200
- 英文文档:chunk_size=1000-1500,chunk_overlap=250
- 技术文档:可适当减小chunk_size以提高精度
3.2.3 向量化与存储
这是系统的核心部分,我们同时生成密集向量和稀疏向量:
python复制from langchain_milvus import Milvus, BM25BuiltInFunction
from langchain_openai import OpenAIEmbeddings
def create_vector_store(documents: List[Document],
collection_name: str) -> Milvus:
"""创建包含双向量表示的Milvus存储"""
# OpenAI的嵌入模型(密集向量)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# BM25配置(稀疏向量)
analyzer_params = {
"tokenizer": "jieba", # 中文分词
"filter": [
"lowercase",
{"type": "stop", "stop_words": ["的", "了", "是"]}
]
}
# 创建向量存储
vector_store = Milvus.from_documents(
documents=documents,
embedding=embeddings,
builtin_function=BM25BuiltInFunction(analyzer_params=analyzer_params),
vector_field=["dense", "sparse"], # 双向量字段
connection_args={"uri": "http://localhost:19530"},
collection_name=collection_name,
)
return vector_store
关键点说明:
text-embedding-3-small是OpenAI最新的轻量级嵌入模型,性价比高- 中文处理必须使用适当的分词器(如jieba)
- 停用词列表应根据实际语料调整
3.3 混合搜索实现
3.3.1 检索器配置
混合搜索的核心是合理设置两种搜索的权重:
python复制def create_retriever(vector_store: Milvus,
dense_weight: float = 0.7,
sparse_weight: float = 0.3,
k: int = 5) -> BaseRetriever:
"""创建混合检索器"""
return vector_store.as_retriever(
search_type="hybrid",
search_kwargs={
"k": k,
"hybrid_search": {
"dense_weight": dense_weight,
"sparse_weight": sparse_weight,
"anns_field": "dense", # 密集向量字段名
"params": {
"nprobe": 16, # 搜索的聚类中心数
"metric_type": "IP" # 内积相似度
}
}
}
)
权重调整建议:
- 术语查询:dense_weight=0.3, sparse_weight=0.7
- 语义查询:dense_weight=0.8, sparse_weight=0.2
- 通用场景:dense_weight=0.6, sparse_weight=0.4
3.3.2 问答链集成
将检索结果传递给大语言模型生成最终回答:
python复制from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
def create_qa_chain(retriever: BaseRetriever) -> RetrievalQA:
"""创建问答链"""
prompt_template = """
请基于以下上下文信息回答问题。如果上下文不包含答案,请回答"我不知道"。
上下文:
{context}
问题:{question}
回答:"""
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
return RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": prompt}
)
提示工程技巧:
- 明确指示模型在不确定时回答"我不知道"
- 对于专业领域,可在提示中加入领域特定的指导
- temperature=0确保回答的确定性
4. 实战优化与问题解决
4.1 同义词与多义词处理
在实际应用中,我们经常会遇到以下问题:
同义词问题:
- 用户搜索"汽车",但文档中使用的是"轿车"
- 技术文档中"SSL"和"TLS"混用
解决方案:
- 同义词扩展:
python复制# 在BM25配置中添加同义词
analyzer_params["synonyms"] = {
"汽车": ["轿车", "车辆", "机动车"],
"SSL": ["TLS"]
}
- 查询扩展:
python复制def expand_query(query: str) -> str:
"""基于同义词扩展查询"""
synonym_map = {
"汽车": "汽车 OR 轿车 OR 车辆",
"电脑": "电脑 OR 计算机"
}
for term, expansion in synonym_map.items():
query = query.replace(term, expansion)
return query
- 调整混合权重:增加语义搜索的权重
4.2 多语言支持挑战
处理多语言内容时的常见问题:
- 中文需要分词,而英文以空格分隔
- 同一文档中包含多种语言
- 词形变化(英文的复数、时态等)
优化方案:
python复制# 多语言分析器配置
analyzer_params = {
"tokenizer": {
"type": "multilingual",
"languages": ["zh", "en"]
},
"filter": [
"lowercase",
{"type": "stemmer", "language": "english"}, # 英文词干提取
{"type": "stop", "stop_words": ["的", "the"]}
]
}
4.3 性能优化技巧
当文档量增大时,系统性能可能下降,以下是几个优化方向:
索引优化:
python复制# 创建集合时指定索引参数
vector_store = Milvus.from_documents(
# ...其他参数...
index_params={
"metric_type": "IP",
"index_type": "IVF_FLAT",
"params": {"nlist": 1024}
}
)
查询优化:
- 限制返回结果数(k=5-10)
- 对热门查询添加缓存
- 异步处理大批量文档
硬件建议:
- 为Milvus单独配置服务器
- 使用SSD存储
- 确保足够内存(向量搜索很耗内存)
5. 完整实现示例
下面是一个端到端的实现示例,展示如何构建完整的问答系统:
python复制import os
from typing import List
from dotenv import load_dotenv
from langchain_core.documents import Document
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_milvus import Milvus, BM25BuiltInFunction
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# 加载环境变量
load_dotenv()
class KnowledgeQASystem:
def __init__(self, docs_dir: str, collection_name: str):
self.docs_dir = docs_dir
self.collection_name = collection_name
self.qa_chain = self._build_system()
def _load_documents(self) -> List[Document]:
"""加载目录中的所有文档"""
# 实现见前文...
def _split_documents(self, documents: List[Document]) -> List[Document]:
"""文档分块处理"""
# 实现见前文...
def _create_vector_store(self, documents: List[Document]) -> Milvus:
"""创建向量存储"""
# 实现见前文...
def _create_qa_chain(self, vector_store: Milvus) -> RetrievalQA:
"""创建问答链"""
# 实现见前文...
def _build_system(self) -> RetrievalQA:
"""构建完整系统"""
print("正在加载文档...")
documents = self._load_documents()
print("正在分割文档...")
chunks = self._split_documents(documents)
print("正在创建向量存储...")
vector_store = self._create_vector_store(chunks)
print("正在创建问答链...")
return self._create_qa_chain(vector_store)
def query(self, question: str) -> dict:
"""执行查询"""
return self.qa_chain({"query": question})
# 使用示例
if __name__ == "__main__":
# 初始化系统
qa_system = KnowledgeQASystem(
docs_dir="./company_docs",
collection_name="company_knowledge"
)
# 示例查询
while True:
question = input("\n请输入问题(输入q退出): ")
if question.lower() == 'q':
break
result = qa_system.query(question)
print(f"\n回答: {result['result']}")
print("\n来源文档:")
for i, doc in enumerate(result["source_documents"][:3]):
print(f"[{i+1}] {doc.metadata['source']}")
print(f"内容: {doc.page_content[:200]}...\n")
这个实现展示了:
- 模块化的系统设计
- 完整的处理流程
- 交互式查询界面
- 结果的可解释性(显示来源文档)
6. 扩展应用场景
除了企业知识库,这个技术栈还可以应用于:
6.1 法律文档分析
- 混合搜索处理法律术语和案例描述
- 自动生成法律意见书草稿
- 法条关联分析
6.2 医疗问答系统
- 医学文献检索
- 患者咨询自动回复
- 药品相互作用检查
6.3 电商产品搜索
- 结合精确产品编号搜索和语义产品描述搜索
- 个性化推荐
- 多模态搜索(结合图片向量)
7. 经验总结与避坑指南
在实际部署这类系统时,我总结了以下关键经验:
7.1 文档预处理的重要性
- 清理HTML标签、特殊字符
- 统一日期、数字格式
- 处理表格和图表内容
- 识别并合并重复内容
7.2 分块策略的优化
- 技术文档按章节分块
- 对话记录按对话轮次分块
- 对于代码,可以按函数/类分块
- 动态调整块大小(小段落不强制分割)
7.3 查询性能监控
- 记录查询响应时间
- 统计缓存命中率
- 监控Milvus资源使用情况
- 定期优化集合索引
7.4 安全注意事项
- 文档上传前的敏感信息过滤
- 查询输入的防注入处理
- API访问权限控制
- 回答结果的内容审核
8. 未来改进方向
虽然当前方案已经相当强大,但仍有改进空间:
- 动态权重调整:根据查询内容自动调整密集/稀疏向量的权重比例
- 多模态扩展:支持图像、音频等非文本内容
- 增量更新:实现文档的增量索引,避免全量重建
- 个性化搜索:结合用户历史行为优化搜索结果
- 联邦学习:在保护隐私的前提下利用多方数据改进模型
这个LangChain+Milvus的技术组合,在实际项目中已经证明了其价值。它不仅解决了"全文检索vs语义搜索"的取舍难题,还提供了一套灵活、可扩展的框架,能够适应各种复杂的应用场景。
