ARTICLE DETAIL

资讯详情

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

context-mode实战:大模型上下文分层管理与Token优化

context-mode实战:大模型上下文分层管理与Token优化 1. 先搞清楚 context-mode 到底解决什么问题做 LLM 应用的人聊着聊着一定会撞上一堵墙上下文窗口。窗口不够了、上下文被无关信息污染了、模型把前面的关键意图忘了这些问题我踩了不下几十次。context-mode 这个词说白了就是针对这一类问题诞生的——它不是某个大模型的固有功能而是一套在应用层实现的上下文管理策略核心目标是让模型始终拿到当前最该看的信息而不是所有信息。我最早接触这个思路是在给一个客服机器人做对话系统的时候。当时直接用把所有历史消息都塞进 prompt的笨办法结果 token 消耗爆炸回答质量却越来越差。后来把上下文切成模式来管理——根据当前用户意图、当前任务阶段、当前会话紧急程度动态决定塞哪些内容进 prompt、哪些内容压缩、哪些内容直接丢弃——整个系统才真正稳下来。context-mode 能解决的痛点很具体长会话场景下早期的重要信息用户需求、约束条件会被淹没在大量寒暄和无效对话里。上下文窗口有硬上限塞满之后被迫暴力截断截断的部分往往恰好是模型最需要的。多任务混合的会话中模型分不清当前到底在做什么回答经常串台。成本失控每轮请求都带全量上下文token 费用呈线性甚至超线性上涨。这篇文章不是讲某个具体产品的使用说明书而是把我自己从零搭建 context-mode 调度系统的完整思路、代码实现、踩坑记录都摊开来讲。不管你是做 RAG 应用、Agent 工作流、智能客服、还是本地知识库问答这套方法都可以直接借鉴。我会尽量少讲抽象概念多给能抄作业的代码和配置。2. 核心机制上下文怎么收集、怎么排序、怎么压缩2.1 三层上下文结构全局层、会话层、即时层context-mode 的第一个关键动作是给上下文分层。很多人管理上下文是一个大口袋什么都往里装这必然导致优先级混乱。我自己的实践是分成三个层次层级内容更新频率典型大小全局层系统设定、用户画像、固定业务规则几乎不变1~2K tokens会话层当前会话的目标、已确认关键信息、任务进度每轮更新2~4K tokens即时层最近几轮对话、当前提问、临时检索结果每轮重建1~3K tokens全局层对应的是这个 AI 是谁、它在帮谁干活、有什么规则不能违反。比如客服系统里全局层就是你是某品牌的客服助手退款政策是xxx不能承诺xxx。这部分永远放在 prompt 最前面保证每次调用都在。会话层是 context-mode 的灵魂。它不存原始对话而是从原始对话里抽出来的结构化结论——用户真正想要什么、已经确认了哪些需求、还有哪些待定项。这就像是会议纪要开会时候的闲聊不用保留但结论和待办必须在。即时层最像传统聊天方案的上下文直接带上最近几轮对话原文让模型感知语气和连续性。但它的长度是受控的通常只保留最近 2~3 轮。这里有个很反直觉的点会话层比原始对话更有价值。原始对话里用户说我希望价格便宜一点但也不能太便宜质量要有保障会话层提炼后就是用户关注性价比价格敏感度中等拒绝低价低质。提炼后的信息密度高得多模型不需要从一大段话里自己猜。2.2 Token 预算分配给每个模块发额度有了分层还要有预算。Token 窗口说白了就是内存不规划就会被各种模块抢光。我常用的分配策略是总预算设为模型上下文窗口的 60%~70%预留余量给生成回复和即时输入。全局层固定占 10%这部分不可压缩。会话层占 30%~40%根据重要性动态调整。即时层占 20%~30%最近对话原文和检索结果在这里竞争名额。实际计算时我用一个简单的 token 预估函数中文按一个字约等于 1~1.5 个 token来粗估英文按一个词约等于 1.3 个 token。不用精确到个位上下文裁剪本来就是个近似工程。预算分配的目的是建立稀缺感。当系统知道全局层只能占 1K token就不会允许某次产品说明塞进 3K 的内容。所有模块都必须证明自己的价值才能进入 prompt。2.3 上下文压缩与遗忘策略压缩和遗忘是 context-mode 里最容易做砸的部分。很多人一上来就想着让模型帮我总结上下文结果每次总结都要额外烧一轮 token而且总结抽象程度不可控——可能把关键细节丢光也可能把废话全保留了。我采用的策略是消息级衰减给每条消息标一个权重权重随时间衰减用户明确的关键指示带必须千万不要最重要的是等措辞权重最高永久保留。业务操作结果如订单已创建编号20250301权重次之保留 10~20 轮。寒暄、确认性的话、语气词好的嗯嗯谢谢权重最低直接丢弃。实际操作里保留用户的关键指示比保留机器人自己的回答重要得多。模型最怕的是用户已经说过的东西被自己生成的平庸回答淹没了。所以我会对每条机器人回答做清洗只保留其中有信息量的部分比如生成的结果、必要的确认信息其余丢掉。遗忘策略的核心原则是宁丢细节不丢意图。细节丢失了用户可以重新说意图丢失了模型就开始胡扯。3. 实操从零实现一套 context-mode 调度系统3.1 第一步定义上下文数据结构和优先级先定义一个干净的上下文数据结构。我用 Python 实现核心是 ContextItem 和 ContextManager 两个类from dataclasses import dataclass, field from enum import IntEnum from typing import Any, Optional class ContextLevel(IntEnum): GLOBAL 3 # 全局层最高优先级 SESSION 2 # 会话层 RECENT 1 # 即时层 class ItemType(IntEnum): SYSTEM_RULE 4 # 系统规则 USER_INTENT 3 # 用户意图 FACT 2 # 业务事实 ACTION_RESULT 1 # 操作结果 CHITCHAT 0 # 寒暄最低优先级 dataclass class ContextItem: content: str level: ContextLevel item_type: ItemType created_round: int # 创建时的会话轮次 last_access: int # 最近一次被用到的轮次 weight: float 1.0 # 基础权重 metadata: dict field(default_factorydict) def decayed_weight(self, current_round: int) - float: # 衰减公式权重随时间指数衰减但最低保留 30% age current_round - self.last_access return max(0.3, self.weight * (0.9 ** age))这里有个细节值得说明我给每条 Item 单独维护last_access而不是created_round。因为上下文的价值不取决于它存在多久而取决于它多久没被用到了。一个在 50 轮前创建、但每一轮都被检索命中的用户需求应该永远保留一个 5 轮前创建、再也没用过的临时信息反而该早点淘汰。3.2 第二步实现上下文采集与注入管线采集管线解决的是一条新消息进来它算什么。这一步用规则 LLM 辅助的混合方案最稳。class ContextCollector: def collect(self, message: str, role: str, round_num: int) - ContextItem: if role system: return ContextItem( contentmessage, levelContextLevel.GLOBAL, item_typeItemType.SYSTEM_RULE, created_roundround_num, last_accessround_num, weight1.0 ) if role user: intent_level self._detect_intent_level(message) # 检测到关键意图词提升权重 if any(kw in message for kw in [必须, 千万不要, 最重要的是, 一定]): return ContextItem( contentmessage, # 完整保留不压缩 levelContextLevel.SESSION, item_typeItemType.USER_INTENT, created_roundround_num, last_accessround_num, weight1.0 ) # 短消息小于15字且无关键信息直接降级 if len(message) 15 and intent_level 0: return ContextItem( contentf[简短确认] {message}, levelContextLevel.RECENT, item_typeItemType.CHITCHAT, created_roundround_num, last_accessround_num, weight0.3 ) # 一般消息进入会话层 return ContextItem( contentself._extract_and_condense(message), levelContextLevel.SESSION, item_typeItemType.FACT, created_roundround_num, last_accessround_num, weight0.8 )_extract_and_condense是采集管线的重头戏。我的做法是先用正则做粗提取抓数字、订单号、日期、地点、明确的动作词再用一个轻量 LLM 调用做二次提炼把消息压成一句结构化描述。注意这里用的 LLM 调用很便宜——输入不超过两百字只要求输出按压后的结论每次调用成本几乎可以忽略。注入管线的逻辑刚好反过来从 ContextManager 里按优先级取 Item拼成最终的 prompt。class ContextManager: def __init__(self, max_tokens: int 8000): self.items: list[ContextItem] [] self.round 0 self.max_tokens max_tokens self.budget { ContextLevel.GLOBAL: int(max_tokens * 0.1), ContextLevel.SESSION: int(max_tokens * 0.35), ContextLevel.RECENT: int(max_tokens * 0.25), } def add_item(self, item: ContextItem): self.items.append(item) self.round 1 def build_prompt(self, recent_history: list[dict]) - str: # 用当前轮次计算每个 item 的衰减权重 scored [(it, it.decayed_weight(self.round)) for it in self.items] # 按层级优先、权重次优排序 scored.sort(keylambda x: (x[0].level, x[0].decayed_weight(self.round)), reverseTrue) # 按预算逐层拼装 result [] used {level: 0 for level in ContextLevel} for item, weight in scored: token_est estimate_tokens(item.content) if used[item.level] token_est self.budget[item.level]: result.append(item.content) used[item.level] token_est item.last_access self.round # 命中即刷新活跃度 # 最后拼上即时层的最近对话 for msg in recent_history[-3:]: result.append(f{msg[role]}: {msg[content]}) return \n.join(result)这里有个在线上系统里验证过的细节排序时层级是主键权重是次键。全局层永远排在会话层前面哪怕某条会话层消息的权重是 1.0 而全局层是 0.5。这不是拍脑袋定的——全局层放的是系统规则规则被挤掉了整个系统就失控了。权重只能决定同一层级内谁先进窗口不能跨层竞争。3.3 第三步动态上下文裁剪算法静态预算有个问题会话刚开始时所有消息都新鲜、权重都高预算很快被塞满会话进行到中段时真正重要的信息反而可能被早期积累的冗余挤出去。所以我在预算分配之后又加了一个动态裁剪环节。裁剪的核心算法是多头注意力式筛选——从当前用户的问题里抽关键词计算每条上下文与关键词的相关度然后按相关度调整实际注入顺序和注入比例。import re from collections import Counter class ContextPruner: def __init__(self): self.stopwords set(的了在是和我有就 \ 都而及与着或者好吧啊嗯呢呀.strip()) def extract_keywords(self, question: str) - list[str]: # 粗分词按常见分隔符切分过滤停用词 tokens re.findall(r[\u4e00-\u9fa5]{2,}|[a-zA-Z0-9], question) filtered [t for t in tokens if t not in self.stopwords] counter Counter(filtered) return [w for w, _ in counter.most_common(5)] def relevance_score(self, context_item: ContextItem, keywords: list[str]) - float: if not keywords: return 0.5 # 无关键词时给中性分 hit sum(1 for kw in keywords if kw in context_item.content) return min(1.0, hit / 2.0)实际运行时我会把相关度得分乘到衰减权重上作为最终的排序分值。这样一条 20 轮前的用户要求次日发货如果当前用户问什么时候能到它依然能排进前列如果用户问的是退款怎么操作这条旧消息就该让位给退款政策相关内容。这个机制的灵感来自搜索引擎的 TF-IDF 思路——上下文本质上也是文档集合当前问题就是查询词。做开发时不用把这个机制想得多玄妙它就是让模型每次看到的上下文都跟当前问题最相关。裁剪后的总预算再做一次余额检查如果相关度高的 Item 加总后仍然超出预算就按从低到高逐条裁剪优先裁掉CHITCHAT类型最后才动USER_INTENT类型。3.4 第四步接入 LLM 调用层管线搭好后接入层反而最简单。关键是把build_prompt()的产物作为系统提示词注入而不是塞到用户消息里。def run_llm_call(user_input: str, manager: ContextManager, llm_func, recent_history: list[dict]): # 1. 采集新输入 new_item collector.collect(user_input, roleuser, round_nummanager.round) manager.add_item(new_item) # 2. 动态裁剪 keywords pruner.extract_keywords(user_input) manager.apply_prune(keywords) # 内部调 relevance_score 重排序 # 3. 构建 prompt context_prompt manager.build_prompt(recent_history) # 4. 调用 LLM messages [ {role: system, content: context_prompt}, {role: user, content: user_input} ] response llm_func(messages) # 5. 反馈回采集LLM 的回复里有新事实也要入上下文 assistant_item collector.collect(response, roleassistant, round_nummanager.round) # 助手消息默认权重打 7 折避免机器人的话盖过用户意图 assistant_item.weight 0.7 manager.add_item(assistant_item) recent_history.append({role: user, content: user_input}) recent_history.append({role: assistant, content: response}) return response接入层有个容易忽略的坑千万不要把每个模块独立串行的中间结果也塞进上下文。我最开始做的时候把意图识别结果、关键词抽取结果、相关度得分全部拼进 prompt觉得信息越全模型越聪明。结果 token 被大量元数据占满模型反而开始分析我的调参日志不回答用户问题了。经验教训是prompt 里只放用户能理解的信息中间过程数据自己内部消化别让模型看到。4. 常见问题与排查技巧实录4.1 上下文越聊越乱怎么收敛我遇到过最经典的现象会话进行到 30 轮左右模型开始重复问已经确认的问题或者把早先用户的需求张冠李戴。排查下来根因通常是会话层里抽出的用户意图条目被后续的低权重消息挤出了预算。解决方案是给USER_INTENT类型的 Item 一个保底名额——不管预算怎么紧张至少保留最近 3 条最关键的用户意图。实现方式很简单在build_prompt里先预留这部分空间def build_prompt(self, recent_history: list[dict]) - str: # 先强制保留最高优先级的用户意图 reserved [it for it in self.items if it.item_type ItemType.USER_INTENT and it.weight 1.0] reserved_tokens sum(estimate_tokens(it.content) for it in reserved) # 从 SESSION 预算里扣除保留名额 self.budget[ContextLevel.SESSION] - min( self.budget[ContextLevel.SESSION], reserved_tokens)这个保底机制上线之后模型忘事的问题基本绝迹。核心逻辑是用户明确说出口的需求是对话的锚点锚点不能被冲走。4.2 Token 被无关信息占满另一个高频问题是prompt 里塞满了无关信息用户问 A 事项模型却老想着 B 事项的细节。这多半是采集阶段什么都收导致的。我的处理思路是加一道相关性闸门。新消息进来后先计算它与当前活跃主题的相关度。如果相关度阈值太低比如 0.2就只进即时层不进会话层——保留它的上下文但不让它长期占用高优先级位置。def should_promote_to_session(self, item: ContextItem, active_topic: str) - bool: if item.item_type in (ItemType.SYSTEM_RULE, ItemType.USER_INTENT): return True # 与当前活跃主题相关的才升入会话层 return active_topic in item.content or len(item.content) 80这个阈值逻辑要按业务微调。客服场景里如果用户在倾诉一个复杂问题content 80字的长消息应该升入会话层哪怕主题有点偏离——因为用户可能正在表达一个还没被你完全理解的新需求。但如果是简短消息嗯好的那就这样吧无论如何都不该升层。4.3 多轮对话的记忆漂移问题记忆漂移是指模型回答的内容跟早前确立的目标越来越不一致。比如用户一开始说要轻量级方案聊到后面模型开始推荐重量级方案。根因通常是会话层里用户要轻量级这条信息权重不够高而后来的一系列技术讨论都是重量级相关把它挤下去了。解决办法是引入目标一致性校验每一轮生成回答之前把当前会话层里的用户意图和系统将要生成的回复风格做一次快速比对。比对不用模型用关键词 向量余弦相似度都行。不一致时强制把最相关的用户意图提到 prompt 最前面并在系统提示里加一句先确认用户原始诉求。我在生产环境里用的是轻量向量化方案每条用户意图存一个 embedding每次新问题进来也做 embedding算相似度。相似度最高的那条意图强制优先注入。这个方案跑得很稳唯一的成本是 embedding 接口的调用费用但单次不过几厘钱完全划得来。4.4 性能与成本的平衡context-mode 本身是省 token 的因为它的目标就是少带、带准。但如果在采集阶段频繁调用 LLM 做提炼省下来的钱又会被提炼花掉。我的经验是给提炼操作设一个触发条件不要每条消息都走 LLM 提炼消息长度小于 30 字不做提炼直接原样存。消息里有明确的结构化信息订单号、日期、数字用正则抽不调 LLM。消息长度大于 30 字且无结构化信息才调用 LLM 提炼。每轮最多调用一次提炼 LLM多出来的消息走规则降级。这个策略实测下来提炼成本占整体 token 成本的比例能控制在 5% 以内。很多人做 context-mode 做成烧钱机器就是忽略了哪些步骤该用规则哪些才配用 LLM。还有个性能细节estimate_tokens别用精确的 tokenizer否则每次 build_prompt 都会卡在分词上。我直接用len(text) // 2做中文 token 估算偏差在 20% 以内对裁剪决策来说足够。接口真正的耗时瓶颈在网络调用不在估算。4.5 一个贯穿始终的调试技巧最后分享一个调试 context-mode 系统的实用技巧给每条进入 prompt 的上下文打上溯源标记。def build_prompt(self, recent_history: list[dict]) - str: # 调试模式输出注入日志 if self.debug: print(f[Round {self.round}] 注入 {len(result)} 条上下文) for item in injected_items: print(f - [{item.level.name}] ftype{item.type.name} fweight{item.decayed_weight(self.round):.2f} fcontent{item.content[:50]}...)线上出问题时先看日志里每个 round 注入了哪些条目、权重多少、哪些被裁剪掉了。我踩过的坑里有一半是看日志就秒懂不看日志猜半天。context-mode 属于那种黑盒感很强、但一旦可视化就非常透明的系统花 20 分钟把日志做好后面能省 20 个小时的排查时间。5. 结语前的最后几条经验5.1 从最简单的分层开始不要一上来就做全套如果你刚开始做 context-mode别急着把所有机制全上。我的建议是分阶段落地先做分层全局层、会话层、即时层三件套手动指定每层的 token 预算。再加衰减用last_access做活跃度刷新让常用信息自动留在窗口里。然后上相关度接关键词或 embedding让当前问题能拉取相关上下文。最后才是动态裁剪和 LLM 提炼这两块对系统的稳定性要求最高等前几步稳定了再动。我见过太多团队第一步就想做个全自动、自学习、完美压缩的上下文系统折腾两个月还在调参。实际上 80% 的收益在前三步就拿到了第四步只是锦上添花。5.2 上下文管理的本质是取舍做 context-mode 最深的体会是上下文管理的本质不是存储而是取舍。你不可能把所有信息都塞进窗口塞得越多模型越分不清重点。真正好的 context-mode 系统是让模型每次调用时面对的都是最精炼、最相关、最新鲜的信息集。这就像给一个人做简报——把 50 页的报告浓缩成两页要点比把 50 页原封不动放他桌上效果好十倍。模型和人也一样输入质量决定输出质量。5.3 这套思路也可以往反方向扩展context-mode 不只用于省 token、提准确率它还能反过来用于知识注入。我在另一个项目里把同样的分层机制用在了企业知识库问答上全局层放企业规章制度会话层放用户的提问历史和相关文档摘要即时层放实时检索的文档片段。效果比直接做 RAG 要好——因为 RAG 只解决检索什么不解决检索到的内容怎么组织优先级而 context-mode 恰好补上了这一环。这也是我觉得这套方法最值得分享的原因它不是一个一次性脚本而是一个可以反复套用的架构模式。无论你是在做大模型应用、Agent、还是传统推荐系统的特征拼接上下文优先级的组织思路都能迁移过去。以后你要是遇到模型总是答非所问token 成本压不下来长会话聊着聊着就崩这类问题可以先想想你的上下文是一锅烩还是真正有层级、有取舍、有优先级的context-mode这个转变可能比换更大的模型、调更长的窗口实在得多。
返回列表