用 Map-Reduce 并行化处理长文本总结:基于 LangGraph 的实践指南

在处理长文本总结时,我们常常会遇到一个棘手的问题:当文本长度超过大语言模型(LLM)的上下文窗口时,如何高效且准确地生成全局摘要?Map-Reduce 范式为我们提供了一个优雅的解决方案。通过将任务分解为并行的映射(Map)阶段和聚合的归约(Reduce)阶段,我们可以轻松应对任意长度的文本,今天我们就来聊聊如何基于 LangGraph 框架实现这一流程。

一、Map-Reduce 核心思想:分而治之

Map-Reduce 的核心逻辑可以概括为 “先分后合”:

  1. 映射阶段(Map):将每个子文档独立输入 LLM 生成局部摘要。这一步天然适合并行处理,我们可以同时向多个 LLM 实例发送请求,大幅提升处理效率。
  2. 归约阶段(Reduce):将所有局部摘要合并为一个全局摘要。这里需要注意,如果局部摘要的总长度仍超过模型上下文窗口,需要递归执行 “分割 - 摘要 - 合并” 的过程,直到结果符合要求。

LangGraph 作为 LangChain 生态的重要扩展,为 Map-Reduce 工作流提供了原生支持。它具备三大优势:

  • 流式控制:支持单个步骤的流式处理,我们可以精确监控每个摘要生成过程;
  • 容错机制:通过检查点技术实现错误恢复,还能无缝集成人工审核环节;
  • 灵活扩展:图结构的设计让我们可以轻松修改节点逻辑或调整工作流。

二、关键步骤实现:从提示工程到流程编排

1. 定义核心提示模板

首先准备两个关键提示:
映射阶段提示(生成局部摘要):

python

from langchain_core.prompts import ChatPromptTemplate
map_prompt = ChatPromptTemplate.from_messages([
    ("system", "对以下内容撰写简洁摘要:\n\n{context}")
])

这里我们也可以通过 LangChain 的 Prompt Hub 直接获取预设提示,只需一行代码:

python

from langchain import hub
map_prompt = hub.pull("rlm/map-prompt")  # 从提示中心加载映射提示

归约阶段提示(合并全局摘要):

python

reduce_template = """
以下是一组摘要:
{docs}
请将这些内容提炼为一个关于核心主题的最终合并摘要。
"""
reduce_prompt = ChatPromptTemplate([("human", reduce_template)])

2. 文本分割:处理超长文本的前提

使用CharacterTextSplitter将原始文档分割为符合模型处理能力的子文档:

python

from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=1000,  # 单个子文档最大长度
    chunk_overlap=0    # 子文档间无重叠(可根据需要调整)
)
split_docs = text_splitter.split_documents(docs)  # docs为原始文档列表
print(f"生成{len(split_docs)}个子文档。")

这里以 Tiktoken 编码为例,实际使用时需根据目标模型调整chunk_size,确保每个子文档不超过模型的上下文限制。

3. 构建状态图:定义工作流逻辑

LangGraph 通过状态图(StateGraph)描述整个流程,我们需要定义以下核心组件:

状态定义

python

from typing import Annotated, List, Literal, TypedDict
from langchain_core.documents import Document

class OverallState(TypedDict):
    contents: List[str]          # 原始子文档内容列表
    summaries: Annotated[list, operator.add]  # 收集所有局部摘要(使用加法合并)
    collapsed_summaries: List[Document]  # 中间合并结果
    final_summary: str           # 最终全局摘要
核心节点定义
  • 生成局部摘要:每个子文档独立调用 LLM 生成摘要

python

async def generate_summary(state: SummaryState):
    prompt = map_prompt.invoke(state["content"])  # 生成提示
    response = await llm.ainvoke(prompt)  # 异步调用LLM
    return {"summaries": [response.content]}  # 返回单个摘要

  • 合并中间结果:递归处理超过上下文限制的摘要列表

python

from langchain.chains.combine_documents.reduce import split_list_of_docs
token_max = 1000  # 设定单次处理的最大token数

async def collapse_summaries(state: OverallState):
    # 按token数分割摘要列表
    doc_lists = split_list_of_docs(
        state["collapsed_summaries"], 
        lambda docs: sum(llm.get_num_tokens(doc.page_content) for doc in docs),
        token_max
    )
    # 对每个分组单独合并
    results = []
    for doc_list in doc_lists:
        results.append(await acollapse_docs(doc_list, _reduce))
    return {"collapsed_summaries": results}

  • 条件判断:决定是否需要继续递归合并

python

def should_collapse(state: OverallState) -> Literal["collapse_summaries", "generate_final_summary"]:
    total_tokens = sum(llm.get_num_tokens(doc.page_content) for doc in state["collapsed_summaries"])
    return "collapse_summaries" if total_tokens > token_max else "generate_final_summary"
组装状态图

python

from langgraph.graph import StateGraph, START, END

graph = StateGraph(OverallState)
# 添加节点
graph.add_node("generate_summary", generate_summary)
graph.add_node("collect_summaries", lambda state: {"collapsed_summaries": [Document(summary) for summary in state["summaries"]]})
graph.add_node("collapse_summaries", collapse_summaries)
graph.add_node("generate_final_summary", lambda state: {"final_summary": await _reduce(state["collapsed_summaries"])})
# 添加边(控制流程走向)
graph.add_conditional_edges(START, lambda state: [Send("generate_summary", {"content": c}) for c in state["contents"]])
graph.add_edge("generate_summary", "collect_summaries")
graph.add_conditional_edges("collect_summaries", should_collapse)
graph.add_conditional_edges("collapse_summaries", should_collapse)
graph.add_edge("generate_final_summary", END)

4. 执行与监控

通过流式执行可以实时观察处理步骤,还能设置递归限制防止无限循环:

python

async for step in graph.compile().astream(
    {"contents": [doc.page_content for doc in split_docs]},
    {"recursion_limit": 10}  # 最多递归10层
):
    print(f"当前步骤:{list(step.keys())}")  # 输出当前执行的节点名称

三、关键技术点解析

  1. 并行化优势:映射阶段可同时处理多个子文档,处理时间主要取决于最慢的单个任务,而非文档总数;
  2. 递归合并:通过split_list_of_docsacollapse_docs实现动态分组,确保每次输入 LLM 的内容不超过上下文限制;
  3. 状态管理OverallState统一管理所有中间结果,避免数据丢失或混乱。

四、总结与实践建议

通过 LangGraph 实现的 Map-Reduce 工作流,我们能够高效处理远超 LLM 原生上下文限制的长文本总结任务。实际使用时需注意:

  • 根据目标模型调整chunk_sizetoken_max
  • 复杂场景可通过检查点技术(Checkpointing)实现断点续传;
  • 提示模板可通过 Prompt Hub 共享,提升团队协作效率。

如果你在处理法律合同、学术论文等长文本时遇到效率问题,不妨尝试这种分治策略。觉得有帮助的话,别忘了点赞收藏,后续我们会分享更多 LangChain 生态的实战技巧~

你可能感兴趣的:(LangChain,python,langchain)