1. 先搞清楚问题出在“找”还是“编”
做RAG(检索增强生成)项目的人,十有八九都遇到过这种场景:知识库明明有正确答案,模型却一本正经地胡编乱造。我最初做本地知识库问答时也这样,系统答非所问,第一时间就去调模型、换大模型、调temperature,折腾一圈下来问题纹丝不动。后来踩了几次坑才醒悟:RAG这条链路里,模型只是最后“写答案”的人,真正决定它能不能看到正确答案的,是前面的向量检索环节。
RAG的本质,用大白话说就是“先查资料,再写作业”。系统拿到用户问题,先把问题转成一串向量(一串浮点数),然后去向量数据库里找最相似的知识片段,把这些片段拼进提示词,交给大模型生成回答。所以答非所问,无外乎两种情况:要么没检索到正确资料(检索环节出问题),要么检索到了但模型没理解对(生成环节出问题)。绝大多数情况下,问题出在第一种。
为什么我建议先查向量?因为检索环节是“输入决定输出”的根源。向量检索出的片段质量,直接决定了模型“看到”什么。如果检索结果本身就是错的、偏的、碎片化的,那再强的模型也无力回天。而且从排查成本看,检查向量相关配置(切块大小、Embedding模型、相似度阈值、TopK)远比调模型参数更省时间、更可控。先放过模型,把检索链路的每一个环节按顺序捋一遍,往往还没走到模型那一步,问题就已经定位了。
这篇文章不扯高深理论,就从实际排查的角度,把RAG答非所问的常见原因、排查顺序、具体操作和工具选型都拆开讲一遍。适合正在调RAG项目、经常被“幻觉输出”困扰的开发者和算法工程师,也适合刚入门、想把本地知识库做稳定的朋友。所有内容都以个人实操经验为基础,该给配置给配置,该给命令给命令,看完基本能照着做。
2. 先看向量检索链路,这是80%问题的老巢
2.1 向量质量:Embedding模型的选型与更新
向量检索的第一步,是把文本变成向量。这一步如果出了问题,后面全白搭。很多答非所问,根源就是Embedding模型选得不对。
我之前接过一个项目,知识库全是医疗专业术语,结果用了一款通用型Embedding模型,向量相似度计算出来完全抓不住专业实体之间的语义关系。比如用户问“心绞痛的治疗方案”,检索出的片段全是关于“胸痛鉴别诊断”的泛泛内容,看似有关联,实则偏差。后来换成针对医疗语料微调过的Embedding模型,检索准确率直接上升一大截。
这就引出一个关键认知:Embedding模型决定“语义空间”。不同模型对文本的理解方式不同,向量空间中的距离关系也不同。通用场景可以用BGE、M3E、OpenAI等通用模型,但如果知识库是垂直领域(法律、医疗、金融、科研),强烈建议先用领域内样本做评测,或者直接选用领域微调版本。
实操中有一个简单的评测方法。先用少量有代表性的问题集,在现有Embedding模型下做检索,人工判断Top5结果的相关性。如果相关性偏差,不要急着换大模型,先把Embedding模型换掉试一轮。另外要注意,同一体系内,Embedding模型和向量数据库最好是配套的——有些向量数据库原生支持特定的Embedding算法,混用可能导致距离计算方式不匹配,检索效果打折扣。
还要提醒一点:Embedding模型不是一劳永逸的。知识库内容更新、行业术语变化,都会让已有向量逐渐“失真”。如果线上出现了越来越多答非所问,不妨考虑定期对全量数据重新生成向量,或者至少对增量部分做一致性校验。
2.2 切块策略:一块知识切多大,直接影响检索命中率
向量检索的输入不是整篇文档,而是切好的“块”(chunk)。块的大小、重叠度、切分粒度,直接决定检索粒度。我见过最常见的坑,是盲目把整段长文本扔进去Embedding。比如一个PDF章节两三万字,直接作为一个向量,后果就是用户一句话问题根本找不到匹配位置,向量相似度被长文本稀释。
反过来,切得太碎也不行。我曾经把知识库切成了200字符一片,结果用户问“高血压患者能不能服用布洛芬”,检索到的是“布洛芬说明书”的碎片,丢失了“高血压”的限定条件,模型自然就答偏了。切块本质上是找到一个平衡点:让每块内容尽量是一个完整语义单元。我现在的默认经验是,中文场景下,单块长度控制在500~800字左右,同时设置80~120字的重叠(overlap),保证跨块语义不断裂。
但光有规则还不够,更高级的做法是按结构切。如果知识源是Markdown或标题分明的文档,优先按章节标题、小节、段落来切。这时候用的是“结构化切块”,比纯字符数切块精度高很多。实际操作中,我通常先做一次文档结构解析,把二级标题作为块边界,若标题下内容过长再按段落二次切分。
这里给出一个简单的切块逻辑示例(以Python为例):
from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=600, # 单块字符数 chunk_overlap=100, # 重叠字符数 separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) chunks = splitter.split_text(document_text)注意分隔符的顺序,有优先级,中文场景下最好把句号、问号放在比较靠前的位置,避免把一句话硬生生拆开。如果知识库里有代码片段、表格,建议单独走专用解析器,不要混在普通文本切块里。
2.3 查询侧向量:用户问题的改写与扩展
很多人只给知识库文本做向量化,却忽略了用户问题本身也需要处理。同样一句话,“高血压注意事项”和“平时血压高要注意什么”,语义相同但字面差异很大,如果直接对原始问题进行Embedding,检索效果可能大打折扣。这属于“查询侧向量”优化问题。
亲测有效的一招是:在检索前,先让大模型把用户问题改写为更规范化、更适合检索的问句。比如用户问“那个药能不能喝”,改写成“该药物是否允许口服,适应症是什么”。改写后的查询向量,往往能显著提升检索命中率。这一步本质上是做“查询扩展”或“查询理解”,成本很低,收益很大。
更进阶的做法是“多查询检索”(Multi-Query)。一次提问,让模型生成多个角度的子问题,分别做向量检索,再把结果合并去重,最后一起喂给生成模型。这种方式能有效对抗单次检索的不确定性,检索召回率明显提高,代价是响应时间变长、token消耗增加。做生产级RAG时,时间允许可以上多查询,简单Demo则没必要。
还有一种是“混合检索”(Hybrid Search),把向量相似度与关键词匹配(BM25等)结合。向量检索擅长语义相关但字面不重合的情况,关键词检索擅长精确匹配专有名词、编号、型号。两者互补,合并结果再重排,基本是当前RAG落地的标配写法。我的项目里,无论规模大小,我都建议至少保留一个关键词兜底通道,不然遇到“项目文档编号A-2023-001”这种查询,纯向量经常翻车。
2.4 向量数据库的相似度算法与阈值
不知道有多少人在调RAG时,会把注意力放在向量数据库的“距离算法”上。其实这一个选项,就可能让结果天差地别。常见算法包括余弦相似度、欧氏距离、内积。大部分向量数据库默认用余弦相似度,它衡量的是方向一致性,适合文本语义匹配场景,这也是大多数RAG项目的默认选择。
但你一定要知道自己的Embedding模型支持哪种距离。有些模型训练时用的就是特定距离度量,如果数据库里配置成另一种,计算出来的相似度排序可能完全歪掉。比如有些模型基于点积内积训练,如果硬要用余弦距离,效果自然差。最简单的确认方法:去查模型文档里的推荐用法,或者直接在本地做一个小样本测试,分别用不同距离算法检索同一个查询,肉眼对比结果排名。
另一个隐蔽的坑是“相似度阈值”。很多项目会设置一个最低阈值,低于该值就不检索,直接让模型说“不知道”。阈值设太高,很多相关文档被过滤掉,模型得不到上下文,开始瞎编;设太低,一堆无关片段堆进提示词,模型被噪声带偏,照样答非所问。我的建议是,阈值不要一刀切,先不看阈值,看看TopK返回的片段里,切到第几位之后开始明显不相关。这个“明显不相关”的位次,就是适合的TopK;再取该位次对应片段的相似度分数作为参考端点,调阈值就心中有数了。
3. 检索到的片段质量,决定了模型能看到什么
3.1 排序与重排:TopK不是越大越好
检索结果一出来,很多人习惯直接把TopK=5或10的片段全部塞进上下文。这个习惯很危险。假设知识库里有1000条片段,向量检索返回了10条,最相关的可能排在了第3、第7位,其余几段都是干扰项。大模型面对一大段拼接起来的混合文本,很容易被无关信息带偏。
解决方案是引入重排(Rerank)模型。Rerank模型相比向量检索,会逐条对“查询-文档对”做精细打分,能把最相关的片段提到最前面。我实测下来,加不加Rerank,回答准确性差距巨大。用Cohere Rerank或bge-reranker这类模型,召回率上升的同时,最终答案的相关性也会更集中。
配置示例(以LangChain为例):
from langchain.retrievers.document_compressors import CohereRerank reranker = CohereRerank(model="rerank-multilingual-v2.0", top_n=3) compressor = DocumentCompressorPipeline(transformers=[reranker])重排之后,TopK可以适当调小。我一般会把最终送入模型的片段控制在2~4条,视片段长度而定,宁缺毋滥。这一步对“答非所问”的改善是立竿见影的,比换一个更大的生成模型效果来得更快。
3.2 上下文拼接的顺序与去重
即便检索结果都是相关的,上下文拼接顺序也有讲究。模型对输入中间部分的注意力通常不如开头和结尾(位置偏差),所以最重要的片段应该放在开头或结尾。我习惯把重排后得分最高的片段,放在提示词中离问题最近、或离任务指令最近的位置,让模型一眼看到“关键证据”。
另外需要做去重。同一个知识点可能被切成多个重叠块,检索时可能同时返回好几条意思相近的片段。如果不做合并,提示词会冗余,也容易让模型觉得“这些内容都是重头”,反而忽略了真正重要的信息。我在代码里通常会加一个简单逻辑:按片段embedding的余弦相似度做聚类,将高度相似的片段合并为一个条目再拼接。
还有一个小细节,拼接时要把每一个片段的来源信息带上去,比如“来自文档A第2章”。这有两个好处:一是能帮助模型理解上下文来源,二是做答案溯源时方便排查。很多答非所问其实源于上下文里的信息互相矛盾,如果片段A说能禁用、片段B说可以服用,模型会根据注意力机制随机采信其中一条,结果就飘了。带来源标注能让你在日志里快速定位是哪条片段在捣乱。
3.3 元数据过滤:让检索范围更精准
知识库如果很大,纯靠语义相似度在全库范围内检索,容易把不同业务域的资料混在一起。比如一个综合知识库,同时包含产品操作手册、售后政策、财务报销流程,用户问“报销要多久到账”,理论上就应该只在财务类文档里找。这种场景,光靠向量“模糊匹配”是不够的,必须引入元数据过滤。
做法很简单:上传文档时,给每个块打上标签(部门、文档类型、日期、版本号等),写入向量数据库时作为元数据字段存储。检索时,先根据元数据条件圈定候选集,再做向量相似度排序。这一步能把检索范围从全库缩小到一个业务域,噪音大幅下降,检索准确率自然上来。
在Milvus或Qdrant这类支持过滤的向量数据库中,都可以在查询时带上filter条件:
# Qdrant示例 client.query_points( collection_name="knowledge_base", query_vector=query_emb, query_filter=models.Filter( must=[ models.FieldCondition( key="department", match=models.MatchValue(value="finance") ) ] ), limit=5 )元数据过滤的思路,本质上是在“向量语义”之外增加一层“结构化过滤”,既提高了精确度,也大大降低了相似度计算的压力。如果你发现RAG对某些专业领域问题答得不准,先想想有没有可能是在错误的范围里做检索。
4. 生成模型环节的排查与调优
4.1 提示词结构:上下文+指令+问题,一个不能少
检索环节全查完了,没问题,结果模型还是答非所问,这时候再怪模型也不迟。但即便是模型的问题,也要先看提示词写得好不好。我见过太多项目,把一堆上下文毫无说明地拼在一起,然后直接问“请回答”。模型根本不知道这些片段是权威资料还是闲聊,自然容易乱编。
一套可靠的标准提示词结构,至少包含三部分:任务指令、检索到的上下文片段集合、用户问题。任务指令要明确告诉模型“只基于以下上下文回答,禁止编造,无法回答时明确说不知道”。实验证明,加入这些约束后,幻觉率大幅下降。
给一个可复制的提示词模板:
你是知识库问答助手。请严格基于以下检索到的资料片段回答用户问题。 规则: 1. 只使用资料片段中的信息,不额外编造事实。 2. 如果片段不足以回答问题,直接说明“根据现有资料无法回答”。 3. 回答时先给出结论,再引用片段编号作为依据。 资料片段: [片段1] (来源:运营手册第二章) ... [片段2] (来源:产品说明V2.3) ... 用户问题: ...这是最基础但最有效的优化。很多“答非所问”,单纯是把提示词写清楚后就好了。自测时可以拿同样的问题,分别在“裸问”和“结构化提示词”下跑一遍,对比结果落差,你会有惊喜。
4.2 温度、TopP等生成参数的影响
有些答非所问,确实是模型生成参数太“放飞自我”导致的。RAG场景下,我们想要的是忠实于上下文的回答,而不是发散创作。所以temperature通常建议调低,比如0.1~0.3之间,TopP也建议限制在0.8左右。这样模型会更愿意取用上下文里的信息,减少自由发挥。
注意,不要照抄别人的参数,因为不同模型对温度敏感度不同。我的经验是先跑一轮测试集,分别用0.1、0.3、0.5、0.7几个档位,观察回答质量和幻觉率。另外,关闭默认的“丰富扩展”选项(比如部分平台默认会做同义改写润色),在RAG场景下反而有害。答案可以简洁,但必须准确。
还有一点容易忽略的,是“max tokens”设置。如果生成长度限制太短,模型可能还没把话说完就被截断,导致后半截答案看起来“答非所问”。先检查一下设置,再考虑怪模型。
4.3 模型本身的局限性:什么时候才真正需要换模型
当你确认检索链路全流程无误、提示词也没问题,还是答非所问,那才可能是生成模型的能力瓶颈。比如知识库内容涉及复杂推理、多跳问答、需要综合多段信息才能回答,小模型往往力不从心。这时候换更大更强的模型才有效。
判断方法是做一个“理想上下文测试”。手动把正确答案片段直接拼进提示词,让模型回答,如果能答对,说明模型没问题,问题一定在检索链路。如果直接喂答案都答不对,再考虑换模型。这个测试是最快的定位手段,我强烈建议入手RAG项目时先做这个测试。
另外一个“怪模型”前要注意的点:有些模型上下文窗口有限,如果拼接的片段总字数超过窗口,模型会丢弃部分内容,你根本不知道它丢的是哪一段。需要检查每次请求的token数是否安全。如果总长度超过模型窗口的80%,就要考虑削减片段数量、压缩片段长度,或者干脆换一个上下文窗口更大的模型。
5. 实操排查流程:从问答现象定位病灶
5.1 一条完整的排查路径与问题速查
总结下来,我遇到“答非所问”时,按下面的顺序排查,效率最高:
- 先做“直接喂答案测试”,确认生成环节是否正常。
- 再查检索结果:把当前问题的检索返回片段打印出来,人工看TopK是否相关。这一步能立刻判断是不是向量检索的锅。
- 如果检索结果不相关,依次排查:Embedding模型、切块策略、查询改写、元数据过滤条件。
- 如果检索结果相关但杂音多,检查TopK数量、重排是否配置、相似度阈值是否合理。
- 如果检索结果全是相关片段但模型仍答错,再调提示词结构、生成参数。
- 最后才是换模型或增大上下文。
每排查完一层,用同一批问题跑回归,不要凭感觉判断。我自己维护了一个50条左右的高频测试问题集,覆盖正常问题、边界问题、模糊问题、跨域问题,每次改动后全量跑一遍,对比前后回答质量。没有这个测试集,你很难确定某次改动到底是变好了还是变差了。
5.2 典型问题实录:三个真实案例复盘
案例一:切块过大导致答案“泛”且“偏”。一个运维知识库,问题“Nginx 502错误如何排查”,系统回答了一大段关于HTTP错误码的泛泛叙述,完全没提具体排查步骤。打印检索结果后发现,命中的片段是一整篇“Nginx常见错误”文档的开头部分,5000多个字符,502的具体内容在文档中后段,但没有被单独切成块。后来调整切块策略为按段落切,并下调chunk_size,同样的502问题直接检索到了对应段落,回答立刻精准。
案例二:查询改写缺失导致同义词检不到。法律知识库里的“劳动仲裁时效”问题,用户问“打劳动官司有没有时间限制”,系统答非所问,检索结果全是关于“诉讼时效”的片段。给查询加上改写层,把用户问题转成“劳动仲裁时效规定”,TopK召回的全是仲裁相关条款,问题解决。
案例三:元数据过滤缺失导致跨域干扰。综合企业内部知识库,用户问“打印机卡纸怎么解决”,结果系统回答的是“打印申请审批流程”。因为它检索到的相似片段里既有“打印机硬件故障排除”,也有“打印服务申请流程”,向量相似度相近。加了“doc_type=handbook”过滤条件后,只检索操作手册,干扰信息消失,回答正确。
这三个案例集中在同一个结论上:80%的答非所问,根因都在检索侧,不在模型侧。先查向量再怪模型,不是一句口号,而是最高效的排查策略。
6. 工具选型与配置建议
6.1 向量数据库怎么选:Milvus、Qdrant、Chroma还是ES
很多人选向量数据库先看热闹,我直接讲结论。项目刚起步、数据量小(万条以下)、团队想快速跑通,用Chroma或FAISS就够了,零运维,直接在Python里用。数据量中等(十万到百万级)、需要持久化和简单过滤,Qdrant很合适,单机部署容易,带本地文件存储,重启不丢数据,接口简洁。
如果数据规模大、查询并发高、未来要支持分布式,那Milvus或Elasticsearch 8以上版本(带向量插件)更靠谱。Milvus性能强悍,但也更重,适合已经有了稳定基础设施的团队。ES适合那种“已经用了ES做全文检索,顺带想加向量能力”的场景,一次搞定混合检索,不用维护两套系统。
我自己现阶段最常用的组合是Qdrant + Python客户端,原因很简单:部署轻、元数据过滤完善、支持混合检索。图省事的朋友可以直接上云厂商托管向量数据库,省去运维精力,但要注意数据安全,知识库敏感的话尽量本地部署。
6.2 Embedding和重排模型的取舍
Embedding模型推荐优先试bge-m3(中文效果好、多语言均衡)、bge-large-zh、M3E系列。如果是OpenAI生态,text-embedding-3-small性价比高,但中文场景下其实不一定比bge强。重排模型方面,bge-reranker-v2-m3在中文评测里表现不错,Cohere Rerank也稳定,各有侧重。
建议先跑一个离线小评测:准备30条问答对,分别用不同Embedding+重排组合,统计Top1命中率和Top3命中率,用数据选型,不要靠感觉。我踩过的坑是:因为觉得大模型比较强,就随便用了个小Embedding,结果召回率只有40%,后面换对模型,一句没改,召回率到了75%。Embedding模型的记忆能力是基础中的基础。
6.3 框架选型:LangChain4j、LlamaIndex还是自研
搜索热词里频繁出现“LangChain4j easy rag”和“langflow如何配置”,说明大家很关注框架。我自己的体验是,框架能帮你快速搭骨架,但别让它限制你调试。首选是LangChain生态,资料多、组件全,尤其是文本切分、各种Retriever、文档加载器都有现成的。如果项目是Java技术栈,LangChain4j是个不错的选择,虽然组件相对少,但基础RAG链路完整。
LlamaIndex对文档索引、知识库分类处理更深入,适合做重文档型知识库。LangFlow这类低代码平台适合原型验证,但对于生产调优,我还是偏向写代码控制每个环节,因为调试时能看到中间结果。
还有一个观点需要表达:RAG真正的竞争力,不在框架选谁,而在于你能否看清每个中间步骤的输入输出。所以无论是用什么框架搭,务必在检索结果和生成结果之间加日志输出。这一步带来的调试效率提升,远比换任何框架都大。
7. 关于持续优化与避坑的几点心得
写到最后,分享几个常踩的坑和习惯层面的建议,都是真金白银换来的。
第一,监控“检索质量”而不是只看“最终答案”。很多人只记录问答好坏,但不记录检索到的片段。建议每次问答都记录检索片段ID和相似度分数,周期性观察TopK命中率变化。如果检索命中率在下滑,说明知识库或Embedding出了问题,早发现早处理。
第二,谨慎修改向量数据。知识库更新时,务必把新增、删除、修改的文档单独处理,而不是直接全量重算。全量重算容易导致线上向量版本混乱、检索结构突变。另外,删除文档时同时删除对应向量,巩固版本一致性,不然会出现“文档没了但检索还能出来”的幽灵结果。
第三,别忽视日志与可追踪性。答非所问是你最不想在生产环境面对的问题,但如果不给每轮请求带上链路追踪ID,出问题时你连哪条检索片段导致问题都找不到。如今的做法是,每次请求记录从用户问题、改写后查询、检索TopK片段列表、重排结果、最终提示词、生成回答全链路日志。排查时打开日志,一眼定位断层点。
第四,有一个小技巧很实用:把用户问题直接带到检索片段里去“人工模拟”。如果你自己看着检索TopK都判断不出来该片段是否能回答问题,那模型更判断不出来。所以每次觉得答非所问,先以人眼替代模型,看一遍检索回来的TopK。你能从中看出问题,多半就是向量这里出问题了;你看不出问题,那再往模型方向查。
最后再分享一个体会,这个项目调优反复验证了一个道理:RAG系统的精度是设计出来的,不是碰运气调出来的。从切块、Embedding、查询改写、重排、元数据过滤到提示词结构,每一层都可以显式地影响最终答案质量。只要每一层都留足观察点,把“答非所问”当成检索质量问题来对待,绝大多数坑都是能提前踩平或迅速定位的。别一上来就大动干戈换模型,先静下心把向量链路盘一遍,这才是RAG调优的第一性原理。