题解生成中的长文本处理:上下文窗口管理与分段理解策略
一、当一道题的题解超出了模型的上下文窗口
使用 LLM 生成算法题解时,理想情况是:把题目描述、样例、约束条件一并丢给模型,然后拿到一份完整的题解。
现实却没这么简单。有些题目描述本身就超过 2000 字,加上样例和约束条件,总 token 数轻松突破 4K。如果题目还涉及到复杂的图结构或依赖外部知识库(如特定算法的背景介绍),上下文窗口很容易被塞满。当输入超过模型的上下文上限时,模型要么截断,要么丢失关键信息,导致题解质量急剧下降。
更麻烦的是,你还希望模型在生成题解的同时,能参考历史对话中的纠错反馈。这意味着输入文本的规模只增不减。如何在不丢失关键信息的前提下管理长文本,是这道工程题的核心。
二、信息密度模型:不是所有内容都同等重要
解决长文本问题的第一步,是承认一个前提:题目描述中不同部分的「信息密度」是不同的。
flowchart TD subgraph 高密度信息 A[约束条件: 1 ≤ n ≤ 10^5] B[时间复杂度要求: O(n log n)] C[特殊条件: 数组已部分有序] end subgraph 中密度信息 D[题目描述正文] E[输入输出格式说明] end subgraph 低密度信息 F[背景故事描述] G[示例的详细解释文字] H[提示与备注] end A --> I[必须保留] B --> I C --> I D --> J[可摘要] E --> J F --> K[可裁剪] G --> K H --> K基于这个分类,我们可以设计一套分层摘要策略:
- 约束条件、时间复杂度要求:原样保留,不进行任何压缩。
- 题目描述正文:用轻量模型做摘要,提取核心算法需求。
- 样例输入输出:保留数值,丢弃解释性文字。
- 背景故事、提示:直接裁剪,仅在首次生成时作为补充信息注入。
三、实现:带摘要缓存的长文本管理器
from dataclasses import dataclass, field from typing import Optional import hashlib import json @dataclass class TextSegment: """文本片段:标记内容、类别与重要性""" content: str category: str # "constraint" | "description" | "example" | "hint" priority: int # 1=必须保留,2=可摘要,3=可裁剪 summary: Optional[str] = None # 摘要缓存 class LongTextManager: """长文本管理器:负责对题目描述进行分层压缩 设计思路: 1. 将输入文本按类别拆分为多个片段。 2. 根据优先级决定保留、摘要还是裁剪。 3. 使用哈希做缓存,避免对相同内容重复摘要。 4. 输出在 token 预算内的精简文本。 """ def __init__(self, max_tokens: int = 4096): self.max_tokens = max_tokens self._summary_cache: dict[str, str] = {} # 粗略估计:中文约 1.5 字符/token,预留安全边界 self._chars_per_token = 1.5 def process( self, segments: list[TextSegment], enable_summary: bool = True, ) -> str: """处理文本片段,返回压缩后的文本""" # 第一阶段:高优先级内容原样保留 result_parts: list[str] = [] remaining = self.max_tokens for seg in segments: if seg.priority == 1: result_parts.append(seg.content) remaining -= self._estimate_tokens(seg.content) elif seg.priority == 2 and enable_summary: summary = self._get_summary(seg) result_parts.append(summary) remaining -= self._estimate_tokens(summary) # priority == 3 的内容直接跳过 # 第二阶段:如果仍然超出限制,对 priority=2 做进一步压缩 while remaining < 0 and result_parts: # 从末尾开始逐步移除低优内容 popped = result_parts.pop() remaining += self._estimate_tokens(popped) return "\n\n".join(result_parts) def _get_summary(self, seg: TextSegment) -> str: """获取文本摘要(带缓存)""" cache_key = hashlib.md5(seg.content.encode()).hexdigest() if cache_key in self._summary_cache: return self._summary_cache[cache_key] # 简易规则摘要:提取首段 + 关键词 # 生产环境此处应调用轻量 LLM 做智能摘要 lines = seg.content.strip().split("\n") # 取前三行作为"快速摘要" quick = "\n".join(lines[:3]) if len(lines) > 3: quick += f"\n(共 {len(lines)} 行,已省略后续内容)" self._summary_cache[cache_key] = quick return quick def _estimate_tokens(self, text: str) -> int: """估算文本的 token 数量""" return int(len(text) / self._chars_per_token) class ProblemContextBuilder: """题目上下文构建器:将题目信息组装为 LLM 可消费的格式""" @staticmethod def build_prompt( problem: dict, manager: LongTextManager, ) -> str: """构建 prompt:将原始题目转换为分段后的压缩文本""" segments = [ TextSegment( content=f"约束条件:{problem.get('constraints', '无')}", category="constraint", priority=1, ), TextSegment( content=f"题目描述:{problem.get('description', '')}", category="description", priority=2, ), TextSegment( content=f"样例:{json.dumps(problem.get('examples', []), ensure_ascii=False)}", category="example", priority=1, ), TextSegment( content=f"提示:{problem.get('hints', '无')}", category="hint", priority=3, ), ] compressed = manager.process(segments) return f"""请根据以下题目信息生成题解: {compressed} 要求: 1. 给出完整代码实现 2. 分析时间与空间复杂度 3. 解释核心思路""" # ---- 使用示例 ---- if __name__ == "__main__": problem = { "constraints": "1 ≤ n ≤ 10^5, -10^9 ≤ nums[i] ≤ 10^9", "description": ( "给定一个整数数组 nums 和一个目标值 target," "请你在数组中找出和为目标值的那两个整数," "并返回它们的数组下标。你可以假设每种输入" "只会对应一个答案。但是数组中同一个元素" "不能使用两遍。" * 20 # 模拟长文本 ), "examples": [ {"input": "nums=[2,7,11,15], target=9", "output": "[0,1]"} ], "hints": "可以使用哈希表优化到 O(n)", } manager = LongTextManager(max_tokens=2048) prompt = ProblemContextBuilder.build_prompt(problem, manager) print(f"Prompt 长度: {len(prompt)} 字符")四、边界分析与权衡
4.1 简单规则摘要 vs LLM 摘要
当前实现使用取前三行的规则摘要。这做法的优点是零延迟、零成本,缺点是摘要质量不稳定。对于复杂题目描述,可能需要调用轻量 LLM(如 7B 模型)做真正的语义摘要。这是一个典型的成本-质量权衡。
4.2 上下文窗口的碎片化
分层压缩后,文本的非连续性会影响模型对上下文的理解。约束条件独立于描述文字,可能导致模型忽略了两者之间的隐含关系(如约束条件暗示了算法选择)。缓解方案是在片段间加入衔接提示语。
4.3 Token 估算的误差
_estimate_tokens使用固定的字符/token 比例。对于中英文混合文本,这个估算可能有 20%-30% 的偏差。生产环境应该使用模型对应的 tokenizer 做精确计数。
4.4 多轮对话的上下文膨胀
随着对话轮次增加,历史消息也会占用上下文窗口。需要设计消息淘汰策略——保留最近的纠错反馈,丢弃较早的一般性对话。
五、总结
长文本处理不是模型的问题——模型的能力上限就在那里。工程化的思路是:在模型能力不变的前提下,通过分层管理、优先级调度、缓存复用等手段,让有限的上下文窗口装下尽可能多的有价值信息。这本质上是一种信息密度优化,与搜索引擎的摘要生成、推荐系统的特征筛选面临同样的挑战。