尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LLM上下文管理实战:context-mode策略让模型不再“失忆”

LLM上下文管理实战:context-mode策略让模型不再“失忆” 做 LLM 应用开发这两年我踩过最大的坑不是模型能力不够而是上下文没管好。同样一个 128k 的模型窗口有人能把它用出 8k 的轻快效果有人却越用越乱最后连模型都开始“精神分裂”。后来我把上下文的管理方式整理成一个叫 context-mode 的模块针对不同任务匹配不同的上下文策略问题才真正开始收敛。这篇文章会把这套模块的来龙去脉、关键参数、落地配置和踩坑记录完整写出来。正在做 AI 客服、文档问答、Agent 开发的团队或者想在 RAG 里做精细化控制的朋友可以直接参考。1. 为什么需要 context-mode从上下文窗口到上下文模式1.1 长长的历史消息为什么不能全丢给模型早期做 LLM 应用大家习惯很简单把用户问题、历史消息、参考资料一股脑塞给模型。在 demo 阶段确实能跑通一旦真实用户用起来问题就全暴露了。我记得第一次做客服机器人上线第二天就收到运营反馈用户聊到第 20 轮模型已经不记得他一开始问的是退款流程还是换货规则。我打开日志一看好家伙每轮请求里塞进了全部历史消息单次输入超过 20k token模型还能记住前面说了什么才怪。这就是最典型的“上下文越长模型越糊涂”现象。原因是多方面的信噪比下降。历史消息里大量重复、寒暄、无关内容把真正关键的指令挤到了窗口边缘模型注意力被稀释。位置偏置。很多模型对 prompt 开头和结尾更敏感中间一大段容易被忽略。历史越长关键信息越容易落在“中间地段”。成本线性上涨。按 token 计费的接口每多几千历史成本就涨一截。高峰时段并发一多账单数字非常感人。延迟明显变高。输入越长首字返回时间越长。用户等 5 秒没反应基本就流失了。所以我在团队里一直强调一句话大模型应用出问题八成出在上下文而不是模型本身。模型再强喂进去的是噪声吐出来的也只会是噪声。单纯“扩大窗口”不是解法真正要做的是把“放什么进上下文、不放什么、以什么顺序放、放多久”变成一套可配置、可切换的策略。这就是我理解并最终落地成 context-mode 的核心原因。1.2 context-mode 是三个问题的答案很多人以为 context-mode 是一种“压缩算法”其实不对。它更像是一套上下文管理策略的预设组合。每个模式只需要回答三个问题。第一个是范围也就是当前这个任务需要覆盖多少信息。是只看当前这一轮对话还是看到整个会话还是看到整个项目或者知识库范围定义错了后面全错。第二个是保真度信息进来的时候是保留原始形态还是经过摘要和提炼是要细节和原文还是只要结论第三个是生命周期信息什么时候进入上下文什么时候被淘汰什么时候归档到长期记忆或者外部存储用一个生活化的类比同一个厨房做快餐和做宴会食材准备、火候控制、上菜顺序完全不同。如果给所有菜式套同一套流程结果一定是又慢又乱。context-mode 就是给不同“菜式”准备不同“厨房流程”。它不是某个独立算法而是窗口大小、system prompt、历史裁剪、检索策略、摘要策略等多个参数的组合。刚开始我尝试用一套通用上下文管理代码兼容所有任务结果发现调参调到怀疑人生。后来我干脆把这些参数打包成几个固定模式每个模式对应一类场景。新任务来了先判断任务类型再套用现成模式顶多微调少数参数。开发效率和线上稳定性都高了一大截。1.3 三种基础模式速览基于我实际接触的项目大部分场景可以归成三种基础模式compact、detail、global。我把它们的核心差异整理成一张表方便对照。模式窗口占用信息组织方式核心依赖延迟典型场景compact低4k~8k摘要加最近N轮对话摘要模型低客服、闲聊、简单问答detail中8k~32k检索片段加原文引用向量检索、RAG中文档问答、论文分析、知识库查询global高32k以上结构化记忆加全局计划规划器、记忆模块高Agent、多步任务、代码生成compact 模式的核心思路是“少而精”。只留最近几轮对话超过阈值就把旧历史压缩成摘要。detail 模式的核心思路是“用检索代替硬塞”。文档再长也不怕切块、召回、只把最相关的片段带进上下文。global 模式的核心思路是“状态显式化”。把任务目标、已完成步骤、中间结果用结构化数据维护起来让模型随时看到全局状态而不是从一大堆历史消息里自己猜。很多团队一上来就直接上 global 模式觉得窗口越大越安全。我见过最夸张的例子把 128k 窗口全部塞满结果模型的回答既慢又空泛。正确的做法是先搞清楚业务到底属于哪种模式再选择对应的策略。大部分客服、营销文案场景用 compact 就够了硬上 global 只会增加成本和延迟。2. context-mode 的关键参数与配置驱动思路2.1 先搞懂这几个核心参数我再往深一层拆解。模式看起来是“概念”落到工程上其实全是参数。搞不清楚这几个参数配置模式就是瞎调。第一是 context_window也就是单次请求允许的最大输入上限。它不是模型的硬上限而是你主动设置的预算。第二种是 system prompt 的权重指令性内容在上下文中的优先级。很多人只关心用户消息多长忽略了 system prompt 可能已经悄悄占了几千 token。第三是记忆预算也就是短期记忆和长期记忆各占多少比例。短期记忆就是刚发生的几轮对话长期记忆是经过摘要或归档的高价值信息。第四是检索参数包括 chunk_size、top_k、score_threshold 这些。第五是压缩参数比如摘要触发的轮数、摘要保留的长度、是否保留原文。这些参数单看都不难难在它们之间相互影响。比如把 top_k 调大碎片信息变多系统 prompt 里的指令容易被稀释把摘要触发轮数调小长期记忆占比变大短期细节丢失用户会觉得机器人“失忆”了。context-mode 的价值就是把这些参数组合预置好不用每次上线都重新调一遍。我给团队定的规矩是参数必须可配置禁止在代码里写死。任何一次线上问题都要能通过调整配置快速恢复而不是拉代码改参数再发版。2.2 用配置文件定义模式而不是硬编码这是我个人最坚持的一点。模式应该用声明式配置来表达而不是在业务代码里 if-else。下面是我项目中常用的配置文件结构。context_modes: compact: max_tokens: 8192 system_prompt: 你是一个客服助手回答要简洁、准确。 history_policy: recent_n recent_n: 10 summary_trigger: 5 summary_model: gpt-4o-mini use_rag: false temperature: 0.3 detail: max_tokens: 32768 system_prompt: 回答必须基于提供的资料并标注来源。 history_policy: summary_plus_recent recent_n: 5 use_rag: true chunk_size: 512 top_k: 8 score_threshold: 0.7 temperature: 0.1 global: max_tokens: 65536 system_prompt: 你是任务规划助手先制定计划再执行。 history_policy: structured_memory use_rag: true memory_enabled: true max_plan_steps: 8 temperature: 0.2配套的调度器伪代码长这样。class ContextEngine: def __init__(self, config): self.modes { name: ContextModeConfig(**cfg) for name, cfg in config[context_modes].items() } def switch(self, mode_name, session): mode self.modes[mode_name] context self.build_context(mode, session) return context def build_context(self, mode, session): parts [] parts.append({role: system, content: mode.system_prompt}) if mode.use_rag: docs self.retrieve(mode, session.query) parts.append({role: system, content: self.format_docs(docs, mode)}) parts.extend(self.build_history(mode, session)) return self.trim_to_limit(parts, mode.max_tokens)为什么坚持配置驱动三个原因。第一新场景上线不用发版改配置就行。业务同学提了一个新需求开发只需要把新的模式写进配置测试环境验证通过直接上生产。第二可以在线上做 A/B。同一种模式切换成不同参数看看哪组效果好灰度一段时间再全量。第三出问题可以秒级回滚。有一次线上会话质量突然下降我直接把模式的 summary_model 切回旧版本瞬间恢复不用改一行代码。2.3 压缩、检索、记忆的配比关系模式之间最大的差异在于压缩、检索、记忆三种手段的配比。这一点我最初没想明白导致配置时顾此失彼。compact 模式主要靠摘要压缩检索基本不用。历史超过 5 轮就触发摘要把旧的聊天内容浓缩成几句话。detail 模式主要靠检索长文档切成小块选出最相关的 8 段带进上下文保留原文方便引用。global 模式则是三管齐下既有压缩也有检索还有结构化记忆。单轮对话里可能同时包含系统指令、检索片段、计划列表、工具返回结果、任务状态。一个常见误区是只有 detail 模式才需要 RAG。其实不然compact 模式下用户也可能问知识库内容。我给 compact 模式配了一个“按需检索”开关平时不启动检索检测到用户的问题涉及具体产品或政策时临时拉起一个轻量检索。这样既控制了延迟和成本又保留了知识问答能力。配比关系不是固定的最好做成可调参数根据线上数据持续修正。3. 三种模式怎么落地三个真实场景的完整配置3.1 客服机器人场景compact mode先看最快的场景。一个售前售后客服机器人日均会话量高老板要求首响快、成本低运营还希望机器人在 80% 场景下能直接解决问题。最初没有上下文管理所有历史消息全量发送单次请求经常超过 20k tokenP95 延迟 4 秒多部分用户等不及直接转人工。我接入 compact 模式之后做了四件事。第一把窗口限制在 8k超过部分直接截断不允许无限增长。第二精简 system prompt只保留“角色设定、服务边界、高频话术”三块总长控制在 500 token 以内。第三历史只保留最近 10 轮超过 5 轮后启动摘要。第四摘要单独存成一个 summary message随每次请求一起发送避免冷启动时丢上下文。def build_history(mode, session): history session.history[-mode.recent_n:] if len(session.history) mode.summary_trigger: summary summarize(session.history[:-mode.recent_n], mode.summary_model) history.insert(0, {role: system, content: f[会话摘要] {summary}}) return history效果很明显P95 延迟降到 1.2 秒左右单会话成本下降约 60%用户满意度没有下降。关键点在于客服场景里用户真正关心的是“我这个问题怎么解决”过多历史只会干扰模型判断。把旧内容摘要成一两句话既能保留前情提要又不占窗口。3.2 文档问答场景detail mode再看一个文档问答场景。企业知识库里的文档动辄几十页包括操作手册、政策文件、故障案例用户希望答案有依据、可以溯源到具体章节。早期直接把整篇文档塞进 prompt模型只记住开头和结尾中间细节经常张冠李戴。有一次用户问“报错码 E103 怎么处理”模型引用了一段完全无关的说明被客户当场投诉。接入 detail 模式后我调整了检索链路。文档按章节加段落切块chunk_size 512 token块与块之间留 50 token 重叠避免关键信息被切断。检索召回 top_k8score_threshold 设成 0.7低于阈值直接告诉用户“资料库没有找到相关内容”不硬编答案。回答格式强制带引用标记模型输出固定格式的 [1] [2]对应下方来源列表。def retrieve(mode, query, index): if not mode.use_rag: return [] embed embed_model.encode(query) hits index.search(embed, top_kmode.top_k) hits [h for h in hits if h.score mode.score_threshold] return hits上线之后回答的引用准确率明显提升用户可以自己点开原文核对“AI 瞎编”的投诉大幅减少。有一个容易踩的坑score_threshold 设太高会把有效内容全部过滤掉模型只能回答“不知道”设太低又会引入大量噪声。我建议先用日志观察一周看检索命中分数分布再逐步调整阈值。3.3 Agent 规划场景global mode最后是复杂度最高的 Agent 场景。让模型根据用户要求执行多步任务比如查库存、算价格、生成订单、预约发货。这类任务最怕模型“走一步忘一步”更怕上下文被工具返回结果淹没。最初每一步都把全部工具输出追加到历史里第五步时上下文已经堆了上万 token模型开始犯低级错误比如把上一步算好的价格丢进后续计算。接入 global 模式后我做了四件事。第一增加结构化记忆用 JSON 块记录任务目标、已完成步骤、关键中间值。第二每步只保留最新一轮工具输出加记忆摘要丢掉旧工具输出。第三设置最大计划步数 8超过后强制模型做阶段总结把总结写回记忆。第四工具调用设置独立超时和异常信息避免异常输出污染上下文。{ goal: 生成订单, done: [查询库存, 计算价格], prices: {A: 10, B: 20}, next: 生成订单 }改造后任务完成率从 63% 提升到 86%。我最深的体会是对 Agent 来说上下文不只是“历史对话”更是“任务状态”。global 模式的核心是把任务状态显式结构化让模型随时能看见目标、进度和中间结果而不是从一个越来越长的消息列表里自己脑补。4. 切换 context-mode 最容易踩的坑和排查技巧4.1 模式切换导致“失忆”多模式上线后最常见的坑是切换模式时把旧模式的摘要丢了。客服会话从闲聊切到售后咨询时系统从 compact 切到 detail模型突然不记得用户之前说过商品型号和购买时间。原因在于 detail 模式的历史策略只保留最近 5 轮之前 compact 模式的摘要没有带过来。解决方法是所有会话对象持有一个 summary_card 字段任何模式切换都先把它注入新模式的上下文。摘要不能丢但也不能盲目复用。我给摘要加了一个“冻结时间戳”切换时判断摘要是否过期过期就重新生成不过期直接复用。这样既避免失忆也不浪费成本。4.2 system prompt 吃光 token 预算有一次配置了 max_tokens8192但模型实际只收到了 2000 token 的有效问答区域。打开调试信息一看system prompt 里塞了工具说明、few-shot 示例、输出格式、品牌话术加起来 6000 多 token用户的问题只能挤在最后。这类问题的隐蔽性很强因为流程可以跑通只是效果越调越差。我把 system prompt 拆成多个可组合的 prompt block按模式动态组装。角色 block、工具 block、格式 block、话术 block 分开放置每组 block 独立做 token 计数超过预设上限直接告警。实测下来system prompt 能削减 50% 以上为真实对话腾出大量空间。def build_system_blocks(mode): blocks [] if mode.need_role: blocks.append(role_block) if mode.need_tools: blocks.append(tool_block) if mode.need_format: blocks.append(format_block) return trim_blocks(blocks, limit1500)4.3 检索内容与任务错位detail 模式下用户问“这个产品质保几年”系统检索出的却是安装步骤文档。表面看是检索不准本质是查询和文档类型不匹配。用户原话经常是口头表达直接拿去做向量检索会召回一堆表面相似、实际无关的内容。我的做法是在进入检索前先做一次查询改写把用户问题分型为“概念型、操作型、故障型”三类再决定检索范围和 chunk 策略。概念型问题检索百科类文档操作型问题检索步骤类文档故障型问题检索案例类文档。这个预处理在工程上比换模型更有效。另外混合类型文档要拆到不同索引避免互相干扰。4.4 常见问题排查速查表最后整理一份排查速查表。上下文问题有一个特点现象千奇百怪但根源往往集中在那几个地方。症状可能原因快速排查方法建议处理回答开始跑题、丢关键信息上下文窗口被无关内容占满查看请求日志里的 token 分布占比检查是否触发了摘要和裁剪策略切换模式后忘了旧信息摘要没有随模式切换迁移确认 summary_card 是否注入新上下文切换时统一走 summary 迁移逻辑模型频繁说“不知道”score_threshold 设置过高统计检索命中分数分布调低阈值或改用混合检索成本突然暴涨历史消息无限堆积监控单会话平均输入 token收紧 recent_n 或缩短摘要周期同一会话回答不稳定模式被频繁切换且无缓存检查切换日志和 prompt 版本号给模式切换加最小间隔限制system prompt 占比异常prompt block 未做裁剪统计各 block token 占用拆分 block 并设置独立上限排查时我最推荐的技巧是给每一条进入上下文的内容打上来源标签和模式版本号比如[history:summary_v2]、[rag:chunk_018_ver3]。一旦输出质量变差从日志里立刻能看出是哪部分上下文、哪个版本的策略导致的回滚也只需要切换模式版本号。这个习惯帮我省下了大量排查时间。做 context-mode 这套东西的过程中我最大的体会是不要一上来就上最复杂的配置先从小窗口、少模式开始建立日志和监控再根据数据逐步增加模式种类和参数维度。上下文管理不是模型能力问题而是工程问题。把策略模式化、配置化、可观测化LLM 应用的稳定性和成本控制都会上一个台阶。
返回列表