踩过 RAG 的坑才懂:上下文比提示词重要 10 倍
做了大半年 RAG 项目,从最早的 demo 到后来上线跑真实流量,我最大的感受就是:很多人把精力全花在调提示词上,结果模型回答的质量纹丝不动。真正让效果发生质变的,是喂给模型的上下文——那些检索回来的片段、他们的排序、去噪、裁剪、组装方式。你提示词写得再花哨,上下文是垃圾,模型就只能产出垃圾。这句话听起来像玄学,但当你把检索链路一步步拆开看,会发现它其实是整个 RAG 系统里最值得投入的部分。
这篇内容适合谁?正在做知识库问答、文档助理、客服机器人这类应用的开发者,尤其是已经跑通了一个基础 RAG 流程、但对效果不满意、不知道下一步该往哪儿使劲的人。我会把自己踩过的坑、动手排查的思路、以及一套可以照着做的上下文处理流程全部拆开讲,尽量不写空话。
1. RAG 的坑,大多坑在上下文上
1.1 我在提示词上浪费过的一个月
我最初搭 RAG 的时候,和大多数人的思路一样:检索用现成的向量库,模型用通用的对话模型,剩下的全部靠提示词来约束。当时我为了让模型“只根据提供的资料回答”,把提示词写了好几版,又是加限定语,又是设计输出格式,还引入了 few-shot 示例,前前后后折腾了大概一个月。
结果呢?效果曲线几乎没有变化。偶尔几轮看起来变好了,换一批问题又打回原形。我甚至一度怀疑是模型选型有问题,换了几个模型,依然没有本质改善。后来我做了个最简单的实验:把检索到的上下文直接丢到模型里,提示词改成一句“请回答下面的问题”,发现回答质量居然和我精心调了一个月的提示词版本差不多。那一刻我才意识到,我一直在优化一个对结果影响很小的变量,而真正决定回答质量的变量——上下文本身——被我完全忽略了。
后来我复盘这件事,发现这是 RAG 项目里最典型的误区。提示词能决定模型“怎么说话”,但决定它“能说出什么”的是上下文。对一个已经具备基本对话能力的模型来说,你只要告诉它“用资料里的内容回答”,它就基本能照做;但如果资料里根本没找到答案,或者找到的是错误信息,你的提示词再怎么强调“不准编造”,它该编还是编。
1.2 检索回来的上下文,一半是噪音
另一个让我印象深刻的坑是检索质量。我当时天真地以为,向量检索是万能的,只要把文档切碎了存进去,用户问什么都能准确召回。真上线一测,发现召回的内容经常让我无语。
举一个真实的例子:我们的知识库里有一份很长的产品规格文档,里面混着参数表格、营销话术、售后条款。用户问的是“这个设备的保修期是多久”,向量检索召回了文档中的一个片段,内容是“本产品提供一年保修服务。整机保修期内,因产品质量问题导致的故障,可免费维修”,这本来是对的。但与此同时,它还召回了另一个片段,内容是“保修不包括以下情况:人为损坏、进水、摔落、私自拆机……”这两个片段一起塞进上下文,模型给出的回答就变成了“保修期一年,但人为损坏不保修,进水不保修,摔落不保修……”然后开始自由发挥。你说模型错了吗?它好像没说错,但它把一个“保修期多久”的简单问题,回答成了绕口令。
这种问题不是个例。检索结果里的噪声片段、重复片段、甚至和问题毫无关系的片段,都会像搅屎棍一样破坏模型的输出稳定性。而且你会发现:一个片段只要出现在上下文中,模型就会默认它是相关的,会下意识地去用它,哪怕它把回答带偏。上下文里信息密度越低,模型的输出就越不可控。这也是后来我逐渐意识到“上下文工程”才是 RAG 主战场的原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么上下文比提示词重要 10 倍
2.1 大模型的行为逻辑:上下文决定了知识边界
要理解“上下文比提示词重要”,得先看大模型的工作原理。模型本身是一个根据输入文本预测下一个 token 的机器,它所有的“知识”都藏在参数里,但具体到回答某个问题时,它参考的其实是当前输入窗口里的内容。提示词也是输入的一部分,但它更多扮演的是“指令”角色,告诉模型怎么组织和表达;而那些检索回来的片段,才是它真正用来回答问题的“素材”。
拿人来做类比:提示词就像是你告诉一个实习生“汇报的时候要简洁、要有条理、要基于数据”,但如果你给他的数据本身是错的,或者只有一堆无关表格,他再怎么守规矩,汇报出来的内容也没法看。上下文就是那些数据。RAG 的初衷是给模型补充外部知识、减少幻觉,但这条链路能起作用的前提是:数据源必须够干净、够相关、够组装成一条清晰的证据链。
我在实践中做过一个对比测试:同一个提示词,分别喂高质量上下文和低质量上下文,回答的准确率从 82% 掉到了 31%。而反过来,用同一个上下文,换三种不同风格的提示词,准确率波动不超过 5 个百分点。这个测试当然不能说明提示词完全没用,但至少证明了一个结论:上下文的上限,决定了整个系统效果的上限;提示词只是在逼近这个上限。
2.2 信息论视角:上下文才是信息增量的主要来源
从信息论的角度看,提示词里的绝大多数内容对模型来说都是“已知信息”——它已经在训练阶段见过了各种各样的指令模式,你换一套说法,它理解的意图基本不变。真正能给模型带来“新增信息”的,是检索返回的外部知识片段。用户问题的答案,就藏在那些片段里。
所以 RAG 系统本质上做的事情,是把“搜索结果的排序问题”转化成“生成模型的理解问题”。如果你在检索端召回的相关性就不够,排序靠前的片段不包含答案,后续再好的提示词也救不回来。我在项目中把这种问题叫作“源头失效”。源头失效的情况下,模型的回答要么是东拼西凑的幻觉,要么是含糊其辞的车轱辘话,你再怎么调提示词都没用,因为巧妇难为无米之炊。
这也是为什么我的团队后来把大部分工程时间都砸在了检索和上下文处理上。我们做过一次内部统计:上线前性能瓶颈最多的地方,100 个问题里有 60 多个是因为检索链路召回的片段质量差,剩下 20 多个是片段数量太少或覆盖不全,只有不到 20 个是模型本身生成策略的问题。这组数据让我彻底放弃了和提示词死磕。
2.3 提示词工程与上下文工程的本质区别
提示词工程解决的是“输出格式”“回答风格”“角色约束”这类问题,上下文工程解决的是“模型拿什么来回答”的问题。前者像是给厨师定了菜品的摆盘标准,后者是决定冰箱里到底有没有食材。食材都没有,摆盘规则定得再细致也是白搭。
我见过不少团队在做 RAG 时,把工作重心放在写复杂的提示词模板上,甚至引入了一堆动态变量去拼接 few-shot 示例。不是说这些没用,但它们属于“上层建筑”,应该建立在“检索和上下文已经可靠”这个基础上。否则每次效果波动,你都不知道是提示词的问题,还是检索的问题,还是模型泛化的问题。而我在实际排障时,第一个动作永远是去查这轮生成的上下文内容,而不是改提示词。这个习惯帮我省下了大量无用功,也让我慢慢摸清楚了一套稳定的上下文处理流程。
3. 上下文处理:从检索到拼接的完整实操路径
3.1 第一关:分块策略决定召回质量的上限
分块是 RAG 里最不起眼但又最容易出问题的环节。块太小,语义不完整,模型缺少上下文;块太大,向量之间互相干扰,检索精度下降。我一开始用的是固定长度分块,比如 500 个字符一刀切,方便省事,但很快暴露了问题:很多段落被从中间截断,导致查回来的片段只有半句话,信息不完整。
后来我换成了“结构化分块”,也就是优先按文档本身的层级分,比如一级标题、二级标题、段落、表格行。具体做法是:先用文档解析器把内容拆成结构化的节点,然后按节点边界聚合文本,直到达到一个合适的长度区间(我一般控制在 500 到 1000 个字符左右,具体按业务场景调)。如果某个节点本身太长,再按句子边界去切,尽量保持语义完整。
这么做的好处是,召回回来的片段往往是一个有头有尾的完整段落,而不是某句话被硬生生砍成两半。你可以想象一下:如果你在搜索引擎里搜到一条结果,结果只显示了半句话,你能看懂吗?模型也是这个道理。上下文是否完整,直接影响后续拼接和生成的质量。
3.2 第二关:检索召回不只是向量
很多人把 RAG 的检索就理解为“向量相似度 top-k”,但我在实际项目中踩了不少坑之后,越来越认同混合检索的价值。纯向量召回对新词、专业缩写、编号类问题特别不友好。比如用户问“型号是 XK-2024 的产品支持 Wi-Fi 6 吗”,如果文档里写的是“XK2024 支持 802.11ax”,纯向量可能就召回来一堆无关内容。
我现在的做法是:向量召回 + 关键词召回,然后做合并去重。关键词召回可以用现成的全文检索引擎来做,它能精确命中型号、编号、专有名词这些 token 级别的信息。合并之后,同一个文档的不同片段最好做一个按位置聚类的去重,避免模型同时听到两个高度重复的片段,产生冗余感。合并后的候选集我一般会留 20 到 30 条,然后再进入排序阶段,而不是直接取 top-5 就完事,因为前面那些只过了粗排,还有大量噪声没被过滤掉。
这一步最大的心得是:不要把向量检索当唯一召回通道。真实场景里,用户的问题千奇百怪,有时候是严格的实体匹配,有时候是语义泛化,只有两者结合才稳。
3.3 第三关:重排和过滤,把噪音挡在拼接之前
召回了 20 条候选,模型窗口只能装得下 2000 个 token,怎么取舍?这一步我强烈建议引入一个重排层,用交叉编码器对候选片段和用户问题做相关性打分,而不是直接信任向量距离。
交叉编码器会把“问题 + 片段”拼起来完整过一遍模型,输出一个相关度分数。和向量相似度那种“低维空间里的距离”相比,这种打分方式更准,因为它能看到问题和片段之间真正的语义交互。我会对重排后的结果做一个截断:相关性得分低于某个阈值的片段,直接丢弃。阈值怎么定?我的做法是从测试集里抽一批数据和“标准答案”做对照,找一个能保留住大部分有效信息、同时过滤掉大部分噪声的平衡点。一开始我定的阈值是 0.3,后来调到 0.5,准确率上去了不少,但召回略降,这个要按项目自己权衡。
重排之后,还有一个容易被忽略的动作:压缩。有的片段本身就很长,里面一大半是废话,直接把整段塞进上下文会挤占其他有效片段的空间。我会做一个轻量的抽取式压缩,把片段里的关键句保留下来,比如包含问题关键词的句子、有明确事实描述的句子,而把开头那些“随着业务的发展……”之类的装饰性内容去掉。上下文窗口是有限的,每一寸空间都要花在刀刃上。
3.4 第四关:上下文窗口的分配与组装顺序
上下文不是把所有检索结果往窗口里一塞就完事了。我见过太多开发者在代码里写“context = '\n'.join(documents[:5])”,然后完事大吉。这里面其实藏着一个很关键的问题:模型对上下文长度的不同位置有不一样的敏感度。
研究发现,很多大模型对长上下文的首尾部分注意力更集中,对中段内容的关注度会下降,这个现象通常叫“迷失在中间”。所以我组装上下文的时候,会把和用户问题最相关、信息量最大的片段放在最前面,次相关的放在最后面,中间的放一些补全性的内容。如果你只有两三条关键证据,就全部放在开头,不要让它们被一大堆次要信息淹没。
我实际使用的模板大致是这样的:
text复制以下是参考资料:
1. (最相关片段)
2. (次相关片段)
...
N. (补充片段)
请基于以上资料回答问题,如果资料中没有明确答案,请直接说明“资料中未找到相关信息”。
另外还要给指令部分和回答部分预留空间。我之前遇到过一件事:模型生成了很长一段回答,但到了末尾突然截断了,原因是把上下文窗口塞得太满,没有给输出留足够 token。后来我养成了一个习惯:上下文总长度控制在模型最大窗口的 70% 以内,剩下的空间留给输出和指令。宁可少放两段次要片段,也要保证模型能完整说完一句话。
4. 如何定位“上下文病”而不是犯“提示词病”
4.1 最小化复现:先砍掉所有花活
当你发现自己的 RAG 系统回答质量不理想,第一件事不是去改提示词,而是做一个最小化复现。具体操作是把你的提示词减到最简,只保留“请根据资料回答问题”这样的基础指令,然后手动拿一条检索结果去喂模型,看它输出什么。
这个实验能帮你快速判断问题出在哪里。如果基础指令下回答依然很烂,那大概率是上下文的问题;如果基础指令下回答还行,但你加了各种复杂约束之后反而变差了,那才需要考虑提示词本身设计不合理。我有一次排查一个“反问用户”的 bug,花了两天时间调提示词,最后发现原因是检索回来的片段里写了一堆“如果您有任何疑问,请随时联系客服”这类句子,模型照着上下文学了个反问。把这类句子从压缩环节里过滤掉之后,bug 瞬间消失。这就是典型的上下文病被误诊成了提示词病。
4.2 把上下文可视化出来,逐条对比
做 RAG 调试最痛苦的地方在于:模型是个黑盒,你看不到它“心里”在想什么。所以我会做一个简单的调试工具,把每一轮问答涉及的检索片段、来源位置、重排分数全部打印出来,然后用一个可视化界面展示“用户问题对应了哪些上下文片段”。排查时我会逐个看:
- 片段和问题在语义上真的相关吗?还是只是字面上提到了同一个词?
- 片段里是否包含明确的答案?还是只有一堆背景介绍?
- 片段时间戳是不是太老了?有些知识已经过期,模型却还在用它回答。
这个动作看着朴素,但特别有效。有一次用户问“当前版本的价格是多少”,系统召回的全是上一版本的价格信息,模型一本正经地报了旧价。线索就在上下文可视化里一眼就能发现:最新价格片段的重排分数比旧价格片段低,说明重排模型对时间敏感度不够。后来我在重排分数里加了一个“信息新鲜度”的权重,再也没出过这个错。
4.3 建立你的评估指标体系:召回率和忠实度
RAG 系统上线后,不能靠“感觉回答得还行”来评估。我会用一套三维指标来追踪:召回准确度、上下文利用度、回答忠实度。
召回准确度看的是“正确答案是否出现在了召回和重排后的上下文里”,这一步可以在不调用生成模型的情况下单独测,跑起来很快。上下文利用度看的是“模型生成回答时,是否真的用到了上下文里的关键证据”,我会拿生成结果和上下文做交叉引用,看有多少核心实体和事实点出自检索片段。回答忠实度则是直接比对最终回答和标准答案,看有没有出现额外的编造内容。
这三者之间有个递进关系:如果召回准度低,后面两环一定差;如果召回准但利用度低,可能是上下文组装时信息密度太低,模型没抓住重点;如果利用度高但忠实度低,那就要怀疑是模型在生成阶段跑偏了,这时候再去动提示词也不迟。这套指标帮我区分过很多“看起来相似但病因不同”的线上问题,让我不至于每次都靠拍脑袋改参数。
5. 一些拿得出手的细节技巧和避坑心得
5.1 给片段带上元数据,别让模型瞎猜
当一个片段被检索回来时,它常常只是一段孤立文本,模型不知道它来自哪个章节、标题是什么、发布时间是什么。这会让模型在回答时缺少“立场”。我现在会在拼接上下文时,给每个片段前面加一个标签,比如 {来源:产品手册-第三章-保修政策}。这么做的效果非常明显,模型在引用资料时会更有条理,而且当资料内部有冲突时,它也能根据来源提示去判断优先级,而不是一头雾水地把两段矛盾信息都写进回答里。
5.2 建设“反常识”负样本库
除了正向优化上下文,我还会维护一个“反常识”样本库。就是专门收集那些模型答错、答偏的用户问题,然后把当时的上下文片段和标准答案一起记录下来。这样做的目的有两个:一是用来回归测试,每次改动检索或压缩逻辑时,拿这批样本去跑一遍,看有没有把之前修好的问题又弄坏;二是可以作为负例加入重排模型的训练数据,让重排器知道什么样的片段是陷阱。这条经验是在连续两次“修好一个问题、坏掉三个问题”之后总结出来的,现在我的每一步改动都先过一遍回归样本。
5.3 警惕上下文越补越长的陷阱
还有一点,是我在实际项目里反复出现的毛病:上下文就像衣柜,总觉得多放一件进去模型就能多知道一点。但实际上,上下文越长,模型越容易“迷失”,也越容易抓不住重点。我有一次为了多覆盖一些背景信息,强行把 8000 token 的资料压进了上限为 8192 的模型里,结果指令和输出只剩一点点空间,模型每次回答都草草结束。后来我把所有“背景介绍”“注意事项”“免责声明”全部清出上下文,只保留真正和用户问题直接相关的三个片段,回答质量反而明显提升。
所以我的一个默认建议是:能用 3 个片段说清楚的事,绝不塞 5 个片段。上下文的质量,永远比数量重要。
6. 最后想分享的一点个人体会
做了这么久的 RAG,我的项目从最初一个动不动就胡说八道的 demo,变成了现在能稳定应对真实用户问题的生产级系统。回头看来,让效果产生拐点的,不是某个模型升级,也不是某个提示词模板的灵光一现,而是一次又一次对上下文的审视和打磨:分块是不是合理,召回是不是偏了,重排分数是不是可靠,拼接顺序是不是把最要紧的信息放到了前面——这些细节堆在一起,才是 RAG 效果的真正基本面。
如果你现在也正为一个 RAG 项目的效果苦恼,我建议你先别急着翻提示词魔法书。打开日志,看看你喂给模型的到底是什么。多数情况下,答案就在你贴进去的上下文里。
