资讯动态

LLM应用上下文管理实战:从踩坑到落地的context-mode方案

发布时间:2026/10/8 22:45:37 来源:尧图企业网站定制
做LLM应用的人十有八九都会遇到同一个问题对话一长要么钱烧得慌要么模型开始胡言乱语。我刚接手第一个AI客服项目时上线第三天就被老板叫去谈话说用户再多聊两句你的账单就要比营收还高了。后来我把上下文这块拆出来专门做了一个可配置的context-mode模块所有对话的记忆、摘要、剪裁、注入逻辑都在这个模块里统一处理。今天这篇就当是复盘把我从踩坑到落地的完整过程连同参数怎么定、代码怎么写、问题怎么排查一起整理出来。先说清楚context-mode是什么。简单说它就是一套管理到底给大模型喂什么背景信息、喂多少、按什么结构喂的机制。大模型本身不记事儿你能不能让它看起来记事儿全看你怎么组织和塞入历史对话。而context-mode就是把这种组织方式抽象成可切换的模式有的场景适合零上下文有的场景适合滚动窗口有的场景适合摘要压缩。这个模块解决的核心问题有三类控制Token成本、保证多轮对话的一致性、让不同业务场景共用一套对话能力而不互相干扰。这篇内容适合正在做大模型应用开发、AI客服、智能助手、Agent类产品的工程师和架构师也适合那些刚接触Prompt Engineering、想搞清楚上下文管理到底怎么落地的人。我会给到可以抄作业的配置参数、核心代码思路和完整的排查记录。1. 为什么要把上下文管理单独抽成一个模块1.1 对话系统里的四个典型痛点先说第一个痛点Token成本失控。大家都用大模型API按Token计费很多人早期就把整个历史对话一股脑塞进请求里。用户聊到第10轮的时候光历史消息可能就有5000到8000个Token而真正有用的信息可能只占两成剩下全是寒暄和重复内容。我曾经在日志里看了一个真实会话用户和机器人聊了大概30轮每一轮请求的输入Token都超过1.2万其中九成是历史消息。这种方案跑一个月成本高得离谱。第二个痛点是多轮对话的一致性崩坏。有人觉得既然全塞历史那么贵那把历史清空不就行了结果用户上一秒问帮我查一下上个月杭州的销售额机器人答完下一秒用户说那环比呢机器人直接反问哪个数据——因为它根本不记得上一条说的是什么。这还算好的更麻烦的是用户修改了前置条件比如把日期改成第四季度其他条件不变如果没有上下文支撑机器人根本不知道其他条件指什么。第三个痛点是不同业务场景的上下文需求差异巨大。客服机器人需要完整记住用户报修流程里的每个细节营销问答机器人可能只需要单轮知识检索而数据分析助手需要把用户提到的所有筛选条件一直带在上下文里。如果全系统只写死一种上下文策略要么客服不够用要么营销场景白白浪费Token。这个矛盾在业务线多、对话入口杂的项目里尤其明显。第四个痛点是多个会话之间会串数据。曾经有个经典事故A用户问了一句我住在北苑帮我看看附近的门店结果B用户在同一时间也得到了关于北苑门店的推荐。根因就是我把历史消息存在了一个全局内存列表里所有用户共用。这个问题如果不把上下文做隔离、做成模式化的模块越到后面越难收拾。1.2 三种最常用的context-mode介绍围绕着上面几个痛点我把上下文管理分成了三种基础模式之后的所有变种都是在这三种之上加规则。第一种是零上下文模式zero-context mode。这种模式下请求里只携带当前这一条用户消息和系统指令不拼接任何历史对话。它的特点是省Token、响应快、绝对不会有历史污染但完全不具备多轮能力。适合单轮知识问答、意图识别、文本润色这类场景只要把系统提示词写好效果就很稳。第二种是滑动窗口模式sliding-window mode。这种模式保留最近N轮对话更早的内容直接丢弃。它是目前在普通闲聊和客服场景里用得最多的方案复杂度低效果直观可预期。窗口大小是一个核心参数N设得太小上下文不够用N设得太大Token成本又会上去。后面的章节我会专门讲这个N怎么算出来。第三种是摘要压缩模式summary mode。这种模式会把较早期的历史对话先用大模型总结成一段摘要再把摘要和最近几轮完整对话拼在一起发给模型。它兼顾了长记忆和成本控制但引入了额外的一次摘要调用开销而且如果摘要写得不好信息会损失。适合那种用户一聊就是几十轮、而且前面聊的内容后面还会反复引用的深度咨询场景。在实际系统里这三种模式不是互斥的通常会用一条规则链把它们串起来短会话用滑动窗口长会话自动升级为摘要模式某些特殊接口强制走零上下文。这就是context-mode的核心调度思想。2. 语境模式的核心设计决策引擎与上下文管线2.1 决策引擎怎么判断当前该用哪个模式把模式做成了模块下一步就要解决谁来选模式的问题。我的方案是做一个小的决策引擎用一组规则来做路由。规则优先于模型判断因为它便宜、快、可解释不需要为了选个模式多调一次大模型。决策引擎的第一输入是会话轮数第二输入是当前累计的Token占用第三输入是业务线指定的默认模式。举个例子我定的规则是总轮数小于3直接走滑动窗口模式因为这个时候历史量还很小省钱和效果都占轮数超过3但低于12仍然走滑动窗口但窗口大小只保留最近6轮轮数超过12自动升级为摘要模式把第1轮到第10轮的内容先压缩再和第11、12轮拼在一起。轮数超过30的极长会话摘要模式保留最近8轮完整消息剩下的全部交给摘要层。还有一个维度是意图识别。比如用户消息里出现总结回顾之前说的这类词说明他要跨历史查找信息这时候即使轮数很少我也会强制切到摘要模式或者全量召回模式避免窗口只留了最近几轮、把关键信息丢了。这种关键词匹配不算聪明但在生产环境里非常实用覆盖了绝大多数真实的跨历史诉求。决策结果会记录在每条请求的日志里方便后续复盘。我记得第一次加上这个日志时发现原本以为客服场景会大量使用摘要模式结果跑了三天百分之八十的请求其实都落在滑动窗口模式上摘要模式的占比只有不到百分之十。这个数据直接帮我调整了系统的整体预算分配。2.2 上下文组装管线从用户消息到最终Prompt决策引擎确定了模式之后接下来就是组装最终发给模型的Prompt。我把这个过程封装成了一条管线一共五步。第一步是接收用户输入同时把当前会话ID传进来。第二步是根据会话ID从会话存储中拉取原始历史消息列表这一步只拉元数据不做任何加工。第三步是让决策引擎根据历史长度、意图、业务配置选中模式。第四步是执行模式对应的上下文构建策略零上下文模式直接返回空历史滑动窗口模式从历史列表末尾切出最近N轮摘要模式则先从摘要存储里取最近的摘要再决定要不要触发一次新的摘要压缩。第五步是最关键的一步把系统指令、用户画像比如姓名、偏好、历史标签、召回出来的长期记忆、历史消息摘要、最近几轮对话、当前用户问题按固定顺序拼接到一起。注意拼接顺序不是随便来的我实测发现大模型对Prompt开头和结尾的内容敏感度远高于中间所以系统指令放最前面当前用户问题放最后历史消息夹在中间这样能最大化指令的约束力。管线中还要做一个Token预算检查。拼接完后用tiktoken实时计算整套Prompt的Token数如果超出配置的预算上限会先尝试裁剪滑动窗口的轮数如果还不够就把中间那段完整历史拿出来做一次摘要替换用摘要顶替原始消息。这样做的好处是不会因为某一次对话特别长就把整个请求打崩。2.3 关键参数怎么定Token预算的计算过程我在很多文章里只看到别人说窗口大小设成10轮但从不说这个数字是怎么来的。这里补一个真实可用的推导过程。假设你用的模型是GPT-4o-mini输入上下文上限是128K但你不想让单次请求的输入Token超过4000这样可以控制延迟和成本。系统指令一般占用200到300个Token当前用户问题平均占用100到200个Token如果用户会粘贴代码或长文本预留给当前输入的Token就要放宽到800。剩下的Token预算大约3000分给历史消息。历史消息平均每轮的Token占用怎么算我统计过自己的数据普通中文客服会话用户消息平均约50个Token模型回复平均约150个Token加上角色标记和格式符号单轮合计在220到250个Token之间。3000除以250约等于12轮但因为偶尔有长消息保守一点把窗口设在6到8轮留出冗余。如果换一个更贵的模型比如把单次输入预算控制到2000Token计算方法一样就是2000减去300系统指令、减去200当前消息、减去200格式和预留剩下1300给历史折合约5到6轮。参数一定要基于自己业务的数据来算直接抄别人窗口大小而不知道预算分配很容易在项目跑到一半时出现问题。这套参数我后来做成了配置项在context-mode模块的初始化文件里用字典管理不同业务线各自调自己的数值互不影响。3. 会话记忆的分层实现短期、长期、摘要的协同3.1 短期记忆存储结构与滚动机制先来说短期记忆也就是最近几轮完整对话。存储上我一开始用的是Redis里的一个普通List键名格式是conversation:{id}:messages每个消息体是一个JSON包含role、content、timestamp、message_id。之所以用Redis而不是内存是因为服务重启后会话不能丢而且后续如果做多副本部署Redis天然是共享的。滚动机制其实很简单每次用户发消息把用户消息追加到列表尾部模型返回后再把assistant消息也追加进去。读取时根据窗口大小N用lrange取出列表末尾的2N条消息N轮对话对应2条消息。这里有个容易踩的坑不要在前端就截断历史再传进来一定要在服务端做否则用户改个客户端就能绕过你的成本控制。还有一点值得单独说就是模型自己的上一轮输出要不要放进上下文。答案是必须放而且要放在对应的用户消息之后。只放用户消息不放模型回复模型就不知道它上一步说过什么经常会出现同一个问题被来回问两遍的情况。这个坑我在早期版本里踩过当时客服系统里用户每说一次你好模型都像第一次见到对方一样回复完整欢迎词。除了List我还会在Redis里维护一个轻量级的消息索引用来支持按时间范围和消息ID做查询。这个索引不是必须的但当用户说你刚才说的那个方案再讲一遍时没有索引就只能靠遍历整个List效率会低不少。3.2 长期记忆语义召回与关键信息注入短期记忆能覆盖最近几轮但覆盖不了用户三天前说过自己养了一只猫这种事。所以我在context-mode里加了长期记忆层专门存那些跨会话、需要被反复引用的信息。长期记忆的来源有两个。第一个来源是运行时抽取每次用户回复里出现明显的偏好、属性、约束条件比如我喜欢简洁的回答我们的预算是5万以内我会用一条轻量级的抽取提示词把它们变成结构化的键值对存起来。第二种来源是定期从摘要里回捞把摘要中识别出的关键信息写入长期记忆这个回捞任务放在后台异步跑。查询长期记忆时我会把当前用户问题做向量化然后到向量库里做相似度检索取回TopK条最相关的记忆再注入到Prompt的独立区域。K值我一般设成3到5条。为什么不能多每一条记忆都是Token而且跟当前问题太不相关的记忆还可能误导模型。TopK召回的结果会按相关性排序相关性低于阈值的会被过滤掉我目前阈值为0.45。这里有个设计细节很重要长期记忆和短期历史在Prompt里必须分区块放不能混在一起。因为模型对哪些是当前的对话哪些是历史知识的判断依赖于Prompt的结构。我会用清晰的标签把它们分隔开比如区块标题写明以下是从历史中检索到的用户偏好信息这样模型才能正确区分两类信息的权威级别。3.3 摘要压缩实现思路与执行时机摘要压缩是长会话里最核心的一环我分享下自己的实现思路。简单版的做法是拿整段历史直接丢给模型让它总结但这种做法在历史超过模型输出上限、或者内容太长导致摘要质量崩掉时就会失效。我采用的是分段压缩再合并的方式思路类似Map-Reduce先把历史对话按每50轮切成一个片段对每个片段单独走一次总结产出一段段独立摘要然后把这些片段摘要再拼起来走第二次总结得到最终摘要。这样单次处理的数据量被控制住了摘要质量稳定很多。摘要的执行时机更讲究。我之前尝试过在用户每一次发消息时都同步触发摘要结果发现用户体感延迟增大因为摘要压缩本身要一次模型调用耗时可能超过2秒而且会把原本正常的请求拖慢。后来我改成异步策略会话轮数超过阈值时服务端把原始历史消息发到一个后台任务队列由worker线程生成摘要并写回存储下一条用户消息到达时读到的就是已经更新的摘要。这个方案也有代价就是如果用户在摘要生成完成之前连发两条消息第二条消息可能读到的还是旧摘要。我的处理办法是给摘要加一个版本号如果请求到达时发现正在生成中的摘要版本比当前版本新就等一个极短的时间比如200毫秒再重试读取。实测下来绝大多数情况下都能读到新版本。摘要内容我还会额外要求模型保留三个关键点用户明确说过的偏好、未完成的待办事项、已给出的否定条件。这三个点是客服场景中最容易被后续对话引用的信息如果摘要漏掉了后续回复很容易出问题。4. 常见问题与排查技巧实录4.1 语境串台问题会话隔离没做好语境串台是我遇到过的第一个线上事故。当时我把历史消息存在一个全局变量数组里想着简单跑通就好结果两个用户并发接入时历史消息互相覆盖A用户的对话被B用户带走了。排查的时候我在日志里发现某条回复引用的姓名和当前用户完全对不上顺着请求ID往下查才发现存储层的Key根本没有带会话维度。修复方法不复杂所有历史消息、摘要、长期记忆的存取强制使用conversation_id作为Key的一部分并且在使用之前校验当前会话归属。我还在上下文组装管线的入口加了一道守卫检查传入的会话ID与存储中的会话ID是否一致不一致直接报错防止并发调用把数据写串了。这个经验后来沉淀成一条规范context-mode里的任何存储要么Key带会话ID要么把会话ID作为Hash字段的成员绝不允许使用任何无业务边界的全局集合。因为一旦出现串号问题轻则回复诡异重则泄露用户数据这个风险完全无法接受。4.2 Token开销失控剪裁策略失效上线摘要模式后我以为Token成本问题已经解决了结果有一天账单还是突破了预期。查日志发现是触发了摘要替换的分支但替换逻辑只替换了历史摘要没有处理窗口内的完整历史。也就是说长对话升级到摘要模式后新消息不断追加每次请求的历史块仍然越变越大摘要根本没起到压缩作用。排查思路是把每次请求的Token组成打到日志里按系统指令历史完整消息摘要块当前用户消息分段统计。一看数据就明白了问题出在我把滑动窗口的读取逻辑放在摘要模式判断之前了。修复方式是调整管线顺序先判断模式和当前总Token再决定到底拉取窗口消息还是拉取摘要把裁剪动作统一放到组装之后。处理后我又加了一条兜底规则无论什么模式组装后的总Token超限时强制从最靠近中间的部分开始裁剪先裁完整历史再裁摘要细节最后才是系统指令和当前消息。这条兜底规则后来救过我好几次比如用户一次性粘贴了超过2000Token的日志文本时系统不会因为单条消息就爆掉。4.3 摘要越压越薄关键信息反而丢了摘要压缩模式跑了一段时间我发现一个隐蔽的问题用户聊到第20轮、30轮之后模型开始忘记一些早期的关键约束。比如用户在第2轮说过排除所有红色商品到第20轮它又开始推荐红色商品了。表面上看是模型记性差实际是我每次做摘要时把旧摘要和新片段合并压缩再压缩早期信息被一层一层磨损掉了。我的解决办法是把摘要层和长期记忆层打通在生成最终摘要之后追加一个异步任务把摘要中出现的所有偏好、否定条件、待办事项抽取出来写入长期记忆库。后续请求组装上下文时长期记忆区块会把召回到的约束条件重新注入Prompt里。即便摘要把历史对话压得很薄关键的长效信息也能被长期记忆保住。操作上我给摘要Prompt增加了结构化输出要求让模型返回一段JSON里面区分preferences、negations、todos三个字段然后由代码把JSON拆开分别写入存储。其实一开始我就想到了这个方案但当时图省事没做结果花了两周才追查回来。这个教训告诉我摘要压缩做得再精致也不如把关键信息单独拿出来存一份更保险。4.4 多轮对话质量下降上下文结构太乱最后一个高频问题是对话质量下降而且常见于会话进行到十几轮之后。我排查过一批案例发现很多问题的根子在Prompt结构上历史消息、长期记忆、摘要被代码一股脑拼成了一个很长的字符串没有做分区和标签模型分不清哪些是它自己说过的内容哪些是用户偏好上下文一长就开始混乱。修复手段是严格的Prompt模板化。我给context-mode定义了一个固定的区块顺序系统指令、业务规则、长期记忆、历史摘要、最近对话、当前用户消息。区块之间用不可省略的分隔标签隔开代码里不能随便调整顺序。这样一来模型拿到一个结构稳定的Prompt多轮对话的一致性明显改善同样场景下的有效回复率从大概七成上升到了九成左右。如果发现质量还是不行我会再做一个动作把最近五轮消息单独拉出来看检查是不是模型自己的历史回复里出现了错误或幻觉内容然后被后续对话当成前提继续传播了。这种自污染问题没办法靠上下文管理根治但可以在检测到模型回复质量异常时主动停止把该条回复追加到历史里避免一句话带偏整场对话。5. 实测效果与一些可以抄作业的配置5.1 一组实测对比数据为了让你更直观地感受这几种模式的区别我贴一组真实项目的对比数据。场景是模拟一个客服机器人连续回答用户50轮问题每轮平均生成约200Token回复。数据来自我自己搭建的测试环境模型统一用同一个配置。方案50轮总输入Token最后一轮历史Token量关键词引用准确率明显上下文错误次数全量历史约62.5万约3.2万92%2滑动窗口最近8轮约9.8万约210085%6摘要模式约7.1万约150088%3摘要长期记忆约7.4万约190094%1注意看全量历史的准确率虽然不低但Token消耗是摘要模式的8倍还多在真实生产环境里根本扛不住。滑动窗口虽然便宜但超过20轮后准确率掉得快因为早期约束被丢了。摘要长期记忆的组合在成本只比纯摘要多一点的条件下把准确率拉到最高。我们最终上线用的就是最后一种方案。5.2 一套可以直接改的初始化配置示例以下是我目前生产环境里context-mode模块的核心配置用Python字典维护你根据自己的业务调整数值就行。context_config { default_business: customer_service, max_input_tokens: 4000, system_prompt_tokens: 300, reserved_current_message_tokens: 400, sliding_window_rounds: 8, summary_trigger_rounds: 12, summary_window_rounds: 8, long_term_memory: { top_k: 5, similarity_threshold: 0.45, }, max_token_exceed_action: trim_middle_first, async_summary_enabled: True, prompt_section_order: [ system_instruction, business_rules, long_term_memory, history_summary, recent_messages, current_message ] }注意summary_window_rounds的含义触发摘要模式之后最近8轮保持完整原始消息更早的全部走摘要。这两个参数滑动窗口轮数和摘要窗口轮数要按我之前说的Token预算推导方法来定一套配置全公司共用看起来省事但在不同业务模型里很难同时最优。如果你用的是LangChain或者是Dify这类框架思路也是一样不要被框架自带的Memory机制束缚把Memory的获取逻辑替换成自己的context-mode实现通常也就是在组装Prompt之前加一道预处理。我试过LangChain自带的ConversationSummaryMemory它在小型Demo里还行到生产环境里就无法满足自定义的压缩时机和长期记忆召回需求最后我还是选择了自己维护这层逻辑。结尾关于context-mode的几点个人体会做完这个模块后我对上下文管理的理解变了不少。以前总觉得给模型塞的信息越多越稳现在反而觉得上下文管理的核心是克制——克制的历史轮数、克制的长期记忆条数、克制的摘要内容。每次增加一条注入信息都应该先问一句这一条信息对当前问题是有帮助的还是只是让我感觉安心再分享一个实用技巧上线之后一定要保留完整的请求日志至少记录每个请求用了什么模式、各类区块的Token分布、模型回复质量的人工抽检结果。没有这些数据你永远说不清楚某个线上问题到底是模型的错、Prompt的错、还是上下文管理的错。我大概花了三个星期才把日志维度补全但这笔投入比优化算法本身的回报大得多。最后context-mode这个思路不只适用于大模型对话任何带状态的系统都可以借鉴把状态如何影响请求这件事显式化、模式化而不是散落在业务代码的各个角落。下次如果你的对话系统出现成本高或者记忆差的问题不妨先检查一下自己有没有一个真正算是模式化的上下文模块而不是临时拼凑的字符串数组。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑