做AI应用开发的朋友应该都有过这种体验:模型本身答案质量没问题,但一涉及多轮对话、长文档问答或者复杂任务编排,效果就开始飘。问题往往不在模型能力,而在“喂进去的上下文”没管好。我最近把一套内部工具重构成了明确的context-mode架构,从源头解决上下文混乱的问题,这篇就完整复盘一下设计思路、核心实现、踩坑过程和优化心得。
这里说的context-mode,指的是一套面向大语言模型应用的上下文管理模式。它把“对话历史怎么留、知识内容怎么放、任务指令怎么排”这些原本散落各处的逻辑,统一抽象成几种可切换、可组合的模式,让开发者能精准控制每次请求给模型看什么,而不是一股脑全塞进去。适合正在做LLM应用、智能体框架、以及被上下文越长越乱、成本越烧越高困扰的技术团队参考。
很多人一开始觉得上下文管理就是“把聊天记录都发过去”,实际上这是一个典型的全局性陷阱。等上下文窗口占满、关键信息被淹没、token费用失控的时候,再回头改架构,代价就大了。这篇我不会只讲概念,会把模式划分、代码实现、参数计算这些都可以直接抄作业的内容全部展开。
1. context-mode设计与核心思路拆解
1.1 为什么需要独立的上下文模式层
先聊一个我自己的真实经历。早期做客服机器人,初版逻辑很简单:用户每次提问,我直接把全部历史消息拼进prompt发给模型。前期测试没问题,但运行两周后,对话轮次稍微多一点,问题集中爆发了——模型开始遗忘用户最早提到的订单编号,或者被几轮前的一个错误假设带偏,回答质量肉眼可见地滑坡。更糟的是,因为历史消息不断累积,每次请求的token数量只增不减,日成本翻了接近三倍。
这就是没有上下文管理模式的问题。LLM本身是无状态的,每次请求都像第一次见到用户,你得主动决定“带哪些记忆、哪些知识、哪些指令”给它。如果这个决定是失控的、随意的,整个应用就像一辆没有仪表盘的汽车——你不知道现在还有多少燃油(上下文空间),也不知道哪个零件(关键信息)正在失灵。
把这些决策过程收拢成一整套context-mode,本质上就是把“上下文处理”从业务代码里剥离出来,变成独立可维护、可观测、可切换的一层。这带来三个直接的好处:第一,业务逻辑里不用再到处写拼接prompt的碎代码;第二,不同场景(闲聊、问答、代码生成)可以复用同一套上下文处理框架,模式切一下就行;第三,问题排查看得见摸得着,知道当前请求具体走了哪种模式,哪一段内容是被怎么处理的。
1.2 从“一把梭”到“按需组装”的转变
常规的prompt处理方式是单块的,所有内容一视同仁:系统指令、历史消息、知识片段、工具结果全部塞进一个字符串。这在简单场景下可行,但一旦任务变复杂,模型其实很难分清“哪部分是规则、哪部分是资料、哪部分是对话记录”,结果就是上下文之间相互干扰。
context-mode的设计核心是把“按需组装”变为主旋律。每一个模式都明确定义了上下文的构成维度:保留哪些对话历史、如何压缩旧信息、知识库内容怎么注入、系统指令如何与其他内容隔离。这种模式划分不仅仅是为了方便,它其实模拟了人类处理信息的方式——临时记忆有限,就记最重要的;参考文档很长,就看跟当前问题相关的章节;规则归规则,事实归事实,不要混在一起。
我把它形象地叫做“三层抽屉模型”。第一层是指令抽屉,放系统级提示词和任务规则;第二层是记忆抽屉,放经过筛选或压缩的对话历史;第三层是知识抽屉,放来自外部资料库、工具调用结果的动态内容。context-mode架构就是给这三个抽屉分别装上开关和阀门,按场景决定哪个抽屉打开、哪个抽屉合并、哪个抽屉只留摘要。
1.3 模式划分的内在逻辑:四种基础模式
实践中我整理出了四种最常用的context-mode,基本覆盖90%以上的应用场景。第一种是short-term模式,注重最近对话,适合客服、闲聊等高频短交互场景——保留最近几轮完整信息,更早的只保留摘要。第二种是summary模式,面向长对话和长期记忆,核心思路是用模型做实时摘要,把真正重要的信息提炼出来,旧内容尽量但不无脑丢弃。第三种是knowledge模式,面向RAG检索场景,重点在于外部知识的选择性注入,让模型基于检索片段作答。第四种是full-context模式,对token预算比较宽裕或者必须保留全部细节的任务(比如代码仓库分析、长文档精读),直接把完整上下文加上精心设计的压缩指令一起送进去。
这些模式之间不是互斥的,我实际使用中经常按请求做动态编排。比如:用户问一个需要查资料的问题,那前几轮完整对话走short-term,历史更早内容走summary模式压缩,外部知识按检索相关性截断后走knowledge模式注入,最后统一在组装层合并成一个结构化请求。这就是单一模式到模式组合的思路跃迁。
2. 核心细节解析与实操要点
2.1 模式选择:不要猜,用规则和信号
什么时候用哪种模式?我建议不要凭感觉选,而是建立一套可解释的决策机制。当前对话轮次数、累积token预估、用户意图分类、内容时效性需求,这四类信号都可以作为模式选择的依据。
比如轮次少(小于3轮),直接走short-term模式;轮次多(大于10轮),再检查累计token是否接近窗口阈值,如果是就切到summary模式。RAG类任务在意图识别阶段就标记出来,直接跨到knowledge模式。我自己的实现里,这个决策器只是一个纯规则函数,输入当前的会话状态、意图标签和token吞吐量,输出要采用的模式列表。规则的好处是稳定、好调试、可回放,初期不建议为了“智能”引入复杂的模型决策——你会很难定位为什么莫名其妙切了模式。
2.2 窗口分配:不做预算,后面全都失控
窗口分配是context-mode中最关键、最容易被忽略的环节。上下文窗口是有限的资源,系统指令、历史消息、知识内容、当前问题、输出保留区,每一项都要提前分配合适的token预算。我们以8k窗口为例,一个常用的分配思路:
系统指令固定预留800到1000 token,因为规则部分要完整保留,不能被截断。当前问题和输出区预留1500到2000 token,输出足够的空间。剩余5000到5500 token留给历史与知识内容,按照7比3的比例拆分。历史消息的预算之内,最近的两轮完整保留(可能是800到1200 token),更早内容压缩成摘要,控制在整个历史预算的一半左右。剩余预算给知识库检索结果。
这个分配不是固定死的,但它必须是一个显式的、受控的过程。我见过很多项目在prompt里不设预算,结果某个用户粘性高的对话轻松突破窗口限制,系统只能报错或者静默截断,模型结果自然不可空。更合理的做法是:在组装请求前跑一次估算函数,把token占用量算出来,如果超预算,优先压缩历史,其次减少知识片段,系统指令永远不缩放。
2.3 历史消息压缩:摘要不是让你丢信息
做summary模式最常犯的错误,是直接让模型“总结一下对话”,然后把总结塞回去。这个做法的问题在于,模型的摘要是概括性的,会丢失用户明确表达过的偏好、精确的数值、具体的事件细节。而这些细节往往是后续回答的关键锚点。
我的做法是结构化摘要。除了自然语言总结,额外维护一个“关键信息槽”:实体列表(人名、订单号、产品名)、用户明确偏好(“不要推荐超过500元的产品”)、未决事项(“待确认的收货地址”)。压缩时,自然语言摘要占大头预算,关键信息槽单独占用小头预算,两者都进入上下文。这样模型既能把握对话脉络,又能精确访问核心事实。
2.4 知识库注入:检索召回不是越多越好
在knowledge模式里,我踩过最深的坑,就是把top-K设得很大,以为“喂给模型的内容越多,回答越准”。实际上召回片段越多,噪声越大,模型还容易在不同片段的信息冲突之间反复横跳。正确思路是相关性优先、数量克制。
一般来说,3到5个高相关片段就足够模型作答了。超过5个片段,边际收益骤降,有时甚至为负。另外,每个片段在注入前要做截断处理——只保留与query语义最接近的段落,而不是把整个文档都塞进去。我在实现中会对召回片段做二次相关性打分,再用关键词匹配把真正相关的几句话提取出来,作为知识注入单元。这样,知识部分的token占用大幅度下降,回答精度反而提升。
3. 实操过程与核心环节实现
3.1 搭建基础架构:把context-mode从概念变成代码
这一节直接用一个精简但完整的示例说明实现路径。为了不过度绑定某个具体框架,我用Python伪代码风格展示核心逻辑,思路可以平滑迁移到TypeScript、Go或者任意你正在用的语言栈。
第一步,定义基础数据结构。一个ContextRequest代表一次完整的请求组装过程,包括指令、记忆、知识三部分的内容载体,以及它们各自的元信息:
from dataclasses import dataclass from typing import List, Optional @dataclass class ContextBlock: content: str block_type: str # "instruction", "memory", "knowledge" priority: int # 优先级,越大越不可丢 token_estimate: int = 0 metadata: dict = None @dataclass class ContextRequest: session_id: str blocks: List[ContextBlock] mode_signature: str # 当前采用的模式组合,例如 "short-term+knowledge"ContextBlock是整个架构的基石,每一段要进上下文的文本都会被打上类型标签和优先级。为什么这样设计?因为在token预算吃紧的时候,组装层可以按照优先级倒序裁剪内容,而不是随机截断。裁剪是有序的、可预测的,这在生产环境里极其重要。
第二步,实现token估算。不需要精确计算,用tiktoken等词元统计工具做近似估算即可:
import tiktoken encoding = tiktoken.get_encoding("cl100k_base") def estimate_tokens(text: str) -> int: if not text: return 0 return len(encoding.encode(text))token估算不是一个可有可无的辅助函数,它直接决定了预算分配和裁剪决策的可行性。估算的误差允许存在,但它必须在同一个量级内保持稳定,否则后面做窗口分配就成了空中楼阁。
第三步,实现组装器。组装器接收ContextRequest,按照预算和优先级,把blocks拼成最终的prompt结构:
def assemble_prompt(request: ContextRequest, system_instruction: str, budget: int = 8000) -> str: # 按类型分配预算 instruction_budget = 800 content_budget = budget - instruction_budget # 把非指令块按优先级降序排列 content_blocks = sorted( [b for b in request.blocks if b.block_type != "instruction"], key=lambda x: x.priority, reverse=True ) selected_blocks = [] used_tokens = 0 for block in content_blocks: if used_tokens + block.token_estimate <= content_budget: selected_blocks.append(block) used_tokens += block.token_estimate else: # 超预算:尝试截断低优先级的knowledge块 if block.block_type == "knowledge" and block.token_estimate > 500: truncated = truncate_knowledge_block(block, content_budget - used_tokens) selected_blocks.append(truncated) break else: break parts = [system_instruction] + [b.content for b in selected_blocks] return "\n\n".join(parts)这个组装器的逻辑其实很简单,但它在生产环境里非常可靠:先保护指令,再保高优先级内容,最后尝试对知识块做截断。这种“有序放弃”机制,就是context-mode架构与普通prompt拼接最大的区别。
第四步,把模式决策器加进来。决策器是组装器的前置模块,负责根据会话状态输出模式签名,再由一个模式解析器把签名翻译成具体的block构建规则:
def decide_mode(session_state) -> str: if session_state.turns < 3: return "short-term" if session_state.is_knowledge_task: return "short-term+knowledge" if session_state.estimate_tokens > 7000: return "summary+knowledge" return "full-context"3.2 四种模式的代码落地
决策器模式解析出来以后,每种模式对应不同的block构建规则。
short-term模式实现比较简单,直接取最近N轮消息构建memory块,超过的部分丢弃或者只做最粗粒度的截断:
def build_short_term(session_state, turns=4): recent_messages = session_state.messages[-turns * 2:] # 用户+助手各turns条 memory_block = ContextBlock( content=format_messages(recent_messages), block_type="memory", priority=80, token_estimate=sum(estimate_tokens(m.content) for m in recent_messages) ) return [memory_block]summary模式要复杂一些。我需要一个压缩器,它针对“要丢掉的旧消息”运行摘要任务,同时维护关键信息槽:
def build_summary(session_state, old_messages): summary_text, key_slots = compress_with_slots(old_messages) memory_block = ContextBlock( content=f"对话摘要:{summary_text}\n关键信息:{key_slots}", block_type="memory", priority=90, token_estimate=estimate_tokens(summary_text) + estimate_tokens(key_slots) ) return [memory_block]knowledge模式的核心是检索后处理。每次检索召回后,把相关片段做截断和重排,再构建knowledge块:
def build_knowledge(retrieved_docs, query, max_tokens=2000): scored = [(rank_by_similarity(doc, query), doc) for doc in retrieved_docs] scored.sort(key=lambda x: x[0], reverse=True) truncated_docs = [] used = 0 for score, doc in scored[:5]: excerpt = cut_to_fit(doc, max_tokens - used) truncated_docs.append(excerpt) used += estimate_tokens(excerpt) if used >= max_tokens: break knowledge_block = ContextBlock( content="\n\n".join(truncated_docs), block_type="knowledge", priority=50, token_estimate=used ) return [knowledge_block]full-context模式则直接跳过压缩,但对消息做结构标记,让模型能区分不同来源:
def build_full_context(session_state): raw_messages = session_state.messages marked = mark_message_boundaries(raw_messages) block = ContextBlock( content=marked, block_type="memory", priority=70, token_estimate=estimate_tokens(marked) ) return [block]3.3 模式组合的编排实例
单一模式写完后,真正的挑战是组合。这里我分享一个完整的业务场景,方便你理解组合的精髓。
假设用户正在做一个多轮数据分析任务,说“对比我上个月和这个月的支出”,然后连续追问了四五次细节。这个会话状态是:轮次较多,知识库里有财务文档,累计token已经比较高。决策器选择“summary+knowledge”组合。
组装过程如下:系统指令占800 token,说明“你是一个财务分析助手,用简洁的话术回答,基于给定的数据回答,不要臆测”。记忆部分用summary模式,把最早三轮对话的摘要和关键信息槽注入,占1800 token。知识部分用knowledge模式,检索财务文档中关于“支出分类”和“月份对比”的段落,截断后占2000 token。当前这一轮的用户问题完整保留,占200 token。最后一条请求的总token大约在6800,正好落在8k窗口内,留出1100 token的输出余量。
这个例子里最微妙的地方是“摘要+知识”的组合不是随意叠加,而是各有侧重:摘要承接对话逻辑的连续性,知识提供事实依据,指令框定输出约束。三者互不干扰,模型拿到的是一个结构分明的输入,而不是一个混杂着聊天记录和文档摘录的大杂烩。
3.4 组装结果的观测与验证
组装完不是就结束了,上线前一定要做可观测性验证。我每次组装都会保存一条JSON格式的记录,至少包含:模式签名、各块token预算与实际用量、每一步的裁剪决策、最终prompt前100个字符、模型输出的长度。
这份记录的价值非常大。线上效果一旦波动,我可以用它做回放,直接看到当时请求的组装策略是否合理。有一次我排查用户反馈“答案越来越泛”,就是因为summary模式虽然压缩了历史,但关键信息槽里没有记录用户此前多次提到的“仅限北京地区”,导致后续回答丢了这个约束。这个信息在日志里一眼就定位到了,修复也很简单,把地区约束纳入关键信息槽的强制保留字段。
4. 常见问题与排查技巧实录
4.1 固定轮次截断导致记忆断层
short-term模式保留了最近4轮,但有个隐藏问题:如果用户在第5轮追问“我刚才说的那个订单号是多少”,而此时订单号出现在第6轮之前,它就已经被丢掉了。这是固定轮次截断的天然缺陷。
解决思路是把“固定轮数”改成“按关键实体回溯”。我在记忆块构建时,会额外扫描所有消息中的实体正则(订单号、日期、金额等),只要命中已知实体类型的消息,即使超出轮次范围也会被保留到关键信息槽。这样既控制token,又不丢失核心事实,算是用极小成本补上了截断的漏洞。
4.2 摘要模型漂移与累积错误
summary模式用模型做摘要,本身有不确定性,第一次压缩时引入的小错误,会在后续轮次被当成事实基础,错误越积越大。这在大模型应用里是非常典型的“漂移问题”。
我现在会在每轮摘要任务里加一个原样引用约束:要求摘要把对话中的数值、日期、名称等实体内容原样保留,不允许改写。同时在关键信息槽里用双写校验——模型摘要里的关键实体,必须和原文中标注出的实体一致,不一致就以原文为准。这样相当于给摘要加了一道防漂移锁,虽然不能完全杜绝错误,但能把错误率控制在一个可以接受的范围内。
4.3 知识检索噪声干扰模型判断
knowledge模式最常见的故障是:检索到的文档片段和当前问题虽然有关联,但内容本身包含大量无关信息。模型分不清哪些是真正要用的,容易被带偏。
我的处理方法是在知识块前面加一个“使用说明”,明确告诉模型:下面是检索到的参考内容,只能使用其中与当前问题直接相关的部分,如果有冲突以最近的文档为准。这个提示词看起来不起眼,但在实测中能显著减少模型“乱引用”的现象。另外,我强烈建议做一个后置校验:让模型在回答末尾标注使用了哪几条知识片段,这样出现幻觉时可以快速回溯。
4.4 Token预算失控与成本飙升
线上的token消耗经常超出估算,根因一般不在组装器,而在于上游数据量不可控。比如用户上传了一个超大附件,或者检索文档库里有一篇超长文本被召回,哪怕只截断到2000 token,实际处理时中间过程的拼接可能消耗了一倍以上的临时token。
我的经验是每一层都做token熔断。组装器有总预算,知识检索有单文档截断长度,摘要任务单独限制输入规模,任何一个环节超限就直接降级到更省token的模式。成本统计也要单独记录context-mode各部分的token消耗比例,如果发现某个模式长期超支,就该考虑是不是该调预算甚至换模式了。
5. context-mode的进阶扩展与长期优化路径
5.1 引入压缩评估器而不只靠规则
纯规则摘要长期跑会有两个问题:一是摘要质量不稳定,二是固定预算不一定适配所有场景。我建议进阶阶段,在summary模式输出后加一个轻量评估器,对摘要做一次打分,主要维度包括关键实体覆盖率、是否有新增事实、是否引入矛盾。如果评分低于阈值,退回更大的token预算重新摘要,或者直接改用full-context模式兜底。
这个思路本质上是在“压缩的收益”和“细节的损失”之间做动态平衡。用户场景若是客服支持,细节损失影响大,阈值就设高一点;若是长文内容提取,压缩收益更多,阈值可以放低。
5.2 把context-mode做成在线调参服务
到后期,context-mode没必要再跟着业务代码发版。我把它独立成一个配置驱动的服务:模式规则、预算分配、实体类型、摘要提示词模板,全部存成了可热更新的配置。运营或者产品同学可以在不发布代码的情况下,调整某个场景的上下文策略。比如大促期间客服咨询量大,把short-term模式预算调低一点,保证请求成功率;活动结束后再调回来。
这种配置化也带来了一个额外的能力——A/B测试。可以同时跑两套context-mode策略,比如一套summary压缩偏保守,一套偏激进,看谁的最终用户满意度指标更好。上下文管理的调优从此不再靠拍脑袋,而是有了数据反馈的闭环。
5.3 多模态场景下的扩展预案
现在的context-mode还处理的是纯文本,但实际应用中图片、表格、结构化数据也经常出现在上下文里。我在新版本的规划里,给ContextBlock增加了一个内容类型的属性,支持text、image_ref、table三种类型。图片不再直接传像素,而是传多模态模型的引用ID,表格则先转成文本摘要或者markdown结构再注入。
多模态场景的预算逻辑也要相应调整:图片的token成本相对高,一个中等分辨率的图片ref可能抵得上几百上千个token。所以知识检索如果召回了图片,必须先判断当前问题是否真的需要图片信息,否则宁可丢弃,也不能让一张图占掉整块知识预算。这个决策逻辑,正在成为我后续几个月迭代的重点。
6. 关于context-mode的经验总结
做了大半年context-mode架构,最大的体会是:上下文管理不是一个prompt工程问题,而是一个系统工程问题。它涉及资源预算、数据结构、模式决策、可观测性、降级策略,每一个环都要有明确的设计意图。如果只是堆提示词,短期猛如虎,中期问题百出,长期必然重构。
给正准备引入context-mode的团队三个建议。第一,先跑通日志体系,再上线模式优化,没有观测就没有迭代依据。第二,从最简模式开始,固定轮次截断跑一个月,把数据看明白了,再上summary和知识编排。第三,预留降级链路,context-mode的终极价值不在于每个请求都最优,而在于最坏情况下所有请求都能有兜底方案,不会因为上下文策略的意外让整个应用不可用。
最后分享一个小技巧:在每次请求的组装记录里,把模式签名和最终答案一起存入会话元数据。后面做用户行为分析或者模型效果评估时,这就是金矿级的数据关联。你可以精确回答“哪个模式下的回答更受用户认可”“哪种压缩策略影响了用户的留存”这类高价值问题,而不再依赖猜测。