context-mode:大模型对话中的上下文编排与工程实践
1. 为什么需要 context-mode从一次线上事故说起先讲一个我实际经历过的场景。之前给一家企业做智能客服系统业务方提了个需求用户咨询时如果能知道“他刚才在浏览哪个页面”“当前是售前还是售后阶段”“是否已经确认过订单信息”回答会准很多。听起来不难对吧结果做的时候才发现真正的难点不在于“多轮对话”而在于“上下文是怎么混进来的”。系统对接了三个数据源用户的实时浏览行为、CRM里的历史工单、以及大模型的多轮会话记录。一开始我们图省事把所有信息一股脑拼进提示词里结果问题立刻暴露用户明明在问退货流程系统却因为历史工单里有“退款金额争议”的记录把回答带偏成了“请您联系财务核对”。更离谱的是有一次会话里同时出现了“新品推荐”和“物流异常”两个话题模型直接宕机式输出了一篇两不像的回复。这就是典型的上下文污染。我当时的第一反应是得给这个系统加一个“context-mode”也就是上下文模式让研发人员能显式定义当前会话到底该用哪些上下文、按什么优先级组合、到什么时候应该丢弃。后来我花了差不多三周时间把整套方案落地效果立竿见影回答准确率从68%提到了91%上下文Token消耗降了40%左右。这篇文章就把整个过程拆开来讲。适合谁看如果你正在做AI对话类应用、智能客服、知识库问答或者你在用LangChain、Semantic Kernel这类框架、但发现“上下文”越来越难管这篇应该能给你一些可直接拿来用的思路。不涉及晦涩的算法推导偏工程实践但会把原理讲透。2. context-mode 的核心设计思路2.1 先搞清楚“上下文”到底包含几层很多刚接触的人会把“上下文”简单理解成“历史聊天记录”这是最大的误区。我在实际设计context-mode时把上下文拆成了四个独立维度会话上下文Session当前用户与系统之间多轮对话的内容包括问题、回答、追问、修正等是最基础的一层。场景上下文Scene用户当前所处的位置或阶段比如“在商品详情页”“正在提交订单”“已进入售后流程”这决定了对话的目标与策略。业务上下文Business从外部系统CRM、ERP、订单系统等拉取的结构化数据比如用户积分、订单状态、历史工单。知识上下文Knowledge从知识库/文档中检索到的、与当前问题相关的片段通常经过向量召回或关键词匹配。context-mode 最核心的思想就是把这四层拆开、并且允许每一层单独配置开关、权重和生命周期。而不是像之前那样一股脑全塞进去。2.2 三种基础模式对应不同的交互场景在设计时我没有一开始就做很复杂的规则引擎而是先定义了三种基础模式覆盖了绝大多数业务场景严格模式Strict Mode只使用会话上下文忽略场景和业务上下文。适合那种“就事论事”的问答场景比如FAQ解答、政策咨询。严格模式的好处是输出稳定、Token消耗低、几乎不会跑偏。智能模式Smart Mode默认模式同时启用会话场景业务上下文但设置了优先级场景上下文优先于业务上下文。适合智能客服、售前导购这类复杂场景既能理解用户意图又能结合用户画像给出个性化回答。知识增强模式Knowledge Mode在智能模式的基础上额外启用知识上下文也就是把向量检索的结果拼入提示词。适合知识库问答、内部文档检索。这里要注意知识上下文一旦引入检索质量和上下文拼接顺序会显著影响最终效果。这三者不是三选一而是支持按轮次动态切换的。比如用户一开始问“这个手机多少钱”可以用严格模式当识别到用户意图是“比较两款手机”时自动切到智能模式并拉取业务上下文如果用户问“保修政策是什么”则临时切到知识增强模式。2.3 为什么要有独立的上下文模式层有人说那我直接在代码里写if判断根据用户输入切换不同的提示词不就完了理论上可以但工程上完全不可行。原因有三第一提示词会爆炸。每个业务场景都要精心构造不同的提示词模板功能多了以后维护成本指数级上升改一个词可能影响几十个场景。第二上下文来源不一致。会话上下文存在Redis里业务上下文存在数据库里知识上下文需要实时检索向量库。如果没有一个统一的管理层每次拼接上下文都得写一遍获取逻辑重复代码极多还容易漏。第三排查极其困难。上线后如果模型回答出了问题你怎么定位到底是提示词写错了还是历史消息取多了还是检索结果相关度太低没有上下文模式这一层抽象你面对的就是一坨无法观测的“输入拼接”。所以context-mode本质上不是“一个功能”而是一种上下文编排层Context Orchestration Layer。它把“获取上下文”、“裁剪上下文”、“拼接上下文”、“生命周期管理”这四件事统一收口向上对业务方暴露简洁的配置接口向下屏蔽各数据源的差异。这样做之后任何一次Prompt构造都可以被记录、被复现、被审计这比“能跑通”重要得多。3. context-mode 的工程实现细节3.1 配置驱动的上下文策略我最终采用的方式是“配置驱动”没有把逻辑写死在代码里。每一类对话流程对应一份YAML或JSON配置研发人员只需要修改配置就能调整上下文的使用策略。mode: smart context_sources: session: enabled: true max_turns: 6 max_tokens: 1200 scene: enabled: true resolver: from_url_and_session priority: high business: enabled: true resolver: from_crm_api required_fields: - user_id - order_status - membership_level cache_ttl: 300 knowledge: enabled: false fallback: on_business_failed: degrade_to_session_only on_scene_unknown: use_default_scene这份配置的核心在于每个上下文源都有独立的开关、获取方式和容量上限。max_turns控制会话历史取多少轮max_tokens控制这块上下文最多占多少Token超了就要做截断或摘要。fallback则定义了异常情况下的降级策略避免因为某个数据源挂了导致整个对话不可用。这里我踩过一个坑一开始max_turns和max_tokens只限了会话源没限业务源。结果某个大客户的CRM接口返回了极长的订单历史一次性把7k Token吃满了模型输出质量严重下降。后来统一对所有上下文源都加了上限问题才解决。3.2 上下文裁剪策略截断、摘要与多级压缩上下文窗口有限而真实业务里用户的历史消息、引用文档、业务数据往往是海量的。context-mode 必须内置裁剪策略我把它分为三级第一级数量截断最朴素的做法只保留最近N轮对话。N的取值需要考虑两个因素模型的最大上下文窗口以及回答所需的最少信息量。以主流模型的32k窗口为例如果知识库检索结果占8k业务上下文占4k那么留给会话历史的就只有20k左右按每轮约1k Token算一般保留10-15轮。第二级重要性保留简单截断的缺点是用户可能在前面几轮提到关键信息直接丢掉会导致语义断裂。所以我会按“消息类型”做加权保留系统消息、带有结构化信息的消息如订单号、地址、用户明确表达偏好或情绪的消息优先级高于闲聊类消息。哪怕超过轮数限制这些高优先级消息也会被保留。第三级摘要压缩当历史信息实在太长、且重要信息分散在各处时就用LLM做一次“增量摘要”把前文压缩成几百字的短摘要再接上最近几轮完整对话。我记得有一次用户连续问了二十多分钟历史记录累积超过15k Token摘要压缩后只剩1.2k既保留了关键信息又大幅降低了Token成本。def build_context(session_history, scene, business_data, knowledge_hits): session_part trim_history( session_history, max_turnscurrent_mode.session_turns, max_tokenscurrent_mode.session_tokens, high_priority_keys[order_id, address, preference], ) # 场景源不超长就直接拼超长则只保留scene_id scene_part scene.to_prompt_fragment() # 业务源优先展示结构化字段长文本字段做摘要 business_part fuser_id{business_data.user_id}\n business_part forder_status{business_data.order_status}\n if len(business_data.remark) 200: business_data.remark summarize(business_data.remark, max_tokens100) business_part fremark{business_data.remark}\n # 知识源保留topK个结果且按相关度倒序 knowledge_part \n\n.join( [h[content] for h in sorted(knowledge_hits, keylambda x: x[score], reverseTrue)[:topK]] ) return assemble_prompt(modecurrent_mode, parts{ instruction: current_mode.system_prompt, scene: scene_part, business: business_part, knowledge: knowledge_part, session: session_part, })3.3 模式的动态切换与场景识别严格、智能、知识增强三种模式的静态定义只是基础真正让context-mode发挥作用的是“对话过程中能够自动切换”。我在实现里加了一个轻量级的意图识别模块每一次用户消息进来会先做一个快速分类判断当前对话属于哪种状态。场景识别我用了两条腿走路一是规则兜底。比如URL匹配如果用户是从/order/detail/12345页面发起的会话那场景上下文直接定为“订单详情咨询”如果用户点击了“申请售后”按钮场景是“售后流程”。这些规则简单、可靠、零延迟适合高频且路径清晰的场景。二是模型兜底。当规则无法判断场景时用小模型对用户首条消息做分类。你不需要在整个对话过程中反复调用大模型只在会话开始或用户换话题时触发一次就行。这里可以用一个几千样本的分类模型也可以直接调大模型API但一定要控制调用频率否则延迟和成本都受不了。动态切换还有一个容易忽略的问题切换时要同步清理旧上下文。比如用户上一轮还在走“退货流程”的业务上下文这一轮突然问“那你们有没有新品推荐”如果不把旧的业务上下文清掉模型大概率会继续往退货方向带节奏。我在代码里对模式切换定义了一个context_refresh事件一旦场景变化相关上下文源自动失效下一次组装时重新拉取。3.4 Token预算如何算在做context-mode时我养成了“先算预算再写代码”的习惯。这里给一个可以照抄的计算模板。假设模型窗口是32k安全系数建议留20%余量也就是实际可用约25k。需要用到的上下文源包括系统提示词约2k固定场景上下文约1.5k固定业务上下文约3k平均知识检索结果约6k最多用户当前消息约1k可变那么会话历史最多能占用的Token就是25 - 2 - 1.5 - 3 - 6 - 1 11.5k。如果你的对话平均每轮1k Token那最多只能放11轮左右。这个数字就是后续配置里max_turns和session_tokens的取值依据。这套计算方法帮我避免过很多次“上线后才发现上下文被截断得厉害”的尴尬。先定预算再定参数不要拍脑袋。4. context-mode 上线后的高频问题与排查技巧4.1 对话“跑偏”了如何快速定位是哪个上下文源的问题AI对话系统最让人头疼的就是“明明刚才还好好的怎么突然答非所问”。context-mode 带来的一个巨大好处是由于上下文被显式拆分成多个源排查时可以逐个排除。我的排查顺序是固定的先看场景上下文是否正确。打开日志看当前场景识别成了什么。如果用户问售后场景却识别成了售前那答案基本必歪。再看业务上下文是否有脏数据。比如订单状态字段过时了、用户身份拿错了模型基于错误的数据给出“合理但错误”的回答。然后看知识上下文的检索结果。最常见的问题是检索出来的topK文档和用户问题只存在字面匹配、没有语义相关性。最后才怀疑会话历史。看是否因为历史消息过长导致早期关键信息被截断或者模式切换时旧上下文没有清干净。我一般会在日志里给每个上下文源加上唯一的标记ID例如ctx:session:12、ctx:business:998这样在排查时可以直接看到最终拼进Prompt的每一块内容是什么、来自哪里。上下文要可观测否则出了问题只能靠猜。4.2 多轮对话中Token消耗激增怎么压Token消耗过高通常有两个原因一是历史消息越积越多二是一次检索返回的文档太多。如果你用的是截断策略但发现仍超出预期要重点检查是不是“重要性保留”环节出了问题。我遇到过一种情况用户每轮都发“好的”“嗯”这些消息虽然没有实际信息量但仍然占Token。后来我对消息做了“有效内容判断”纯语气词或极短消息直接跳过不进上下文Token消耗立刻降了一截。知识库方向一个非常管用的优化是为知识片段设置更细的切分粒度。不要整篇文档一梭子喂进去按标题、段落、甚至语义块切分检索时按块召回每块控制在300-600字左右。这样做之后同样的问题检索结果更精准Token占用也更低。另外还有一个容易被忽视的点缓存的业务上下文不要放太长时间。有些接口的数据是频繁变动的比如物流轨迹如果把旧数据缓存5分钟用户就会觉得“回答不实时”。但反过来如果每个上下文源都实时请求延迟又会飙升。我最后的策略是“高频变动的字段实时查低频字段走缓存”双管齐下效果最好。4.3 模式切换生效了但模型还是“记着”上一轮的内容这个坑我调试了好几天。代码逻辑上模式切换后旧上下文源已经被清除了但模型回答里仍然带着上一轮的错误信息。后来发现原因不在拼接层而在模型的服务端会话缓存。很多模型API支持传入conversation_id来维持多轮记忆如果你在切换context-mode时没有换一个新的会话ID服务端仍会保留旧的消息记录相当于你拼接的只是“可见”上下文而模型实际“看到”的还有一截历史。解决方式有两种一是切换模式时生成新的conversation_id二是如果同一会话需要保留部分上下文则要在服务端手动清洗历史只保留你想让模型看到的那部分。从工程稳妥性来看我更推荐直接换新会话ID然后把你认为有价值的历史消息以普通文本形式写进系统提示词里。这样就完全掌控了模型“看到”什么不会被服务端缓存干扰。4.4 一张问题速查表直接保存到团队Wiki里症状可能原因排查步骤回答偏离主题场景识别错误或未切换检查scene resolver输出确认当前场景值回答过于啰嗦历史轮数太长低价值消息多降低max_turns增加有效内容判断上下文太长报错Token预算没算好用公式重算各源上限加截断模式切换后仍沿用旧数据服务端会话缓存未清理切换模式时替换conversation_id知识库回答空洞检索相关度低或切块太大检查topK与片段长度优化切分策略业务数据滞后缓存TTL过长缩短高频字段缓存时间或实时查询这张表基本覆盖了我上线后的绝大多数工单。后来我把这套排查流程沉淀成了团队的SOP新同学碰到类似问题先照着表过一遍基本能解决80%的Case不用再来敲我门。5. 关于 context-mode 的几条实战忠告做得越多越觉得context-mode不是“写个配置”那么简单它本质上是给AI系统建立一套“信息过滤规则”。有几条心得我觉得比代码本身更值钱。第一宁可少给上下文也不要多给。我之前总觉得上下文越丰富回答越聪明结果经常因为一个无关字段把答案带沟里去。信息越多模型越容易“想太多”。context-mode的真正价值在于“克制的融合”而不是“无限的堆叠”。第二上线前一定要做回归测试集。不要只测手工挑的几个好例子。我从实际对话日志里抽了200条按业务场景分类做成自动化回归集每次改策略就跑一遍准确率下降自动报警。有了这个底子在后续调参才有安全网不然改一次坏一次改到后期根本不敢动。第三模式配置要收敛不要追求无限灵活。我见过有的团队把context-mode做成了一套可视化编排工具支持任意拖拽组合结果没人能维护。灵活性和复杂度是伴生的你要什么就要承担什么。对我目前遇到的大部分业务来说三种基础模式加一套fallback规则已经足够了再多就是过度设计。最后再分享一个我后来一直在用的技巧每次要调整context-mode之前我会先把当前用户的完整上下文导出成纯文本人肉读一遍看哪些信息是真正有用的、哪些是干扰项。这个方法听起来原始但极其有效。因为只有你真的站在“模型视角”去看眼前这堆输入时才会理解为什么有些莫名其妙的回答会冒出来。这个习惯帮我省掉了不计其数的线上Debug时间。