1. 从一个真实困境说起为什么模型越强效果反而越不稳定很多人有过类似的体验同一个模型同一套提示词模板昨天跑出来的结果堪称惊艳今天再跑一遍输出却像换了个脑子。更让人抓狂的是在一个对话窗口里聊到第十几轮之后模型开始忘事——前面明确说过的约束条件它当没看见前面已经确认过的方案它又绕回去重新讨论。你以为是模型退化了其实大概率是上下文工程没做好。先把概念说清楚。所谓上下文工程指的是围绕模型单次推理所能看到的全部信息进行的设计、组织、裁剪与调度工作。它包含但不限于系统指令怎么写、历史对话保留多少、外部知识怎么注入、工具返回结果怎么压缩、多轮状态怎么维护。它和提示词工程不是一回事——提示词工程关注的是这一句话怎么说上下文工程关注的是这一整屏信息怎么摆。这个区别很关键。提示词工程像是给一个人交代任务时的措辞技巧而上下文工程像是给这个人准备一整套工作台桌面上放哪些资料、哪些先放、哪些收进抽屉、哪些直接扔掉。措辞再好如果桌面上堆了一百份互相矛盾的文档对方照样干不好活。这篇文章适合三类人看一是已经在做智能体、知识库问答、多轮对话系统但被效果不稳定折磨的开发者二是刚接触大模型应用想知道除了调提示词还能做什么的入门者三是负责把原型推向生产、需要关心成本和延迟的工程负责人。我会从设计思路、核心细节、实操落地、问题排查四个层面把上下文工程这件事拆开讲透尽量给到可以直接抄作业的方案。2. 上下文工程的整体设计思路拆解2.1 上下文窗口不是越大越好的仓库一个常见的误解是既然模型支持很长的上下文窗口那就把所有能塞的东西都塞进去反正它自己会挑重点。实测下来这个想法会踩两个坑。第一个坑叫中间遗忘。大量实践观察表明模型对上下文开头和结尾部分的信息利用效率明显高于中间部分。你把一份关键约束放在第三千字的位置它被忽略的概率远高于放在开头。这不是玄学而是注意力分配机制在长序列上的自然表现。所以塞满不等于用好。第二个坑叫噪声稀释。上下文里每多一段无关内容模型判断什么才是当前任务重点的难度就上升一分。当无关内容占比过高时模型容易抓错重点甚至把历史对话里已经废弃的方案当成当前方案来执行。我见过一个客服机器人因为把全部历史工单都塞进上下文结果它开始用别的用户的问题来回答当前用户场面相当尴尬。所以上下文工程的第一原则是上下文是稀缺资源要像管理内存一样管理它。不是能放多少就放多少而是该放什么、该丢什么、该压缩什么都要有明确策略。2.2 分层设计把上下文切成几个功能区比较成熟的做法是把上下文分成几个逻辑层每层职责清晰层级内容是否常驻典型长度占比系统层角色设定、硬性约束、输出格式常驻10%~20%任务层当前任务目标、验收标准常驻5%~10%知识层检索到的外部资料、文档片段动态30%~50%记忆层历史对话摘要、关键状态动态15%~25%工具层工具调用返回结果动态10%~20%这样分层的好处是当上下文超长时你知道该从哪一层开始裁剪。系统层和任务层是宪法轻易不动知识层和工具层是临时材料用完即弃记忆层需要定期压缩成摘要。提示分层不是目的可裁剪才是目的。任何一层如果无法被安全裁剪说明它的设计有问题。2.3 为什么现在特别强调这件事早几年做应用模型能力有限大家把精力都花在怎么把话说清楚上。现在模型能力上来了单轮指令遵循已经相当可靠瓶颈转移到了信息组织上。尤其是智能体场景一次任务可能涉及十几次工具调用、几十轮交互上下文管理稍有不慎就会崩盘。另一个推动因素是成本。长上下文意味着更高的推理开销和更长的响应时间。一个把上下文从两万 token 压到六千 token 的优化可能直接让单次调用成本降一半、延迟降三分之一。在规模化场景下这不是锦上添花而是能不能跑通商业模型的问题。3. 核心细节解析与实操要点3.1 系统指令的写法约束要可判定系统层最容易犯的错是写一堆要专业、要友好、要准确这类无法判定的形容词。模型没法验证自己是否专业只能靠感觉结果就是每次表现都不一样。正确的做法是把约束写成可判定的规则。比如不说回答要简洁而说回答不超过三句话每句不超过四十字不说要准确引用资料而说每个事实性陈述后面用方括号标注来源编号没有来源的陈述不要写。我整理了一个对比表方便你自查模糊约束可判定约束语气要专业使用陈述句不使用感叹号和口语词回答要详细分点作答每点不少于两句不要跑题只回答与当前任务目标相关的问题无关问题回复该问题超出当前范围引用要准确引用内容用引号包裹并标注来源编号可判定约束的另一个好处是你可以写自动化测试来验证模型是否遵守。模糊约束没法测可判定约束可以。3.2 历史对话的处理摘要 关键状态双轨制多轮对话里历史不能全留也不能全丢。我的做法是双轨制一条轨道是滚动摘要。每积累若干轮对话就让模型把这段对话压缩成一段摘要保留决策、结论、未解决问题丢弃寒暄和试错过程。摘要本身也要控制长度太长就再压缩一次。另一条轨道是结构化状态。把对话中产生的关键变量抽出来用键值对形式单独维护。比如用户已经确认的预算、时间、偏好这些不放在自然语言摘要里而是放在一个结构化的状态对象里每轮都完整注入。{ user_confirmed: { budget: 5000以内, deadline: 本月底, preferred_style: 简约 }, open_questions: [是否需要发票, 配送地址未确认], decisions_made: [方案A已否决, 方案B待评估] }这样做的原因是自然语言摘要容易在反复压缩中丢失细节而结构化状态是精确的、不会漂移的。两者配合既有上下文连贯性又有关键信息的确定性。3.3 外部知识的注入先排序再截断最后标注知识层最常见的做法是检索一堆文档片段直接拼进去。问题在于检索结果的相关性参差不齐直接拼进去会稀释重点。我的处理流程是三步重排序检索回来的片段先按相关性重新排序把最相关的放最前面。很多检索系统返回的顺序并不理想重排序这一步能显著提升利用效率。截断设定一个知识层的 token 预算从高到低填充填满即止。宁可少放几段也不要把预算撑爆。标注每段资料前面加上编号和来源标识方便模型引用也方便你排查问题。注意知识片段之间的分隔符要清晰。我习惯用明显的分隔标记避免模型把两段不相关内容连起来理解。3.4 工具返回结果的压缩别把原始 JSON 直接塞进去工具调用返回的结果往往是冗长的 JSON包含大量模型不需要的字段。直接塞进上下文既浪费预算又干扰判断。我的做法是在工具层和上下文层之间加一个结果处理器只提取模型真正需要的字段把嵌套结构拍平把长列表截断成前若干项加一个总数说明。比如一个搜索工具返回二十条结果上下文里只放前五条的标题和摘要再加一句共二十条结果已展示前五条。这一步看起来琐碎但在多轮工具调用场景下节省的上下文非常可观。我做过一个对比同样的任务加了结果压缩之后平均上下文长度下降了约四成任务成功率反而略有提升。4. 实操过程与核心环节实现4.1 搭一个最小可用的上下文管理器下面给一个可以直接参考的实现思路用伪代码加说明的方式语言无关。核心是一个上下文组装函数输入是当前任务、历史、检索结果、工具结果输出是最终拼好的消息列表。def build_context(task, history, retrieved_docs, tool_results, budget): # 1. 固定层系统指令 任务目标优先占用预算 fixed [system_prompt, task_prompt] fixed_tokens count_tokens(fixed) remaining budget - fixed_tokens # 2. 状态层结构化状态完整注入 state_block render_state(history.structured_state) remaining - count_tokens(state_block) # 3. 知识层重排序后按预算截断 ranked_docs rerank(retrieved_docs, task) doc_block, remaining fill_until_budget(ranked_docs, remaining) # 4. 工具层压缩后的结果 compressed_tools [compress(r) for r in tool_results] tool_block, remaining fill_until_budget(compressed_tools, remaining) # 5. 记忆层滚动摘要用剩余预算填充 summary_block, remaining fill_until_budget([history.summary], remaining) return assemble(fixed, state_block, doc_block, tool_block, summary_block)这个函数的关键在于预算分配的顺序。固定层和状态层优先级最高先占知识层和工具层按相关性竞争剩余预算记忆层用剩下的边角料。这样即使预算紧张最核心的约束也不会丢。4.2 预算怎么定从模型上限倒推预算不是拍脑袋定的。我的方法是查清模型的最大上下文长度记为 M。预留输出空间一般留 M 的 20% 到 30% 给模型生成。剩下的作为输入预算 B。再给 B 打个八折作为安全边际因为 token 计数有误差。举个例子假设模型上限是三万两千 token预留三成给输出输入预算是两万两千四百。打八折后实际使用约一万八千。这个数字就是你分配各层预算的总盘子。提示不同模型的分词方式不同同一个字符串的 token 数可能差百分之十几。预算一定要留余量不要卡着上限用。4.3 滚动摘要的触发时机与写法滚动摘要不要每轮都做那样开销太大。我的触发条件是当历史对话的 token 数超过记忆层预算的百分之七十时触发一次压缩。压缩的提示词也有讲究。不要只说总结一下而要明确要求保留什么、丢弃什么请将以下对话压缩成不超过两百字的摘要必须保留 1. 用户已确认的所有参数和偏好 2. 已经做出的决策及理由 3. 尚未解决的问题 请丢弃寒暄、重复确认、已被否决的方案细节。这样压出来的摘要信息密度高后续注入时不会浪费预算。4.4 一次完整的上下文组装现场记录我拿一个实际的咨询类任务走一遍流程让你看到每一步的取舍。任务背景用户要选一款办公设备已经聊了八轮中间查了两次资料调用了一次比价工具。第一步固定层注入系统指令和任务目标约八百 token。系统指令里写明了输出格式和硬性约束。第二步状态层注入结构化状态预算已确认、用途已确认、两个待定问题。约一百五十 token。第三步知识层。检索回来六段资料重排序后取前三段约一千二百 token。第四段开始相关性明显下降果断截断。第四步工具层。比价工具返回了十五条记录压缩成前五条加总数说明约三百 token。第五步记忆层。八轮对话的滚动摘要约两百 token。总计约两千六百五十 token远低于预算上限。这意味着还有余量可以在知识层多放一段资料或者把摘要写得更详细。整个组装过程在代码里是毫秒级的真正的开销在检索和重排序上。5. 常见问题与排查技巧实录5.1 模型忘记了前面的约束这是最高频的问题。排查顺序如下排查项检查方法常见原因约束是否还在上下文里打印最终组装的消息被预算裁剪掉了约束位置是否太靠中间看约束在消息中的位置中间遗忘效应约束是否被后续内容覆盖检查是否有矛盾指令知识层或工具层注入了冲突信息约束是否可判定对照可判定约束表约束本身太模糊我的经验是把最重要的约束同时放在系统层开头和任务层结尾形成首尾呼应。这样即使中间内容再多约束也不容易被忽略。5.2 多轮之后回答开始串味表现为模型把之前话题的内容混进当前话题。原因通常是历史摘要里保留了太多旧话题的细节。解决办法是在摘要时明确标注话题边界并在状态层里记录当前活跃话题。当话题切换时主动清理旧话题的细节只保留结论。5.3 上下文没超限但响应变慢不一定是上下文长度的问题也可能是内容结构的问题。嵌套很深的 JSON、大量重复的格式标记都会增加模型解析负担。把结构拍平、去掉冗余标记往往能改善响应速度。5.4 检索结果明明相关模型却不用先检查检索结果在上下文中的位置。如果被放在很靠后的位置模型可能没注意到。把最相关的资料往前放或者用显式指令引导请优先参考资料一和资料二。另一个可能是资料之间互相矛盾模型不知道该信哪个。这时候需要在注入前做一次冲突检测或者明确标注资料的时效性和可信度。5.5 预算总是不够用如果反复遇到预算紧张说明你的信息组织方式需要优化而不是简单加大预算。可以尝试把长文档预先做结构化抽取只注入抽取后的要点把常用知识做成固定模板减少每次检索的量把工具返回结果做更激进的压缩检查是否有重复注入的内容我踩过的一个坑是系统指令和任务指令里写了重复的约束白白浪费了预算。后来做了一次去重直接省出百分之十五的空间。6. 几个容易被忽略的进阶技巧6.1 给上下文加目录当上下文很长时在开头加一段简短的目录说明后面各部分的顺序和内容。比如接下来依次是任务目标、已确认参数、参考资料、工具结果、历史摘要。这段目录本身很短但能帮模型快速建立全局认知减少迷路。6.2 用占位符管理动态内容不要把动态内容直接拼进模板字符串而是用占位符在组装阶段替换。这样模板可以复用也方便做单元测试。比如模板里写{{knowledge_block}}组装时再填入实际内容。6.3 记录每次组装的上下文指纹每次组装完上下文算一个哈希值或者记录各层的 token 数存到日志里。当出现问题时你可以回溯是哪次组装、哪一层出了问题。这个习惯在排查偶发问题时特别有用。6.4 定期做上下文体检每隔一段时间抽样检查实际组装的上下文看看有没有冗余、有没有矛盾、有没有该丢没丢的内容。我一般每周抽十条看一遍经常能发现可以优化的地方。7. 我在实际项目中的几点体会做上下文工程这几年最大的感受是它更像是一门信息策展的手艺而不是纯技术活。技术手段检索、压缩、重排序是工具但真正决定效果的是你对什么信息在什么时刻最重要的判断力。我见过太多团队把精力全花在换模型、调参数上却忽略了上下文组织这个更底层的变量。实际上把上下文理顺带来的提升往往比换一个更强的模型更明显而且成本更低。还有一个体会是上下文工程没有一劳永逸的方案。任务类型变了、用户群体变了、模型版本变了上下文策略都要跟着调整。把它当成一个持续迭代的过程而不是一次性的配置。最后分享一个小技巧当你不知道该怎么组织上下文时先问自己一个问题——如果我是接这个任务的人我需要看到哪些信息才能干好把这个清单列出来再按重要性排序你的上下文结构基本就出来了。这个方法比任何框架都管用。