
1. 为什么需要显式的“上下文模式”一次对话事故引发的重构年初在做一个内部知识库问答机器人时我被一个看似简单的问题折磨了整整两周——系统明明把用户的所有历史对话都拼进 prompt 传给了模型结果模型还是频频答非所问。最典型的一次事故是用户周一咨询过“公司年假制度”周三再问“那婚假呢”模型居然把周一的年假政策当成前提生成了“根据您刚提到的年假条款婚假同样适用”这种荒谬回答。排查到最后问题根源不在模型能力而在上下文本身。所有历史记录都一股脑塞进去真正相关的核心信息反而被噪音淹没。也就是从那时候起我开始正式研究所谓的“context-mode”。context-mode上下文模式本质上不是某个开源库或标准协议而是一类显式声明“本次回复该参考哪些上下文、参考多少、以什么优先级参考”的配置与运行机制。它解决的是 LLM 应用里最容易被忽视却又最致命的问题——模型该“记住”什么、该“忘记”什么、以及该“优先看”什么。这篇文章我打算用自己做知识库问答机器人的完整经历作为主线把 context-mode 从概念拆到实现再拆到踩坑与产品化。适合正在做 AI 应用开发的朋友也适合那些已经跑通 demo、却在真实业务里被上下文整得焦头烂额的人。我不会只给结论每个设计点都会还原我当时为什么这么选。2. 拆解上下文模式的三张“旋钮”窗口、来源与策略要理解 context-mode先忘掉那些花哨概念。我的经验是任何上下文模式都逃不开三个核心维度窗口范围、内容来源、取舍策略。三个维度各取一个值就组成了一种具体的上下文模式。2.1 窗口维度短窗、长窗与滑动窗窗口决定模型能回溯多远。我在项目中定义了三种基本窗口短窗模式short-window只保留当前对话最近 3-5 轮。适合意图明确、依赖少的高频操作型问答比如查个快递、改个密码。优点是不容易被历史带偏缺点是跨轮推理能力几乎为零。长窗模式long-window把整个 session 的所有对话都纳入。适合需要持续记忆的咨询场景比如 HR 政策解答、法律条款咨询。缺点是 token 消耗高且历史越长噪音越大。滑动窗模式sliding-window窗口按固定大小滑动比如始终保留最近 12 轮但如果某轮被标记为“重要信息”就单独移到持久记忆区。这个模式是我后面实际业务里的主力它有一句核心原则——“让模型忘记细节但别让它忘记承诺”。实测数据我后面专门用一节讲。这里先强调一个容易被忽略的事实上下文窗口长度和有效上下文长度是两回事。模型号称支持 128k token但真把 128k 全灌进去中间部分几乎都会变成“无效记忆”。滑动窗的价值不是省 token而是重构信息密度。2.2 来源维度对话、检索命中与结构化记忆来源决定了上下文里的内容从哪儿来。我在项目里做了一套来源分层的机制分三层对话层即用户和机器人的历史往来。注意用户原话和机器人回复要分开存后面做轮换策略时才知道哪些能丢、哪些必须留。检索层从向量库或全文索引里捞回来的文档片段。这是 RAG 系统的基石但它天生携带一个隐患——检索结果和对话记忆可能冲突。记忆层结构化的用户画像或业务事实比如“用户所在部门是销售部”“当前审批流程已完成第一阶段”。这一层通常以 JSON 或 KV 形式存储token 占用小、信息密度极高。三层来源在同一个 context-mode 下的占比直接决定了模型的“性格”。检索层占比高时模型更像一个查资料的助手记忆层占比高时模型更像一个懂你的管家。我当时犯过的错是把三层全部混在一起不分轻重地拼接结果模型一会像管家一会像搜索框行为极不稳定。2.3 策略维度截断、压缩、遗忘与加权策略是 context-mode 里最灵活、也最考验功底的部分。我按由简到繁的顺序做一个对比表策略核心做法适用场景典型缺陷截断超窗即丢弃从最旧开始砍短对话、简单任务硬切可能砍掉关键承诺压缩用摘要模型把旧对话浓缩成几条长会话、知识咨询摘要本身会丢细节且多一次模型调用遗忘按重要度打标低权重历史直接不载入多轮复杂任务重要度打分不准时误删关键信息加权同一条信息在不同位置重复强调需要模型锚定关键事实重复过多会造成 token 浪费甚至诱发复读截断是最省事的但千万不要简单按“最早发生的时间”来砍。我自己吃过亏用户在第 3 轮说过“我在北京”在第 50 轮问“那我办居住证需要什么材料”结果第 3 轮被当成陈年旧事砍了模型直接懵掉。后面我才设计了一套“重要信息锚定”机制某些实体或意图一旦出现就自动复制到记忆层不占滑动窗的坑位。这就是加权和遗忘的配合使用。3. 落地实现一套可切换的 context-mode 调度器设计聊完概念说点实在的。我在项目里没有把 context-mode 做成一堆硬编码的 if-else而是设计了一个轻量调度器让每种模式像插件一样注册进去业务层按场景动态切换。这套结构很通用用伪代码讲解大家也能直接迁移到自己的项目里。3.1 模式描述文件把“模式”变成数据第一步先定义模式描述文件。我用 JSON 来描述一种 context-mode它把前面讲的三个维度全部规范化。下面是我项目中一份真实模式的简化版{ mode_name: hr_policy_consult, window: { type: sliding, size: 12, anchor_strategy: entity_important }, sources: { dialogue: true, retrieval: true, memory: true }, strategy: { truncate_policy: important_anchor, summarize_threshold: 40, retrieval_weight: 0.6, memory_weight: 0.8 } }这样一个文件的价值在于模式不再是藏在代码逻辑里的隐式行为而是可以配置、可审计、可 A/B 测试的显式数据。产品同事想调参时不用再提工单找开发改代码直接改 JSON 就能上线实验。我当时踩过一个小坑早期模式描述文件里只有 window 和 sources没有 strategy 字段导致不同策略逻辑散落在组装代码里。后来某个夜晚排查一个线上问题发现“为什么同样窗口大小表现差这么多”翻开代码才意识到策略那层完全被写死了。所以提醒大家context-mode 的三张旋钮从第一天起就要全部建模进配置里别偷懒。3.2 上下文组装流水线收集、筛选、排序、格式化调度器核心是一套流水线。我把它分成四步每一步都对应一个可插拔的函数收集Collect从对话存储、向量检索、记忆后端分别拉取原始材料。筛选Filter按模式的 sources 开关过滤掉不需要的来源再按窗口规则裁剪尺寸。排序Rank对保留下来的上下文片段打分排序。分数由三部分加权相加与当前 query 的语义相似度通常用 embedding 余弦、信息的时间衰减因子越近越高、以及来源权重memory 来源天然比普通闲聊权重高。格式化Format把不同来源的片段组装成模型友好的 prompt 结构必要时给每个片段加 role 标记、日期标记、来源标记。这个流水线最核心的设计是把“排序”从“筛选”里独立出来。很多人的第一版实现都是筛选完直接拼接导致模型总在优先看一份已经过时的旧文档。我独立出 Rank 步骤后调参才变得容易——想让模型更重视检索结果就调高 retrieval_weight想让模型更听记忆层的“人设信息”就调高 memory_weight。3.3 一个最小可跑的调度核心示例我写一个 Python 风格的最小示例展示组装核心骨架。这一版为了易读做了极大简化但层次是完整的class ContextModeScheduler: def __init__(self, mode_config, collector, ranker, formatter): self.mode_config mode_config self.collector collector self.ranker ranker self.formatter formatter def assemble(self, query, session_id): window_conf self.mode_config[window] source_conf self.mode_config[sources] raw_items [] if source_conf[dialogue]: raw_items.extend(self.collector.fetch_dialogue(session_id)) if source_conf[retrieval]: raw_items.extend(self.collector.fetch_retrieval(query, top_k5)) if source_conf[memory]: raw_items.extend(self.collector.fetch_memory(session_id)) kept self.apply_window(raw_items, window_conf) ranked self.ranker.rerank(kept, query) prompt_chunks self.formatter.format(ranked) return prompt_chunks注意 apply_window 那一步不只是数条数还要处理“重要锚定”。真正的锚定逻辑通常不在流水线里硬编码而是由上游预处理阶段给每条记忆打一个 importance 标签流水线只负责按标签决定是“保留”还是“丢进摘要池”。提示窗口大小单位别只用“轮数”来衡量。真实业务中一轮用户发言可能只有 5 个 token也可能长达 2000 token。我在实践中改用“按 token 预算动态计算轮数”窗口 size 设的是 3000 token 而非 12 轮。模式文件里写 round12 只是给人看的真正执行前会换算成 token 预算。4. 实测对比两个业务场景下的模式表现与代价做这套调度器时我手头正好有两个典型场景可以实测。一个偏“短平快”的客服问答一个偏“长绵密”的文档分析。只有把真实测试数据拉出来对比才知道不同模式的适用边界在哪。4.1 实验场景与评测口径场景 A客服问答用户就“退款流程”追着问了 20 轮中间穿插闲聊和催促最后问“所以我现在该点哪个按钮”。评测标准操作引导准确率 平均响应延迟。场景 B长文档分析用户先上传一份 50 页的年度报告然后逐步追问 30 轮涉及不同章节的数据对比。评测标准关键数字引用准确率 结论一致性。两组对照组都跑同一个模型同一型号、同样温度参数唯一变量就是 context-mode 的配置组合。4.2 结果与代价分析整理成表一目了然模式组合场景A准确率场景B准确率平均输入token/轮备注全量历史基线61%72%18,400场景B表现尚可但成本最高短窗截断83%35%2,800场景B彻底崩跨章记忆全丢滑动窗重要锚定87%68%6,200两场景均衡但锚定命中不稳滑动窗检索加权79%84%8,900场景B最佳场景A被检索干扰记忆层增强长窗82%81%11,500两场景都还行但成本高数据很有启发性。最让我意外的是“短窗截断”在场景 B 的准确率竟然只有 35%——比随机猜也强不了多少。原因是第 22 轮追问时模型已经看不到第 3 轮定下的“对比口径是‘扣除退货后净额’”这一关键前提所有后续数字引用全部建立在错误框架上。反过来看场景 A 里“滑动窗检索加权”反而比基线更差。原因也很典型每次用户问操作按钮检索层都会捞回一堆相似度高的退款政策文档把之前对话中“用户已经上传了购物凭证”这个关键状态给冲淡了。这印证了一个观点——检索层是把双刃剑在过程性任务里对话状态往往比文档片段更关键。4.3 结论没有“最好”的模式只有“最匹配场景”的模式这份测试给我最大的启发是别指望一套 context-mode 通吃所有业务。模式之间不是版本迭代关系而是互相补充的平行方案。我把结论总结成三条经验**过程性任务填表、操作、引导**优先重对话状态检索权重压低source 里对对话层给最高权重**知识性任务查资料、对比数据**优先重检索质量但一定要给关键前提做锚定防止被长窗吞掉混合型任务用记忆层作为“粘合剂”把核心前提和中间结论抽成结构化记忆无论窗口怎么滑都不丢。这些听起来简单但没有那两张实验表我的感觉是模糊的。数据不会骗人建议每位做 context 优化的朋友都建立自己的“三表”准确率表、token 成本表、失败案例表。没数据做支撑的上下文调优都是玄学。5. 踩坑记录轮换、注入与引用锚点的教训再往下写是最值钱的部分——我在这个项目里实打实踩过的坑。有三个坑最为隐蔽每个都花了我至少两三天才定位。讲出来希望大家别重走一遍。5.1 轮换时机不是“超过窗口就砍”而是“砍之前先问一句”滑动窗最常踩的坑是轮换策略过于机械。早期我按“轮数超过 12 就丢弃第 1 轮”结果把一个极其重要的隐式承诺丢掉了。用户在第 2 轮说“我只需要价格信息别给我推荐别的”模型在第 15 轮居然开始推荐竞品用户当场摔手机。后面我改成了“三重确认”轮换机制第一重待丢弃的对话片段是否包含重要性标记为 high 的实体若有则不丢弃而是转移到记忆层。第二重待丢弃的片段是否曾作为“上一个问题的答案”被用户后续引用过若有保留其摘要。第三重如果片段既不含重要实体、也没被引用过才允许直接丢弃。这套机制本质上是把轮换从“定时清理”变成了“语义评判”。代价是每次轮换时多花几百 token 做判断但这笔钱非常值得。上线后因为历史被误砍导致的 bug 数量直接降了七成。5.2 重复注入检索片段和上一轮摘要“打架”第二个坑出现在我把摘要策略引入之后。我原本的做法是对话超过 40 轮就把前面的内容压成摘要放入上下文的开头。结果发现模型很容易“精神分裂”——开头的摘要说“用户最终决定选择方案 B”中段检索回来的文档里又有句“方案 B 已被否决”模型往往不知道听谁的于是输出里出现自相矛盾。这个问题我管它叫“上下文打架”。解决思路不是让摘要更准确而是给每块上下文建立明确的“时间戳”和“优先级声明”。在格式化阶段我在每个片段前面加一行元信息比如[来源: 对话摘要 | 时间: 2024-11-20 10:30 | 优先级: 高] 用户最终决定选择方案 B [来源: 产品文档检索 | 时间: 2023-06-01 | 优先级: 中] 方案 B 因预算原因已被否决这样模型至少能看出“新摘要比旧文档更接近当前事实”。但我也承认这是治标不治本——真正治本的做法是在检索阶段就加入时间衰减过滤把时效性冲突的文档直接降权。后面我把这两件事一起做了冲突率才真正降下来。5.3 引用失效让模型说清楚“这句话来自哪里”最后一个坑和引用锚点有关。用户问我“你刚说的这个数据是今年 Q3 的还是全年累计的”系统答不上来。原因是我在格式化阶段没有给检索片段保留文档 ID 和页码信息模型只看到了文字没看到出处。这让我意识到上下文模式的边界不只是“模型能看到多少”还包括“模型能否知道自己看到的是什么”。后来我在格式化每个检索片段时强制加上来源标识、文档名、页号、块 ID。具体改动很小就是把 source 字段从纯文本变成结构化对象{ text: 第三季度营收同比增长 12%, doc_id: annual_report_2024.pdf, page: 37, chunk_id: chunk_4f7a2 }这带来的好处不止是能回答引用出处更重要的是模型在引用时会更谨慎——它知道答案可以被溯源潜意识里会更倾向于只使用有据可依的信息幻觉率也明显下降。这是我在整个 context-mode 项目里投入产出比最高的一笔改动强烈建议第一时间做。6. 产品化思考把上下文模式做成用户看得懂的开关技术实现到一定阶段我开始思考产品层面的问题。context-mode 这种偏底层的能力要不要暴露给最终用户怎么暴露才不让人困惑这节聊我最终的做法和一些思考过程。6.1 为什么不能全自动很多人觉得上下文模式应该全自动用户无感知才对。早期我也是这个思路把所有模式选择都交给后端一个不可思议的“意图分类器”。结果发现自动切换经常翻车用户问“这个方案怎么填”系统判断是操作任务切到短窗模式结果丢掉了用户 10 分钟前说的“预算上限是 5 万”这个关键条件。后来我想明白了自动化和确定性之间要有平衡。与其让模型猜不如把模式的选择权交还用户但用非常轻量的方式——不是让用户配置窗口大小或权重参数而是用业务语言表达场景。6.2 每个模式对应一个“人话标签”我在产品界面里设计了三种模式对应三种标签“就事论事”对应短窗高对话权重适合当前问题只依赖最近几轮不让历史干扰。“工单模式”对应滑动窗重要锚定记忆层适合跨很多轮的复杂任务前提条件不能丢。“研究模式”对应检索加权长窗结构化记忆适合连续追问、查资料、对比数据。实际效果比我想象的好。用户不一定理解 context-mode 的技术含义但“就事论事”和“研究模式”这种标签让他们秒懂切换后会发生什么。一周后后台数据显示约 38% 用户会主动切换模式而不是默认走过场。这里也埋一个我亲测有效的细节模式切换的 UI 不要藏太深放到输入框上方的三个 pill 按钮就行。每次切换时在对话框顶部显示一小行提示比如“已切换到工单模式将保持您之前提供的关键信息”这样用户对行为变化有预期不会觉得模型“突然变笨了”。6.3 最后的实操技巧如果让我给后来者一条最实用的建议那就是别一上来就追求动态模式切换先把你业务里最核心的 3-4 种固定模式做扎实。固定的、可被用户理解的模式比复杂的动态 routing 可靠得多。等固定模式的数据跑稳了再考虑加一层轻量的自动推荐比如根据 session 长度或意图置信度给出默认模式但永远保留手动覆盖的入口。上下文模式的实现过程本质上是一次对“模型到底该关注什么”的持续逼近。它没有终点但有很好的中间态。只要把窗口、来源、策略这三张旋钮建模清楚把测试矩阵和数据跑起来再辅助一套走心的产品包装你的 LLM 应用在“记忆力”这件事上就已经胜过绝大多数同类产品了。后面再遇到上下文相关的玄学问题至少你手里有了可以对话的框架和抓手。