
1. 从一个真实困境说起为什么模型总是“答非所问”我最早接触大语言模型应用开发的时候踩过一个特别典型的坑。当时我在做一个智能客服原型的问答模块用户问“我上周买的那个订单什么时候到”模型给出的回答是“请提供您的订单号”。看起来没毛病对吧但问题是用户上一轮已经说了订单号模型在多轮对话里把它“忘”了。我当时的第一反应是模型能力不行换了个更大的模型结果还是一样。后来我才意识到问题根本不在模型本身而在于我喂给它的上下文是残缺的。这就是上下文工程要解决的核心问题。很多人把它和提示词工程混为一谈觉得写个好提示词就万事大吉了。但实际做项目你会发现提示词只是冰山一角真正决定模型输出质量的是你在每一次调用时塞进模型窗口里的那整段上下文——包括系统指令、历史对话、检索到的知识片段、工具返回结果、格式约束甚至这些内容的排列顺序。上下文工程说白了就是在模型有限的上下文窗口里通过选择、组织、压缩、排序等手段让模型在每一次生成时都能拿到“恰好够用且不冗余”的信息。它要解决的问题是模型能力再强如果你给它的信息是错的、缺的、乱的、超长的它照样会给出让你抓狂的结果。这篇文章适合谁看如果你正在做基于大语言模型的应用开发不管是对话系统、知识问答、代码助手还是自动化工作流只要你需要控制模型的输入来获得稳定输出上下文工程就是你绕不过去的基本功。我会从设计思路、核心细节、实操过程到问题排查把这件事拆开讲透尽量让你看完就能用到自己的项目里。2. 上下文工程到底在工程什么整体设计与思路拆解2.1 上下文窗口不是“越大越好”的容器很多人有一个直觉既然模型支持越来越长的上下文窗口那我把所有相关信息全塞进去不就行了我早期也是这么想的结果发现两个问题。第一成本飙升。上下文长度直接和token消耗挂钩每次调用都塞几万字账单会让你怀疑人生。第二效果反而下降。这不是玄学而是有实际原因的模型在超长上下文中会出现“中间遗忘”现象也就是开头和结尾的信息记得牢中间大段内容容易被忽略。所以上下文工程的第一层设计思路不是“能塞多少塞多少”而是“该塞什么、不该塞什么”。这就像你给同事交接工作你不会把过去三个月的所有邮件都转发给他而是挑出关键决策、当前状态、待办事项。上下文工程做的就是这件事的自动化版本。具体来说一个完整的上下文通常包含以下几类信息每一类都需要单独设计策略上下文类型作用常见问题处理策略系统指令定义角色、行为边界、输出格式指令冲突、过于冗长精简、分层、优先级排序历史对话维持多轮连贯性无限增长、旧信息干扰滑动窗口、摘要压缩检索知识补充模型不知道的事实召回不准、片段过长重排序、截断、去重工具结果提供实时数据或计算结果格式混乱、信息冗余结构化、字段筛选格式约束控制输出结构与内容指令冲突放在靠近生成位置这张表不是让你照搬而是提醒你上下文里的每一类信息都有它的“脾气”你得分别对待不能一锅炖。2.2 为什么“选择”比“堆砌”更重要我后来在另一个项目里做了一个对比实验。同样的用户问题同样的模型我用了两种上下文策略。第一种是把检索到的前10个文档片段全部塞进去每个片段500字总共5000字。第二种是先用一个轻量模型对10个片段做相关性打分只保留最相关的3个片段并且每个片段压缩到200字总共600字。结果第二种策略的答案准确率反而高了将近20个百分点。这个实验让我彻底明白了一件事上下文工程的核心不是“信息量”而是“信息密度”。模型处理信息的能力是有带宽限制的你给它太多噪音它就会在噪音里迷失。选择哪些信息进入上下文、以什么形式进入、放在什么位置这些决策的质量直接决定了最终输出的质量。从工程实现的角度我通常会把上下文处理分成三个阶段召回阶段负责从各种来源拉取候选信息筛选阶段负责打分、去重、压缩组装阶段负责按优先级和位置策略拼装成最终输入。这三个阶段各自有各自的技术选型和坑后面我会逐一展开。2.3 上下文工程和提示词工程的边界在哪里这里有必要把两个概念理清楚因为我在社区里看到太多人把它们混着用导致讨论问题时鸡同鸭讲。提示词工程主要关注的是“怎么措辞能让模型理解我的意图”比如角色设定怎么写、few-shot示例怎么给、输出格式怎么描述。它的作用范围是单次指令的文本表达。上下文工程的范围更大它关注的是“模型在生成这一刻眼睛能看到的所有东西”。提示词只是上下文的一部分。除了提示词你还要管历史对话怎么截断、检索结果怎么注入、工具调用结果怎么格式化、多个信息源冲突时怎么排序。换句话说提示词工程是上下文工程的一个子集而且是偏“静态”的那部分。上下文工程更多是“动态”的因为每次调用的上下文都可能不同。我自己的经验是当你发现模型输出不稳定、时好时坏的时候八成不是提示词写得不好而是上下文组织出了问题。这时候你应该去检查上下文的组装逻辑而不是反复改提示词。3. 核心细节解析与实操要点把上下文当成一个数据结构来设计3.1 系统指令的优先级分层与冲突消解系统指令是上下文的“宪法”它定义了模型的行为边界。但很多人写系统指令的时候喜欢把所有要求堆在一起结果指令之间互相打架。比如你既要求“回答要简洁”又要求“尽可能提供详细解释”模型就会精神分裂。我的做法是把系统指令分成三层。第一层是身份层定义模型是谁、服务什么场景这部分通常很短一两句话。第二层是行为层定义模型应该怎么做、不应该怎么做比如“不确定时要说不知道”“不要编造事实”。第三层是格式层定义输出的结构比如JSON schema、Markdown格式要求。分层的好处是当出现冲突时你可以按层级来消解。身份层最高行为层次之格式层最后。如果格式层的要求和行为层冲突以行为层为准。比如行为层说“不要输出多余内容”格式层说“输出JSON”那就在JSON内部组织内容而不是在JSON外面加解释。注意系统指令里最忌讳的是“既要又要还要”。每一条指令都会占用模型的注意力指令越多每条指令被遵守的概率就越低。我一般会把系统指令控制在300字以内超过这个长度就要考虑是不是有些要求应该放到其他位置。3.2 历史对话的压缩策略滑动窗口不是唯一解多轮对话是上下文膨胀的重灾区。用户和模型聊了20轮历史记录可能已经上万字了。如果你全部保留成本高不说旧信息还会干扰当前轮次的判断。最常见的做法是滑动窗口只保留最近N轮。但滑动窗口有个明显缺陷如果用户在第三轮说了一个关键信息到第十五轮才用到滑动窗口早就把它滑没了。我试过几种改进方案这里分享两个我觉得比较实用的。第一个是关键信息抽取在每轮对话结束后用一个轻量模型抽取这轮对话里的关键实体、意图、约束条件存到一个结构化的“对话状态”里。每次组装上下文时把最近几轮原文加上这个对话状态一起放进去。这样既保留了近期细节又不会丢失远期关键信息。第二个是分层摘要把对话历史分成近期、中期、远期三段。近期保留原文中期用一段摘要概括远期只保留一句话级别的主题描述。这种分层结构比单纯的滑动窗口更符合人类记忆的工作方式实测下来在多轮任务型对话里效果提升明显。策略优点缺点适用场景滑动窗口实现简单、成本低远期信息丢失短对话、闲聊关键信息抽取保留核心事实需要额外模型调用任务型对话分层摘要兼顾细节与全局实现复杂度较高长对话、复杂任务3.3 检索增强中的上下文注入技巧检索增强生成是上下文工程里最常被讨论的场景。基本流程是用户提问系统从知识库检索相关片段把片段和问题一起送给模型。但这里有个细节很多人没注意检索到的片段怎么“注入”到上下文里对结果影响很大。我见过最常见的错误是把检索结果直接拼接在问题前面中间没有任何标记。模型有时候分不清哪些是用户说的、哪些是检索到的资料导致把资料里的内容当成用户意图来回应。正确的做法是用明确的分隔标记把不同来源的内容区分开比如用XML标签或者特殊符号包裹检索片段并在系统指令里说明“以下内容来自知识库仅供参考”。另一个技巧是片段重排序。检索系统返回的片段通常是按相关性打分的但相关性高不代表对当前问题最有用。我会在注入之前做一次重排序把包含具体答案的片段放在最前面把背景介绍类的片段放在后面。因为模型对上下文开头和结尾的信息更敏感把最可能包含答案的内容放在开头能显著提升回答准确率。还有一个容易被忽略的点是片段截断。检索到的片段往往很长但真正有用的可能就一两句话。我通常会在注入前对每个片段做一次截断只保留和问题最相关的部分。截断的策略可以是按句子粒度选择也可以用一个轻量模型做抽取式摘要。这样能把上下文长度压下来同时提高信息密度。3.4 工具调用结果的格式化与精简当模型需要调用外部工具时工具返回的结果也会进入上下文。这里的问题在于工具返回的数据往往是给程序看的不是给模型看的。比如一个天气API返回的JSON可能有几十个字段但模型只需要知道温度、天气状况、风力这几个关键信息。我的做法是在工具结果进入上下文之前加一层“格式化适配器”。这个适配器负责从原始结果里提取关键字段转换成自然语言描述或者精简的结构化格式。比如把一大段JSON转成“当前温度25度晴风力3级”这样一句话。这样既减少了token消耗又降低了模型理解难度。提示工具结果的格式化适配器最好和工具本身解耦。也就是说工具只负责返回原始数据适配器负责转换。这样当你换了一个同类工具时只需要改适配器不需要改上下文组装逻辑。4. 实操过程与核心环节实现从零搭一个上下文组装管线4.1 定义上下文的数据结构与优先级在动手写代码之前我习惯先把上下文的数据结构定义清楚。这就像盖房子先画图纸不然后面改起来很痛苦。我通常会把上下文定义成一个有序列表每个元素是一个“上下文块”每个块包含内容、来源、优先级、token估算这几个字段。class ContextBlock: def __init__(self, content, source, priority, token_estimate): self.content content self.source source # system / history / retrieval / tool / format self.priority priority # 1-10, 数字越小优先级越高 self.token_estimate token_estimate优先级的设计很关键。我一般把系统指令设为优先级1当前用户问题设为优先级2工具结果设为优先级3检索结果设为优先级4历史对话设为优先级5。当上下文总长度超过预算时从优先级最低的开始裁剪。这样能保证最关键的信息永远在上下文里。4.2 上下文预算的分配与动态调整上下文窗口是有硬限制的你必须给每次调用分配一个token预算。我的经验是不要把预算用满留出20%左右的余量给模型生成。比如模型支持8000 token的上下文那我最多用6400 token来组装输入剩下1600 token留给输出。预算分配也不是平均主义。我会根据任务类型动态调整。如果是知识问答类任务检索结果的预算占比会高一些可能占到40%。如果是多轮对话任务历史对话的预算占比会高一些。如果是工具调用任务工具结果的预算要留够因为工具返回的数据往往不可预测。具体实现上我会先按优先级从高到低累加token估算当累加到预算上限的80%时停止剩下的20%作为缓冲。如果某个高优先级块本身就超预算那就需要对它单独做压缩而不是直接丢弃。4.3 组装顺序对模型注意力的影响上下文块的排列顺序不是随意的。基于我对模型行为的观察以及一些公开研究的结论模型对上下文开头和结尾的信息注意力更集中中间部分容易被忽略。所以我会把最重要的信息放在开头和结尾。一个典型的组装顺序是这样的系统指令放在最开头因为它是全局约束。紧接着是当前用户问题让模型明确任务。然后是工具结果和检索结果这些是支撑材料。最后是历史对话摘要放在靠近生成位置的地方帮助模型保持连贯性。格式约束如果比较短可以放在最后紧挨着生成位置这样模型在输出时更容易遵守。当然这个顺序不是固定的需要根据实际效果调整。我建议你在项目初期多做A/B测试同一组问题用不同的组装顺序跑一遍看哪种顺序的答案质量更高。这种测试成本不高但收益很直接。4.4 一个完整的上下文组装函数示例下面是我在一个项目里实际用过的上下文组装函数简化版用Python写的逻辑比较清晰你可以参考这个结构改成自己需要的版本。def assemble_context(user_query, history, retrieval_results, tool_results, max_tokens6000): blocks [] # 系统指令优先级最高 system_prompt load_system_prompt() blocks.append(ContextBlock(system_prompt, system, 1, count_tokens(system_prompt))) # 当前用户问题 blocks.append(ContextBlock(user_query, query, 2, count_tokens(user_query))) # 工具结果格式化后注入 for tr in tool_results: formatted format_tool_result(tr) blocks.append(ContextBlock(formatted, tool, 3, count_tokens(formatted))) # 检索结果重排序后截断 reranked rerank(retrieval_results, user_query) for rr in reranked[:3]: truncated truncate_to_relevant(rr, user_query) blocks.append(ContextBlock(truncated, retrieval, 4, count_tokens(truncated))) # 历史对话摘要 history_summary summarize_history(history) blocks.append(ContextBlock(history_summary, history, 5, count_tokens(history_summary))) # 按优先级裁剪保证不超预算 blocks.sort(keylambda x: x.priority) total 0 selected [] for b in blocks: if total b.token_estimate max_tokens * 0.8: selected.append(b) total b.token_estimate # 按组装顺序排列 order {system: 0, query: 1, tool: 2, retrieval: 3, history: 4} selected.sort(keylambda x: order.get(x.source, 99)) return \n\n.join([b.content for b in selected])这个函数的核心逻辑就是先收集所有候选块按优先级裁剪再按组装顺序排列。实际项目中你还需要考虑缓存、并发、错误处理等工程问题但核心思路就是这个。4.5 参数选择与效果验证上下文工程里有很多参数需要调比如检索返回几个片段、历史对话保留几轮、摘要压缩到什么程度。这些参数没有万能值必须结合你的具体场景来调。我一般会先设一个保守的初始值然后通过小规模测试逐步调整。举个例子检索片段数量这个参数。设得太少可能漏掉关键信息设得太多噪音增加。我的做法是先用5个片段跑一批测试问题记录答案准确率。然后分别用3个和8个再跑一遍看准确率的变化曲线。通常你会发现准确率在某个数量之后就不再提升了甚至下降。那个拐点就是比较合适的值。验证上下文工程效果的时候不要只看最终答案对不对。我建议同时记录几个中间指标上下文总token数、各来源块的token占比、检索片段的相关性得分分布。这些指标能帮你定位问题出在哪个环节。如果答案错了你可以先看检索片段里有没有正确答案如果有但模型没用上那可能是组装顺序或格式的问题如果没有那就是召回环节的问题。5. 常见问题与排查技巧实录5.1 模型忽略上下文中的关键信息怎么办这是最常见的问题之一。你明明把答案放在上下文里了模型却视而不见。我排查这个问题一般按以下顺序来。先检查位置。如果关键信息在上下文的正中间模型忽略它的概率会比较高。试着把它移到开头或结尾。再检查格式。如果关键信息淹没在一大段文字里没有明显的标记模型可能注意不到。用加粗、分隔线、特殊标签把它标出来。最后检查指令。如果系统指令里没有明确告诉模型“要优先使用提供的资料”模型可能会依赖自己的参数化知识而不是你给的上下文。我遇到过一个典型案例检索片段里明明有答案但模型就是不用。后来发现是因为检索片段和用户问题之间隔了太长的历史对话模型在生成时注意力被历史对话带偏了。把检索片段移到历史对话前面之后问题就解决了。5.2 上下文太长导致响应变慢或截断上下文长度直接影响推理时间和成本。当你发现响应变慢或者输出被截断首先要做的是量化分析统计每次调用的上下文token数看看哪个来源占比最大。我做过一个统计发现很多项目的上下文膨胀主要来自三个地方历史对话无限增长、检索片段没有截断、工具结果没有格式化。针对这三个地方分别处理通常能把上下文长度压下来50%以上。如果压缩之后还是超长那就需要考虑更激进的策略比如把部分上下文做成“按需加载”。也就是说不是每次调用都把所有信息塞进去而是让模型先判断需不需要某类信息需要的话再发起一次调用去获取。这种“懒加载”模式能显著降低平均上下文长度但会增加交互轮次适合对延迟不敏感的场景。5.3 多来源信息冲突时的处理策略当系统指令、检索结果、历史对话、工具结果之间存在矛盾时模型会怎么处理答案是不确定。它可能随机选一个也可能把矛盾的内容都输出出来。这是上下文工程里比较棘手的问题。我的处理原则是在组装阶段就消解冲突而不是指望模型自己判断。具体做法是给不同来源设定明确的可信度等级。系统指令最高工具结果次之检索结果再次历史对话最低。当发现冲突时在上下文里显式标注比如“注意以下工具结果与历史对话不一致请以工具结果为准”。注意不要假设模型能自动理解来源的可信度差异。你必须在上下文里明确告诉它。这就像给新员工交接工作你得说清楚“这份数据是财务刚确认的那份是上周的草稿以财务的为准”。5.4 常见问题速查表问题现象可能原因排查方向解决思路模型答非所问上下文缺少关键信息检查检索召回和组装逻辑补充召回源、调整组装顺序模型忽略给定资料资料位置不佳或格式不突出检查资料在上下文中的位置移到首尾、加标记响应变慢上下文过长统计各来源token占比压缩历史、截断检索、格式化工具结果输出格式不稳定格式约束位置太靠前检查格式指令的位置移到靠近生成位置多轮对话丢失关键信息历史压缩过度检查摘要是否保留关键实体改用关键信息抽取策略工具调用结果被误读结果格式不适合模型阅读检查工具结果是否格式化加格式化适配器5.5 几个我踩过的坑和对应的经验第一个坑是过度依赖模型自动理解。我早期总觉得模型足够聪明只要把信息给它它就能自己判断哪些重要哪些不重要。实际做下来发现模型在信息筛选上的能力远不如在信息理解上的能力。你必须在组装阶段就帮它做好筛选而不是把筛选工作留给它。第二个坑是忽略token估算的误差。不同模型的分词方式不同同一个文本在不同模型上的token数可能差20%以上。我一开始用估算值来分配预算结果经常出现实际超限的情况。后来改成用目标模型的分词器做精确计算虽然慢一点但稳定多了。第三个坑是上下文块之间缺少过渡。当你把系统指令、检索结果、历史对话拼在一起时如果它们之间没有自然的过渡语句模型可能会把它们当成不相关的独立段落。我后来在块与块之间加了一些引导语比如“以下是相关知识库内容”“以下是之前的对话摘要”模型的理解准确率明显提升。第四个坑是没有做上下文版本管理。上下文组装逻辑改来改去有时候改了一个地方另一个地方的效果就退化了。后来我养成了习惯每次调整上下文策略都记录版本号和变更内容配合A/B测试来验证效果。这样即使出了问题也能快速回滚到之前的版本。6. 上下文工程的持续优化思路上下文工程不是一次性的工作而是一个持续迭代的过程。随着你的应用场景变化、模型版本更新、数据源增减上下文策略也需要跟着调整。我自己的做法是建立一个“上下文质量监控”机制每次调用都记录上下文长度、各来源占比、模型输出质量评分这几个指标。当某个指标出现异常波动时就去排查对应的环节。另外我建议把上下文组装逻辑做成可配置的而不是硬编码在代码里。比如用配置文件来定义优先级、预算分配比例、组装顺序这些参数。这样当你需要针对不同场景调整策略时改配置就行不用改代码。这个做法在项目从原型走向生产的过程中特别有用因为生产环境往往需要针对不同用户群体、不同任务类型做差异化处理。还有一个值得关注的方向是自适应上下文。也就是说上下文组装策略不是固定的而是根据当前问题的类型、复杂度、历史交互情况动态调整。比如简单问题用短上下文复杂问题用长上下文新用户多用检索结果老用户多用历史对话。这种自适应策略能进一步提升上下文的使用效率但实现复杂度也更高适合在基础管线稳定之后再逐步引入。我个人在实际操作中的体会是上下文工程最考验的不是技术能力而是对业务场景的理解。你得知道在这个场景里什么信息是真正重要的什么信息是噪音用户最在意的是什么。这些判断没法从文档里抄只能从实际项目中一点点积累。每次模型输出不理想的时候不要急着换模型或改提示词先去看看你喂给它的上下文是不是出了问题。十有八九答案就在那里。