news 2026/10/13 16:13:46

上下文工程实战:从线上事故到RAG与Agent的上下文管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
上下文工程实战:从线上事故到RAG与Agent的上下文管理

1. 从一个被忽视的线上事故说起

去年冬天,我参与维护的一个智能问答系统出了一次不大不小的状况。用户反馈说,同一个问题,早上问和下午问,得到的答案质量完全不一样。早上回答得条理清晰、引用准确,下午就开始胡言乱语,甚至把两个不同产品的参数混在一起。排查了半天,模型没换、知识库没动、接口也没改,最后发现问题出在一个谁都没注意的地方——上下文拼接逻辑。

具体来说,这个系统会把用户最近几轮对话、检索到的文档片段、系统提示词拼成一个长字符串喂给模型。上午流量小,检索模块返回的文档少,拼接后的上下文干净利落;下午流量一大,检索模块为了“保险”多召回了几篇文档,其中混进了不少相关性很低的噪声,再加上历史对话越积越长,模型被一堆无关信息干扰,输出质量自然断崖式下跌。

这件事让我彻底意识到一个事实:在真实项目里,决定一个AI应用好不好用的,往往不是模型本身有多强,而是你喂给它的那堆上下文组织得怎么样。这就是“上下文工程”要解决的问题。它不像提示词工程那样只关注“怎么问”,而是关注“在什么背景下问、带着哪些信息问、按什么顺序问”。这篇内容,我想把上下文工程这件事掰开揉碎讲清楚:它到底在管什么、为什么它比很多人想象的更重要、以及在实际项目里怎么落地。

如果你正在做RAG应用、智能客服、代码助手、Agent工作流,或者任何需要把外部信息塞给大模型的系统,那这篇内容应该能帮你少走不少弯路。即便你只是刚接触大模型应用开发,理解上下文工程的思路,也能让你在调试时更快定位问题。

2. 上下文工程到底在管什么:拆开“上下文”这个黑盒

2.1 上下文不等于提示词,它是一整套信息供给系统

很多人第一次听到“上下文工程”这个词,会下意识觉得它就是“提示词工程”换了个说法。其实两者有本质区别。提示词工程关注的是指令怎么写——角色设定、任务描述、输出格式约束,核心是“怎么把话说清楚”。而上下文工程关注的是模型在生成回答之前,它的“工作记忆”里到底被塞进了什么。

打个比方。提示词工程像是你给一个员工下达任务时说的话:“帮我写一份季度总结,要突出增长数据。”而上下文工程像是你把哪些资料放到了他桌上:去年的总结、本季度的报表、客户的反馈邮件、公司的模板。员工能不能写好,很大程度上取决于你桌上放的东西对不对、全不全、有没有把无关的废纸也堆上去。

从技术角度看,一个典型的大模型调用中,上下文通常包含以下几类信息:

  • 系统指令:定义角色、行为边界、输出规范。
  • 历史对话:之前几轮用户和助手的交互记录。
  • 检索内容:从知识库、数据库、文档中召回的片段。
  • 工具返回结果:Agent调用外部工具后拿到的数据。
  • 用户当前输入:这一轮的问题或指令。
  • 元信息:时间戳、用户身份、会话状态等辅助信息。

上下文工程要做的,就是决定这些信息哪些该放、哪些不该放、放多少、按什么顺序放、怎么压缩和格式化。它是一套信息供给系统,而不是一句简单的指令。

2.2 为什么模型对上下文如此敏感

要理解上下文工程的重要性,得先理解大模型处理上下文的基本机制。当前主流的大模型都基于注意力机制,模型在生成每一个词的时候,会“看”一遍上下文里的所有内容,然后分配注意力权重。这个机制有两个关键特性。

第一,注意力是有限的。虽然现在很多模型宣称支持很长的上下文窗口,比如128K甚至更长,但“支持”和“用好”是两回事。研究表明,当上下文长度增加时,模型对中间位置信息的注意力会明显下降,这就是所谓的“迷失在中间”现象。你把一堆文档塞进去,模型可能只对开头和结尾的内容敏感,中间的关键信息反而被忽略了。

第二,上下文里的噪声会主动干扰生成。模型没有“忽略无关信息”的开关,它会把上下文里所有内容都当作潜在相关。如果检索回来的文档里有一半是无关的,模型就可能把无关内容里的表述混进答案。这就像你让一个人参考资料写报告,结果桌上堆了一堆不相干的杂志,他写着写着就可能把杂志里的观点带进去。

所以,上下文工程的核心目标可以概括为一句话:在模型有限的注意力预算内,最大化有效信息的密度,最小化噪声的干扰。这句话听起来简单,但落地时涉及的决策非常多。

2.3 上下文工程和RAG、Agent的关系

有人可能会问:RAG不是已经解决了“给模型补充知识”的问题吗?为什么还要单独讲上下文工程?

RAG解决的是“从哪里找信息”的问题,它关注检索的召回率和准确率。但检索回来的东西怎么用,是上下文工程的范畴。我见过不少RAG项目,检索模块的指标很漂亮,召回率90%以上,但最终回答质量就是上不去。原因往往出在上下文拼接环节:检索回来的五个片段,有三个是重复的,有一个是过时的,还有一个格式混乱带着一堆HTML标签,模型被这些内容一搅和,输出自然好不了。

Agent场景下这个问题更突出。Agent会多轮调用工具,每一轮的工具返回结果都会进入上下文。如果不做管理,上下文会迅速膨胀,几轮之后模型就被大量中间结果淹没,开始忘记最初的目标。所以上下文工程在Agent系统里几乎是生死攸关的一环。

3. 上下文超载:为什么“多给点信息”反而是灾难

3.1 注意力稀释的实测表现

我在一个文档问答项目里做过一组对比测试,想看看上下文长度对回答质量的影响。测试问题是关于某产品保修政策的,知识库里有一段200字左右的精准描述。我设计了三种上下文配置:

配置上下文内容总长度回答准确率
A仅精准片段约300字96%
B精准片段+2篇相关文档约1500字91%
C精准片段+8篇检索文档约5000字73%

结果很说明问题。当上下文从300字膨胀到5000字时,准确率掉了23个百分点。而且错误类型很有意思:模型不是完全找不到答案,而是把其他文档里的相似但不同的条款混了进来。比如把另一款产品的保修期限当成了目标产品的。

这就是注意力稀释的典型表现。上下文越长,每个token分到的注意力权重越低,关键信息被“淹没”的概率越大。更麻烦的是,这种错误很隐蔽,模型输出的句子读起来通顺合理,不仔细核对根本发现不了。

3.2 噪声的三种典型来源

在实际项目里,上下文噪声主要来自三个地方,每一个都值得单独拿出来说。

第一种是检索噪声。检索模块为了保召回,往往会返回一批相关性参差不齐的文档。向量检索尤其容易出这个问题,因为语义相似不等于真正相关。比如用户问“退款流程”,检索可能返回“退货流程”“换货流程”“支付流程”的文档,它们语义上都很接近,但内容完全不同。如果不做二次筛选,这些都会进入上下文。

第二种是历史对话噪声。多轮对话里,早期的对话内容可能已经和当前问题无关了。比如用户先问了一个产品功能问题,聊了五轮之后转去问账单问题,但前面五轮的功能讨论还留在上下文里。这些内容不仅占位置,还可能让模型误以为当前问题仍然和功能相关。

第三种是格式噪声。从网页、PDF、数据库里抓来的内容,往往带着大量无关的格式信息:HTML标签、导航栏文字、页脚版权声明、表格线、乱码字符。这些东西对模型理解内容毫无帮助,纯粹是干扰。

3.3 一个反直觉的结论:少即是多

基于上面的分析,可以得出一个反直觉但非常重要的结论:在上下文工程里,删减信息往往比增加信息更能提升效果。很多团队的第一反应是“检索不够就多召回一些”,但正确的思路应该是“先保证召回的每一条都是精品,再考虑数量”。

我现在的习惯是,在上下文拼接之前加一道“准入检查”:每一条候选内容都要回答三个问题——它和当前问题直接相关吗?它提供了当前上下文里没有的新信息吗?它的表述清晰、格式干净吗?三个问题有一个答不上来,就不放进去。这道检查看起来简单,但能砍掉大量噪声。

提示:不要用“宁可多给不可漏给”的思路做上下文。模型不是人,它不会自动忽略无关内容。你给它的每一条信息,它都会认真对待,包括那些你希望它忽略的。

4. 上下文工程的核心操作:筛选、排序、压缩、格式化

4.1 筛选:把“可能相关”变成“确实相关”

筛选是上下文工程的第一道关口。检索模块返回的结果通常是一个按相似度排序的列表,但这个列表不能直接拿来用。我一般会做两层筛选。

第一层是阈值筛选。给相似度设一个下限,低于这个值的直接丢弃。阈值定多少需要根据实际数据调,我通常从0.7开始试,然后看丢弃的内容里有没有真正有用的。如果发现有用内容被误丢,就适当降低;如果发现留下的还是有很多无关的,就提高。

第二层是去重和多样性筛选。检索结果里经常有内容高度重复的片段,比如同一段话在不同文档里出现。这些重复内容除了浪费位置没有任何价值。去重之后,还要考虑多样性——如果留下的五条内容都在讲同一个方面,那不如换成三条讲不同方面的。我常用的一种做法是,对候选内容做聚类,每个簇里只保留最相关的一条。

# 一个简化的筛选逻辑示意 def filter_context(candidates, query, threshold=0.7, max_items=5): # 第一层:相似度阈值 filtered = [c for c in candidates if c.score >= threshold] # 第二层:去重(基于内容指纹) seen = set() unique = [] for c in filtered: fingerprint = hash(c.content[:200]) if fingerprint not in seen: seen.add(fingerprint) unique.append(c) # 第三层:多样性(简单版:限制同一来源的数量) source_count = {} result = [] for c in unique: if source_count.get(c.source, 0) < 2: result.append(c) source_count[c.source] = source_count.get(c.source, 0) + 1 if len(result) >= max_items: break return result

这段逻辑不复杂,但效果立竿见影。在一个客服问答项目里,加上这层筛选后,上下文平均长度从3000字降到1200字,回答准确率反而提升了8个百分点。

4.2 排序:把最重要的信息放在模型最敏感的位置

前面提到过“迷失在中间”现象,模型对上下文开头和结尾的内容注意力最高,中间部分容易被忽略。这个特性直接影响了上下文的排序策略。

我的做法是:把最关键的指令和约束放在最前面,把最相关的检索内容放在最后面,中间放次要的辅助信息。具体来说,系统提示词和输出格式要求放开头,当前用户问题放结尾附近,检索到的核心文档片段紧挨着用户问题,历史对话和其他辅助信息放中间。

这个顺序不是拍脑袋定的。开头放指令是因为模型需要先明确任务边界;结尾放核心内容是因为生成是从后往前“看”的,靠近生成位置的内容影响最大。中间部分虽然注意力低,但也不是完全没用,放一些背景信息刚好合适。

还有一个细节:如果有多条检索内容,按相关性从低到高排列,最相关的放在最靠近用户问题的位置。这样模型在生成时,最后看到的就是最相关的内容。

4.3 压缩:在不丢关键信息的前提下缩短长度

压缩是上下文工程里技术含量最高的环节。目标是把长内容变短,但关键信息不能丢。我常用的有三种方法,各有适用场景。

第一种是抽取式压缩。从原文里挑出最关键的句子,拼成摘要。这种方法适合结构清晰的文档,比如产品说明、政策条款。实现上可以用关键词匹配或者小型模型做句子打分,选出得分最高的几句。优点是信息保真度高,缺点是可能丢失句子之间的逻辑关系。

第二种是生成式压缩。用一个小模型把长文档改写成短摘要。这种方法适合叙述性内容,比如用户反馈、会议记录。优点是灵活、可读性好,缺点是有引入错误的风险,需要校验。我一般会要求压缩后的内容必须保留所有数字、日期、专有名词,这些是最容易出错也最不能错的地方。

第三种是结构化压缩。把非结构化内容转成表格或键值对。比如把一段产品描述转成“型号、价格、保修期、主要功能”这样的字段。这种方法压缩率最高,而且格式统一,模型理解起来也容易。缺点是只适合信息本身结构化的内容。

压缩方法适用内容压缩率风险
抽取式政策、说明文档中等丢失逻辑关系
生成式叙述性内容高引入错误
结构化产品信息、参数最高仅适合结构化内容

实际项目里,这三种方法往往是组合使用的。比如先做结构化抽取,把能结构化的部分转成表格,剩下的叙述性内容再做生成式压缩。

4.4 格式化:让模型“一眼看懂”而不是“费力解析”

格式化经常被忽视,但它对效果的影响不小。同样一段信息,用不同的格式呈现,模型的利用效率差别很大。

我总结了几条格式化原则。第一,用清晰的分隔符区分不同来源的内容。比如用“---”或者“【文档1】”这样的标记,让模型知道每段内容的边界在哪里。第二,给每段内容加上简短的标题或标签,说明它是什么、来自哪里。第三,统一格式风格,不要一段是Markdown、一段是纯文本、一段是JSON,模型在格式切换时容易出错。第四,去掉所有无关的格式字符,HTML标签、多余空格、特殊符号,能清就清。

【系统指令】 你是一个产品客服助手,只根据下方提供的资料回答问题。如果资料中没有答案,直接说“我没有找到相关信息”。 【历史对话摘要】 用户之前询问了产品A的保修政策,已告知标准保修期为一年。 【参考资料】 [文档1 | 来源:产品手册 | 相关度:高] 产品A的保修期为自购买之日起12个月。保修范围内包括非人为损坏的硬件故障。 [文档2 | 来源:售后政策 | 相关度:中] 延长保修服务需在购买后30天内申请,费用为产品价格的15%。 【用户问题】 保修期内维修需要付费吗?

这种格式看起来简单,但比把一堆内容直接拼接在一起效果好得多。模型能清楚知道每部分内容的角色,生成时也更不容易混淆。

5. 多轮对话与Agent场景下的上下文管理

5.1 历史对话不是越长越好

多轮对话场景下,历史对话的管理是个老大难问题。最朴素的做法是把所有历史都带上,但这样上下文会线性增长,几轮之后就没法看了。我见过一个客服系统,用户聊了二十轮之后,上下文里塞了上万字的历史记录,模型完全抓不住重点。

我的处理策略是分层管理。最近三轮对话保留完整原文,因为这部分和当前问题最相关。三轮之前的对话做摘要,只保留关键信息,比如用户提到的产品型号、已确认的需求、已经排除的选项。更早的对话直接丢弃,或者只保留一个极简的会话状态描述。

摘要的粒度需要控制。太细了等于没压缩,太粗了会丢关键信息。我的经验是,每轮对话摘要控制在一两句话,只保留“谁说了什么关键信息”,去掉寒暄、重复确认、无关闲聊。

5.2 Agent工具调用结果的上下文处理

Agent场景比普通对话更复杂,因为每一轮工具调用都会产生新的返回结果。如果不做管理,上下文会被工具返回的原始数据迅速填满。

我的做法是工具结果即时摘要。工具返回结果后,不直接把原始数据放进上下文,而是先做一次提取:这次调用得到了什么关键信息?和当前任务目标的关系是什么?提取出来的摘要才进入上下文,原始数据存在外部存储里,需要时再查。

举个例子。一个Agent在帮用户查航班,调用了航班查询接口,返回了20个航班选项。直接把20个选项塞进上下文,模型很难处理。更好的做法是提取关键信息:“查询到20个航班,最早一班是早上7点,最晚一班是晚上10点,价格区间在800到1500元,其中3班有折扣。”这样模型能快速理解结果,需要具体信息时再按需查询。

5.3 上下文窗口的预算分配

把上下文窗口想象成一个固定大小的预算,每一类信息都要从中分一杯羹。我的分配习惯大致是这样的:

  • 系统指令和格式约束:10%
  • 当前用户问题:5%
  • 核心检索内容:40%
  • 历史对话摘要:20%
  • 工具结果摘要:15%
  • 预留缓冲:10%

这个比例不是固定的,要根据任务类型调整。比如纯问答任务可以给检索内容更多预算,Agent任务则需要给工具结果留更多空间。关键是要有预算意识,不能任由某一类信息无限膨胀。

注意:不同模型对上下文长度的实际处理能力差异很大。标称128K的模型,在实际任务中可能超过32K就开始明显退化。建议在自己的任务上做长度测试,找到效果开始下降的临界点,以此作为预算上限。

6. 上下文工程的评估与迭代

6.1 怎么判断上下文质量好不好

上下文工程做得好不好,不能只看最终回答的准确率,因为准确率受太多因素影响。我一般会从三个维度单独评估上下文质量。

第一个维度是信息覆盖率。回答当前问题所需的关键信息,在上下文里出现了吗?这个可以通过人工标注或者用模型做检查。如果关键信息没出现,那是检索的问题;如果出现了但回答没用上,那是上下文组织的问题。

第二个维度是噪声比例。上下文里有多少内容和当前问题无关?这个比例越高,模型被干扰的风险越大。我通常会把上下文里的每条内容都过一遍,标记相关或不相关,算出噪声比例。健康的值应该在20%以下。

第三个维度是信息密度。单位长度的上下文里包含多少有效信息?这个比较主观,但可以通过对比不同配置下的回答质量来间接衡量。如果压缩一半长度后回答质量没下降,说明原来的密度太低。

6.2 一个可复用的迭代流程

上下文工程的优化不是一锤子买卖,需要持续迭代。我总结了一个四步流程,在实际项目里反复用。

第一步,建立基线。用当前最朴素的上下文拼接方式跑一批测试问题,记录回答准确率和上下文长度。这是后续优化的参照点。

第二步,定位瓶颈。分析错误案例,看问题出在哪个环节。是检索没召回?是筛选太严?是排序不对?还是压缩丢了信息?把错误归类,找到最主要的瓶颈。

第三步,单点优化。每次只改一个环节,比如只调整筛选阈值,或者只改排序策略。改完重新跑测试,看指标变化。这样能清楚知道每个改动的影响。

第四步,固化配置。找到最优组合后,把参数和逻辑固化下来,形成配置模板。后续新任务可以基于这个模板快速起步。

这个流程看起来笨,但比凭感觉乱调靠谱得多。我见过太多团队在上下文工程上“拍脑袋”,今天加个规则明天删个规则,最后谁也说不清为什么效果好或不好。

6.3 常见误区与避坑清单

在上下文工程这件事上,我踩过的坑和见过的坑都不少,挑几个最有代表性的说说。

误区一:把上下文窗口当存储用。有人觉得既然模型支持长上下文,那就把所有能拿到的信息都塞进去。这是最典型的错误。上下文窗口是工作记忆,不是硬盘。该放外部存储的放外部存储,需要时再取。

误区二:一套配置打天下。不同任务对上下文的需求完全不同。问答任务需要精准的检索内容,创作任务需要丰富的背景素材,Agent任务需要清晰的状态跟踪。用同一套拼接逻辑处理所有任务,效果必然打折。

误区三:只关注内容不关注顺序。同样的内容,换个顺序效果可能差很多。尤其是当上下文里有相互矛盾的信息时,模型倾向于相信更靠后出现的内容。所以关键信息的位置需要刻意设计。

误区四:忽略格式的一致性。格式混乱会增加模型的解析负担,消耗本就不多的注意力。保持格式统一、干净,是性价比很高的优化。

误区五:不做长度测试。每个模型在不同任务上的有效上下文长度都不一样。不测试就不知道自己的系统在什么长度开始退化,也就无法合理分配预算。

7. 我在实际项目里沉淀的几条经验

聊了这么多原理和方法,最后分享几条我在实际项目里摸爬滚打出来的经验,都是些文档里不会写、但确实管用的东西。

第一条,上下文拼接逻辑要可观测。我现在的习惯是,每次模型调用都把完整的上下文存下来,带上请求ID。出问题时能直接回看当时喂了什么,比事后猜高效得多。这个习惯帮我省了无数次排查时间。

第二条,给上下文加“版本号”。上下文拼接逻辑一变,效果就可能变。我会给每套拼接配置打上版本号,记录变更内容和对应的效果指标。这样能清楚知道哪个版本效果最好,回滚也有依据。

第三条,压缩环节一定要做校验。生成式压缩有引入错误的风险,我一般会在压缩后加一道检查:压缩前后的关键实体(数字、日期、专有名词)是否一致。不一致就回退到抽取式压缩或者保留原文。

第四条,不要迷信“最优解”。上下文工程没有放之四海皆准的最优配置,只有最适合当前任务和数据分布的配置。别人的经验可以参考,但一定要在自己的数据上验证。

第五条,留出人工干预的口子。再好的自动化逻辑也可能出错,我会在系统里留一个手动调整上下文的入口。遇到疑难case时,人工调整一下上下文,往往能快速解决问题,同时也能积累优化线索。

这些经验听起来都不复杂,但每一条都是踩过坑之后才总结出来的。上下文工程这件事,说到底就是对模型的工作记忆保持敬畏,精心管理每一条进入其中的信息。模型能力再强,喂进去一堆垃圾,出来的也只能是垃圾。反过来,把上下文组织好了,中等能力的模型也能给出让人满意的回答。这个投入产出比,值得每个做AI应用的人认真对待。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 16:08:50

ASP环境JSON解析库JSON_2.0.4完整版实战指南

简介&#xff1a;本资源是面向ASP与Visual Basic开发者的JSON数据处理工具包&#xff0c;专为Web后端动态页面开发及VB桌面应用中JSON序列化/反序列化需求设计&#xff0c;解决传统ASP环境缺乏原生JSON支持、VB项目手动解析效率低等实际问题。压缩包共4个文件&#xff0c;含2个…

作者头像 李华
网站建设 2026/10/13 16:08:31

CATIA V5R13汽车线束设计全流程:从原理图到展平出图的避坑指南

简介&#xff1a;CATIA V5R13汽车线束讲解是一份面向CATIA电气初学者及汽车线束设计人员的PPT课件&#xff0c;系统介绍电气安装&#xff08;Electrical Harness Installation&#xff09;工作台中的线束建模流程&#xff0c;包括几何线束组与线束段的定义、分段、联结管理、支…

作者头像 李华
网站建设 2026/10/13 16:08:20

主成分分析MATLAB源代码:带注释可运行的降维方案

简介&#xff1a;数据降维是机器学习和数据分析中常用的预处理手段&#xff0c;主成分分析&#xff08;PCA&#xff09;通过协方差矩阵的特征值分解&#xff0c;将高维数据映射到方差最大的低维空间&#xff0c;在保留核心信息的同时消除特征间的相关性。对于使用MATLAB的工程师…

作者头像 李华
网站建设 2026/10/13 16:08:00

医学图像分割四模型实战:Unet/AttentionUnet/R2Unet/R2AUet全栈复现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华