
凌晨两点线上客服机器人开始胡说八道。它明明拥有十万 token 的上下文窗口对话记录也都完整塞进去了可偏偏在用户问“刚才我说要退款的事你帮我办了没”时回答成了“您没有申请过退款”。我翻日志发现关键的那句退款诉求早就被后面几轮闲聊挤出了有效注意力范围。这不是模型笨是上下文没做好。这类问题在大模型应用里太常见了。模型本身的智商摆在那里但喂给它的“上下文”长什么样直接决定了它能不能答对。上下文工程本质上是给大模型设计一套信息供给系统包括窗口管理、消息编排、记忆压缩、检索增强这四件事。很多人把它当成“提示词写得好不好”的附属品其实它更像系统的架构设计属于比 prompt 工程更靠前、更能决定成败的一层。这篇笔记是系列第 21.8 篇专门把这几块的核心策略拆开讲透适合正在做 LLM 应用的开发者、正在调 RAG 流程的算法工程师以及被越调越乱的上下文折腾过的所有同行。1. 上下文窗口不是越大越好预算制是被低估的一套反馈闭环1.1 从一次生产事故说起10万token上下文为何还是答非所问我用过一次真实生产事故当开场因为它足够说明上下文工程的残酷。当时我们接的是国内某家大厂的模型上下文窗口 128K看起来很大于是团队直接把整段对话历史、全套业务说明、几份政策文档全塞进去觉得“反正放得下”。结果线上效果差得离谱用户问一个简单的订单状态模型反而开始引用文档里的无关条款用户明确表达过退款意愿后续模型却像失忆一样。后来我做了个实验把上下文里的内容分成“最新用户消息”“最近 8 轮对话”“业务规则”“历史长尾对话”四块分别测它们的注意力占比。虽然不是研究员但从 token 位置和输出质量的对应关系来看处于窗口中间偏后的长尾历史几乎对生成结果没有贡献反而会把关键信息挤得七零八落。问题出在哪窗口大不等于有效信息多。模型对上下文不同位置的注意力天然不平均中间部分容易被稀释再加上长上下文的指令遵循能力会下降塞得越满噪声越大。一个实用的心态是把上下文窗口当成预算不是仓库。百分之十留给系统指令百分之十五留给必要的业务规则百分之五十留给当前会话主体其余留给工具结果和临时信息。1.2 把窗口当“团队工位”分配比扩容更关键打一个更容易理解的比方上下文窗口像一个团队的工位区域模型就是那个坐在工位上的人。工位面积大不代表这人效率高。你得把最重要的资料放在手边把不常用的档案放到柜子里而不是把所有文件堆到桌面上。桌面越乱他找东西越慢甚至拿错。所以在做上下文工程时我从来不优先看“窗口够不够大”而是看“重点信息有没有被放在最该放的位置”。什么是最该放的位置通常来说系统指令放在最前面因为它要统领全局当前用户问题放在最后面因为模型需要针对它生成答案而中间区域必须按照“相关性从高到低”排序把最重要的外部知识放在离问题尽可能近的位置。有一类问题被反复验证过当用户问题偏晚时模型对前面长上下文中的细节记忆会模糊。如果你把需要严格遵守的事实放在窗口开头又把最新的用户问题放在窗口末尾这中间一旦隔着大量无关内容事实的“引用距离”就太远了。这就是为什么要把知识分段、然后把最相关的段落在最后时刻插入而不是提前放好。1.3 一个可落地的 Token 预算表我自己在项目里维护了一张动态预算表不是固定值而是按场景调整比例。这里可以给出一个通用版本多数对话类应用可以直接照搬系统指令与角色设定10% 以内尽量精简能写成 300 token 就不写 1000 token。业务规则与少样本示例10%~15%只在必要任务中给普通闲聊直接砍掉。当前用户问题不超过 5%因为问题本身通常很短但它的向量表示和关键词会用于检索。关联记忆/摘要20%~25%这是压缩后的历史不是原始全文。检索增强结果30%~40%按相关性排序每段 200~500 token最多取 3~5 段。缓冲余量10%给模型生成、工具返回和意外数据留出空间。这套分配的心理学基础是模型在生成每个 token 时其实是在对整个上下文做条件概率计算。信息越多选择的路径越分散。你把预算严格卡住等于强迫系统每次都做一次信息筛选。筛选不是损失是服务。窗口管理的第一条原则就出来了不是能塞多少就塞多少而是塞之前已经完成了一次完整的信息价值排序。排序的能力决定了窗口利用的最终效率。2. 窗口管理基于优先级和滚动路径的上下文调度2.1 硬裁剪与软裁剪为什么直接截断会“破相”窗口管理的初级做法是硬裁剪超出长度就扔掉前面的消息只保留最近 N 条。这个做法实现简单但经常出问题。比如你扔掉的恰好是用户早些时候说过的核心诉求模型后面就会答非所问又比如你保留了中间几段但把上一轮问题的上下文丢了模型会对当前问题一头雾水。硬裁剪的本质问题是它不理解“哪部分信息重要”只按时间顺序截断。时间顺序和重要程度没有必然关系。用户可能在一小时前交代过家庭住址中间聊了十轮无关内容最后你问“送到哪里”如果硬裁剪把住址丢了自然答不出来。我用的替代方案是软裁剪。先按语义和用途把历史内容划分成模块再给每个模块标记优先级。优先级由两个因子决定一是与当前问题的语义相似度二是该模块中是否存在用户显式给出的持久型信息比如地址、订单号、目标偏好、临时承诺。每次构造上下文时按优先级降序叠加直到预算用完低优先级模块被丢弃或改为摘要形式。2.2 会话级窗口状态同步不让历史与当前令牌错位另一件容易忽略的事是状态同步。当你用外部存储管理聊天记录时数据库里存的内容是一套真正发给模型的上下文又是另一套。两者一旦错位模型就会看到断裂的对话用户在前一轮提到了一个文件到了这一轮那条消息还在但文件附件内容不在或者你在上一轮对历史做了摘要但摘要忘了更新导致模型引用旧信息。我常用一种叫“会话视图”的结构来管理把原始消息流只当作数据源每次请求真正使用的是一份动态渲染视图。视图由若干上下文块组成每块有类型、内容和生效时间。比如一个上下文块的类型可能是“系统指令”“长期事实”或“轮次摘要”当新的用户消息到来时调度器重新渲染视图把已经过期的块移除把需要更新的摘要块重写。做到这一步之后窗口管理就不再是简单的“字符串拼接”而是像操作系统的虚拟内存一样有一层调度逻辑哪些页常驻哪些页换入换出哪些页被交换到摘要区。对一个对话系统来说长期事实就是常驻页面轮次内容就是工作集摘要就是磁盘交换文件。2.3 关键内容保温机制让上下文“常驻”的三种做法如果某些信息特别重要比如用户身份、偏好、当前任务目标我建议做“保温”处理。所谓保温就是不让它随着消息滚动被挤出窗口而是通过机制让它始终出现在模型视野内。做法一关键信息前置。把提取出的用户核心信息放到系统指令末尾或者紧跟系统指令的区域。比如“用户偏好速览用户只接受顺丰快递”“用户当前目标申请退款”。这些短语不占用多少 token却会在整个会话中持续影响模型。做法二轮次尾部复述。如果你确实需要模型记住某个新出现的重要信息可以在最新一轮的 assistant 回复之后加上一个隐藏的“记忆确认”字段让模型自己复述一次关键点。这一步在实际效果上往往会强化模型对信息的编码。注意这里不是让模型把重复信息输出给用户而是作为结构化记忆留存在上下文中。做法三时间衰减的摘要刷新。当一个信息块超过若干轮没有被引用时把它从“全量原文”降级为“单行摘要”一旦它再次被检索命中就重新提升为全量原文。这种动态升降级配合 RAG 使用效果很好相当于让窗口内容处于流动状态永远只保留“当下最重要”的组合。窗口管理的核心结论上下文不是录像带不能从头到尾平等对待。它更像是编辑剪辑出来的视频你要根据剧情重点决定哪个镜头给特写、哪个镜头只留快剪。3. 消息编排结构化的角色、指令与数据分段3.1 一条消息块该长什么样system/user/assistant 的边界窗口管理解决的是“哪些信息进来”消息编排解决的是“进来的信息用什么姿势摆好”。很多人在调模型时把规则、对话、知识一股脑写在一个 user 消息里模型勉强也能工作但一旦场景复杂效果就会崩塌。我通常按角色区分三个主层system 负责稳定指令user 负责当前诉求与输入数据assistant 负责历史输出与中间推理。稳定指令要写得像“操作手册”不随对话变化当前诉求要放在最新一条 user 里避免被淹没历史 assistant 消息则用于提供生成轨迹让模型知道“我之前说过什么”从而保持前后一致。这里有一个容易踩的边界坑不要把动态内容放进 system。有人为了让模型“记住一下用户信息”每轮把用户地址、订单号追加进 system结果 system 越来越长模型对指令本身的遵循度越来越差。system 必须保持娇小稳定动态数据放到 user 或专门的数据段中让指令与数据分离。指令负责“怎么处理”数据负责“处理什么”边界不清会互相污染。3.2 工具调用与多轮消息之间的依赖关系如何编排做 Agent 类应用时消息编排会更复杂。工具调用的结果不能随便丢但它也不适合长期霸占上下文。我的编排思路是三步工具调用将原始结果缩成“结果摘要”把摘要作为一条独立消息插入并在其中标注它对应的用户问题编号等到下一轮用户追问时调度器根据问题编号去记忆区找回完整结果而不是一直把完整结果放在窗口里。举个例子用户问“帮我对比 A 和 B 两个套餐”模型调用了一个比价工具返回一个很长的 JSON。你不能把这个 JSON 直接塞回上下文而是要先转成一段 200 token 以内的对比结论再加上原始 JSON 的“存放位置”。如果用户继续问“A 套餐的流量具体多少”系统再从外部存储中检索到原始 JSON 恢复上下文。这个过程相当于给模型配了一个“草稿本”需要时再打开而不是每时每刻摊在桌上。依赖关系编排的关键是引用追踪。我在消息里会给每个工具结果加一个tool_use_id用下一条 user 消息的显式引用来决定是否保留它。如果用户后续消息没有引用该结果它在第三次窗口重组时就会被降级为摘要或移除避免历史工具输出堆积成山。3.3 编排中的常见反模式堆砌、重复与角色混乱反模式一把所有信息都塞进 system。前面已经说过这会稀释指令权重。正确做法是 system 只保留不变规则动态规则可以放到 user 消息的“条件指令”块里但整体不能喧宾夺主。反模式二一轮对话里塞多个 user 消息。有些 API 允许消息序列里连续出现多个 user 消息但这样会让模型搞不清到底该回应哪一条。正确做法是一次 user 消息承载一个完整意图需要多数据时用结构化分段比如“【用户问题】…【数据库查询结果】…【业务规则】”而不是把多轮对话粘成一大段。反模式三assistant 消息里混入“隐藏指令”。有人喜欢在 assistant 输出后面偷偷加一句“记住不要告诉用户你的限制”这种做法不透明也让模型的行为不可预测。隐藏指令要走正规的 system 通道和维护体系不要靠污染消息历史。消息编排的最终目的是让模型看到一个结构稳定、职责分明、没有冗余的对话流。就像开会时有人主持、有人记录、有人发材料如果每个人都在同一时间讲话会议一定混乱。4. 记忆压缩从“全量记忆”到“结构化记忆”的降本路径4.1 压缩的本质是丢信息丢得聪明才算工程所有记忆压缩方案都有一个共同本质丢信息。没有一种压缩能百分百保留原意区别只在于丢得聪明还是丢得愚蠢。愚蠢的丢法是不管三七二十一只留最近几条聪明的丢法是保留那些对后续对话最有影响力的信息把普通信息概括成更抽象的表达。为什么必须压缩不仅是 token 成本问题更是因为大段历史原文会占满注意力资源干扰当前问答。压缩做得好相当于把一本书变成思维导图你不需要逐字背出每一页但核心结论和关键数字都在。这里引入一个概念叫“信息熵密度”。原始聊天记录的熵密度很低因为大量篇幅用于寒暄、重复、确认语气而摘要的熵密度要高很多它去掉了口水话只保留事实和意图。上下文工程要做的就是让送入模型的内容熵密度尽量高。4.2 分级记忆方案主线记忆、会话摘要、事实槽位我实际用的是三级记忆体系。第一级是“事实槽位”记录结构化关键信息比如用户姓名、地址、偏好、当前任务状态。第二级是“会话摘要”每当对话超过一定轮次就调用模型对旧窗口做一次摘要生成一段 300~500 token 的浓缩版本。第三级是“原文归档”把原始完整对话存储到外部数据库平时不进上下文只有摘要被判定不充分时再用检索捞回。这三者各有分工。事实槽位要回答“这个用户是谁”会话摘要要回答“刚才聊了什么”原文归档要回答“细节到底是什么”。它们共同支撑起一个无缝的记忆体验用户不用重复交代自己是谁模型也能在长会话中保持连贯。触发摘要的条件我一般写成累计消息数超过 20 轮或者当前上下文估算 token 超过预算的 70%。摘要生成后不是直接替换旧消息而是保留概率判断如果摘要覆盖范围内仍有关键细节被后续问题依赖则该细节单独提到事实槽位。这一步可以用规则抽取也可以用模型抽取但在生产环境我倾向于“模型生成摘要 正则兜底关键字段”。4.3 压缩触发时机与效果验证压缩最怕的就是触发太晚导致一次请求直接溢出窗口或者触发太频繁让模型每每丢失正在讨论的细节。我实践下来的触发策略分两步第一步硬性水位线。估算当前上下文 token 数超过 70% 开始触发压缩超过 90% 强制进入“只保留系统指令 最新 5 轮 摘要 当前问题”的最小模式。第二步语义变化触发。当用户话题发生明显切换时比如从“退款咨询”跳到“改收货地址”理解旧话题的完整历史不再重要可以先对旧话题生成摘要并把新话题设置为当前关注点。效果验证不能只看用户满意度。我会用一组“记忆点测试题”每次压缩前后人为构造几个追问比如“用户刚才说的收货地址是什么”“退款金额是多少”。在压缩前模型回答正确压缩后也应当回答正确。如果压缩后答错说明摘要丢掉了关键事实槽位需要把该信息提升到一级记忆。这套回归测试跑通了记忆压缩才敢上线。还有一个小技巧压缩摘要时给摘要加上时间戳与标签。这样当模型需要区分“昨天聊的事”和“刚才聊的事”时不会被混在一起。标签可以是“已解决”“待处理”“用户情绪信息”它们能帮模型更快定位相关信息。记忆压缩不是让模型“记得更多”而是让模型“记得更对”。在一个长会话系统里这往往比模型参数大小更影响体验。5. 检索增强应用的落地实践RAG 在上下文工程里的位置5.1 RAG 不是简单的“查一下拼进去”RAG检索增强生成做到后面你会发现它本质是上下文工程的一个下游模块外部知识经过检索、切片、筛选、排序最终以合适的形态注入有限窗口。很多人第一次写 RAG demo就是 embedding 一下、向量库查 top-k、拼到 prompt 里。demo 能跑线上稀碎因为检索到的内容没做上下文适配。有一段我心里的警句RAG 检索的目标不是“找到相似文本”而是“找到能补全模型缺失知识且不与现有上下文冲突的信息”。相似文本不等于有用信息用户问题中出现的词可能在文档里反复出现但真正关键的那句结论反而因为措辞不同没被检索到。这就是为什么需要做查询改写、混合检索、重排序等环节。在上下文工程框架里RAG 的产出必须受“注入约束”限制每段不超过 500 token、总共不超过 5 段、按与当前问题相关度排序、段与段之间用分隔标签指明来源。不能让 RAG 结果变成第二个失控的聊天记录。5.2 切片粒度、嵌入模型与重排的联动调优切片粒度是 RAG 中最容易“一动不动就出事”的地方。切片太短语义不全检索到的片段可能只讲了半句话切片太长噪声太多向量表示会被稀释同时浪费上下文窗口。我常用的是递归字符分割法先按标题结构分块再按段落大小 300~500 字切分重叠 50~100 字。但不同文档类型要单独调合同类适合按条款切FAQ 类适合按 QA 对切技术文档适合按章节切。嵌入模型的选择也会影响最终质量。通用向量模型对专有名词多的场景往往不友好我会做一个小评测集准备 20 个真实用户问题每个问题标注对应的标准答案片段然后测试不同 embedding 模型的召回率。召回率至少要做到 80%否则后续重排再强也无济于事。重排模型也很重要它能把相关性分数重新校准很多时候 top-5 里正确的文档初始就排在 top-20但经过 rerank 后能进入目标位置。在上下文注入时不要只注入“命中的片段”还要让模型知道该片段的出处和可信度。比如某个关键数据来自两年前的文档你必须在文本中标注“来源2023 年产品手册”否则模型很容易把过期信息当最新事实甚至自己在回答时“脑补”更新数据。5.3 混合检索和上下文注入时的格式工程纯向量检索有天然缺陷它对精确词匹配、特定编号、反义词不敏感。比如用户问“不支持哪些支付方式”文档里写的是“支持支付宝、微信、银行卡”向量检索可能因为语义相似而把这段捞出来但模型却很可能搞反否定关系。更稳的做法是混合检索向量检索负责语义召回BM25 关键词检索负责精确匹配再用重排模型融合两类结果。融合权重不是拍脑袋定的我会先跑离线实验统计两类检索各自召回正确答案的比例再按比例加权。像“订单号 ER-2024-8899”这种查询BM25 的权重必须明显更高。格式工程上RAG 片段注入前要转换成“人话”。举例来说原始文档可能是表格直接灌入 Markdown 表格模型也能处理但在 token 有限的情况下我会先把表格转成“Key-Value 列表”或“自然语言结论”优先保留与问题直接相关的字段。每次注入时我给每个片段加一个标签行比如“【参考资料 1】来自《退货政策》第 3 节”模型在回答时就知道引用来源。这样不仅提高准确性还方便后续做可解释性追踪。如果你的系统有多个知识库务必在每个片段上标注知识库名称。否则模型很可能把 A 库的条款与 B 库的条款混在一起生成一个看似合理却完全不存在的结论。RAG 的上限由检索质量决定下限由上下文注入格式决定两者缺一不可。6. 我能反复复用的调试流程与避坑清单6.1 六步调试法从失败样本反推上下文问题很多团队拿着 badcase 就去改 prompt改来改去效果原地打转。我更建议按六步来定位上下文问题第一步复现失败并记录完整输入输出把发出去的 messages 全部存下来包括系统指令、历史、检索结果。第二步人工检查上下文里是否存在“正确答案”。如果存在说明问题出在信息的摆放位置、权重分配或格式上如果不存在问题出在检索丢失或记忆压缩过度而不是模型不行。第三步用“最小上下文法”测试。只保留系统指令和当前问题看模型能否答对排除上下文噪声干扰。第四步逐步加入检索结果和历史每加一部分就测一次找出让答案从“对”变“错”的引入点。第五步检查消息边界看是否存在角色混乱、指令被数据覆盖、工具结果残留在错误位置。第六步修正后回归全部记忆点测试和检索测试确保不是“修好一个坏十个”。这套方法不需要高深的框架只要你在日志里把上下文完整记录下来坚持几轮你会发现自己比想象中更快找到症结。6.2 避坑清单我踩过的、帮你提前躲开的坑坑一漏掉“隐含否定”类检索。用户问“不要带壳的”检索结果却全是“带壳手机壳推荐”。解决方法是针对每类业务维护一个否定词表查询改写时把否定意图显式化并过滤掉包含“不、免、无需”等反向词的文档。坑二压缩摘要时把“数字”丢了。人类写摘要时经常忽略“用户要求 3 天内发货”这个数字但模型后续决策必须依赖它。解决方案是在摘要生成后用正则把所有数字、日期、价格、编号抽出来单独放到事实槽位。坑三system 指令被动态内容无限膨胀。一旦你允许把动态规则堆进 system它三个月后可能变成 5000 token 的怪物。解决办法是固定 system 的上限新增规则必须走外部知识库或规则引擎而不是直接堆 prompt。坑四盲目追求“上下文越长越好”。我在开头已经提过长上下文容易导致注意力稀释、成本飙升、响应变慢。真正的高质量服务是短、准、稳而不是大而全。坑五RAG 片段里混入相互矛盾的信息。多个文档之间经常不一致比如 A 文档说“7 天无理由退货”B 文档说“生鲜不支持 7 天无理由”。如果你不加校验直接注入模型大概率会选择某个它觉得更可信的答案但很不稳定而且一旦用户追问两个文档的关系就穿帮。解决方法是新增一层“冲突检测”如果检索段之间存在明显冲突把冲突点显式告诉模型并让模型做说明而不是强行二选一。6.3 一套最小可用的上下文工程模板最后给出一套我常用的模板可以直接复制到项目里做骨架再根据场景调整。消息结构示例这是一个对话请求的 messages 数组简化版本[ { role: system, content: 你是客服助手。回答前必须阅读【用户偏好】和【参考资料】。若信息不足直接说明。 }, { role: user, content: 【用户偏好】用户ID 88231常用收货地址杭州市西湖区文一西路 100 号当前目标申请退款。\n\n【关键历史摘要】用户于 10 月 20 日购买订单 A110 月 22 日表示商品破损要求退款。\n\n【参考资料】\nref id1 source退货政策生鲜类商品不支持 7 天无理由退货但存在质量问题可在签收 24 小时内申请退款。/ref\n\n【当前问题】订单 A1 的商品是生鲜还能退款吗 } ]这个模板的核心特点是所有动态信息都归入 user 消息的数据段system 只保留稳定的行为规则历史以摘要而不是原文存在参考资料标注来源。如果模型回答仍然不准优先调整数据段的顺序、篇幅和相关性而不是改 system 里的咒语。另外我建议在 API 调用前加一个“上下文预算检查”函数用 tokenizer 统计 messages 总长度一旦超过阈值就自动走摘要或截断流程。这个简单的拦截器能避免很多生产事故。用总结的方式说一句上下文工程不是某个单一技巧而是一套持续迭代的配置管理方法。把窗口当预算把消息当结构把记忆当分级把检索当注入四者联动才能让大模型真正发挥实力。我在实际项目中得到的最大体会是与其不停换更强的新模型不如先把每次请求的上下文治理好——这一步的投入产出比远超想象。