☰
用RAG破解大模型幻觉:本地知识问答系统搭建实战
2026/10/18 1:40:01 网站建设 项目流程

最近我被一个大模型的回答搞得有点哭笑不得——问它某台旧设备的具体端口序号,它斩钉截铁地给了个"标准配置",语气比谁都确定,结果翻开官方手册一比对,完全对不上。这种一本正经地胡说八道,圈里人叫幻觉,是每个用过大模型的人都绕不开的坑。与其反复调提示词,或者靠"你再想想"这类玄学操作,不如直接试试检索增强生成(RAG)——让模型在开口之前,先到你给的资料库里查证一遍。

这篇文章从幻觉的成因讲起,拆解RAG的完整工作链路,记录我实际搭建一个本地知识问答系统的全过程,也整理那些"明明用了RAG却还在胡说八道"的翻车现场和排查顺序。内容偏实操,不绕理论,适合正在为模型可靠性头疼的开发者,以及想在业务里落地知识库问答的产品和技术同学参考。

1. 大模型为什么会一本正经地撒谎:幻觉的三张面孔

1.1 模型的本质是"接话大师",不是"知识库查询器"

先说底层机制。基于Transformer架构的语言模型,训练时做的是最朴素的一件事:给定前面几千个词,预测下一个最可能出现的词。训练的语料是海量文本,模型从中学会的是"什么词接在什么词的后面,听上去像人话"。它本质上是个高级的"把话接下去"的机器,不是一个真理引擎,也不是一个知识查询接口。

这就能解释很多怪现象。你问它"某设备COM口的默认速率是多少",它并不像人一样去翻手册,而是把这句话当作"开头",然后从训练时见过的相似句式里,挑选条件概率最高的一串词续写下去。如果训练语料里恰好有设备的说明,它大概率能说对;如果没有,它会从相似文本片段里提取一个最"像样"的值填进去,再用各种逻辑连接词包装得严丝合缝。越是大模型,这种"包装能力"越强,于是就有了"一本正经地胡说八道"。

从根上说,这不是模型藏了坏心思,而是它的工作方式决定的。把大模型想象成一位非常有礼貌、说话头头是道的朋友,你问什么他都能接上话,但前提是他脑子里得真有存货。RAG的思路就是:别让他空着肚子接话,先把相关的资料递到他手里,再让他照着资料说。

1.2 幻觉的三种常见面孔:编造、错位、时空漂移

我实际用下来,幻觉大体分三类,认识清楚是哪一类,才好对症下药。

第一类是无中生有型。模型会直接编造不存在的实体、数据、引用。比如问某个软件版本号是多少,它给你报一个"v3.7.2",但官方压根没出过这个版本。这类幻觉最坑,因为编出来的东西往往结构完整、格式标准,非专业人士根本看不出真假。

第二类是张冠李戴型。信息本身是正确的,但被安到了错误的对象上。比如A产品的参数被当成B产品的参数回答,或者是把某个老版本的接口配置说成新版本的行为。这类幻觉常见于知识体系相似、命名相近的领域,模型在语义空间中做了"就近匹配",结果错位了。

第三类是时空漂移型。多见于时间敏感问题。模型训练数据有时间截止点,之后发生的变化它不可能知道。问"当前最新的稳定版本是多少",它可能报一个训练截止前的最新版。这不是模型想骗你,是它的知识天然滞后。

这三种幻觉在RAG场景下表现还不一样:无中生有最容易被RAG抑制,因为上下文里已经给了可信资料,模型很难完全无视;张冠李戴则考验检索质量——如果系统召回了一段貌似相关、实际讲的是另一款设备的内容,模型会照着错的方向答;时空漂移则需要你在构造提示词时明确告诉模型"以提供的资料为准,资料没有的就说不清楚"。

1.3 为什么越流畅的答案越危险

还有一个容易被忽视的陷阱:模型的幻觉往往伪装得极其自然。它不会在句子中间突然结巴,也不会用"可能""也许"这类不确定词,反而会用"根据标准配置""一般情况下""业内惯例"来加固回答的可信度。

人判断信息真伪时,通常会依赖表述的流畅性和细节的丰富度。当前大模型在这两个维度上都远超平均水平,这就形成了"流畅感导致的过度信任"。我做测试时经常遇到这种情况:把模型输出和原文摆在一起,模型引用了原文里根本不存在的章节编号,但语气之肯定,连我自己都要愣一下才反应过来有问题。RAG并不能天然解决这种"语气问题",但可以借助"引用定位"来约束——让模型在回答中标注信息来源的具体段落,把看似权威的叙述拉回到可验证的层面。

2. RAG是什么:让模型开口之前先翻书

2.1 一句话版本

RAG全称检索增强生成,核心思路不改变模型参数,不在模型内部"塞知识",而是在生成回答之前,先从外部知识库中检索出相关的资料片段,把这些片段拼进提示词,让模型在参考资料的约束下做总结和回答。

用类比说,原来模型回答问题像闭卷考试,全靠训练时背下来的东西;RAG把考试改成开卷,允许模型在答题时翻一本特定的参考书。参考书上有的内容,回答就有了依据;参考书上没有,模型至少会被提示词约束为"资料里没找到",而不是硬编一个。

2.2 两个阶段:先建库,再问答

RAG系统拆开来看,是两大段:离线阶段的索引构建,和在线阶段的检索问答。

索引构建阶段做的是把原始文档变成模型能快速检索的结构。大致流程是:加载文档,清洗掉页眉页脚、水印、多余空白;按结构切分成长度合适的片段;调用嵌入模型把每个片段转成一个向量;最后把所有向量连同原文一起存进向量数据库。这部分是一次性的工作,数据更新时重新跑一遍即可。

在线问答阶段做的事是:拿到用户问题,用同一个嵌入模型把问题也转成向量;在向量数据库里做相似度搜索,找出最相关的若干个片段;把这些片段按顺序拼接到提示词模板里,加上明确的指令约束,发给大模型;模型基于检索到的内容生成最终回答。如果做得好,还可以在回答里标注引用来源,方便用户核对。

2.3 检索增强的"增强"到底增强了什么

很多人以为RAG只是在提示词里多塞几段资料,效果类似"带资料聊天",这个理解不算错,但低估了它的价值。

第一,它把大模型的知识范围扩展到动态更新的外部语料。训练数据是静态的,更新一次成本极高;而外部知识库随时可以增删改。今天改了某设备的端口号,重新跑一遍索引,模型回答就会跟着变,不需要重新训练或微调。

第二,它给了模型一个"事实锚点"。没有资料约束时,模型需要靠记忆里的模糊印象组织答案;有了资料,回答可以逐句对应到原文片段,既降低编造概率,又方便核验。

第三,它让"我不知道"变得可控。只要提示词约束得当,模型在被问到资料覆盖不到的问题时,可以明确表示"未检索到相关资料",而不是硬着头皮演下去。这一点对企业场景特别重要——很多业务问题宁可回答"查不到",也不能给一个带节奏的错误答案。

2.4 RAG不是微调的替代品,也不是提示词工程的附属品

经常看到有人把RAG和微调放在一起比较,问"哪个好"。我的体会是,它们解决的问题层级不一样。微调是改变模型的行为模式和领域语调,RAG是改变回答时用到的信息内容。如果只是想让它说话更像某个风格的客服,微调更直接;如果你关心的是"它说的内容准不准、有没有最新依据",RAG的性价比要高得多。

这里也提醒一句:RAG不是提示词工程里"多加几段参考文档"那么简单。检索到的内容是否相关、切片是否保留了完整的上下文、多个片段之间的顺序和冲突如何处理,这些都会直接影响最终质量。单纯把十几页资料一口气塞进上下文,那不叫RAG,那是堆砌上下文,效果可能还不如精确检索后的三段。

3. 从0到1搭建本地知识问答系统:我的实操记录

3.1 场景设定与数据准备

我做了一个模拟项目X:把某个内部产品的30多份PDF操作手册整理成一个可问答的知识库。这些手册有老有新,格式不统一,有的带目录和页眉,有的扫描质量一般。核心诉求是:团队成员问"某功能怎么配置""这个参数的取值范围是多少"时,能快速给到带出处的答案,而不是自己翻几十份PDF。

数据准备阶段最容易低估。我先做的是把PDF批量转成文本,提取每一页的文字内容。这个环节里,页眉页脚是需要重点处理的噪音,否则每章都会混入"公司内部资料""第N页"这类无效文字,检索时容易被误命中。表格内容也要小心,PDF转换工具处理表格时经常把行列关系打乱,导致切出来的片段语义断裂。

3.2 分块设计:不是简单按长度截断

切分是RAG系统里非常关键的一步。我一开始图省事,直接按每500个字符硬切,结果一个完整的技术参数表被从中间拦腰切断,上半段在A块,下半段在B块,检索时哪一块都不完整。

后来我改成按文档原本的结构来切:先识别Markdown风格的标题层级,尽量保持一个章节或子章节作为一个完整块;如果某个章节太长,再按固定长度做二次切分,并且设置相邻块之间有50到100个字符的重叠。这样可以大幅减少关键信息恰好落在切口上的情况。重叠段的作用是给上下文一个"缓冲",避免一个问题所需的背景知识被硬生生分开。

我给当时的参数留了个参考值:普通说明文本按300到500个字符切分,重叠50到100个字符;技术参数表尽量整体保留,上限放宽到800个字符;代码示例独立成块,避免和叙述文字混在一起。这是基于我的数据特征的实践值,不必照搬,但可以作为一个起点来调。

3.3 嵌入模型与向量化

嵌入模型负责把文本转成向量,它是检索质量的地基。我选了社区里常用的开源中文嵌入模型,维度不大不小,中文语义理解均衡。选型时的几点经验:优先看它在垂直领域数据上的表现,而不只看通用榜单;向量维度会在一定程度上影响检索速度和存储占用;模型的上下文长度决定了它能处理的切片上限,不能超过模型限制。

向量化环节我单独写了一个服务,便于统一管理和更新,避免每次跑实验都重新加载模型。以下是当时的简化代码示意,实际项目里可以替换成你选的嵌入服务和向量数据库。

import requests EMBED_URL = "http://127.0.0.1:8000/embed" def embed_texts(texts): resp = requests.post(EMBED_URL, json={"texts": texts}) data = resp.json() return data["vectors"] # 使用示例 chunks = [ "xxx设备的默认端口为8080,可在配置文件中修改。", "若遇到连接超时,请检查防火墙设置。" ] vectors = embed_texts(chunks) print(len(vectors[0])) # 向量维度

3.4 向量库检索:相似度搜索的实用细节

搭建向量库的时候,我选择了自建方案,用开源向量索引库存向量和原文。对单机几十万条以内的规模,这类方案完全够用,索引速度快,查询延迟低,不需要上分布式架构。

建库时我不仅存了向量本身,还把原文、来源文件名、章节标题、页码一并作为元数据存进去。这些元数据有两个作用:一是回答时可以做引用标注,二是排查时可以快速定位"这条答案到底取自哪里"。实际使用中,元数据的价值会被严重低估,等你需要解释某个错误回答的出处时,就明白它有多重要了。

检索时我做了两路召回然后合并:一路是向量相似度检索,取语义相近的片段;一路是关键词过滤,用BM25或简单的词频匹配,兜底处理那些语义模型容易忽略的专业缩写。两路结果去重后按融合分数排序。这一步让我明显感觉到,纯向量检索在上面的场景里偶尔会把意思相近但对象不同的片段排得过高,混合检索后召回质量更稳。

3.5 提示词拼装与生成约束

检索到的片段有了,接下来是把它们拼进提示词。我整理了一个固定的模板,核心要素如下:

你是一个知识库问答助手。请仅依据以下资料回答问题。如果资料中没有相关信息,请明确回答"未检索到相关资料",不要编造。回答时若引用了某个资料片段,请在句末标注对应编号。资料如下:\n1. [片段] \n2. [片段] …

这里"仅依据"和"不要编造"是两个关键的约束词。不加这些约束时,我实测模型还是会按自己的惯性发挥,尤其是资料里的信息不足以完整回答时,它倾向于自行脑补来补全。加了之后,至少"敢承认不知道"的比例明显提升。

生成阶段我直接调用大模型接口,把拼装好的提示词发送过去。为了让输出更稳定,我把温度参数调低到0.1附近,关闭一些随机采样选项。这一步对减少表述上的随意性有帮助,但需要说明的是,它解决不了检索错误的问题——资料本身不对,模型再稳定也是稳定地错。

3.6 效果验证:用一批带标准答案的问题测

搭完系统后,我没有急着宣称"搞定",而是从手册里人工整理出20个有标准答案的问题,覆盖参数查询、功能配置、故障排查三类,挨个跑了一遍。结果记录两件事:回答是否准确,以及回答是否能在资料中找到对应出处。

实测下来,20个问题中有16个回答正确且能定位到出处,2个部分正确但引用了次要片段,2个检索召回的内容不相关导致回答错误。针对后两类问题,我后续调整了分块大小和检索权重,让系统整体效果又提升了一些。这里想强调一点:RAG系统没有"一次配置永久有效"这回事,它需要一组验证集来持续迭代。哪怕只是20条问题,也能帮你快速发现检索和生成环节的松动。

4. 决定RAG效果上限的三个环节:切块、召回、排序

4.1 分块不是越大越好,也不是越小越精细

分块策略会同时影响召回和生成的质量,需要成体系地权衡。

块太大,比如把整章一万字都作为一块存进向量库,虽然上下文是完整的,但向量表示会被"平均化"。一个块里如果同时讲了配置、排障、升级三个话题,它的向量会是一个四不像,谁都沾边,谁都匹配不准。检索时表现就是"好像相关,但答案必须到块中间去找",大模型读到这块时注意力也会被无关内容分散。

块太小,比如按100字切,单个块语义是够聚焦了,但很可能丢失必要的上下文。最典型的例子:原文写"该参数默认值为100",前面的块写"通过配置项max_conn修改",切成两块后,第二块的"该参数"成了无源之水,模型根本不知道它指谁。

我自己的平衡经验是:按文档原有结构优先,结构不清晰的再按长度切。长度落在300到500字之间,重叠区间50到100字。对于表格、代码、数据字典这类结构化内容,尽量单独成块或加大上限,不要被普通文本的长度约束限制死。你可以先用一套初始参数,跑一批验证题,再根据失败案例反推调整。

4.2 召回阶段的"准"和"全"是两回事

召回质量是RAG最容易被卡住的瓶颈。"准"指的是召回出的片段与问题真正相关,"全"指的是正确答案对应的片段确实在召回列表里。二者往往不能兼得。

我遇到过不少情况:向量检索Top-1的结果看起来很高分,语义上也确实贴近问题句,但仔细读发现它讲的是"另一个型号的设备也有类似问题",根本不适用。这种虚假相关的片段,对最终回答的干扰非常大。相反,真正包含答案的片段,因为用了大量专业缩写和特定格式,语义相似度反而不突出,排到了第五第六位。

解决"全"的问题,我用的是混合检索,把关键词匹配的结果也拉进来合并。解决"准"的问题,靠的是后面要说的重排。这里提一个实操建议:检索阶段不要把Top-K取得太小。我最初设的是Top-3,结果经常漏,后来保守地扩到Top-10或Top-15,召回率明显改善。反正后面还有重排把关,先召回多一点,把判断交给更精细的模型。

4.3 重排:用精排模型修正召回结果

向量检索解决的是"语义相似",它属于偏向粗粒度的匹配;而用户问题对信息的需求往往是更细的,比如"某某功能的开启条件在哪个页面有写"。如果需要更高精度,重排这一步基本绕不开。

重排的思路是:第一阶段用向量检索+关键词检索,快速把候选从全量缩小到几十条以内,保证"全";第二阶段用精排模型对每个候选片段和用户问题做细粒度的相关性打分,它打分的方式是直接把两段文本拼接起来逐字交互,比向量检索那种先各自编码再算相似度的方式更精细。分数高的排到前面,再截取Top-3到Top-5送给大模型。

用了重排之后,我感觉到两个直观变化:一是答案有了明确的出处支撑,模型引用的片段基本都是相关度最高的那一条;二是"答非所问"的比例下降得非常明显。代价是多一次模型推理,延迟会略微增加,但对于一个内部知识库工具来说,这点延迟换来的准确性完全值得。

5. 实测里的翻车现场:RAG并没有消灭幻觉的几种情况

5.1 上下文里有正确答案,模型却还是自由发挥

第一个让我纠结的翻车案例是:检索结果里明明已经包含了正确答案,原文白纸黑字写着"默认端口为8080",模型偏偏在回答里写了8090。我把检索结果打印出来,对着看,资料没错,模型也没瞎——但它确实没有严格遵循"仅依据资料回答"的约束。

排查下来,问题出在提示词的表述太弱。最早的模板写的是"请参考以下资料回答问题",这话在模型看来是"参考",不是"必须依据"。模型觉得可以结合自己的知识自由发挥。改成"请仅依据以下资料回答,资料中没有的信息一律不要补充",并加上"如果资料与你的知识冲突,以资料为准"之后,这类情况基本消失。看来提示词里的约束强度,直接决定了模型对检索内容的尊重程度。

5.2 检索到了垃圾,答案自然就是垃圾

还有一类翻车不是模型的问题,是检索系统本身召回了错误片段。有一次我问"如何重置登录密码",系统召回了一段"密码策略设置说明",谈的是密码复杂度要求,不是重置流程。片段与问题关键词高度重合,导致向量相似度很高,重排也没把它压下去。

这个案例让我意识到,关键词重合和语义相关之间的差距,比想象中大得多。后面我在清洗数据时做了两处改进:一是把手册里每个章节的标题提炼出来,作为检索时的辅助字段,提高"本节主题"的匹配权重;二是在重排打分时加入标题与问题之间的相关性作为加权因子。调整之后,这类"同词不同意"的错配明显减少。

5.3 一个完整答案被切成两半,谁也救不回来

还有一次翻车的原因在数据端。某设备的一个故障处理流程横跨了两个章节:前一章讲如何进入诊断模式,后一章讲具体执行的操作命令。按章节切块时,这两部分被分到了不同的块。用户的问题是"进入诊断模式后执行什么命令",检索阶段命中了前一章,后面那个包含命令的块没有被召回。

这个问题让我吃足了切块的教训。解决的办法有几条路:对同一主题的章节做合并或按父子关系重建块;在检索时允许跨块重组的逻辑,把邻近片段中有先后关系的都返回给模型;或者在分块时保留章节之间的"描述性连接词"。我的做法是写了一个简单规则:当检索到的片段属于某个章节的子章节时,把父章节的开头部分一并附上,为模型补全前因后果。效果不错,也让回答更像"人话"。

5.4 排查顺序:先看检索,再看提示词,最后怪模型

踩过几轮坑之后,我总结出一个固定的排查顺序,分享出来:当RAG系统给出的回答不对时,永远先看检索阶段输出了什么。

第一步,把用户问题对应的检索结果原文打出来,人工判断召回内容是否相关。如果检索阶段就错了,后面生成阶段再怎么调提示词都白搭。第二步,看提示词里是否明确约束了"仅依据资料回答",以及资料的数量和后处理方式是否合理。第三步,才考虑是不是模型能力问题,试着换一个更强的模型或调整温度参数。

这三个步骤顺序不能乱。大多数翻车现场,根子都不在模型本身,而在于我们没把正确的资料送到它面前,或者没让它知道必须只使用这些资料。先怀疑检索和提示词,能少走很多弯路。

6. 什么时候该上RAG,什么时候该想别的办法

6.1 RAG的适用边界:确定性、可溯源性、动态更新

用了这么久RAG,我对它适合什么场景也算有了比较清楚的判断。最典型的三类:第一,知识内容频繁更新,比如产品文档、政策规则、内部流程说明,这类数据更新快,用微调根本来不及;第二,回答需要可溯源,用户不只是要一个答案,还要看到依据,RAG天然能把回答和资料片段一一对应;第三,领域特有名词密集,通用大模型未必见过这些词汇的准确用法,RAG至少能保证回答使用的术语和内部文档一致。

6.2 不推荐硬上RAG的场景

如果问题是开放式的、需要多轮全局推理的,比如"帮我想几个产品改进方向""这段代码的整体架构有没有什么问题",RAG能提供的帮助有限。这类问题不是"查资料"能解决的,更像"开脑洞"和"综合推理",这时候直接调大模型反而更自然。

另外,如果业务问题极其通用,比如"今天天气怎么样""介绍一下端午节",大模型本身已经掌握得很好,没必要在外面套一层检索。加了RAG反而可能引入不必要的延迟和检索噪音。

6.3 RAG、微调、超长上下文怎么选

很多人会在RAG、微调、直接塞超长上下文这三条路之间犹豫。我的对比视角是这样的:

维度RAG微调超长上下文
知识更新成本低,改索引即可高,需要重新训练中,每次拼接都要塞全文
可溯源强,可标注出处弱,知识融入参数后无法定位中,资料在上下文里但可能超出注意力
回答准确率提升高,信息有锚点中,模型学到风格和边界中,依赖模型长文本处理能力
适用场景知识库问答、动态文档领域风格迁移、固定格式输出短文档精读、一次性分析

实际组合使用时,我的建议是:知识获取交给RAG,行为风格的调整通常靠提示词就能完成,只有当你希望模型在"说话方式"上有根本性变化时再考虑微调。超长上下文适合文档量少的场景,一旦知识总量变大,每次都把所有文档塞进去,成本和效果都不如RAG来得稳。

6.4 一个值得尝试的混合方案

最后分享一个我在项目后半段采取的混合策略:先用关键词检索做第一道粗筛,把候选缩小到一个数量可控的范围;再用重排模型精排,取前3到前5个片段;接着把这几个片段以"编号+原文"的形式放进提示词,并在指令中明确"回答必须引用编号"。

这个方案算不上多高级,但实测下来稳定性和可解释性都很好。每次回答都能明确地说出"根据手册第几章第几节的描述",团队成员用起来也放心。等积累更多反馈之后,我打算把一些高频问错的片段做成人工维护的"标准答案覆盖表",在检索结果出来时先查这张表,命中就直接走标准答案,不命中再走RAG链路。这算是RAG系统成熟之后的一个常见进化方向:用外部规则和人工沉淀来对抗长尾噪音。

搭建这套系统之后,我个人最大的体会是:RAG不是万能的,但它把大模型从"背书的优等生"变成了"会翻书的上班族",用一套可理解的机制,把幻觉从"必然事件"降级成"偶发事件"。如果你也被模型的胡言乱语折腾过,不妨从一个小数据集动手,先迈出第一步,再根据实际翻车情况慢慢调优。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询