大模型长文本分块策略与上下文窗口管理的后端架构

发布时间:2026/6/11 18:33:43
大模型长文本分块策略与上下文窗口管理的后端架构 大模型长文本分块策略与上下文窗口管理的后端架构一、超长输入的截断困境大模型上下文窗口的工程挑战大模型的上下文窗口从 4K Token 扩展到 128K 甚至 1M但实际工程中超长输入的处理远比塞进窗口复杂得多。一份 50 页的合同文档、一段 2 小时的会议录音转写文本、一个包含数千条记录的数据集——这些输入动辄数十万 Token远超单次请求的上下文容量。粗暴截断会丢失关键信息而简单的等长分块又会切断语义完整性。例如将一段第 3 条甲方应在收到通知后 15 个工作日内完成整改否则乙方有权解除合同从中间切断前后两块都无法独立理解。长文本分块策略的核心目标是在上下文窗口限制下最大化信息保留的完整性和检索的精准度。二、分块策略的底层机制与架构设计长文本处理通常采用分块-检索-重组的三段式架构。分块阶段将长文本切分为语义完整的片段检索阶段根据用户问题匹配最相关的片段重组阶段将匹配片段组装为模型输入。flowchart TD A[原始长文本输入] -- B[文本预处理: 清洗/格式统一] B -- C{分块策略选择} C --|结构化文档| D[基于标题层级分块] C --|非结构化文本| E[基于语义边界分块] C --|代码文档| F[基于 AST 语法树分块] D E F -- G[生成 Chunk 列表] G -- H[每个 Chunk Embedding 编码] H -- I[存入向量数据库] I -- J[用户提问到达] J -- K[检索 Top-K 相关 Chunk] K -- L[上下文窗口预算分配] L -- M[组装 Prompt: 系统指令 检索片段 用户问题] M -- N[调用大模型生成回答]分块策略的关键参数包括块大小Chunk Size、块重叠Chunk Overlap和分块边界Chunk Boundary。块大小决定了单次检索的信息粒度块重叠防止语义断裂分块边界决定了切分的语义完整性。三、生产级分块策略的代码实现3.1 基于语义边界的自适应分块Service Slf4j public class SemanticChunkService { private final EmbeddingClient embeddingClient; private final ChunkConfigProperties config; /** * 基于语义边界的自适应分块 * 核心思路在相邻段落的语义相似度低于阈值处切分 * 保证每个 Chunk 内部语义连贯跨 Chunk 语义有区分 */ public ListTextChunk semanticChunk(String document) { // 1. 按自然段落预切分 ListString paragraphs splitParagraphs(document); if (paragraphs.size() 1) { return List.of(TextChunk.of(document, 0, document.length())); } // 2. 计算相邻段落的语义相似度 ListDouble similarities new ArrayList(); float[] prevEmbedding embeddingClient.embed(paragraphs.get(0)); for (int i 1; i paragraphs.size(); i) { float[] currEmbedding embeddingClient.embed(paragraphs.get(i)); double sim cosineSimilarity(prevEmbedding, currEmbedding); similarities.add(sim); prevEmbedding currEmbedding; } // 3. 在相似度低谷处切分语义转折点 ListInteger splitPoints findSplitPoints(similarties); return buildChunks(paragraphs, splitPoints); } /** * 寻找语义转折点相似度低于动态阈值的段落边界 * 动态阈值 均值 - 标准差 × 系数避免固定阈值的不适应性 */ private ListInteger findSplitPoints(ListDouble similarities) { double mean similarities.stream().mapToDouble(d - d).average().orElse(0.5); double stdDev Math.sqrt( similarities.stream() .mapToDouble(d - Math.pow(d - mean, 2)) .average().orElse(0.0) ); double threshold mean - stdDev * config.getSplitSensitivity(); ListInteger points new ArrayList(); for (int i 0; i similarities.size(); i) { if (similarities.get(i) threshold) { points.add(i 1); // 在第 i1 段之前切分 } } return points; } /** * 根据切分点组装 Chunk * 每个 Chunk 包含前后重叠内容防止语义断裂 */ private ListTextChunk buildChunks(ListString paragraphs, ListInteger splitPoints) { ListTextChunk chunks new ArrayList(); int start 0; for (int sp : splitPoints) { String content String.join(\n, paragraphs.subList(start, sp)); // 添加前一个 Chunk 的尾部作为重叠 if (!chunks.isEmpty() config.getOverlapSentences() 0) { String overlap extractTailSentences( chunks.get(chunks.size() - 1).getContent(), config.getOverlapSentences() ); content overlap \n content; } chunks.add(TextChunk.of(content, start, sp)); start sp; } // 最后一个 Chunk if (start paragraphs.size()) { String content String.join(\n, paragraphs.subList(start, paragraphs.size())); chunks.add(TextChunk.of(content, start, paragraphs.size())); } return chunks; } }3.2 上下文窗口预算分配器/** * 上下文窗口预算分配器 * 根据检索结果和系统指令的 Token 占用动态分配各部分的 Token 预算 */ Service public class ContextWindowBudgetAllocator { private final TokenCounter tokenCounter; /** * 分配上下文窗口预算 * 总预算 模型上下文窗口 - 输出预留 * 各部分按优先级分配系统指令 检索片段 用户问题 */ public ContextBudget allocate(String systemPrompt, ListTextChunk retrievedChunks, String userQuery, int modelContextWindow) { int totalBudget modelContextWindow - config.getOutputReserveTokens(); // 1. 系统指令占用最高优先级不可压缩 int systemTokens tokenCounter.count(systemPrompt); // 2. 用户问题占用 int queryTokens tokenCounter.count(userQuery); // 3. 剩余预算分配给检索片段 int chunkBudget totalBudget - systemTokens - queryTokens; if (chunkBudget 0) { log.warn(上下文窗口预算不足: total{}, system{}, query{}, totalBudget, systemTokens, queryTokens); chunkBudget Math.max(chunkBudget, config.getMinChunkTokens()); } // 4. 按相关度排序截断超出预算的片段 ListTextChunk selectedChunks selectChunksWithinBudget( retrievedChunks, chunkBudget ); return new ContextBudget(systemTokens, queryTokens, tokenCounter.countChunks(selectedChunks), totalBudget); } private ListTextChunk selectChunksWithinBudget(ListTextChunk chunks, int budget) { ListTextChunk selected new ArrayList(); int used 0; for (TextChunk chunk : chunks) { int chunkTokens tokenCounter.count(chunk.getContent()); if (used chunkTokens budget) { selected.add(chunk); used chunkTokens; } else { // 部分截断保留预算允许的部分 int remaining budget - used; if (remaining config.getMinChunkTokens()) { selected.add(chunk.truncateToTokens(remaining, tokenCounter)); } break; } } return selected; } }四、长文本分块的边界分析与架构权衡语义分块的 Embedding 开销。对每个段落做 Embedding 编码一篇 100 段的文档需要 100 次 Embedding 调用。如果使用在线 Embedding API延迟和成本都不可忽视。建议使用本地部署的轻量级 Embedding 模型或在文档入库时预计算并缓存。块大小与检索精度的矛盾。块太小如 100 Token检索可能命中多个碎片化的片段上下文不完整块太大如 2000 Token检索可能命中大量无关内容稀释有效信息。生产中建议块大小在 300-800 Token 之间重叠 50-100 Token。结构化文档的特殊处理。合同、论文等有明确标题层级的文档基于标题分块比语义分块更可靠。标题本身就是天然的语义边界且层级关系可以用于构建 Chunk 间的父子引用支持先粗后细的递归检索。适用边界分块-检索-重组架构最适合问答场景。对于需要全局理解的场景如文档摘要、情感分析分块后重组可能丢失跨段落的关联信息此时应考虑 Map-Reduce 或迭代摘要策略。五、总结长文本分块是大模型后端架构中的核心工程问题。基于语义边界的自适应分块可以在上下文窗口限制下最大化信息完整性上下文窗口预算分配器确保各部分 Token 占用可控。落地时需关注 Embedding 开销、块大小与检索精度的平衡、以及结构化文档的特殊处理。建议从固定大小分块开始逐步演进到语义分块同时建立分块质量的评估指标。