用 LangGraph 实现 Small-to-Big 分块检索策略

深入实践:用 LangGraph 实现 Small-to-Big 分块检索策略

引言

在构建企业级 RAG(检索增强生成)系统时,分块策略(Chunking Strategy)是决定检索质量与生成效果的关键一环。传统单粒度分块往往需要在"精确检索"与"上下文完整"之间做出取舍:小块检索精准但上下文孤立,大块信息丰富但噪声多、召回难。为了突破这一困境,Small-to-Big(父文档检索) 策略应运而生,它通过两级分块与递归检索,实现了"鱼与熊掌兼得"的效果。

本文将深入剖析 Small-to-Big 的原理与优势,并基于 LangGraph------一个专为构建状态化、多智能体应用设计的框架------展示如何从零到一落地这一策略。


什么是 Small-to-Big 检索?

Small-to-Big 是一种分层检索策略,它将文档切分为两种粒度的块:

  • 子块(Child Chunks):尺寸较小(如 100~200 token),用于向量检索,确保高精度。
  • 父块(Parent Chunks):尺寸较大(如 500~1000 token 或整个段落),用于生成上下文,提供完整背景。

工作流程

  1. 用户查询向量化,在子块向量库中检索 Top-k 个最相似的子块。
  2. 根据子块记录的父块 ID,从文档存储中取出对应的完整父块
  3. 将这些父块作为上下文输入给大语言模型生成答案。

这种"先检索小块,再获取大块"的机制,既保证了召回精度,又保留了上下文的完整性。


为什么需要 Small-to-Big?

传统单粒度分块的痛点:

  • 小块:检索准确率高,但上下文缺失,LLM 难以理解逻辑关联,容易产生幻觉。
  • 大块:上下文丰富,但噪声多,检索时可能因向量平均化而漏掉关键细节,尤其对于事实性问答效果不佳。

Small-to-Big 的优势:

  • 检索精度提升:子块与查询语义更对齐,能精准命中事实片段。
  • 上下文连贯:父块包含完整背景,支持多跳推理与复杂问题。
  • 灵活性:可动态调整父子块大小,适配不同文档类型(如法律合同、技术手册)。

适用场景

  • 事实性问答:如"公司章程第 3.2 条对董事任期有何规定?"
  • 多跳问题:如"哪个部门的预算超过了去年同期的 10%?"(需要跨段落关联)
  • 长文档精细检索:如科研论文、法律文书、技术规格书。
  • 需引证原文的场景:父块能提供完整段落,便于用户核实。

项目落地:使用 LangGraph 实现 Small-to-Big

为什么选择 LangGraph?

LangGraph 是 LangChain 团队推出的框架,专为构建状态化、多参与者 的应用程序设计。它允许开发者将流程建模为(Graph),节点是处理步骤,边是状态流转。相比 LangChain 的链式(Chain)结构,LangGraph 更灵活,支持循环、分支和复杂状态管理,非常适合实现 Small-to-Big 这类包含多步检索、条件判断的流程。

系统设计

我们设计一个包含以下节点的图:

  1. 分块与索引节点(离线):将原始文档分割为父子块,并存储到向量库与文档库。
  2. 查询节点:接收用户输入。
  3. 子块检索节点:查询向量化,检索 Top-k 子块。
  4. 父块获取节点:根据子块的父块 ID,从文档库获取去重后的父块。
  5. 重排序节点(可选):对父块进行相关性重排序,提升质量。
  6. 生成节点:将父块与查询组装成 Prompt,调用 LLM 生成答案。

在线流程如下:
用户查询
子块检索
获取父块
重排序
LLM 生成
最终答案

核心代码实现

以下代码基于 LangGraph 0.2+ 版本,展示关键节点实现。

1. 定义状态
python 复制代码
from typing import List, Dict, Any
from langgraph.graph import StateGraph, END
from typing_extensions import TypedDict

class RetrievalState(TypedDict):
    question: str
    child_chunks: List[Dict]        # 检索到的子块
    parent_chunks: List[str]         # 获取到的父块内容
    ranked_parents: List[str]        # 重排序后的父块
    answer: str
2. 初始化存储与模型

假设我们已经使用某个向量库(如 Chroma)存储子块向量,并使用一个简单的内存字典存储父块(生产环境可用 Redis 或 PostgreSQL)。

python 复制代码
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.storage import InMemoryStore

vectorstore = Chroma(collection_name="child_chunks", embedding_function=OpenAIEmbeddings())
docstore = InMemoryStore()  # key: parent_id, value: parent_text
3. 节点函数实现

子块检索节点

python 复制代码
def retrieve_child_chunks(state: RetrievalState) -> RetrievalState:
    question = state["question"]
    # 向量检索,返回 top-5 子块
    docs = vectorstore.similarity_search_with_score(question, k=5)
    child_chunks = [{"id": doc.metadata["child_id"], 
                     "parent_id": doc.metadata["parent_id"], 
                     "text": doc.page_content, 
                     "score": score} for doc, score in docs]
    return {"child_chunks": child_chunks}

父块获取节点

python 复制代码
def fetch_parent_chunks(state: RetrievalState) -> RetrievalState:
    child_chunks = state["child_chunks"]
    parent_ids = set(c["parent_id"] for c in child_chunks)
    parent_texts = [docstore.get(pid) for pid in parent_ids if docstore.get(pid)]
    # 去重,保留顺序
    return {"parent_chunks": parent_texts}

重排序节点(可选,使用 Cross-Encoder):

python 复制代码
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

def rerank_parents(state: RetrievalState) -> RetrievalState:
    question = state["question"]
    parent_texts = state["parent_chunks"]
    pairs = [(question, text) for text in parent_texts]
    scores = reranker.predict(pairs)
    ranked = [text for _, text in sorted(zip(scores, parent_texts), reverse=True)]
    return {"ranked_parents": ranked[:3]}  # 取 top-3

生成节点

python 复制代码
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage

llm = ChatOpenAI(model="gpt-4")

def generate_answer(state: RetrievalState) -> RetrievalState:
    context = "\n\n".join(state.get("ranked_parents", state.get("parent_chunks")))
    prompt = f"""基于以下上下文,回答用户问题。如果上下文不相关,请说明无法回答。
上下文:
{context}

问题:{state['question']}
答案:"""
    response = llm.invoke([HumanMessage(content=prompt)])
    return {"answer": response.content}
4. 构建 LangGraph
python 复制代码
from langgraph.graph import StateGraph, END

# 初始化图
graph = StateGraph(RetrievalState)

# 添加节点
graph.add_node("retrieve_child", retrieve_child_chunks)
graph.add_node("fetch_parent", fetch_parent_chunks)
graph.add_node("rerank", rerank_parents)
graph.add_node("generate", generate_answer)

# 设置边
graph.set_entry_point("retrieve_child")
graph.add_edge("retrieve_child", "fetch_parent")
graph.add_edge("fetch_parent", "rerank")
graph.add_edge("rerank", "generate")
graph.add_edge("generate", END)

# 编译图
app = graph.compile()
5. 执行查询
python 复制代码
result = app.invoke({"question": "2024年公司营收增长的主要原因是什么?"})
print(result["answer"])

存储实现细节

在实际项目中,向量存储 (子块)与文档存储(父块)的选择至关重要:

  • 向量存储 :推荐使用生产级数据库如 Milvus、Qdrant 或 Elasticsearch(支持向量)。需存储子块向量及元数据(parent_idchild_id)。
  • 文档存储 :可用 Redis(高性能键值)、PostgreSQL(持久化)或 MongoDB。通过 parent_id 快速检索父块原文。

分块与索引的离线流程可以使用 LangChain 的 ParentDocumentRetriever 简化,但为了与 LangGraph 集成,我们往往需要手动实现索引逻辑,以便控制元数据字段。

增强模块:混合搜索与重排序

为了进一步提升检索质量,可以在 LangGraph 中增加节点:

  • 混合搜索节点:同时执行向量检索和关键词检索(如 BM25),用 RRF 算法融合结果。
  • 重排序节点:如上文所示,用 Cross-Encoder 对父块重排序,能显著提升最终生成质量。

这些节点可以灵活插入图中,实现定制化流程。


优化与挑战

  1. 分块大小调优:父子块的大小需通过实验确定。通常子块 200~400 token,父块 800~2000 token。可借助验证集(如 SQuAD-like 数据)评估召回率。
  2. 存储开销:子块数量多,向量库需支持高并发写入与检索。需估算数据规模并选择合适的数据库。
  3. 延迟 :检索 + 重排序会增加响应时间。可考虑将重排序作为可选步骤,或使用更轻量的模型(如 Qwen3-Reranker-0.6B)。
  4. 去重与冗余:多个子块可能映射到同一父块,务必在获取父块时去重,避免重复输入。
  5. 元数据传递:LangGraph 的状态管理需确保每个节点只访问必要的字段,避免状态膨胀。

总结

Small-to-Big 分块策略通过两级粒度与递归检索,有效平衡了 RAG 系统的检索精度与上下文完整性。借助 LangGraph,我们可以将这一流程建模为清晰的状态图,灵活地加入重排序、混合搜索等增强模块,构建一个可扩展、易维护的企业级检索系统。


延伸资源


相关推荐
大江东去浪淘尽千古风流人物1 小时前
【Sensor】IMU传感器选型车轨级 VS 消费级
人工智能·python·算法·机器学习·机器人
坚持编程的菜鸟2 小时前
互质数的个数
c语言·算法
jay神2 小时前
基于 YOLOv11 的人脸表情识别系统
人工智能·深度学习·yolo·目标检测·计算机视觉
2501_947908202 小时前
试了一下 MaiHH Conn
人工智能
byzh_rc2 小时前
[深度学习网络从入门到入土] 含并行连结的网络GoogLeNet
网络·人工智能·深度学习
ICscholar2 小时前
具身智能‘Affordance‘理解
人工智能·学习·算法
yhdata2 小时前
3.6%年复合增速定调!雾化片赛道未来六年发展路径清晰,潜力稳步释放
大数据·人工智能
乾元2 小时前
对抗性攻击:一张贴纸如何让自动驾驶视觉系统失效?
运维·网络·人工智能·安全·机器学习·自动驾驶
wangwangmoon_light2 小时前
1.2 LeetCode总结(线性表)_双指针
算法·leetcode·职场和发展