开头先直接说结论context-mode这个词在当下这个阶段基本等同于大模型应用落地时绕不开的那道坎——上下文管理。不管你是做 Agent、做 RAG 知识库问答、做长文本分析还是搞什么“AI 套壳”创业最终能卡住你的多半不是模型能力而是模型“记不记得住”以及“记住了哪些东西”。我前阵子帮朋友调一个长文本解析的 Agent场景很简单丢进去一份 200 页的项目文档让模型按照指定格式总结出关键决策点。第一次跑模型输出完全跑题第二次跑前半段正确、后半段开始乱编第三次干脆直接报上下文超限。问题不在模型而在“喂进去的内容怎么被模型理解、组织和遗忘”——这就是 context-mode 要解决的核心问题。这篇东西我打算把自己实际调优过程中的策略、参数、踩坑记录都摊开来讲。适合正在做大模型应用集成、Agent 工作流、或者被上下文窗口折磨的开发者参考。我会从策略理解、构型拆解、实操代码、问题排查几个层面展开尽量做到你看完能直接在项目里动手调。1. 内容整体设计与思路拆解想要搞清楚 context-mode 怎么用先得琢磨清楚一件事大模型的“上下文”到底是个什么东西。大多数人的第一印象是上下文 我多给模型聊几句它就能记住。这个印象对了一半。底层实现上模型确实会把历史对话内容拼接到当前的输入序列中然后在内部通过注意力机制对这些 Token 进行加权处理。但注意力机制的“权重分配”不是平均的——模型会主动“忽略”一部分历史 Token只重点聚焦在相关的那部分。这就导致了一个很反直觉的工程事实你塞给模型一万个 Token模型可能真正“在用”的只有两千 Token。所以 context-mode 的真实含义不是“如何把更多文本塞进窗口”而是“如何设计一个模式让塞进去的内容更加高效地被模型利用”。我一般把它拆成两层来看。第一层是显式上下文模式就是让开发者明确告诉系统“哪些内容是长时记忆哪些是临时对话哪些是工具返回结果”然后系统在构键 Prompt 时用不同的区域逻辑去放置这些内容。目前多数商业框架比如 Claude 的官方 API 已经支持类似 system、messages、tools 这种分区结构这就是最基础的 context-mode。第二层是隐式上下文模式指的是靠算法和策略自动管理上下文比如对历史对话做摘要、对向量检索结果做重排、用压缩算法丢弃不重要的 Token 等等。这一层通常需要开发者自己实现或者借助 langchain 里的 memory 模块、LlamaIndex 的 chat engine 等。1.1 核心需求解析从“塞得下”到“用得好”我接触过不少做 Agent 的开发者最容易犯的错就是把上下文管理理解成“扩容”——模型支持 128K 上下文窗口那我就把 128K 的文档全塞进去。结果呢Token 费用直线上升响应时间成倍增加而输出质量反而经常变差。这里有一个必须掰扯清楚的底层事实——上下文窗口不等于有效注意力范围。我在实测中拿到过一个很典型的数据用当前主流的几款长上下文模型把一份 100K Token 的合同文本丢进去然后要求模型回答一个只在文档中段出现过的细节问题。结果多次测试中模型经常出现“幻觉式回答”——不是它不知道答案而是早期的信息在注意力计算中已经稀疏化干脆“假装”自己知道。那怎么解决答案是从“被动扩容”转向“主动构造模式”。我总结了三件事要区分“必须精准的信息”和“可以模糊的信息”。比如系统指令是必须精准的工具调用说明是必须精准的而历史闲聊则可以模糊化甚至丢弃。要给模型“指路牌”。上下文不是一堆裸数据的堆砌而是应该结构化——让模型知道哪些内容对应哪个角色、哪个时间点、哪种用途。要主动压缩而不是被动容忍。用摘要、向量化、剔除等方式把上下文长期保持在一个“模型最舒适”的区间。我个人的经验基线是复杂对话场景上下文利用率在 40% 到 60% 时输出质量最高超过 80% 时质量曲线开始下跌。1.2 方案选型为什么不能照搬别人的“最优配置”很多人会在 GitHub 上找配置模板比如“Claude 最佳 System Prompt 模板”“Agent 上下文管理最佳实践”之类的。我曾经也干过这事后来发现这东西真的没法照搬。原因不复杂context-mode 的设计和你的业务状态强绑定。你是做单轮知识问答还是多轮对话客服你是靠工具调用吃饭的编程 Agent还是靠文档分析吃饭的 RAG 工具不同场景下上下文的组织方式有天壤之别。举一个实际的例子。同样是“工具调用返回结果”编程 Agent 场景下工具结果往往要直接注入对话流让模型看到“编译错误 → 修改建议 → 再次编译”的完整路径这样才能正确迭代。但知识问答场景下工具返回的检索结果反而是“一次性面条”——用完就不该再留在上下文里否则模型会被一段过期的检索结果拉到错误的方向上。所以我的建议是做好 context-mode 的前提是先盘点你自己的状态机。把对话轮次、工具调用链、文档命中路径全部画出来再看需要在哪个环节配置哪种上下文模式。不要上来就抄别人的 memory 方案大概率水土不服。2. 上下文构型的核心细节与实操要点聊完总体策略进入我认为最值得细看的部分具体怎么构型上下文。这里不空谈概念我把工程上常用的几种模式全部过一遍附带参数取舍和为什么这么选的逻辑。2.1 显式上下文分区System 与 Messages 的边界划分主流大模型 API 里都提供了 system、messages 这种接口分区这就是最基础但也最容易被用错的 context-mode。我在早期项目里犯过一个低级的错把所有的背景知识、任务说明、角色设定、对话历史全部塞进 system prompt。结果模型经常呈现出一种“精分”状态——对话越往后越偏向遵循 system 里后期的指令而忽略用户当前实际提出的问题。后来我细查才发现system prompt 越长它对后续对话的“控制力”就会衰减。这不是模型玄学而是注意力分配机制的自然结果当 system 里有太多互相竞争的信息源时模型在早期编码阶段无法为每一条指令分配足够的注意力权重。我现在采用的划分逻辑是“三个箱子”箱子内容典型示例长度控制稳定指令箱角色、任务、输出格式、硬性约束“你是资深法务顾问只回答与合同相关的问题回答控制在 300 字内”尽量小于 800 Token越短越好动态事实箱当前任务相关的背景资料、文档片段、用户画像“用户所在行业为制造业文档第 3 章提到的技术规格如下……”可长但要优先放最新或最相关的内容瞬时交互箱单轮对话产生的中间结果、工具返回、纠错信息“当前文件解析失败错误类型为编码格式不支持”只保留最近 1 到 2 轮这三个箱子对应到 API 结构上稳定指令箱放 system动态事实箱放 messages 的首条 user 内容瞬时交互箱则作为 messages 里最靠近当前轮次的部分。这么做的好处是模型在每一轮解码时能清晰区分“哪部分是要遵守的规则哪部分是待处理的数据哪部分是刚刚发生的事件”从而把注意力集中在用户当前的真实诉求上。2.2 工具调用上下文的“临时内存”玩法做 Agent 一定会遇到工具调用而工具调用恰恰是上下文管理里最容易失控的地方。一个典型的工具调用链条长这样用户说“帮我查一下上周的销售数据”→ Agent 决定调用 SQL 工具 → 工具返回一张大表 → Agent 根据表生成结论。这条链里“上一轮的工具返回结果”如果在下一轮对话中仍然留在上下文里就会干扰模型对新用户指令的响应——因为模型会下意识地把新问题套到旧工具的返回结果上。我的解决办法是给工具结果设置“有效期”。具体操作上我会在工具结果注入上下文时加一个标记类似[TOOL_RESULT][expired:True]或者干脆在下一轮组装上下文时把上一轮的工具结果从 messages 列表中摘除。对于需要长期保留的数据比如用户近期偏好、任务阶段性结论我会单独抽出来放进“持久事实区”而不是放任它们以工具结果的形式躺在对话历史里。这里给一个大致的行为准则工具结果里包含“结论”性质的文本比如“查询完成总销售额为 1200 万”保留在最近一轮的 System 中作为事实。工具结果里包含“过程”性质的文本比如 SQL 执行的逐行 log、中间计算步骤只保留最近一轮下一轮强制清除。工具结果里包含“原始大数据量”文本比如完整的 CSV 内容不在上下文中直接展示而是先抽摘要再注入摘要。用这套方法之后我那个编程 Agent 的长期多轮成功率明显提升。关键不是“多记”而是“该记的记不该记的扔”。2.3 历史对话的摘要化压缩策略多轮对话场景里历史消息的堆积是最快的。两句聊天可能就有 1000 Token一百轮下来十几万 Token 直接爆掉。主流的方案是“滑动窗口 摘要记忆”组合拳。我实盘验证过这是低成本且有效的方式设定一个窗口阈值比如最近 10 轮完整保留。对于窗口之外的历史消息在每一轮对话结束时用模型生成一段约 200 字的摘要。摘要要写清楚“用户意图 已确定的结论 待办事项”这是三个最关键的信息维度。下一轮组装上下文时用“摘要 最近 10 轮完整消息”的拼接方式。摘要的摘要也要定期生成——比如每 20 轮把前 20 轮的摘要再压缩成一句总纲。这套方案的巧妙之处在于它不是简单地丢历史而是把历史提炼成更高密度的事实。模型拿到的是浓缩后的“用户真实需求”而不是淹没在闲聊中的需求碎片。我实测过同一个客服问答场景摘要化之后模型的意图识别准确率从 84% 提升到了 93%同时 Token 消耗下降了 56%。有几个坑必须提一下摘要生成任务本身也要消耗 Token别在每轮都做否则成本会失控。我的基线是每 5 到 8 轮做一次或者当累计 Token 超过窗口 60% 时才触发。摘要不要让人工写死模板而是用模型生成否则会丢失用户的口语化表达。摘要中必须保留“未完成事项”否则模型会在后续对话中忘记自己承诺过什么。3. 实操过程与核心环节实现理论说再多不如直接上一段能跑的东西。下面的实操是围绕 langchain Claude 类 API 的实现但核心思路也适用于其他框架。3.1 上下文构型的代码抽象先看我最终落地的context-mode抽象核心思路是“三段式上下文组装”import json from typing import List, Dict class ContextManager: def __init__(self, max_tokens: int 12000): self.max_tokens max_tokens self.system # 稳定指令箱 self.facts: List[str] [] # 动态事实箱 self.history: List[Dict] [] # 近期完整对话 self.summary # 压缩摘要 self.tool_results: List[Dict] [] # 瞬时工具结果 def set_system(self, content: str): 只允许设置一次后续尽量不修改保证指令稳定性 self.system content def add_fact(self, fact: str, max_facts: int 5): self.facts.append(fact) if len(self.facts) max_facts: self.facts.pop(0) def add_tool_result(self, result: str, task_desc: str): 工具结果标注用途描述便于后续判断是否保留 self.tool_results.append({ task_desc: task_desc, result: result }) if len(self.tool_results) 3: # 默认只保留最近3条工具结果 self.tool_results.pop(0) def build_messages(self) - List[Dict]: 组装最终消息列表 # 1. 先放 system msgs [{role: system, content: self.system}] # 2. 放动态事实箱带标记让模型知道这部分是背景资料 if self.facts: fact_block \n\n.join( f[背景资料{i1}]\n{f} for i, f in enumerate(self.facts) ) msgs.append({role: user, content: f以下是本次任务的相关背景资料\n{fact_block}}) # 3. 放摘要信息带标记 if self.summary: msgs.append({role: user, content: f[历史对话摘要]\n{self.summary}}) # 4. 放近期对话历史 msgs.extend(self.history) # 5. 放当前工具结果最近1轮内 if self.tool_results: tool_block \n\n.join( f[工具结果-{t[task_desc]}]\n{t[result]} for t in self.tool_results[-1:] ) msgs.append({role: user, content: f刚刚收到工具调用结果\n{tool_block}}) return msgs def compact(self): 当历史过长时触发摘要化压缩 # 这里省略实际的摘要生成代码通常会调用一次模型 # 但核心逻辑是把 self.history 里的旧内容浓缩成 summary pass这段代码里有几个值得解释的设计点。第一个点是“为什么要用 user 角色放背景资料”。很多人习惯把资料也塞进 system但实测效果反而差。因为 models 的 system 区域通常会做特殊处理——系统指令被视为“权威规则”用户消息被视为“待处理内容”。当背景资料以用户身份出现时模型更倾向于把它当作“本次需要参考的原料”而不是需要遵循的指令。这个细微区别在长文本场景下影响很大。第二个点是“工具结果只保留最后一条”。我在前面已经解释过工具结果属于瞬时交互但最后一轮的工具结果又是模型后续推理的关键依据所以要特殊保留一条。第三个点是“分段全部落在 user”没有用 assistant 去主动陈述摘要。这是因为摘要本质上还是给模型看的上下文不需要模型对“摘要本身”做回应。如果放进 assistant 历史里模型会把它当成自己说过的话反而可能导致后续角色混乱。3.2 压缩策略的实现细节压缩是 context-mode 里最吃经验的部分。我直接给出我这里实际在用的压缩函数逻辑def smart_compress(context: ContextManager, llm) - str: 把超过窗口阈值的历史对话压缩成摘要 # 目标保留关键决策、未完成任务、用户偏好 compress_prompt f 请将以下对话历史压缩为一段 200 字以内的摘要必须包含 1. 用户的真实意图和需求 2. 已经确定的关键结论 3. 尚未完成的待办事项 4. 用户表达出的偏好或禁止项 对话历史 {context.history} 摘要 summary llm.invoke(compress_prompt) return summary.strip()这里有个细节压缩 prompt 必须明确“必须包含”那四项缺一个模型就很容易偷懒只写结论。实测不加这四项的时候摘要里丢失“用户禁止项”的概率非常高而“禁止项”恰恰是最影响后续对话质量的隐性信息。压缩的频率控制我建议做成“基于 Token 估算”而不是“基于轮数”。因为一张大表格可能顶得上 50 轮文字对话。我的实现方式是在每次 add_history 后做一个 Token 估算对中文来说大概一个字约等于 1 到 2 Token英文一个词约等于 1.3 Token当估算值超过窗口的 60% 时触发压缩。否则不压缩。3.3 参数选择参考不同场景下的推荐配置不同场景下的参数配置差异很大我直接整理成一张参考表给出我经过多次实验后的基线值供你起步时直接用。参数/配置编程 Agent客服问答长文档分析完整保留的最近对话轮数6 轮10 轮3 轮摘要生成触发阈值占窗口比例50%60%70%动态事实箱最大条目数3 条5 条10 条工具结果保留条数5 条保留编译报错链1 条2 条是否启用摘要的摘要二级压缩是是每 20 轮否System Prompt 最大 Token 量600800400这张表的底层逻辑是编程 Agent 需要看到工具调用链的连续性所以要 5 条工具结果但对话轮次不宜太多所以只保留 6 轮否则模型容易被旧代码片段干扰客服问答需要维持高体验保留 10 轮完整对话让用户感到“被记住”但对工具结果的需求很低通常也就查询一个订单状态长文档分析则完全不同——文档本身是主要上下文对话历史反而是干扰所以保留轮数最少但事实箱可以多放切片结果。这张表不是金科玉律但作为初始配置可以帮你省掉大量试错时间。4. 常见问题与排查技巧实录写到这里我觉得有必要把实际调试中遇到的典型问题拎出来晒一晒。这些问题在官方文档里通常找不到答案但每一个都能让你手忙脚乱一阵。4.1 模型“突然变笨”了上下文污染排查我调试 Agent 时最常遇到的一个情况是前几轮表现完美到了第 8 轮模型突然开始答非所问甚至怀疑自己前面给出的结论。第一反应肯定是“模型崩了”但多数情况下真正的问题是上下文污染。什么叫污染就是上下文里混入了“来历不明”的内容——可能是某一次工具调用返回了错误格式可能是某一段检索文本包含了互相矛盾的数据也可能是历史对话里用户自己说了一句“我记得好像是”然后模型把这个模糊信息当成了事实。排查方法有一个很直观的口诀把 messages 序列可视化打印出来然后“扮演模型”从头读一遍。如果你读到中间会感到“信息源头不明”或者“此处有噪声”那模型大概率也会有同样的感觉。我自己的排查模板先打印 system再逐条打印 user/assistant每条消息前面标注 Token 预估。重点检查三个位置第一条 user 消息是否包含了过期的任务描述最近一条 tool 返回结果是否和当前问题相关摘要里是否存在被错误断言的历史结论。大多数污染问题在这三步检查后都能定位到。4.2 长对话早期的关键信息丢失另一个高频问题是“早期信息遗忘”——用户在第 2 轮提了一句“我不喜欢红色系的设计”到了第 20 轮模型输出了一套纯红色配色方案。滑动窗口确实会丢早期信息但我发现很多场景下不是窗口不够大而是“关键信息没有被放进正确的位置”。用户在早期对话中随口说的偏好如果不主动抽取到动态事实箱它就会随着窗口滑动离开上下文。我的解决方案是给“偏好抽取”单独建一个流程每 5 轮对话结束时做一次轻量级的“用户画像提取”把“禁止项、偏好项、关键身份信息”更新进 facts 列表。这不是所有项目都需要但凡是客服、助手类产品我强烈建议做——因为用户通常不会重复表述自己的偏好丢失一次就彻底没了。具体提示词模板可以参考请从最近的对话中抽取用户的固定偏好、禁止项和身份信息输出为标准 JSON 格式只提取明确表达的不要推断。如果没有任何新增内容输出空对象。这个抽取流程的 Token 成本很低一般一次几百 Token但换来的是长对话稳定性的显著提升。4.3 System Prompt 被“稀释”的动态守卫还有一类问题是我在给一个数据分析 Agent 做调试时发现的System 里明明写了“回答必须包含数据来源说明”但对话进行到中途模型开始不遵守了结果越来越“放飞”。这不是模型忘了规则而是 System 指令在长上下文的注意力分配中失势了。你可以理解为一个房间里突然涌进来 100 个陌生人对话内容你最初和一个朋友定的暗号System 指令自然会被淹没。解决思路是“动态守卫”把 System 中的关键规则抽出来在每轮或每几轮组装上下文时以“本轮提醒”的方式作为独立的 user 消息重新强调一次。比如在工具结果注入之前加一条提醒根据系统指令你现在输出时必须包含数据来源说明。不要忽略这一步。这种“临门一脚”的提醒在实测中能把规则遵守率从 70% 拉回 95% 以上。代价是每轮会多消耗几十 Token但比起返工重跑一次这个成本不值一提。4.4 Token 恐慌预估不准导致的截断事故最后聊一个“低级但致命”的问题上下文总长度预估不准最后一截直接被截断。很多框架的 tokenizer 是按字节或者按词元来计算的但中文文本尤其容易出现预估偏差——我遇到过“我以为 5000 Token实际输出 8000 Token”的情况。一旦超限API 不会报警而是直接静默截断超出部分模型回答瞬间残缺。我现在养成的习惯是组装完 messages 之后不急着发请求先跑一次 token 计数可以用tiktoken或直接调 API 的 token 统计接口。如果超限优先压缩 facts 和 summary而不是粗暴地删对话历史。如果压缩后仍然超限再考虑减少完整保留的对话轮数。另外还有一个细节估算 token 时要额外加“功能 Token”的余量——比如 JSON 格式的标记、特殊分隔符、换行符、工具调用的函数名。这些隐形 Token 加起来能占到总量的 5% 到 10%不预留余量必翻车。5. 写在最后的个人经验说句实在话context-mode发展到今天已经不是“要不要用”的问题而是“怎么用得顺手”的问题。模型本身的能力越强上下文管理的好坏就越会放大或者缩小它的真实表现。我用过最朴素的“一股脑全塞”方案也试过“全链路外部记忆 RAG”的复杂方案最后沉淀下来的反而是这套“显式分区 摘要压缩 动态守卫”的组合。它不追求把上下文管理做成一个花哨的系统而是真正服务于对话质量、成本和稳定性这三个核心指标。最后再分享一个小技巧给上下文结构加“可视化痕迹”。我在开发环境里会把最终的 messages 序列渲染成带颜色的分区视图固定指令区、背景资料区、历史摘要区、当前交互区一眼就能看出模型当前视野里的“信息布局”。很多上下文相关的问题不需要推理看这个布局图就能定位。这个习惯救了我很多次真心建议你也试试。