ARTICLE DETAIL

资讯详情

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

大模型应用中的context-mode:上下文管理选型与实战

大模型应用中的context-mode:上下文管理选型与实战 搞大模型应用的朋友不管是做Agent、聊天机器人还是RAG问答系统早晚都会撞上一个绕不开的门槛上下文管理。尤其是那些一跑就是几十轮、上百轮对话的长会话场景用不了多久你就会发现自己被context-mode这几个字卡得死死的。我的体会是这个模块要是没做对后面的稳定性、成本、效果全是连锁崩盘。所谓context-mode简单说就是“上下文模式”指的是系统在跟大模型交互时用什么样的策略来组织历史对话、保留哪些信息、丢掉哪些信息、以及如何塞进有限的上下文窗口里。它直接决定了模型“记得什么、忘掉什么、能不能连贯理解你在问什么”。不管你用的是商用API还是本地开源模型这套机制都是必须想清楚的底座设计。适合正在做Agent应用、智能客服、多轮对话产品或者刚把项目跑到长上下文阶段、发现记忆混乱和费用飙涨的开发同学参考。这篇文章我就从底层概念讲起拆开context-mode里最常见的几种模式——全量模式、滑动窗口、摘要压缩、分层检索——分别聊聊它们各自解决什么问题、有哪些坑以及我在真实项目里怎么选型、怎么调参、怎么排查问题。内容全部来自实际开发和压测的经验希望能帮你在做方案的时候少走几趟弯路。1. context-mode先搞清楚这个词到底指什么1.1 一个被严重低估的核心模块很多人第一次接触context-mode是在LangChain的Memory配置里或者在OpenAI、Claude的API文档里看到的参数选项。它看起来只是“上下文怎么存”的小配置实际上一旦深入进去你会发现它几乎跟所有高层次的系统行为都绑在一起。我举个生活化的例子。你找客服解决问题如果客服换了个人你得把问题从头到尾再讲一遍如果客服能从工单系统里调出全部聊天记录并且一眼扫到关键信息你的体验就完全不一样。context-mode做的就是这一件事决定模型在每次生成回答的时候眼前摆着哪些历史记录按什么顺序、什么粒度摆。它之所以重要是因为大模型本身是“无状态”的。模型不记得你上一句问了什么除非你把这句“上一句”重新塞进本次请求的输入里。所以多轮对话本质上是一个由开发者手动维护历史的工程活。context-mode就是这一整套维护策略的总称。1.2 上下文窗口的硬约束在做context-mode设计之前必须先理解一个物理约束上下文窗口是有限的。拿主流模型来说有的支持128K有的支持200K甚至更多这个数字听起来很大但实际换算下来并没有那么充裕。我粗略算过一笔账。一段1000字的中文内容大概要占掉1500到2500个token取决于分词器的切分效率。如果你用的是128K窗口的模型满打满算也就能装下大约5万字的对话记录。这看着不低问题在于它的输出也要占窗口系统提示词、工具定义、检索回来的知识片段都要占窗口真正留给对话记忆的空间往往只剩下一半甚至更少。所以context-mode本质上是解决一个资源配置问题在有限的窗口容量里怎么把最有效的信息保留给模型让它在回答问题时既不缺上下文又不被大量冗余信息干扰判断。1.3 四种模式的核心差异我把市面上常见的context-mode实现梳理了一下基本上可以归成四类。它们不是互相替代的关系而是一套可以组合使用的工具箱全量模式所有消息原封不动塞进窗口直到撑爆为止。滑动窗口模式只保留最近N轮更早的全部丢弃。摘要压缩模式把历史消息压缩成摘要用摘要替代原始记录。分层检索模式把历史信息结构化存储按需检索再拼装成上下文。这四种模式在保留信息、消耗token、实现复杂度三个维度上各有取舍。没有哪一种是最优的只有哪一种最适合你当前的业务阶段和模型条件。我后面会各花一段来拆把它们的实现思路、适用场景和踩坑点都讲清楚。2. 模式选型到底该用哪一种context-mode2.1 全量模式适合起步但撑不住长会话全量模式是最好理解也最容易实现的一种方式。就是像记账一样把用户每一次提问、系统每一次回答全部追加到消息列表里然后作为上下文原封不动地交给模型。我自己最早做客服机器人就是这种方案代码量极少只要能维护一个数组就行。效果在前面十几轮对话里也还不错因为信息没有丢失模型永远能看到完整的对话背景。但跑着跑着就发现问题了普通用户不可能只聊三轮五轮一旦会话变长第一个被撞碎的就是成本每轮请求的token数线性增长调用费用肉眼可见地往上涨。第二个是延迟输入token越多首字返回时间越长用户等得越不耐烦。第三个是精度崩溃当窗口被旧消息塞满时模型会被不相干的细节干扰开始“忘掉”更重要的早期信息回答质量呈断崖式下跌。所以我的判断是全量模式适合快速验证想法和demo阶段或者那些明确不会有超长对话的产品场景。一旦发现自己的业务有高频长会话趋势就必须立刻换策略。2.2 滑动窗口简单但容易失忆滑动窗口的做法很朴素维持一个固定大小的队列比如只保留最近10轮对话新的进来旧的挤出去。它最大的优点是不用动脑子无论对话多长提交给模型的输入token始终控制在一个上限内成本可控速度稳定。但它的代价同样明显模型会“断片”。用户可能在第十一轮突然问一句“刚才你说的那个方案A如果换个参数会怎样”这句里的“方案A”在第八轮出现过已经被窗口挤出去了模型只能一脸茫然地答非所问。你会发现滑动窗口比较适合那些“即时问答”类场景每一轮相对独立不需要依赖太久远的历史而对那些聊着聊着会回头引用前文内容的场景它就容易翻车。如果非要用它我建议配合一套外部记忆机制来兜底比如把被窗口挤掉的消息写入数据库当检测到用户提及“之前”“刚才”“上次”这类词时再触发检索把它找回来。这其实就是往更复杂的方向走了。2.3 摘要压缩我目前的主力方案摘要压缩也是我给自己多个生产项目选型时的默认方案之一。它的核心思想是把历史对话定期交给模型做一次提炼生成一段浓缩摘要之后就用这段摘要代表那一整段历史。我在实际项目中会画一条分界线对话进行到第20轮时触发一次摘要生成。把前20轮的内容交给模型让它提取出关键决策、用户偏好、已确认的事实和待办事项生成一段几百字的摘要。之后的请求里上下文结构就变成了“系统提示 摘要 最近几轮消息”。旧消息全部丢弃只保留摘要和新消息token占用被大幅压缩模型的注意力也更集中。这种方式最明显的好处是成本大幅下降尤其是竞争激烈的长会话场景费用能比全量模式降70%以上。而且摘要本身是语义压缩保留了关键信息不会像滑动窗口那样彻底丢光。缺点则是摘要的质量直接决定了系统智商如果压缩时细节丢失后续对话就接不上一旦摘要出问题错误还会被滚动放大。我会在第三节专门讲怎么设计摘要结构来规避这个风险这地方值得多花心思。2.4 分层检索适合知识密集型场景分层检索是进阶方案。它不追求把历史“压缩”成一段而是把所有对话内容拆散、结构化存进向量数据库或ES索引里每次新请求到来时只按需把相关的内容检索出来拼进上下文中。它跟摘要压缩的区别在于摘要是“说个大概”检索是“按图索骥”。如果用户问“我们上次聊迁移方案的时候关于数据校验那部分是怎么说的”摘要压缩模式大概率只能给出非常模糊的应对因为摘要根本不会记这么细而分层检索模式下系统会直接在历史记录里搜“迁移方案”“数据校验”相关的消息片段把原文精确捞回来再配合当前轮问题一起交给模型。这个方案的实现成本是最高的要设计存储结构、要写嵌入和检索逻辑、要处理打分和过滤但换来的是极强的长时记忆能力。适合在线教育、企业知识库问答、复杂项目协作这类对历史细节要求很高的场景。我在做知识型Agent时采用的其实是摘要和检索的组合方案——三分之一用摘要保底上下文连贯三分之二用检索补充细节。3. 实操过程如何手写一个可靠的context-mode模块3.1 会话结构的标准化设计不管选哪种模式第一步一定是把会话数据结构标准化、规范化。我见过很多项目在上下文处理上写得一团乱本质都是数据模型没设计好。这里我推荐一种生产可用结构字段不复杂但覆盖了所有模式需要的核心信息{ session_id: uuid-会话唯一标识, accumulated_summary: , # 滚动累积摘要摘要压缩模式核心 message_history: [], # 原始消息列表包含role/content/time key_facts: [], # 关键事实清单独立于摘要存储 last_processed_ts: 0, # 上次摘要生成时间戳控制触发频率 history_metadata: { total_tokens: 0, # 当前已占用token估算 message_count: 0, # 消息条数 oldest_msg_ts: 0, # 最早一条消息的时间 summarized_count: 0, # 已摘要的消息条数 } }这套结构我在多个项目里复用整体很稳。message_history始终存最近N轮原始消息accumulated_summary存历史摘要key_facts单独剥离出来存一些不能丢的硬信息比如用户的电话号码、订单号、偏好设置。这样做的原因很现实摘要压缩时模型可能会忽略这些细颗粒度的确定性事实但它们恰恰是业务正确性的生命线。3.2 摘要触发的阈值计算触发摘要不能“随手触发”要建立一个明确的量化标准。我采用的核心指标是预估token占用。每次新消息进来先按字符数估算新消息的token增量然后跟累积的total_tokens相加超过预设阈值就触发压缩。在实际项目中我的阈值设定往往还跟着模型输出预留空间走。假设我用的是8K窗口的模型我设定的摘要触发阈值是4500个token这样压缩后立刻能腾出位置给后面的新对话也给模型留足输出空间。计算方式也很简单# 估算token数的简化公式中文场景实测误差在15%以内 def estimate_tokens(text: str) - int: chinese_chars sum(1 for ch in text if \u4e00 ch \u9fff) other_chars len(text) - chinese_chars return int(chinese_chars * 1.5 other_chars * 0.3) 8用这个估算值累加一旦total_tokens大于4500就触发压缩。为什么取4500这个数一方面要给模型输出预留大概2000到3000个token的空间另一方面还要留出系统提示词和检索结果的余量。如果阈值定得太高压缩后还是容易撑爆窗口定得太低又会频繁触发压缩增加额外API消耗。3.3 摘要生成提示词的工程化写法摘要压缩模式效果好不好八成看提示词写得好不好。很多人随便写一句“总结一下这段对话”出来的摘要必然是丢三落四的。我自己迭代了很多版这里分享一个稳定可用的模板思路分为角色定位、内容要求、输出格式三个部分你是一个对话记忆压缩引擎。请阅读给定的对话记录生成一份结构化摘要供后续对话使用。要求 1. 按以下字段输出用户核心诉求、已确认的关键决策、用户的明确偏好或约束、待办事项与未解决问题、涉及的关键事实如编号、时间、金额、人物身份。 2. 保留可验证的确定性事实不添加推测信息。 3. 语义压缩控制总字数在200字以内。 4. 如果对话中包含历史摘要请结合历史摘要进行增量更新而不是重头编写。 5. 用中文输出直接给出摘要内容不要任何前缀后缀。这个模板的关键点是第4条也就是增量更新。它确保了每一轮新摘要是在已有摘要基础上做合并迭代而不是每次重新生成全部内容。这样一方面省token另一方面也避免摘要内容在多次压缩之后发生漂移。提示词里的“输出格式”部分我建议固定为JSON方便程序解析后写回session结构。比如说{ summary: ..., key_facts: [..., ...], unresolved: [...] }3.4 核心代码从消息追加到上下文装配的完整链路下面给出一段浓缩后的核心代码展示如何在每次请求时正确装配上下文覆盖摘要压缩最近窗口的组合模式。这个函数建议直接在项目里复用再根据业务字段微调from typing import List, Dict import time class ContextManager: def __init__(self, max_history_rounds10, summary_threshold4500, max_summary_len200): self.max_history_rounds max_history_rounds self.summary_threshold summary_threshold self.max_summary_len max_summary_len def append_message(self, session: Dict, role: str, content: str): # 追加新消息到原始消息列表 session[message_history].append({ role: role, content: content, ts: int(time.time()) }) # 更新token占用估算 session[history_metadata][total_tokens] estimate_tokens(content) session[history_metadata][message_count] 1 # 触发摘要压缩 if session[history_metadata][total_tokens] self.summary_threshold: self.summarize_old_messages(session) def summarize_old_messages(self, session: Dict): # 提取需要压缩的原始消息早期消息 现有摘要 to_summarize session[message_history][:-self.max_history_rounds] if session[accumulated_summary]: to_summarize [{role: system, content: 历史摘要 session[accumulated_summary]}] to_summarize # 调用模型生成新摘要代码省略LLM调用细节传入messages即可 new_summary call_llm_for_summary(to_summarize, self.max_summary_len) session[accumulated_summary] new_summary[summary] session[key_facts] merge_key_facts(session.get(key_facts, []), new_summary.get(key_facts, [])) # 清理已压缩的消息重置token计数 removed_tokens sum(estimate_tokens(m[content]) for m in to_summarize) session[history_metadata][total_tokens] - removed_tokens session[history_metadata][total_tokens] estimate_tokens(new_summary[summary]) session[message_history] session[message_history][-self.max_history_rounds:] session[history_metadata][summarized_count] len(to_summarize) def build_messages(self, session: Dict, current_query: str, extra_context: str ) - List[Dict]: # 组装最终发送给模型的messages messages [{role: system, content: 你是...业务系统提示}] if extra_context: messages.append({role: system, content: 知识库检索结果 extra_context}) if session[accumulated_summary]: messages.append({role: system, content: 对话摘要 session[accumulated_summary]}) if session[key_facts]: messages.append({role: system, content: 关键事实 json.dumps(session[key_facts], ensure_asciiFalse)}) messages.extend(session[message_history]) messages.append({role: user, content: current_query}) return messages这段代码有几个细节我需要特别说明。第一summary生成时排除掉了最后几轮完整的消息因为这部分内容会在build_messages里以原始形态发给模型没有必要重复压缩。第二压缩之后session里保留了最近10轮完整的消息这个“最近10轮”既保证了对话的即时连贯性也让成本保持在可控范围。第三key_facts用merge_key_facts做了去重和合并避免压缩多次后同一条事实反复堆叠。3.5 内存版与生产版的结构差别上面这套代码直接跑本地demo没问题但生产环境我强烈建议把session结构搬到Redis或者MongoDB里。原因不复杂进程一重启内存数据就全没了用户会话当场断头。尤其在做Agent服务时多实例部署要考虑session的共享访问存本地内存必然会出现负载均衡转发不同实例导致上下文错乱的故障。我在生产项目里的做法是把message_history和accumulated_summary拆成两个key存Redis前缀取session_id。每次请求进来先读RedisContextManager更新后写回。这样即使模型调用超时重试也能保证上下文不会丢。这里还有个容易踩的坑不要把大段摘要直接存成String value然后反复读改写。Redis在大value下的读写性能会急剧恶化阻塞事件循环。我的做法是拆成Hash结构summary和key_facts各自一个字段每次只更新变化的部分明显更稳。4. 常见问题被我踩烂的那些坑4.1 摘要把用户的关键数字给“磨”丢了这是我在客服项目里遇到的第一个大坑。用户说“我三个月前买的产品订单号是XJKD20230815现在想退货”这个订单号出现在第3轮触发摘要后模型的总结写成“用户咨询三个月前购买的产品的退货问题”订单号被当成细节省略了。结果第35轮用户再次确认时系统完全不知道订单号是什么。解决方案就是我在3.1节加入的key_facts独立存储。所有跟业务强相关的确定性事实不依赖摘要的自觉而是通过提示词强制抽取到专门字段再单独存储。关键事实的抽取不需要每轮都做只需要在摘要压缩时做一次就够了。4.2 窗口设得太大模型开始“精神涣散”有些开发同学觉得只要模型支持200K窗口那就把窗口直接拉满什么历史都往里塞不压缩不摘要。我实测过这种做法在指令遵循能力和推理能力上都有明显退化模型会把注意力分配到海量无关信息中回答变得啰嗦、跑题、甚至自相矛盾。直观类比一下你让一个实习生读完一整年的聊天记录再回答你一个非常具体的问题他大概率也会被大量无关信息干扰抓不住重点。模型也是一样的。这也是我在设计阈值时宁愿保守也不贪心的原因。4.3 摘要触发太频繁费用反而上涨摘要压缩本身是调用模型来做的也是要花钱花时间的。如果阈值设得太低每几轮就触发一次压缩消耗的API费用甚至可能超过全量模式得不偿失。我见过有些项目摘要trigger阈值设在1000 token结果每两三轮就压一次账单直接翻倍。经验做法是把阈值设成窗口上限的40%到50%比如8K窗口设在3500到4500128K窗口则设在50K。这样既不会频繁压缩也能保证压缩后留出足够空间。4.4 多轮工具调用的上下文污染在Agent场景中有一个很隐蔽的问题上下文里混杂了工具调用的中间状态。比如第一轮调用了一个搜索工具返回了一堆原始JSON结果第二轮模型又调用了一个别的工具第三轮用户只问了个简单问题但这些工具调用的全过程都被塞进了message history模型会被中间输出干扰甚至以为那些工具返回的数据就是用户给的。我在实际生产中的处理方式是对工具调用的消息做降噪压缩处理。凡是工具返回的原始结果不参与上下文累积只保留工具名、关键输出结论和时间点。这样模型读到的历史更干净不会把内部状态误认为用户事实。4.5 不同模型对context-mode格式的容忍度不同我做过多轮模型替换测试发现一个让我印象深刻的差异有的模型对消息列表里连续多个system角色的容忍度很低可能会忽略后面追加的system内容有的模型则能正确处理。如果你的系统里用了多模型切换建议在build_messages里把多个system角色消息合并拼接成单条system消息而不是给模型发多个system block。5. 工具生态与框架中的context-mode实现5.1 LangChain等框架里现成的Memory组件LangChain提供了多种内置的Memory类比如ConversationBufferMemory对应全量模式ConversationSummaryMemory对应摘要压缩ConversationBufferWindowMemory对应滑动窗口。新一代版本里还支持向量存储记忆、实体记忆等。但我的经验是这些内置组件可以作为学习参考生产环境直接裸用则要谨慎。框架的通用封装往往没有针对你的业务做调优比如摘要触发没有token估算、没有key_facts机制、没有并发访问控制需要二次开发改造。我现在更倾向于用自己的ContextManager把框架的Memory当成设计参考而不是依赖。5.2 模型侧提供的上下文缓存能力另外要提一个容易被忽视的省钱利器上下文缓存。OpenAI和Anthropic都推出了prompt caching功能简单说就是如果你的请求里前缀是相同的内容这部分token的计费价格会大幅下调。既然上下文里固定不变的部分就是系统提示词、历史摘要、知识库检索前缀那把它们拼在prompt前面并开启缓存长会话场景的费用其实可以再降一截。要注意的是缓存命中有时间窗口限制并且对前缀的稳定性有严格要求任何微小的前置差异都会导致缓存失效。所以我在设计时会把摘要和关键事实这类“偶尔才变”的内容严格按照缓存友好顺序去排列系统提示在最前检索结果次之动态消息放在最后。6. 经验总结context-mode选型与调优建议6.1 按业务阶段选择最合适的模式结合我自己的项目经历我建议按下面的思路做选型验证阶段用全量模式先把对话流程跑通把业务逻辑验证对不要过早陷入上下文工程细节。上线初期切换到摘要压缩模式固定阈值配合key_facts机制兜底能扛住大多数生产场景。长会话高频阶段引入分层检索把知识库和长期记忆做向量化存储同时保留摘要作为短期记忆层。Agent工具类场景在摘要压缩基础上增加工具调用的降噪处理避免内部状态污染上下文。没有银弹方案组合式架构才是主流。我自己最终沉淀下来的二段式结构短期对话用“最近N轮滚动摘要”长期记忆用向量检索库系统提示词独立走缓存三层互不干扰各司其职。6.2 给新人的训练方法建议如果你刚接触这个领域我建议不要一上来就去抄别人现成的Memory配置。哪怕先用全量模式把这个过程亲手写一遍自己算token、自己拼prompt、自己写摘要提示词、自己观察模型行为的变化。这是理解context-mode最快的路径。我之前带新人时给过一个训练任务用同一个客服对话数据分别用全量、滑动窗口、摘要压缩三种模式各跑10轮记录每轮回答质量和API费用。跑完对比数据你会比看十篇文档都更清楚模式差异的本质。这个练习通常能帮助建立起对上下文工程的整体直觉后续设计系统时就不会再凭感觉拍脑袋。6.3 踩坑之后沉淀的五个实操口诀最后分享几条我在多次踩坑后沉淀下来的经验摘要只做“语义记忆”不做“事实存储”确定性的信息永远独立放在key_facts里。阈值宁低勿高token估算宁多勿少输出空间要留足。上下文里的system消息尽量合并成一条避免模型对不同system块的解析差异。工具调用的细节不要全部塞进历史保留结论舍弃过程的中间数据。生产环境一定要用好模型侧的prompt caching长会话成本能降一半以上。这些规则看着简单每一条背后都是我实打实付出过账单和线上故障换来的。照着这个框架去做context-mode不敢说你一定不会踩坑但至少能绕开我摔过的那些。
返回列表