ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大模型上下文管理:全量、摘要与检索三种模式实战解析

大模型上下文管理:全量、摘要与检索三种模式实战解析 1. 先理解context-mode它到底解决了什么问题最近一年做LLM应用我越来越觉得决定一个AI助手是能用还是好用的往往不是模型选得多大而是对上下文的管理够不够精细。你可能也遇到过这种情况明明接的是GPT-4或者Claude级别的大模型结果多聊几轮之后模型开始答非所问或者你喂了一份两万字的项目文档进去模型反而把最关键的结论给漏了。问题多半出在上下文窗口被塞满了无关内容模型根本不知道应该优先看哪一部分。context-mode这个词简单地翻译就是上下文模式。它指的是在大模型应用运行时我们把哪些信息进入上下文、以什么结构进入、什么时候更新、用完后如何淘汰这套逻辑显式化形成几种可复用、可切换的运行模式。它不是某个开源框架里的固定API而是一种设计思想只不过最近被越来越多的LangChain、LlamaIndex类项目当作核心模块来做。我做过的几个项目包括知识库问答机器人、代码审查助手、还有多轮客服Agent最后都收敛到同一套结论上下文管理如果不做成模式化就一定会在迭代中腐化。这篇文章会从概念讲起把三种主流模式全量、摘要、检索逐一拆开再给出一套可以直接抄的轻量实现最后聊聊我在生产环境里踩过的那些坑。想自己搭AI应用、或者正在优化现有Prompt和RAG链路的朋友这篇文章应该能帮你把思路理清楚。1.1 被上下文窗口卡住的应用瓶颈先别急着谈模式我们看一个具体场景。假设你在做一个企业知识库助手用户上传了几十份PDF和Word文档总字数大概三十万。模型单次请求的上下文窗口如果是128K token折算成中文大概是八万字左右——也就是说哪怕你只问今年的差旅报销标准是什么模型也没办法把这三十万字全部读一遍再回答。解决办法听起来很简单做检索只把相关片段放进来。但检索天然有召回率问题相关不等于完整。用户追问那上个月新出的补充规定有没有影响如果补充规定里没有出现差旅报销这些关键词向量检索很可能召不回这段内容模型就会给出过时甚至错误的答案。再看另一个场景。你在做一个Agent让它帮用户订机票、查天气、比价、下单整个流程可能需要调用八到十次工具。每次调用的结果都在增长上下文等到第五次调用的时候初始的指令已经被挤到很后面模型开始忘掉用户最初说过的限制条件比如只要靠窗座位或者预算不能超过两千。这类问题不是检索能解决的它需要的是另一种上下文组织方式——摘要、关键状态追踪、或者分层压缩。这两个例子指向同一个核心矛盾上下文窗口是硬约束而信息总量和交互轮次是无限增长的。很多团队的第一反应是换窗口更大的模型但窗口翻倍成本和延迟也在涨并且只是把问题推迟了几个轮次而已。更务实的方案是像操作系统管理内存一样管理上下文该驻留的驻留、该换出的换出、该压缩的压缩。这就是context-mode要解决的问题。1.2 context-mode的概念拆解我把上下文模式理解为一套显式的上下文生命周期管理策略。它至少要回答四个问题选择性Selective哪些信息需要被放进当前请求组织性Structured这些信息以什么形态、什么顺序排列时效性Freshness信息多久更新一次旧信息何时作废排他性Exclusive信息之间是否存在冲突优先级如何确定模式与模式之间的差别其实就是上面四个问题给出的答案不同。全量模式的选择性最低什么都要摘要模式的选择性最高只留归纳后的要点检索模式介于中间根据当前问题动态选取。你不需要在所有时间都用同一种模式甚至在同一次会话的不同阶段也完全可以在三者之间切换。比如一个客服机器人第一轮用检索模式找知识库答案对话超过五轮后切到摘要模式把前面几轮压缩成用户诉求已给出方案未解决问题三行如果用户上传了一份完整合同要求整体审查那就临时切到全量模式把合同原文一次性投进去。这个概念并不是我发明的。很多成熟的产品比如Claude的Projects、Notion AI的QA、各种RAG框架的压缩模式condense mode底层都在做类似的事情。只不过它们把实现细节封装掉了你在配置界面里只能看到一个开关。但作为自己写代码的人搞清楚背后逻辑才能知道什么时候该开、什么时候不该开。2. 三大主流上下文模式选型与适用场景在继续往下讲之前先明确一件事模式没有绝对的好坏只有是不是适合当前任务。下面这三个模式是我项目里最常用的我把它们各自的特点、适用场景、代价整理成了一张对比表方便你对照选型。模式核心思路信息完整性上下文占用适用场景全量模式原文直喂不删减高高代码审计、长合同审查、创意写作摘要模式压缩历史为结构化摘要中低多轮对话、Agent长链路、日志分析检索模式按需检索注入片段中高中知识库问答、私有数据查询、客服助手2.1 全量模式什么时候必须让模型看完整原文全量模式是最朴素的做法把检索、摘要全部跳过把源文档原封不动放进上下文。代价很高但有些场景只有全量才能保证正确性。最典型的是代码审查。你让模型检查一段代码里是否存在空指针风险如果只投喂一个函数片段模型看不到这个函数是怎么被调用的也就无法判断外部传参的安全性。我做过一个代码审查工具最初版本用的是检索模式先按函数名去搜相关代码结果漏报率相当高。后来改成全量模式把整个仓库的关键文件全部塞进去虽然单次请求token数涨了三倍但误报率和漏报率都降到了可以接受的范围。另一个场景是长合同或者论文的审阅。这类文本的结论往往分布在多个章节前后有大量交叉引用比如依据第四条的例外条款这句话如果只看到当前条款而看不到第四条模型分分钟给你一个适得其反的建议。全量模式保证了交叉引用的完整性代价是你得为一次请求付出更多的token费用。好在现代模型普遍有128K到200K的窗口大多数中等长度的单份文档可以直接覆盖。全量模式的实现没什么技术含量但有一个设计要点即使你要全量投喂也要在文本结构上做处理。直接把纯文本拼进去模型很难分清章节边界。我的习惯是加工成带标题层级的分块结构用XML标签或Markdown标题包起来比如chapter index3 title违约责任。这么做能让模型在100K以上的长文本里依然保持对位置的感知回答时也更喜欢引用章节号。实测下来结构化全量相比裸纯文本回答准确率能提升大概8到10个点。2.2 摘要模式多轮对话和Agent链路的救命稻草如果说全量模式是内存够大就全部驻留摘要模式就是内存不够时启用虚拟内存。先说我踩过的一个典型坑。做一个客服聊天机器人单轮回答质量很高但用户连续问五六轮之后模型开始遗忘最早的用户诉求。比如第二轮用户说我要的是企业版报价第六轮模型把它理解成了我要个人版报价。原因很简单前面的消息被后续内容挤出了有效关注区域或者说模型注意力在超长上下文里被稀释了。摘要模式的思路是不要一股脑把所有历史消息都拼进去而是每隔一段时间或者每轮把旧消息压缩成一段结构化摘要只保留当前继续说下去所必需的信息。我自己常用的摘要格式是四条用户核心目标一句话已确认的关键约束事实列表已执行的步骤时间线当前待解决的问题下一步动作这里有一个很重要的经验——摘要必须结构化不能是自由文本总结。自由文本的摘要看似内容完整但模型去读取摘要的时候信息之间的关系是模糊的。比如用户想要快一点的方案这一句到底是用户说了要快还是助手判断应该快结构化之后constraints: [speed: high, source: user, confirmed: true]这种形式就天然回避了歧义。实现上摘要可以放在每次调用工具链路的中间。当对话轮次超过阈值比如五轮或者上下文token数超过预算的60%时触发一次压缩任务把当前消息队列丢给一个快速模型生成上述结构化摘要同时把已压缩的旧消息从队列里清除。压缩的结果可以单独缓存做一个增量更新。比如上一轮的摘要已经存在了新一轮只需要把新增的两轮对话合并进去而不是每次都从头压缩这样既能省token又能避免旧信息被重复压缩后失真。我实测过一套Agent系统用摘要模式前后的表现执行十步工具调用不用摘要模式时第五步之后开始频繁出现忘记前置条件的情况用了摘要模式之后十步以上还能稳定保持正确的约束记忆。代价是每次压缩会额外消耗几百token但换来的是路径成功率从61%提升到了86%这笔账非常划算。2.3 检索模式知识库场景下的上下文精选检索模式是目前RAG类应用的主流方案也是被讨论得最多的一种。它的核心思路是不把整个知识库塞进上下文而是针对当前用户问题先从外部存储向量数据库、关键词索引、关系数据库中检索出与问题最相关的若干个片段再把切片拼接进Prompt。听起来简单但我在实际项目里发现有两个关键参数极大地影响效果切片大小和切片数量。切片大小决定了模型能看到的上下文粒度。切得太小比如一百字模型缺少上下文理解不到位切得太大比如两千字一个片段里夹杂大量无关细节信息密度反而下降。我做过对比实验中文技术文档场景600到800字的切片大小效果最好既保留了完整的段落逻辑又不至于让窗口被撑爆。英文场景可以适当放宽到1000到1200词。这里有一个细节切片的时候尽量做到语义完整不要硬生生从句子中间切断。用分隔符感知的切分方式先按标题分块再按段落裁切最后再按句子兜底比纯长度切分效果好得多。切片数量也值得琢磨。有些团队为了让模型看得全一上来就塞8个甚至10个片段可能有一万多token。实际上我测下来很多标准问答任务塞3到5个高质量片段就够了。片段数量翻倍不仅成本翻倍还引入了噪声——模型更倾向于从第一个片段里找答案后面的片段被淹没了。另外一个容易被忽略的点拼装顺序。按相关度排序通常是合理的但在多跳问答场景里把包含最终答案概率最高的片段放在开头比按相关度排序效果更好因为模型的注意力天然偏向开头。你可以把检索结果交给一个轻量的排序模型reranker来排而不是直接信任向量相似度。检索模式的另一个要点是要设计查不到怎么办。不要指望检索永远命中。当检索结果的相关度低于阈值时与其硬把不相关内容塞给模型不如明确告诉模型知识库中没有直接相关的信息请基于常识回答并注明这是推测。这个兜底逻辑能明显减少模型一本正经地编造答案的情况。3. 自己动手实现一个轻量context-mode管理器讲了这么多理论下面进入正题怎么把模式化的上下文管理落地成代码。我会用一个轻量的Python实现打底重点不是代码本身而是设计思路。你完全可以换成TypeScript、Go或者其他语言。3.1 核心数据结构设计我建议先把数据结构想清楚别上来就写一堆if-else。一个可用的上下文管理器至少需要三类对象Message一条原始的对话或工具调用记录记录角色、内容、时间戳和token数。ContextSummary压缩后的结构化摘要包括目标、约束、执行历史、待办并保留一个指向源消息的引用列表。ContextPolicy模式配置包含当前模式名full / summarized / retrieval、触发切换的阈值、切片大小上限等。为什么要把摘要里保留源消息引用列表因为摘要归根到底是有损的。当后续模型因为摘要信息不足而出现疑惑时我们可以快速定位到原始消息做一次精准补全。这个摘要引用的组合相当于给模式化上下文留了一个恢复通道不至于一压缩就丢根。from dataclasses import dataclass, field from enum import Enum from typing import List, Optional class Role(str, Enum): USER user ASSISTANT assistant TOOL tool class Mode(str, Enum): FULL full SUMMARIZED summarized RETRIEVAL retrieval dataclass class Message: role: Role content: str timestamp: float token_count: int metadata: dict field(default_factorydict) dataclass class ContextSummary: goal: str constraints: List[str] executed_steps: List[str] pending: str source_message_ids: List[str] field(default_factorylist) dataclass class ContextPolicy: mode: Mode Mode.FULL summary_trigger_token: int 6000 summary_trigger_rounds: int 5 max_context_tokens: int 12000 retrieval_top_k: int 4这段代码的核心是ContextSummary。你可能注意到了constraints用的是字符串列表而不是自由文本。这就能让后续的Prompt构建直接把约束逐条翻译成自然语言比如用户明确要求预算不超过2000元。3.2 token预算与策略切换逻辑写上下文管理器第一件要搞清楚的事是你手头最多能花多少token。这个预算不是整个模型的上下文窗口大小而是要留出模型回答completion的空间。假设模型窗口是32K我通常预留8K给回答再把2K左右留给系统提示词和模板分隔符那么上下文预算就是22K。公式很简单context_budget model_window - completion_reserve - template_overhead这个预留量不是拍脑袋定的。如果你让模型输出最多4000字的详细报告那completion可能就会吃满8K到10K token留小了请求直接报错或截断。我见过不少同事把32K窗口全当成上下文来用结果一次请求打到30K模型回答到一半戛然而止。有了预算切换逻辑就顺理成章了。我在代码里用一个should_switch_to_summary判断触发条件有两个当前上下文总token超过预算的60%或者对话轮次超过5轮。两者满足其一就触发摘要压缩。class ContextManager: def __init__(self, policy: ContextPolicy): self.policy policy self.messages: List[Message] [] self.summary: Optional[ContextSummary] None def add_message(self, msg: Message) - None: self.messages.append(msg) def current_tokens(self) - int: return sum(m.token_count for m in self.messages) def should_switch_to_summary(self, round_count: int) - bool: ratio self.current_tokens() / self.policy.max_context_tokens if ratio 0.6 or round_count self.policy.summary_trigger_rounds: return True return False def build_context(self) - List[Message]: if self.policy.mode Mode.SUMMARIZED and self.summary: return self._build_summarized_context() return self.messages def _build_summarized_context(self) - List[Message]: summary_block Message( roleRole.ASSISTANT, content( f[历史摘要] 用户目标: {self.summary.goal}\n f已确认约束: {; .join(self.summary.constraints)}\n f已执行步骤: {; .join(self.summary.executed_steps)}\n f待解决: {self.summary.pending} ), timestamp0, token_count200, ) # 保留最近10条未压缩的消息 recent self.messages[-10:] return [summary_block] recent这段代码里有个细节值得注意摘要块的角色我标记成了Role.ASSISTANT而不是Role.SYSTEM。为什么因为摘要内容是会话中真实发生过的信息由助手二次归纳用SYSTEM角色会让模型误以为这是外部指令可能会影响它对事实的采信度。这个细节不一定有理论支撑但我对比过用ASSISTANT角色时模型更愿意把摘要当成可信的对话事实来引用。3.3 完整实现与实测效果上面只是骨架我再补一个完整的summarize触发函数。实际项目中我倾向单独跑一次gpt-4o-mini或者Claude Haiku做摘要生成因为摘要任务对推理能力要求不高但需要输出稳定格式。def trigger_summarization(llm, context_mgr: ContextManager) - None: history_text \n.join( f{m.role}: {m.content} for m in context_mgr.messages[:-6] ) prompt f 把下面的对话历史压缩成结构化摘要严格输出四行 user_goal: 一句话用户目标 constraints: 用分号分隔的关键约束 executed_steps: 用分号分隔的已执行步骤 pending: 当前待解决问题 对话历史: {history_text} raw llm.generate(prompt) # 假设llm返回了格式良好的文本这里做解析 parsed parse_summary(raw) context_mgr.summary ContextSummary( goalparsed[user_goal], constraintsparsed[constraints].split(;), executed_stepsparsed[executed_steps].split(;), pendingparsed[pending], source_message_ids[m.id for m in context_mgr.messages[:-6]], ) # 清掉已压缩的旧消息 context_mgr.messages context_mgr.messages[-6:]我在一个机票预订Agent项目里跑了这个实现10轮工具调用对比三种配置。第一组完全不压缩第二组简单截断只保留最近N条第三组用这里的摘要模式。结果第三组明显胜出任务成功率从57%提升到82%而且用户的显式约束比如只要靠窗在整条链路中被遵守的概率最高。截断那组虽然也能跑到最后但经常出现约束丢失比如用户要求的经济舱被多次升级成商务舱——下游工具调用时模型完全不记得有这个约束了。上面这套代码只覆盖了摘要模式检索模式的核心是切片和召回理论上可以并入同一个管理器把召回片段作为Message(roleUSER, content...)注入。不过实际项目里检索部分通常会有更重的依赖向量库客户端、reranker模型我更推荐把ContextManager做成一个纯内存的编排层外部注入检索结果不要让上下文管理器直接依赖向量库。这么做的好处是方便单测也方便你在不改变主体逻辑的情况下把检索后端从向量库换成搜索API。4. 实战中踩过的坑与排查方法理论讲完代码也给了我来盘点一下我在真实环境中反复踩过的坑。这些坑在官方文档里基本不会写但它们才是决定线上效果的关键。4.1 上下文溢出的三种处理策略怎么选上下文溢出的处理方式从粗暴到精细排序分别是截断、摘要、检索。很多人一上来就用截断因为实现最简单超出预算就丢掉最旧的。但旧消息不一定就是最不重要的。典型的反例是用户在最开始给出了一个硬性约束不要推荐含酒精的产品这个约束在第1轮出现被截断后模型在第10轮可能就会推荐红酒。所以截断的前提是前文信息可以在后续消息中体现或者本身就是低价值的寒暄。摘要模式解决了重要信息可能藏在旧消息里的问题但摘要本身又有损耗。我第一次实现时让摘要模型自由发挥结果它把用户要求2月1日之前发货压缩成了尽快发货把时间条件丢掉了。从那以后我要求摘要必须用结构化字段并且增加了一轮关键字段完整性校验——把摘要里的约束回灌给模型让它自查有没有和原文矛盾或遗漏。这一步虽然多花了点token但大幅减少了静默错误。检索模式的优先级最高适合历史消息本来就是从大知识库里检索而来的场景。但检索也有自己的坑最明显的是检索到的片段并不总是最新——如果你在做实时数据相关应用知识库更新频率很高那缓存就可能导致模型基于过期信息回答。我处理的方式是在检索片段里带上源数据的last_updated字段并在Prompt中说明如果信息更新时间和当前时间差距超过阈值优先相信用户提供的实时数据。4.2 摘要失真、上下文污染与信息溯源先定义一下上下文污染指无关、过期、冲突的信息同时出现在上下文里导致模型输出被带偏。最常见的污染源有三个——工具返回结果、旧的历史摘要、以及被错误检索到的相似但不相关片段。工具返回结果这一块很多人只顾着把工具调用结果追加进上下文但没有标记哪些结果已经被模型消费过。举个例子Agent调用天气API返回了一堆JSON里面包含温度、风速、湿度。如果模型已经回答完明天适合穿短袖吗这些原始JSON还留在上下文里后续当用户问给我推荐几件外套时天气数据又会被关注到模型可能又说一句不过明天温度较高。我的处理方式很朴素每条工具返回消息都在metadata里标记consumed: true/false在构建上下文时已经消费过的工具消息不参与普通消息拼接只定期进入摘要。信息溯源是我觉得最值得强调的一点。当模型基于摘要回答问题时如果摘要里只有约束预算不超过2000元模型无法判断这条约束是哪一轮、由谁、在什么前提下提出的。这个信息如果不从上下文里体现用户在后续对话中一旦说其实预算可以放宽到5000模型可能会困惑该以哪个为准。我在Prompt构建时会把摘要块里的每条约束都标上出处比如[约束1]预算不超过2000元 (第3轮用户确认)模型在遇到用户改口时就能感知到这是一个变更而不是逻辑矛盾。这个改动很小但对多轮一致性很有帮助。上下文的组织顺序也值得检查。我一度把所有工具调用结果统一拼接在历史消息之后导致模型看的时候先看到工具结果再看到用户真正的问题回答质量很差。后来改成用户当前问题 相关上下文 历史摘要的顺序效果明显提升。这个顺序其实符合Transformer结构的注意力特点离问题越近的信息对最终输出的影响越大。4.3 检索失败与编造答案的兜底方案检索模式最让团队头疼的问题就是模型一本正经地编造答案。它可能基于检索到的相似片段组合出一个看起来合理、实际上在知识库里不存在的内容。我排查这类问题时第一步不是调Prompt而是去查模型到底看到了什么。把构建好的Prompt完整打印出来人工检查一遍检索片段的质量。很多时候你会发现向量检索召回了正确的主题但错误的版本比如多份文档里修订版和旧版同时存在向量相似度几乎相同模型选了新版内容却和旧版混在一起了。为了降低这种风险我在检索链路里加了三个模块混合检索向量检索BM25关键词检索结果做加权融合。单纯依赖向量检索关键词精确匹配能力弱专有名词如GB/T 12345-2020很容易召回失败加上BM25后带精确型号的查询命中率高很多。重排Rerank用交叉编码器给召回的10个候选重新打分取前4个。这一步能显著提升排序质量尤其是混合检索结果里混入的噪声在重排阶段往往能被打下去。置信度阈值如果重排后的最高分低于阈值就触发知识库中未找到直接答案的兜底回复而不是硬着头皮生成。最后再补充一个Prompt层的兜底在检索模式下我固定会在Prompt末尾加一句提示——如果以上参考片段没有回答用户问题请明确告知用户根据现有知识库我无法给出确切答案并给出可以尝试的关键词建议。这句话看着简单但实测能明显降低模型胡编的概率因为模型被明确授予了承认不知道这个合法选项不再被迫基于残缺信息填词。5. 经验收尾三个值得长期坚持的上下文管理习惯写了这么多该收个尾了。我在多个项目里反复用到的是以下三个习惯如果只让你记住三件事那就是这三件。第一永远把模型实际看到的token序列作为调试对象。不要盯着链路日志里的中间结果看直接把最终进入模型的Prompt完整导出来看一遍。很多时候你觉得代码没问题但一看Prompt发现旧摘要和新事实发生了矛盾或者指令被放到了最后面、影响力被削弱。建议在你的SDK或者网关层加一个日志开关把每一次请求的最终上下文落盘出问题的时候按会话ID捞出来逐字检查。第二先定义用户当前的目标是什么再去选上下文模式。我之前犯过的错误是预设所有场景都应该用同一个模式结果知识库问答用全量模式烧钱且慢多轮对话又硬套检索模式根本检索不到对话历史里的隐性约束。上下文是否要全量、要摘要、要检索取决于当下这一步模型需要什么样的信息粒度。你可以画一个简单的决策矩阵任务是否有强依赖信息总量多大实时性要求高不高根据答案当场决定策略。第三给上下文管理写专门的测试。很多人测Prompt只测一对一的问答质量但从没测过连续五轮后的表现。上下文管理恰恰是越到后面越容易出问题。我建议你针对三种典型场景各写几个回归用例长对话挽留关键约束、多工具调用保持步骤正确、检索片段缺失时的兜底行为。跑通之后以后每次改Prompt或者换模型至少心里有底。上下文模式这件事本质上就是一个朴素的道理模型的能力再强也只能基于它看到的内容作答。你给它什么它就变成什么。与其反复调Prompt去纠正模型的输出不如先把输入这一关管好。希望这篇基于我个人实践的文章能帮你少走一些弯路。
返回列表