在RAG系统落地实践中,向量基础检索往往存在语义匹配偏差、上下文碎片化,长文档召回不全、关键词漏匹配等问题。单纯的靠固定分块加向量检索的方案,很难适应复杂的业务场景。为了解决这些问题,有一些经典的高级检索优化策略,如双层检索、混合检索等。接下来本文将分享这些策略的实现思路,方便在座的亦菲、彦祖们有个参考。
父子文档检索
将长文档切分为小的 chunk(子文档)进行向量化索引,但在元数据中记录其所属的父文档 ID。检索时命中子文档,但返回整个父文档或父文档的大段落给 LLM。兼顾了检索的精准性(小块匹配度高)和生成时的完整上下文。
流程图
代码示例
import os
import uuid
from dotenv import load_dotenv
from openai import OpenAI
from langchain_core.documents import Document
from langchain_openai import ChatOpenAI
from langchain_classic.chains.retrieval_qa.base import RetrievalQA
from langchain_core.prompts import PromptTemplate
from langchain_classic.retrievers import ParentDocumentRetriever
from langchain_classic.storage import InMemoryStore
from langchain_community.vectorstores import Milvus
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.embeddings import Embeddings
load_dotenv()
# ===================== 配置初始化 =====================
OPENAI_API_KEY = os.getenv("GLM_API_KEY")
OPENAI_BASE_URL = os.getenv("GLM_BASE_URL")
EMBEDDING_MODEL = os.getenv("GLM_EMBEDDING_MODEL", "embedding-3")
MILVUS_URI = os.getenv("MILVUS_URI", "http://localhost:19530")
# 兼容 LangChain 的嵌入适配器
class OpenAIEmbeddingsAdapter(Embeddings):
def __init__(self, client, model):
self.client = client
self.model = model
def embed_documents(self, texts):
response = self.client.embeddings.create(input=texts, model=self.model)
return [item.embedding for item in response.data]
def embed_query(self, text):
return self.embed_documents([text])[0]
client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL)
embed_model = OpenAIEmbeddingsAdapter(client, EMBEDDING_MODEL)
COLLECTION_NAME = "finance_report_parent_child"
# ===================== 文档解析 =====================
def parse_finance_document():
full_text = """
悦享餐饮2020-2023财务综合分析报告
一、短期偿债能力分析:速动比率与流动比率趋势
2020~2023年间,悦享餐饮速动比率分别为2.85、2.07、1.48和1.53,流动比率分别为2.12、2.24、1.61和1.68,整体呈先降后升趋势。2022年受非流动资产增加影响,流动比率大幅回落;2023年加强应付账款管控,流动资产增加,短期偿债能力略有改善。此外2021年会计政策变更对数据有一定影响。
二、长期偿债能力分析:资产负债率与产权比率趋势
2020~2023年,悦享餐饮资产负债率先升后降(21.56%->22.03%->28.15%->26.87%),产权比率同步变动(28.0%->39.9%->36.7%)。2022年受外部冲击,应付账款大增,资产负债率攀升至28.15%;2023年优化资产结构,总资产增至188652.47万元,负债回落,长期偿债能力略有改善。
三、营运能力分析:应收账款与存货周转率
2020~2023年,应收账款周转率从25.12次降至15.34次后回升至18.65次;存货周转率波动较大(10.87->8.23->12.35->11.68)。
四、盈利能力分析:毛利率与净资产收益率
2020~2021年毛利率稳定在60%左右,2022年大幅下滑至-10.23%,2023年回升至8.65%。
五、发展能力分析:营业收入与利润增长趋势
2022年新增5家门店,成本3256.87万元,加剧资金压力;2023年暂停扩张。
"""
return [Document(page_content=full_text)]
# ===================== 构建父子检索器=====================
def build_parent_child_retriever(documents):
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=50)
#auto_id=True
vectorstore = Milvus(
embedding_function=embed_model,
collection_name=COLLECTION_NAME,
connection_args={"uri": MILVUS_URI},
auto_id=True,
)
store = InMemoryStore()
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
#传入ID
retriever.add_documents(documents, ids=[str(uuid.uuid4())])
return retriever
# ===================== RAG 问答链 =====================
def create_qa_chain(retriever):
llm = ChatOpenAI(
model="glm-4", temperature=0.1, api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL
)
prompt = PromptTemplate(
template="""基于以下上下文回答问题,不知道就说找不到信息。
上下文:{context}
问题:{question}
回答:""",
input_variables=["context", "question"],
)
return RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": prompt},
)
# ===================== 运行 =====================
if __name__ == "__main__":
docs = parse_finance_document()
retriever = build_parent_child_retriever(docs)
qa = create_qa_chain(retriever)
questions = [
"2022年悦享餐饮毛利率为什么下滑?",
"短期还债能力怎么样?",
"2022年开店策略有什么影响?",
]
for q in questions:
print("\n问题:", q)
res = qa.invoke({"query": q})
print("回答:", res["result"])
双层检索
把知识分类分为两层:摘要和详情,并在向量库中建立这两层的数据集合。那么查询的时候是先查询摘要层的数据,然后通过摘要层的数据去定位详情层的数据。
比如我代码示例中对财务数据进行了分层:{summary:“摘要信息”,"detail": "详情数据", "category": "类型"},每一类知识都会有摘要和详情,对每一类知识都构建了双层 Milvus 集合。
从专业的角度来描述,分为两个阶段:
建库阶段
- 摘要层:提取知识的简短概括(如“偿债能力分析”、“查询净利润”),将其向量化存入。目的是用轻量级、高密度的语义去快速匹配用户意图,并定位到具体的知识实体。
- 详情层:存储完整的知识内容(如完整的财务详情数据),将其向量化存入,并将摘要层的
title作为关联键。
检索阶段
- 第一层(粗排+分类) :将用户问题向量化,在三个知识分类的摘要层中分别检索,比对相似度得分,选出分数最高的分类和对应的
title。这一步同时完成了“知识分类”和“目标定位”。 - 第二层(精排+过滤) :拿着第一层得到的
title,到对应分类的详情层中,利用标量过滤(filter='title == "xxx"')+ 向量相似度检索,精准捞出最相关的详细知识内容。
流程图
代码示例
import os
import json
from dotenv import load_dotenv
from pymilvus import MilvusClient, DataType
from openai import OpenAI
load_dotenv()
# ===================== 配置初始化 =====================
OPENAI_API_KEY = os.getenv("GLM_API_KEY")
OPENAI_BASE_URL = os.getenv("GLM_BASE_URL")
EMBEDDING_MODEL = os.getenv("GLM_EMBEDDING_MODEL", "embedding-3")
MILVUS_URI = os.getenv("MILVUS_URI", "http://localhost:19530")
client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL)
milvus_client = MilvusClient(uri=MILVUS_URI)
VEC_DIM = 2048 # embedding-3 维度
COLLECTION_SUMMARY = "finance_report_summary"
COLLECTION_DETAILS = "finance_report_details"
# ===================== 1. 模拟数据解析 (展示数据结构) =====================
def parse_finance_document():
"""
将财务文档拆解为摘要和详情双层结构。
摘要:提取核心分析维度与指标名称,用于精准语义路由。
详情:保留完整的前因后果、历年数据对比与评价,用于大模型生成。
"""
knowledge_nodes = [
{
"summary": "悦享餐饮短期偿债能力分析:速动比率与流动比率趋势",
"detail": "2020~2023年间,悦享餐饮速动比率分别为2.85、2.07、1.48和1.53,流动比率分别为2.12、2.24、1.61和1.68,整体呈先降后升趋势。2022年受非流动资产增加影响,流动比率大幅回落;2023年加强应付账款管控,流动资产增加,短期偿债能力略有改善。此外2021年会计政策变更对数据有一定影响。",
"category": "偿债能力"
},
{
"summary": "悦享餐饮长期偿债能力分析:资产负债率与产权比率趋势",
"detail": "2020~2023年,悦享餐饮资产负债率先升后降(21.56%->22.03%->28.15%->26.87%),产权比率同步变动(28.0%->39.9%->36.7%)。2022年受外部冲击,应付账款大增,资产负债率攀升至28.15%;2023年通过优化资产结构,总资产增至188652.47万元,负债回落,长期偿债能力略有改善,但风险仍需警惕。",
"category": "偿债能力"
},
{
"summary": "悦享餐饮营运能力分析:应收账款与存货周转率",
"detail": "2020~2023年,应收账款周转率从25.12次降至15.34次后回升至18.65次;存货周转率波动较大(10.87->8.23->12.35->11.68)。2022年受外部冲击,应收账款拖欠时间延长,但公司及时清理滞销存货使存货周转率提升;2023年建立专项催收机制,资金回收效率有所改善,存货周转保持较高水平。",
"category": "营运能力"
},
{
"summary": "悦享餐饮盈利能力分析:毛利率与净资产收益率",
"detail": "2020~2021年毛利率稳定在60%左右,2022年大幅下滑至-10.23%,2023年回升至8.65%。2022年下滑原因:客户流失率达18.65%、收入降48%、存货跌价损失1256.87万,叠加房租与人工等固定成本刚性及防疫物资支出。净资产收益率持续落后于行业平均水平,盈利稳定性与抗风险机制存在明显缺陷。",
"category": "盈利能力"
},
{
"summary": "悦享餐饮发展能力分析:营业收入与利润增长趋势",
"detail": "2020~2022年各项增长率均为负,2022年降幅峰值(营业利润增长率-523.68%)。2023年营业收入增长率(10.82%)与总资产增长率(3.07%)转正,发展能力现转机。但值得注意的是,2022年在外部环境严峻下仍新增5家门店(扩张成本3256.87万元)加剧资金压力;2023年暂停扩张,集中提升单店效率。",
"category": "发展能力"
}
]
return knowledge_nodes
# ===================== 2. 创建双层集合 =====================
def create_collections():
for name in [COLLECTION_SUMMARY, COLLECTION_DETAILS]:
if milvus_client.has_collection(name):
milvus_client.drop_collection(name)
# 摘要层 Schema
s_schema = milvus_client.create_schema(auto_id=True)
s_schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
s_schema.add_field("vector", DataType.FLOAT_VECTOR, dim=VEC_DIM)
s_schema.add_field("title", DataType.VARCHAR, max_length=500)
s_schema.add_field("category", DataType.VARCHAR, max_length=100) # 增加分类字段,支持过滤
milvus_client.create_collection(COLLECTION_SUMMARY, schema=s_schema)
# 详情层 Schema
d_schema = milvus_client.create_schema(auto_id=True)
d_schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
d_schema.add_field("vector", DataType.FLOAT_VECTOR, dim=VEC_DIM)
d_schema.add_field("title", DataType.VARCHAR, max_length=500)
d_schema.add_field("content", DataType.VARCHAR, max_length=8192) # 存放完整分析逻辑
milvus_client.create_collection(COLLECTION_DETAILS, schema=d_schema)
# ===================== 3. 数据入库 =====================
def build_knowledge_base(nodes):
for node in nodes:
summary_text = node["summary"]
detail_text = node["detail"]
# 入库摘要层
s_vec = client.embeddings.create(input=summary_text, model=EMBEDDING_MODEL).data[0].embedding
milvus_client.insert(COLLECTION_SUMMARY, {
"vector": s_vec,
"title": summary_text,
"category": node["category"]
})
# 入库详情层
d_vec = client.embeddings.create(input=detail_text, model=EMBEDDING_MODEL).data[0].embedding
milvus_client.insert(COLLECTION_DETAILS, {
"vector": d_vec,
"title": summary_text, # 使用相同的 title 作为跨表关联的钥匙
"content": detail_text
})
# ===================== 4. 双层检索 =====================
def search_finance(question):
query_vec = client.embeddings.create(input=question, model=EMBEDDING_MODEL).data[0].embedding
# 第一层:在摘要层快速寻找最相关的知识节点
res = milvus_client.search(
collection_name=COLLECTION_SUMMARY,
data=[query_vec],
limit=1,
output_fields=["title", "category"],
search_params={"params": {"nprobe": 10}}
)
if not res or not res[0]:
return None, None
best_match = res[0][0]
best_title = best_match["entity"]["title"]
best_category = best_match["entity"]["category"]
# 第二层:用 title 精确过滤,在详情层捞出完整上下文
detail_res = milvus_client.search(
collection_name=COLLECTION_DETAILS,
data=[query_vec],
filter=f'title == "{best_title}"', # 标量过滤,精准路由
limit=1,
output_fields=["content"]
)
if detail_res and detail_res[0]:
return best_category, detail_res[0][0]["entity"]["content"]
return None, None
# ===================== 创建索引并加载集合 =====================
def load_collections():
for name in [COLLECTION_SUMMARY, COLLECTION_DETAILS]:
# 1. 准备索引参数 (使用 HNSW 算法,性能远优于 IVF_FLAT)
index_params = milvus_client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 256} # HNSW 的核心参数
)
# 2. 创建索引
milvus_client.create_index(
collection_name=name,
index_params=index_params
)
# 3. 将集合加载到内存
milvus_client.load_collection(name)
print("索引创建完成,集合已加载至内存")
# ===================== 5. 运行测试 =====================
if __name__ == "__main__":
print("正在构建双层索引...")
create_collections()
nodes = parse_finance_document()
build_knowledge_base(nodes)
load_collections()
# 模拟真实业务中的模糊提问
test_questions = [
"2022年悦享餐饮毛利率为什么下滑这么厉害?",
"公司短期还债能力这两年怎么样?",
"2022年开店策略对公司有什么影响?"
]
for q in test_questions:
print(f"\n问题: {q}")
category, content = search_finance(q)
print(f"命中分类: {category}")
print(f"召回详情: {content}\n")
print("-" * 50)
混合检索
混合检索是使用向量检索和关键词检索,这样做的好处就是取长补短,实现好的召回效果。
打个比方,问: “2022年公司毛利率下滑的原因是什么?” 。
只用向量检索,它会关注的是“盈利能力下降的原因”这个语义方向,而容易忽略了“2022年”这个时间限制,可能也会把“2023”年的文档也捞出来,产生事实性幻觉。
只用关键词检索,它会对问题进行分词:["2022年", "公司", "毛利率", "下滑", "原因"],然后去文档里找这些词的词频。谁包含的查询词多,谁就是最相关的。只会关键词匹配,如果换个方式提问,换成“近期盈利表现不佳的原因是什么”的提问方式。那么没有一个词对上,结果为空,召回失败。
用向量检索加关键词匹配的混合检索,关键词会匹配“2022年”时间和“毛利率”等关键词,向量检索语义相关文档,综合评分,更能精确检索出相关文档。
总的来说混合检索是:关键词检索负责句子专有名词的匹配,向量检索负责句子的语义理解,两者结合选出相关性最高的回答。
流程图
代码示例
import os
from dotenv import load_dotenv
from openai import OpenAI
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Milvus
from langchain_classic.retrievers import EnsembleRetriever
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_classic.schema import Document
from langchain_classic.chains import RetrievalQA
load_dotenv()
# ===================== 配置初始化 =====================
OPENAI_API_KEY = os.getenv("GLM_API_KEY")
OPENAI_BASE_URL = os.getenv("GLM_BASE_URL")
EMBEDDING_MODEL = os.getenv("GLM_EMBEDDING_MODEL", "embedding-3")
MILVUS_URI = os.getenv("MILVUS_URI", "http://localhost:19530")
# ===================== 1. 模拟数据解析 =====================
def parse_finance_document():
knowledge_nodes = [
{
"summary": "悦享餐饮短期偿债能力分析:速动比率与流动比率趋势",
"detail": "2020~2023年间,悦享餐饮速动比率分别为2.85、2.07、1.48和1.53,流动比率分别为2.12、2.24、1.61和1.68,整体呈先降后升趋势。2022年受非流动资产增加影响,流动比率大幅回落;2023年加强应付账款管控,流动资产增加,短期偿债能力略有改善。此外2021年会计政策变更对数据有一定影响。",
"category": "偿债能力",
},
{
"summary": "悦享餐饮长期偿债能力分析:资产负债率与产权比率趋势",
"detail": "2020~2023年,悦享餐饮资产负债率先升后降(21.56%->22.03%->28.15%->26.87%),产权比率同步变动(28.0%->39.9%->36.7%)。2022年受外部冲击,应付账款大增,资产负债率攀升至28.15%;2023年通过优化资产结构,总资产增至188652.47万元,负债回落,长期偿债能力略有改善,但风险仍需警惕。",
"category": "偿债能力",
},
{
"summary": "悦享餐饮营运能力分析:应收账款与存货周转率",
"detail": "2020~2023年,应收账款周转率从25.12次降至15.34次后回升至18.65次;存货周转率波动较大(10.87->8.23->12.35->11.68)。2022年受外部冲击,应收账款拖欠时间延长,但公司及时清理滞销存货使存货周转率提升;2023年建立专项催收机制,资金回收效率有所改善,存货周转保持较高水平。",
"category": "营运能力",
},
{
"summary": "悦享餐饮盈利能力分析:毛利率与净资产收益率",
"detail": "2020~2021年毛利率稳定在60%左右,2022年大幅下滑至-10.23%,2023年回升至8.65%。2022年下滑原因:客户流失率达18.65%、收入降48%、存货跌价损失1256.87万,叠加房租与人工等固定成本刚性及防疫物资支出。净资产收益率持续落后于行业平均水平,盈利稳定性与抗风险机制存在明显缺陷。",
"category": "盈利能力",
},
{
"summary": "悦享餐饮发展能力分析:营业收入与利润增长趋势",
"detail": "2020~2022年各项增长率均为负,2022年降幅峰值(营业利润增长率-523.68%)。2023年营业收入增长率(10.82%)与总资产增长率(3.07%)转正,发展能力现转机。但值得注意的是,2022年在外部环境严峻下仍新增5家门店(扩张成本3256.87万元)加剧资金压力;2023年暂停扩张,集中提升单店效率。",
"category": "发展能力",
},
]
return knowledge_nodes
# ===================== 2. 构建LangChain文档 =====================
def build_langchain_documents(nodes):
docs = []
for node in nodes:
# 将摘要和详情融合,构建具备完整上下文的文档
content = f"摘要:{node['summary']}\n详情:{node['detail']}"
docs.append(
Document(
page_content=content,
metadata={"category": node["category"], "title": node["summary"]},
)
)
return docs
# ===================== 3. 创建混合检索器(BM25 + Milvus向量) =====================
def create_hybrid_retriever(docs):
# 1. BM25 关键词检索器 (基于内存,适合中小规模文档)
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 2
# 2. Milvus 向量检索器 (使用LangChain原生集成,自动处理Embedding入库和检索)
embedding = OpenAIEmbeddings(
model=EMBEDDING_MODEL,
openai_api_key=OPENAI_API_KEY,
openai_api_base=OPENAI_BASE_URL,
)
#向量计算 + 连接Milvus + 入库 + 索引创建
vectorstore = Milvus.from_documents(
docs,
embedding,
connection_args={"uri": MILVUS_URI},
collection_name="finance_report_hybrid", # 混合检索专用集合
drop_old=True, # 每次运行重建集合,保证数据最新
)
milvus_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# 3. 混合检索 (EnsembleRetriever)
# BM25负责专有名词/关键词的精准匹配(如: "2022年", "毛利率"),向量负责语义深层召回(如: "还债能力"->"偿债能力")
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, milvus_retriever],
weights=[0.4, 0.6], # 向量语义检索权重略高
)
return hybrid_retriever
# ===================== 4. 创建RAG问答链 =====================
def create_rag_chain(retriever):
llm = ChatOpenAI(
model="glm-4",
temperature=0,
openai_api_key=OPENAI_API_KEY,
openai_api_base=OPENAI_BASE_URL,
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm, retriever=retriever, return_source_documents=True
)
return qa_chain
# ===================== 5. 测试对比效果 =====================
def test_finance_rag(hybrid_retriever, bm25_retriever, milvus_retriever):
# 使用同一个LLM创建不同的QA链用于对比
llm = ChatOpenAI(
model="glm-4",
temperature=0,
openai_api_key=OPENAI_API_KEY,
openai_api_base=OPENAI_BASE_URL,
)
hybrid_qa = RetrievalQA.from_chain_type(
llm=llm, retriever=hybrid_retriever, return_source_documents=True
)
bm25_qa = RetrievalQA.from_chain_type(
llm=llm, retriever=bm25_retriever, return_source_documents=True
)
vector_qa = RetrievalQA.from_chain_type(
llm=llm, retriever=milvus_retriever, return_source_documents=True
)
test_questions = [
"2022年悦享餐饮毛利率为什么下滑这么厉害?", # 包含明确的时间关键词
"公司短期还债能力怎么样?", # 纯语义查询(还债->偿债)
]
print("\n===== 财务知识库 RAG 测试(对比检索效果)=====\n")
for q in test_questions:
print(f"问题:{q}")
print("\n1. 混合检索结果 (BM25 + 向量):")
h_res = hybrid_qa.invoke({"query": q})
print(f"回答:{h_res['result']}")
print("\n2. 纯BM25检索结果 (对比):")
b_res = bm25_qa.invoke({"query": q})
print(f"回答:{b_res['result']}")
print("\n3. 纯向量检索结果 (对比):")
v_res = vector_qa.invoke({"query": q})
print(f"回答:{v_res['result']}")
print("=" * 80)
# ===================== 主入口 =====================
if __name__ == "__main__":
# 1. 解析财务数据并构建文档
nodes = parse_finance_document()
lc_docs = build_langchain_documents(nodes)
# 2. 创建混合检索器
# 注意:这里会连接Milvus并自动入库,请确保Milvus服务已启动
hybrid_retriever = create_hybrid_retriever(lc_docs)
# 获取单独的检索器用于对比测试
bm25_only = BM25Retriever.from_documents(lc_docs)
bm25_only.k = 2
embedding = OpenAIEmbeddings(
model=EMBEDDING_MODEL,
openai_api_key=OPENAI_API_KEY,
openai_api_base=OPENAI_BASE_URL,
)
vs = Milvus.from_documents(
lc_docs,
embedding,
connection_args={"uri": MILVUS_URI},
collection_name="finance_report_hybrid_vs",
drop_old=True,
)
milvus_only = vs.as_retriever(search_kwargs={"k": 2})
# 3. 运行对比测试
test_finance_rag(hybrid_retriever, bm25_only, milvus_only)
总结
| 技术方案 | 设计思路 |
|---|---|
| 父子文档检索 | 小块检索保证精准,大块返回保证上下文完整 |
| 双层检索 | 知识层级拆分、先粗后精检索 |
| 混合检索 | 关键词精准匹配 + 语义相似召回双路互补 |
RAG系统的索引优化策略就分享到这儿,在座的亦菲、彦祖们有想要讨论的,欢迎到评论区留言哦!