在处理长文本总结时,我们常常会遇到一个棘手的问题:当文本长度超过大语言模型(LLM)的上下文窗口时,如何高效且准确地生成全局摘要?Map-Reduce 范式为我们提供了一个优雅的解决方案。通过将任务分解为并行的映射(Map)阶段和聚合的归约(Reduce)阶段,我们可以轻松应对任意长度的文本,今天我们就来聊聊如何基于 LangGraph 框架实现这一流程。
Map-Reduce 的核心逻辑可以概括为 “先分后合”:
LangGraph 作为 LangChain 生态的重要扩展,为 Map-Reduce 工作流提供了原生支持。它具备三大优势:
首先准备两个关键提示:
映射阶段提示(生成局部摘要):
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)])
使用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
,确保每个子文档不超过模型的上下文限制。
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 # 最终全局摘要
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)
通过流式执行可以实时观察处理步骤,还能设置递归限制防止无限循环:
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())}") # 输出当前执行的节点名称
split_list_of_docs
和acollapse_docs
实现动态分组,确保每次输入 LLM 的内容不超过上下文限制;OverallState
统一管理所有中间结果,避免数据丢失或混乱。通过 LangGraph 实现的 Map-Reduce 工作流,我们能够高效处理远超 LLM 原生上下文限制的长文本总结任务。实际使用时需注意:
chunk_size
和token_max
;如果你在处理法律合同、学术论文等长文本时遇到效率问题,不妨尝试这种分治策略。觉得有帮助的话,别忘了点赞收藏,后续我们会分享更多 LangChain 生态的实战技巧~