“提示词写得再漂亮,检索不到对的上下文,模型照样一本正经地胡说八道。”这句话是我把 RAG 项目从“演示能跑”做到“线上能用”之后,最大的体会。去年我做了一个面向内部知识库的问答系统,最初一个月几乎都在调提示词——系统提示词改了几十个版本,各种角色设定、格式约束、少样本示例全都试过,结果客户丢一个稍微偏门的业务问题过来,系统依然答非所问。后来我才意识到,问题根本不在提示词,而在喂给模型的上下文。同样的提示词,不换模型、不改参数,仅仅把检索回来的上下文从“乱七八糟”换成“精准有效”,回答准确率直接从 62% 涨到了 89%。这篇内容就是把这段过程里踩过的坑、验证过的方法、以及最后沉淀下来的一套上下文构建流程完整铺开,希望能帮正在做 RAG 的人少走几个月的弯路。
1. 提示词很香,但它救不了烂上下文
1.1 我的第一版 RAG 是怎么翻车的
我做知识库问答的起因很简单:团队里积累了上千份产品文档、故障处理记录和项目复盘,每次新同事入职,都要花几周时间到处找人问“这个东西在哪个文档里”。当时大模型很火,搭建 RAG 的技术栈也成熟——向量库、嵌入模型、大模型接口都有现成的,于是我们很快搞出了第一版。
这一版的结构看起来非常“标准”:文档按固定长度切片,转成向量存进向量库,用户问题来了之后,取出 Top 4 个片段拼进提示词,交给大模型生成答案。上线第一天,我们拿了一批真实问答去测试,结果让我很尴尬。
比如有人问“服务器重启后服务起不来怎么办”,系统回答的是“建议检查配置文件的格式是否符合规范”——这个回答不能说全错,但完全没有命中“重启后起不来”这个特定场景,等于一句正确的废话。更离谱的是,问“A 系统和 B 系统之间的数据同步失败”,系统把两个完全不相关的文档片段拼在一起,给了一个看起来条理清晰、实则是幻觉的排查步骤。
当时我的第一反应是:提示词有问题。于是我开始调提示词,加限定条件、加输出格式、加“如果你不知道就直说”的约束,甚至尝试让模型先“思考”再回答。折腾了两周,准确率从 62% 提升到了 66%,边际收益越来越低。一次技术评审会上,一个同事问了一句很扎心的话:你们测试的时候,有没有人看过检索出来的上下文原文?我愣住了。
1.2 问题不在模型,在喂进去的东西
后来我把那些答错的 case 翻出来,逐个去看系统实际检索到的上下文到底长什么样,看完之后整个人都不好了。
第一种情况是检索结果不相关。用户问“如何修改数据库的密码”,系统检索回来的 Top 4 片段里,有三个讲的是“数据库备份策略”。从向量相似度看,这两个话题在语义上确实离得不远,都属于数据库运维,但它们根本不是用户要找的内容。
第二种情况是检索结果相关,但信息残缺。有一个片段确实提到了密码修改,但只涵盖了“修改密码后需要重启服务”,恰恰缺少了最关键的一步“在哪里执行修改命令”。模型拿到这个残缺信息,再加点自己的想象,就生成了错误的步骤。
第三种情况是片段之间互相矛盾。有两份文档,一个是旧版本的操作手册,一个是新版本的操作手册,内容冲突。系统把这两个片段一起塞进上下文,模型左右为难,最后给出一个两全其美的错误建议。
这些问题有一个共同点:提示词根本管不了。提示词只能告诉模型“回答的时候要基于上下文”,但无法保证上下文里真的有答案、答案是完整的、答案之间不冲突。模型为了完成回答这个任务,会基于上下文里的碎片信息做“合理推测”,而推测往往会出错。
这个教训用一句话概括就是:检索质量决定 RAG 质量的上限,生成能力只是在下限之上做裁剪。从那以后,我在 RAG 项目里的精力分配彻底变了——提示词可能只占 5% 的优化时间,剩下 95% 的时间全都扑在上下文构建上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 上下文是什么,为什么它决定 RAG 的生死
2.1 RAG 里上下文的完整链路
在 RAG 系统里,“上下文”从来不是一个单一的东西,它是一条完整的链路,任何一环出问题都会直接反映在最终答案上。
我把这条链路分为四个环节。第一个是数据入库环节,包括文档的解析、清理、分块、向量化。这一步决定了下游检索能捞到什么候选内容。第二个是召回环节,也就是根据用户问题去向量库里找出最相关的候选片段,顺便带上一些关键词匹配作为补充。第三个是重排和精筛环节,把召回的结果重新打分,压缩冗余内容,确保进入提示词的片段是真正高价值的信息。第四个才是提示词组装,把系统指令、用户问题、筛选后的上下文、对话历史拼接成最终的请求。
很多人做 RAG 的时候,眼睛只盯着第四个环节——提示词写得好不好、模板怎么设计、要不要加 few-shot 示例。但实际发生的逻辑顺序是前三个环节先决定“模型能看到什么”,提示词只决定“模型怎么看待看到的内容”。视野都不对,看待方式再巧妙也白搭。
我做了一个很简单的类比:RAG 系统像一个带资料库的实习生。提示词是你对这个实习生的叮嘱——“回答之前先查资料,引用资料里的内容,不要自己乱编”。但是如果实习生手里拿到的资料是一堆错页码、缺章节、相互矛盾的复印件,那再多的叮嘱也没用。上下文就是递给实习生的那叠资料,资料烂了,叮嘱就是空话。
2.2 上下文出错的三种典型模式
在踩坑过程中,我把 RAG 上下文的问题归纳成三类,几乎可以覆盖 90% 以上的错误案例。
第一类叫“没有上下文”。也就是检索出来的片段和用户问题基本对不上,模型只能凭训练数据里的泛化知识来回答。这种场景下,模型给出的答案往往是“看似专业但答非所问”,因为大模型很擅长生成通顺、严谨但实际不针对问题的内容。
第二类叫“上下文碎片化”。检索到的每个片段单独看都有点相关,但拼起来信息不完整。比如用户问“如何回滚版本”,文档里的操作步骤被硬切到了两个不同的片段里,模型只看到了“备份当前版本”和“重启服务”,中间最关键的执行命令在片段边界处被拆掉了。这种情况下模型会用想象力补全缺失步骤,补对了算运气,补错了就是灾难。
第三类叫“上下文污染”。多个片段来自不同的文档、不同的时间版本、不同的目标客户,它们之间互相矛盾,或者包含大量噪声。模型无法判断哪个是对的,最后可能会然输出互相矛盾的结论,甚至在大多数情况下直接忽略正确的片段,被错误信息带跑偏。
这三类问题,如果你只看生成的答案,会觉得是“模型能力差”或者“提示词约束不够”。但实际上,只要把检索返回的片段拿出来看一眼,立刻就能定位到问题根源。这也是我想给所有做 RAG 的人一个最核心的建议:下次系统答错了,别急着改提示词,先去查它到底检索了什么。
3. 让检索出来的片段“能用”:四个关键动作
3.1 分块策略:不是所有文档都适合固定字符数
我最早做文档切片的时候,用的方案很省事:按照固定字符数切,每块 500 字,重叠 50 字。这个方案对纯文本、结构简单的内容还行,但换到复杂文档上,问题立刻暴露。
有一次我把一份操作手册拿去建知识库,手册里每个步骤都有编号,且步骤之间有明确的先后依赖关系。固定字符切割恰好把“3.2 配置参数”和“3.3 启动服务”从中间切开,检索的时候,用户问“配置完参数后需要做什么”,系统匹配到了“3.2 配置参数”的后半段——里面包含了参数表,但完全没有提到下一步启动服务,于是答案非常干瘪。
后来我换了一套策略:优先按文档结构切,再用字符数兜底。具体做法是先用文档解析工具把标题和段落结构提取出来,遇到明显的章节层次就按章节切,章节太长的话再按段落或者小节切,实在不行才走固定字符切。同时把标题、章节号作为元数据存进向量库,检索结果返回时带上这些元数据,拼进上下文的时候不仅内容更完整,还能告诉模型“这段内容来自哪个章节”,让回答更有条理。
还有一个之前被忽略的点:分块大小跟嵌入模型也有关系。不同嵌入模型能编码的 token 上限不一样,如果块太长被截断,后面的内容等于没编码进去,检索时自然会丢信息。我建议统一用嵌入模型的 tiktoken 或者 tokenizer 统计一下每个块的实际 token 数,确保远远低于模型上限,留出余量。
3.2 向量化:嵌入模型选型和字段选择
嵌入模型这一步,我踩过一个很多人都会踩的坑:直接用了某个通用领域的中文嵌入模型,没有做任何评测。后来发现,我那个知识库里的文档充满专业缩写和混合中英文术语,通用模型在某些专业术语上的语义理解很差,导致检索结果飘忽不定。
解决方法是构建一个小的评测集,用业务里的真实问题去跑检索,看召回质量。评测集不需要很大,五十个问题就够,关键是覆盖典型场景和不典型场景。把每个问题对应的正确文档片段标出来,然后用命中率来量化对比不同的嵌入模型。我试过几款模型之后,发现专门针对领域微调过的模型,或者语义维度更丰富的 multilingual 模型,效果明显更好。
除了模型本身,检索字段的选择也很关键。早期我把整个文档内容全部向量化,正文、表格、页眉页脚全部混在一起。后来我把“标题”和“正文”分开处理,甚至单独把一些重要的摘要、结论抓出来单独建索引。这样的话,若用户问题恰好匹配标题,召回纯度会大幅提升。
我建议做这样一个拆分:将文档中的一个片段同时保存“主文本”“副文本”和“元数据”。主文本用于向量化检索,副文本用于标注上下文,元数据用于过滤。最终进入提示词的内容,可以通过“主文本 + 标题 + 来源文档”重组,这样既保证召回命中,又保证生成时能理解结构。
3.3 检索前后置:关键词召回与混合检索
纯向量检索在很多时候并不够用。尤其是用户问题包含了某些不可能被语义理解代替的精确词汇时,比如工单编号、错误码、型号名称,向量检索很容易把这些“标识符”当成普通语义来编码,导致召回片段不精准。
我在一次排查里发现,用户问“关于 ERP-2024-0923 这个单子的处理进度”,系统的向量召回结果里反而出现了“ERP 系统整体介绍”这样的泛化文档。那个具体单号根本没有在结果里出现。原因是嵌入模型对于长数字串的语义编码不够敏感,没能把“ERP-2024-0923”当成一个整体去匹配。
这个问题的解法是混合检索。在向量召回之外并行加一路关键词召回,用 BM25 或类似算法做字面匹配,专门抓住精确标识符。然后把两路结果合并去重,再统一进入重排阶段。这个改造在客服问答场景里特别有效,因为用户提问时常常会带上编号、型号、日期这些精确信息。
我搭混合检索的时候还做了一个小技巧:对用户问题做一个简单的规则解析,如果检测到类似单号、版本号、错误码的 pattern,就把这个 pattern 单独提取出来,放到关键词检索那一路;如果没有检测到,就完全走向量语义那一路。这样能避免关键词检索比例太高时召回到一堆不相关的内容。
3.4 重排和压缩:把真正的答案挤到前排
最早我直接把向量召回 Top 4 片段拼进提示词,结果经常发现,真正的答案在 Top 5 或者 Top 6,而排在前面的片段只是看起来相似。这是因为向量相似度衡量的是整体语义相关,而不是“是否包含具体答案”。一个片段可能通篇都在讲“密码策略”,但用户问的是“怎么重置密码”,两者整体语义相关,却答非所问。
解决这个问题需要引入重排模型。重排模型会以用户问题为基准,对召回结果逐条进行精确相关性打分,比向量相似度靠谱得多。我选了一个轻量级的 rerank 模型,跑在 GPU 上延迟不到 20ms,把原来 Top 4 的召回范围扩大到 Top 20,然后用重排模型选 Top 3 进上下文。效果立竿见影,准确率又涨了一截。
重排之后,还要做上下文压缩。Top 3 片段如果每个都有 500 字,拼起来 1500 字,可能会超出上下文窗口,或者让模型抓不住重点。这个时候有很多方法:可以按句子拆分后抽取跟问题最相关的句子;也可以通过 LLM 做一个压缩,只保留关键内容。我的方案是先用规则抽关键句,例如保留包含问题关键词、结论词、步骤序号的句子,其他句子可以用省略号代替。这个规则策略省时省力,而且不增加额外的模型调用延迟。
4. 实操记录:一个客服问答系统从“答非所问”到“基本靠谱”
4.1 改造前的效果基准
为了不让整个优化过程停留在“感觉”,我把当时的改造全程记录了下来。测试环境是一个内部客服问答系统,故障报修相关的知识库大约有两百份文档,包括产品手册、故障处理方法、升级说明以及历史工单。
改造前,我用 60 个真实问题做了一次基准测试,问题类型覆盖“具体操作”“故障排查”“工单状态”“配置参数”四大类。生成评测分三类:完全正确、基本正确(过程方向对但细节有误)、错误。当时的结果是:完全正确 22 个、基本正确 15 个、错误 23 个,综合可用率约 62%。其中有 11 个问题答案里包含明显的幻觉信息,比如编造不存在的报错代码。
这个数据就是当时给团队汇报的“优化前基线”。之后我告诉自己,后续所有改动,不管听起来多酷,都必须用这 60 个问题重新跑一遍,不允许用“主观感觉变好了”作为结论。
4.2 改动清单和参数设置
紧接着我按照前面梳理的思路,分四步对系统进行了改造。
第一步是重构文档入库流程。把所有文档拆成人话结构化的段落,按章节分块,每个块控制在实际 300-600 token 之间,同时把标题、归属文档、版本号作为元数据保留。表格类和列表类文档单独建索引,不再和正文混切。
第二步是换嵌入模型。我对三个候选模型在评测集上做了对比,最终选中了对中英混合专业术语理解更好的那个,同时对专业缩写做了一个小型的术语扩展词典,在向量化之前先做一次术语归一化。
第三步是引入混合检索。在保留向量检索的同时,增加 BM25 关键词通道。对用户问题做规则解析,识别编号、型号、错误码等精确模式并送入关键词通道。两路结果合并去重,取 Top 20 进入重排。
第四步是加入重排模型,并做上下文压缩。重排后取 Top 3 片段,对每个片段抽取出与问题最相关的句子,拼接成 800 token 左右的上下文,再交给生成模型。
提示词在这一轮里几乎没动,系统提示词还是原来那版。唯一的变化是把上下文的格式稍微规范化了一下,加了一个“相关片段来源”的标注,方便模型理解信息的出处。
4.3 改造后的效果对比数据
同样的 60 个问题,同样的评测标准,改造后再跑一遍的结果是:完全正确 41 个、基本正确 13 个、错误 6 个,综合可用率约 90%。幻觉信息出现次数从 11 次降到 2 次。整体回答的完整性也有了明显变化:之前经常出现的“答案太短、像在敷衍”的情况基本消失,大多数回答会带上具体步骤和出处提示。
单独看四个分类的表现:具体操作类从 55% 的可用率涨到 92%,故障排查类从 58% 涨到 88%,工单状态类提升最明显,从 40% 涨到 95%,因为这一类问题里的关键词命中大幅受益于混合检索;配置参数类从 70% 涨到 91%。
我没有做任何提示词层面的复杂改动,也没有换更“聪明”的生成模型。整个过程的变量只有上下文链路。这组数据比我主观看任何结果都更有说服力——当上下文对了,一个普通提示词的潜力远比之前充分。
5. 常见坑和排查方法速查
5.1 “模型不听话”先检查上下文
很多人在调试 RAG 的时候,习惯性地把“回答不符合预期”归因于模型不听话。实际上,大模型在绝大多数情况下都是听话的,它只是基于给到的上下文和指令,做出了最合理但不一定正确的回答。
我的排查顺序是这样:先查检索环节。打印出这次请求实际召回的前 5 个片段,看看片段与问题的相关性、信息完整性。如果检索结果明显不靠谱,就去分块、向量、混合检索里找原因。如果检索结果不错,再判断是不是上下文组装时出了问题,比如片段顺序不合理、关键信息被压缩掉了。最后才回头看提示词。遵循这个顺序,大部分问题都能在十分钟内定位。
5.2 检索结果看起来相关但答案不对
这种是最迷惑人的:片段里确实提到了用户问的东西,但模型给出的答案就是不对。十有八九是因为片段里“有相关词”但“没有完整逻辑链”。
比如用户问“如何彻底删除一个用户”,片段里有一句话“管理员可以通过命令删除用户”,但后续步骤、权限要求、注意事项都在另一个片段里。模型拿到了半句话,就顺着自己的理解把步骤补全了。解决办法就是重排之后做“信息完整性检查”,查看片段是否覆盖了操作的完整步骤链,必要时可以多取一个片段,或者把文档的同级章节一起进入上下文。
5.3 上下文太长造成幻觉
上下文越长,模型对关键信息的注意力越容易被稀释。尤其是把 Top 20 个片段全塞进提示词的做法,我强烈不建议。就算模型上下文窗口能装下,质量也会急剧衰减,因为模型不知道该重点参考哪段。
我实测下来的安全区域是:上下文里的检索片段控制在 3 到 5 个,总 token 数根据生成模型的窗口留出至少三分之一的余量。如果生成的回答经常出现“前后矛盾”,大概率是上下文里塞了太多互相独立的片段。压缩、重排、抽取,要舍得删。
5.4 多轮对话上下文污染
客服场景里,用户往往会连续问好几个问题,尤其是追问场景。“那如果改完密码还是连不上怎么办”——这里的“那”指的是前文里的密码问题。如果系统只拿当前的这个问题去做检索,很容易把上一轮的正确语境丢掉。
我的做法是:每一轮检索前,先用大模型做一次简短的“对话改写”,把这轮的追问改写成一个完整的独立问题,再拿改写后的问题去做检索。这个改写过程很短,只输出改写的疑问句,不输出其他东西,速度和成本都完全可控。改完之后再拼接前几轮的摘要上下文,效果明显比“只检索当前问题”好得多。
6. 几条压箱底的实操心得
整个项目做下来,我最想分享的不是某个具体参数,而是几个观念层面的收获。
第一,RAG 项目要尽早建立评测集和评测流程。没有评测集,所有优化都是感觉。我建议从第一天就把真实问答整理成评测集,哪怕只有二十个问题也好,每次改动都跑一遍,留存记录。评测集是最好的调试帮手,也是避免“改 A 坏 B”的唯一办法。
第二,上下文构建需要用工程思维,而不是模型思维。做 RAG 的人容易把精力放在“模型聪不聪明”上,但实际上,真正决定系统水平的恰恰是那些不太性感的环节——分块是否合理、元数据是否完整、关键词和向量是否配合得好。这些活很琐碎,回报率却极高。
第三,警惕“优化幻觉”:很多时候,模型表现变好不是因为某个技术改动真的起作用了,而是因为测试数据碰巧变了。所以修改评估集本身也要保存版本,尽量用固定的高质量评测集,来约束模型行为。
第四,落地上线之前,一定要去真实场景里挖案例。我在一次预发布阶段,特意找业务运营的同事闲聊,让 TA 们随口问了几个平时会遇到的奇怪问题。果然,立刻发现了几个评测集里没有覆盖到的边界场景。后来把这些场景补进评测集,系统又改进了不少。
最后再说一个小技巧:给每条检索片段加一个“置信度来源”元数据,比如文档更新时间、历史故障等级。当系统拿不准时,优先展示灰度来源的片段,并让模型在回答里标注“该信息来自较早版本,以最新为准”。这个设计在知识库会持续更新的场景里特别值得做,它的价值不是让模型更厉害,而是让你在系统出错时,能更快定位是人、是文档、还是检索链路的问题。
踩过 RAG 的坑,才能真切体会到:提示词只是表面功夫,上下文才是主战场。与其把时间花在“让模型更听话”,不如尽早扑下身子,把喂给模型的内容打磨到极致。这套方法论拿到任何一个知识库问答项目里,都不会过时。
