LLM 长上下文处理:从滑动窗口到分层摘要的工程实践 LLM 长上下文处理从滑动窗口到分层摘要的工程实践一、上下文窗口的容量焦虑长文档处理的工程困境大语言模型的上下文窗口从 4K 扩展到 128K 甚至 1M Token但窗口大不等于用得好。在实际工程中直接将 10 万 Token 的文档塞入上下文窗口会面临三个核心问题推理延迟随 Token 数线性增长、中间位置的迷失效应模型对上下文中间部分的信息提取能力显著弱于首尾、以及 Token 成本的指数级膨胀。某法律科技团队在处理 200 页合同审查时直接将全文输入 128K 窗口的模型单次推理耗时 47 秒、费用超过 2 美元且对合同中间条款的遗漏率高达 35%。长上下文处理的工程本质不是把所有内容塞进窗口而是在有限的窗口内最大化信息密度。滑动窗口、分层摘要和检索增强是三种主流策略各有适用场景和工程权衡。二、长上下文处理的三种策略与架构对比flowchart LR subgraph 输入[长文档输入] DOC[10万 Token 文档] end subgraph 策略A[滑动窗口] A1[窗口大小 W] -- A2[步长 S] A2 -- A3[逐窗口处理] A3 -- A4[结果合并] end subgraph 策略B[分层摘要] B1[文档分段] -- B2[底层摘要] B2 -- B3[中层聚合] B3 -- B4[顶层总结] end subgraph 策略C[检索增强] C1[文档切块索引] -- C2[查询向量检索] C2 -- C3[Top-K 块注入] C3 -- C4[上下文生成] end DOC -- A1 DOC -- B1 DOC -- C1 A4 -- OUT[输出] B4 -- OUT C4 -- OUT style 策略A fill:#ffd,stroke:#333 style 策略B fill:#dfd,stroke:#333 style 策略C fill:#ddf,stroke:#333三种策略的核心差异维度滑动窗口分层摘要检索增强信息完整性高逐段覆盖中摘要有损低仅检索相关块延迟O(N/W) 次推理O(log N) 次推理O(1) 次推理 检索Token 成本高重复处理重叠区中摘要压缩低仅处理相关块适用场景全文遍历型任务理解型任务问答型任务三、分层摘要的生产级实现分层摘要是最适合理解型长文档任务的策略以下实现包含分段、摘要和聚合三个阶段import hashlib from dataclasses import dataclass from typing import Callable dataclass class DocumentChunk: 文档分段 content: str index: int token_count: int hash: str # 内容指纹用于缓存 staticmethod def compute_hash(content: str) - str: return hashlib.md5(content.encode()).hexdigest()[:12] dataclass class SummaryNode: 摘要树节点 summary: str children: list[SummaryNode] level: int # 0底层, 1中层, 2顶层 source_chunks: list[int] # 原始分段索引 class HierarchicalSummarizer: 分层摘要引擎 def __init__( self, llm_generate: Callable[[str], str], chunk_size: int 2000, # 底层分段大小Token chunk_overlap: int 200, # 分段重叠 merge_size: int 5, # 每层合并节点数 max_levels: int 3 # 最大层级 ): self.llm_generate llm_generate self.chunk_size chunk_size self.chunk_overlap chunk_overlap self.merge_size merge_size self.max_levels max_levels self._cache: dict[str, str] {} # 摘要缓存 def _split_document(self, text: str) - list[DocumentChunk]: 按 Token 数分段保留重叠区 # 简化实现按字符数近似分段 # 生产环境应使用 tiktoken 精确计算 char_per_token 1.5 # 中文约 1.5 字符/Token chunk_chars int(self.chunk_size * char_per_token) overlap_chars int(self.chunk_overlap * char_per_token) chunks [] start 0 idx 0 while start len(text): end min(start chunk_chars, len(text)) content text[start:end] chunks.append(DocumentChunk( contentcontent, indexidx, token_countint(len(content) / char_per_token), hashDocumentChunk.compute_hash(content) )) start end - overlap_chars idx 1 return chunks def _summarize_chunk(self, chunk: DocumentChunk) - str: 生成单个分段的摘要带缓存 if chunk.hash in self._cache: return self._cache[chunk.hash] prompt ( 请对以下文本片段生成结构化摘要保留关键实体、 数据指标和逻辑关系。摘要不超过 200 字。\n\n f---\n{chunk.content}\n--- ) summary self.llm_generate(prompt) self._cache[chunk.hash] summary return summary def _merge_summaries(self, summaries: list[str], level: int) - str: 聚合多层摘要 combined \n\n.join( f[片段{i1}] {s} for i, s in enumerate(summaries) ) prompt ( f以下是 {len(summaries)} 个片段的摘要层级 {level} 请聚合为更高层级的摘要保留跨片段的关联信息 去除冗余突出核心结论。不超过 300 字。\n\n f---\n{combined}\n--- ) return self.llm_generate(prompt) def summarize(self, text: str) - SummaryNode: 构建分层摘要树 # 第一层分段摘要 chunks self._split_document(text) leaf_nodes [] for chunk in chunks: summary self._summarize_chunk(chunk) leaf_nodes.append(SummaryNode( summarysummary, children[], level0, source_chunks[chunk.index] )) # 逐层聚合 current_level leaf_nodes level 1 while len(current_level) 1 and level self.max_levels: next_level [] for i in range(0, len(current_level), self.merge_size): group current_level[i:i self.merge_size] merged_summary self._merge_summaries( [n.summary for n in group], level ) all_sources [] for n in group: all_sources.extend(n.source_chunks) next_level.append(SummaryNode( summarymerged_summary, childrengroup, levellevel, source_chunksall_sources )) current_level next_level level 1 return current_level[0] if current_level else leaf_nodes[0] def query_with_context(self, root: SummaryNode, query: str) - str: 基于摘要树的定向查询先定位相关子树再展开细节 # 从顶层摘要开始逐层定位最相关分支 current root while current.children: # 评估每个子节点与查询的相关性 best_child None best_score -1 for child in current.children: score_prompt ( f查询{query}\n摘要{child.summary}\n 相关性评分0-10仅输出数字 ) score_str self.llm_generate(score_prompt).strip() try: score float(score_str) except ValueError: score 0 if score best_score: best_score score best_child child current best_child if best_child else current.children[0] # 到达底层用原始摘要 查询生成答案 answer_prompt ( f基于以下摘要内容回答查询。\n f摘要{current.summary}\n查询{query}\n回答 ) return self.llm_generate(answer_prompt)关键设计要点分段重叠相邻分段保留 200 Token 的重叠区防止关键信息被截断在分段边界处。这是滑动窗口思想在分段层面的应用。摘要缓存通过内容哈希实现摘要缓存相同内容的分段不重复调用 LLM。在文档更新场景下仅重新生成变化分段的摘要。定向查询query_with_context方法实现了从顶层摘要到底层细节的逐层定位避免将全量摘要注入上下文。四、策略选型的 Trade-offs 分析滑动窗口的边界丢失问题。滑动窗口虽然信息完整性最高但跨窗口的关联信息容易丢失。例如一份季度报告中同比增长 15%出现在窗口 A而主要驱动力是海外市场出现在窗口 B两个窗口独立处理时无法建立因果关系。解决方案是增大重叠区但重叠区越大Token 成本越高。分层摘要的信息损失不可逆。摘要过程本质上是有损压缩底层摘要遗漏的信息在中层聚合时无法恢复。对于数据密集型文档如财务报表摘要可能丢失关键数值。建议对数值型内容采用提取而非摘要的策略——将数字直接提取到结构化字段中而非让 LLM 概括。检索增强的召回率瓶颈。检索增强在问答场景下效率最高但严重依赖查询与文档块的语义匹配度。当用户的问题与文档表述方式差异较大时如用口语化提问检索正式文档召回率会显著下降。混合检索向量 关键词可以缓解但不能根治。综合建议理解型任务文档总结、合规审查优先使用分层摘要问答型任务知识库查询优先使用检索增强遍历型任务全文翻译、逐段标注使用滑动窗口。实际工程中三种策略往往组合使用。五、总结长上下文处理的核心矛盾是信息完整性与推理效率之间的权衡。滑动窗口保留完整信息但成本高分层摘要压缩信息但存在损失检索增强高效但召回率受限。工程实践中的最优解不是选择单一策略而是根据任务类型动态组合用分层摘要构建文档的全局理解用检索增强定位局部细节用滑动窗口处理需要逐段遍历的场景。同时摘要缓存、分段重叠和定向查询等优化手段能够在不显著牺牲质量的前提下将 Token 成本降低 40%-60%。