尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【RAG 分层排查】资料在知识库里却答不对:从切片、召回到提示词逐层定位

【RAG 分层排查】资料在知识库里却答不对:从切片、召回到提示词逐层定位 个人主页爱和冰阔乐专栏传送门《数据结构与算法》 、C学习方向C方向学习爱好者⭐人生格言得知坦然 失之淡然博主简介文章目录前言一、先把 RAG 拆成一条可以观察的链路二、建立一个最小可观察检索程序三、第一类问题原文有答案切片以后答案没了3.1 切得太短条件和结论分开了3.2 切得太长一个片段里塞了太多主题3.3 标题被切丢了3.4 重叠不是越大越好四、第二类问题Embedding 模型前后不一致五、第三类问题向量库里混进了旧数据和重复数据六、第四类问题问题太短检索意图不完整七、第五类问题Top K 不是越大越好八、最后才检查生成阶段九、建立一套最小验收题集十、一份更实用的排查顺序10.1 原始资料10.2 切片10.3 向量10.4 数据库10.5 检索10.6 生成排查顺序整理参考资料前言文档已经上传向量库里也能看到数据回答中甚至出现了“根据资料”但结论仍然不对。这类现象可以用一个最小知识库稳定复现切片把条件和结论拆开查询又召回标题相似的旧版本生成模型最终只能根据错误上下文组织语言。此时直接更换生成模型、拉长提示词或把top_k从 3 调到 20都无法证明故障发生在哪一层。排查顺序应当从可以核对的输入开始先确认原文再看切片、向量、召回、过滤与重排最后才检查模型生成。下面沿着这条链路逐项验证。一、先把 RAG 拆成一条可以观察的链路RAG 的名字是“检索增强生成”但真正落地后不止两个步骤。一个最小知识库通常包含读取 PDF、Word、Markdown 等原始资料清洗页眉、页脚、乱码和重复文本按一定规则切成多个片段使用 Embedding 模型把片段变成向量将向量、原文和来源信息写入向量库把用户问题也转换成向量从向量库召回若干候选片段使用关键词、元数据或重排模型继续筛选将最终片段放入提示词大模型基于这些片段生成回答。把这十步拆成索引和查询两条线以后排查顺序会直观很多。只看最终回答相当于系统坏了以后只盯着最后一个页面。前面九步中的任何一步出问题最后都可能表现为“模型胡说”。为了便于排查可以先把问题分成三层。层次要确认什么常见异常数据层原文、切片、来源是否正确文本缺失、标题和正文分离、重复入库检索层问题是否召回正确片段模型不一致、过滤条件错误、Top K 太小生成层模型是否只根据上下文回答上下文过长、指令冲突、没有缺失时拒答规则排查时必须先证明上一层正常再进入下一层。如果一开始分不清故障属于哪一层可以按“现象—证据—处理”关系定位。二、建立一个最小可观察检索程序本文用 Ollama 生成 EmbeddingChromaDB 保存和查询向量。先做一个能看清检索结果的小程序暂不接入完整聊天页面。安装依赖pipinstallchromadb ollama拉取 Embedding 模型ollama pull nomic-embed-textOllama 目前推荐使用/api/embed或对应的ollama.embed()接口。一个很关键的要求是入库和查询必须使用同一个 Embedding 模型。下面先准备三段测试资料frompathlibimportPath DOCUMENTS[{id:deploy-001,text:系统默认监听 127.0.0.1:5000仅允许本机访问。需要局域网访问时应显式监听 0.0.0.0并在防火墙中限制来源。,source:部署说明.md,version:2026-08,},{id:account-001,text:演示环境管理员账号由部署人员初始化首次登录后必须修改密码。项目仓库不得保存真实密码。,source:账号安全.md,version:2026-08,},{id:database-001,text:数据库文件存放在 instance/app.db。迁移项目时需要同时备份数据库文件不能只复制前端页面。,source:数据备份.md,version:2026-08,},]写入 ChromaDBimportchromadbimportollama EMBED_MODELnomic-embed-textdefembed_texts(texts:list[str])-list[list[float]]:responseollama.embed(modelEMBED_MODEL,inputtexts,)returnresponse[embeddings]clientchromadb.PersistentClient(path./chroma_db)collectionclient.get_or_create_collection(project_docs)collection.upsert(ids[item[id]foriteminDOCUMENTS],documents[item[text]foriteminDOCUMENTS],embeddingsembed_texts([item[text]foriteminDOCUMENTS]),metadatas[{source:item[source],version:item[version],}foriteminDOCUMENTS],)这里使用upsert是为了让相同 ID 的记录更新而不是无限重复插入。它并不能自动替我们清理已经变更过 ID 的旧数据所以稳定的 ID 设计仍然很重要。接下来不要急着接大模型先只查向量库defsearch(query:str,n_results:int3)-None:query_vectorembed_texts([query])[0]resultcollection.query(query_embeddings[query_vector],n_resultsn_results,include[documents,metadatas,distances],)print(f\n问题{query})forindex,documentinenumerate(result[documents][0],start1):metadataresult[metadatas][0][index-1]distanceresult[distances][0][index-1]print(f\nTop{index})print(f距离{distance:.4f})print(f来源{metadata[source]})print(f版本{metadata[version]})print(f内容{document})search(怎样让同一局域网的电脑访问这个系统)只有当正确片段稳定出现在前几名里才有必要继续检查模型回答。三、第一类问题原文有答案切片以后答案没了3.1 切得太短条件和结论分开了假设原文是当设备电量低于 20% 时系统暂停高耗能巡检任务如果附近存在充电中继站则优先前往中继站补能。如果按固定字符数强行切成两段可能变成片段 A当设备电量低于 20% 时系统暂停高耗能巡检任务片段 B如果附近存在充电中继站则优先前往中继站补能。用户问“低电量且附近有中继站时怎么办”真正需要的是两个条件组合后的完整规则。只召回其中一个片段回答就会缺一半。切片不能只看字符数还要看语义边界。对于项目文档可以优先按下面的层次切章节标题 → 小节标题 → 段落 → 句子 → 最后才按长度兜底3.2 切得太长一个片段里塞了太多主题片段太长也有问题。一段 2000 字的内容同时包含登录、数据库、告警和部署Embedding 最终只能形成一个整体语义。用户问数据库备份时这个大片段未必比一段专门讲备份的小片段更相似。切片大小没有全行业统一答案。与其背一个 500 或 1000 字的固定值不如建立自己的验证题集再比较不同方案的召回结果。3.3 标题被切丢了很多项目文档的正文会写默认端口为 5000。离开标题“Flask 后端服务”以后这句话的信息很少。知识库里如果还有 MySQL、Redis、前端开发服务器的端口召回就容易混淆。一种简单做法是把标题补到每个片段前面chunk_textf章节{section_title}\n\n{paragraph_text}这样既保留正文也让片段拥有更完整的语义上下文。3.4 重叠不是越大越好适当重叠能避免句子在边界处被截断但重叠太大会产生大量近似片段。结果可能是 Top 5 全来自同一页其他真正有用的来源被挤出去。出现这种情况时应该限制同一文档的候选数量或者先去重再进入生成阶段而不是继续提高top_k。把过短、过长和语义完整的片段放在一起对比更容易发现条件丢失或主题混杂。四、第二类问题Embedding 模型前后不一致这是本地 RAG 里非常隐蔽的一类问题。第一次入库使用nomic-embed-text后来为了测试又改成另一个模型。如果没有重建集合旧向量和新查询向量可能来自不同的向量空间。有时维度不同程序会直接报错更麻烦的是维度碰巧相同程序能运行检索结果却没有意义。入库向量和查询向量只有处在同一语义空间后面的相似度比较才有意义。建议把 Embedding 信息和知识库一起保存{collection:project_docs,embedding_model:nomic-embed-text,chunk_version:v3,document_version:2026-08-09}启动时先校验EXPECTED_MODELnomic-embed-textifstored_config[embedding_model]!EXPECTED_MODEL:raiseRuntimeError(Embedding 模型已变化请重建知识库)不要在已经写入数据的集合上悄悄切换模型。另外官方文档也明确提醒索引文本和查询文本应使用同一个 Embedding 模型。这个规则比“换更大的生成模型”更基础。五、第三类问题向量库里混进了旧数据和重复数据项目文档经常改名部署说明.md 部署说明_最终版.md 部署说明_最终版2.md 部署说明_202608.md如果每次上传都用随机 UUID当作四份新文档写入向量库并不知道它们其实是同一份资料的不同版本。于是可能出现新版写“默认只监听本机”旧版写“默认对局域网开放”两段都能被召回模型在冲突资料中选了旧答案。更稳的做法是让记录 ID 与来源和片段位置关联importhashlibdefbuild_chunk_id(source:str,section:str,chunk_index:int)-str:rawf{source}|{section}|{chunk_index}returnhashlib.sha256(raw.encode(utf-8)).hexdigest()文档替换时应先按document_id删除旧片段再写入新片段。upsert只会覆盖相同 ID并不会判断两份不同文件名的内容是否属于同一文档。还可以把版本和状态写入元数据metadata{document_id:deploy-guide,version:2026-08,status:active,source:部署说明.md,}查询时只搜索有效版本resultcollection.query(query_embeddings[query_vector],n_results5,where{status:active},include[documents,metadatas,distances],)Chroma 的where用于按元数据过滤如果过滤条件写错正确片段甚至没有机会参与相似度排序。六、第四类问题问题太短检索意图不完整用户经常不会写完整问题可能只问打不开怎么办对话里大家知道说的是项目页面但向量检索只看到这六个字。它不知道打不开的是网页、数据库、SSH 还是文件。可以在检索前结合对话状态改写问题原问题打不开怎么办 改写后部署 Flask 项目后局域网其他电脑无法访问 5000 端口应该检查什么问题改写只补全检索对象、环境和异常现象不在这一步生成答案。可以把最终用于检索的问题也打印出来。否则页面显示的是原问题后台查的却是另一个句子排查时很难发现偏差。七、第五类问题Top K 不是越大越好把top_k从 3 改到 20有时能暂时找到答案但也会把大量无关片段塞进上下文。假设真正相关片段排第 6直接把 20 段全部交给模型可能出现三个问题相关片段在长上下文中不够突出多个旧版本相互冲突提示词变长响应更慢占用更多上下文。更合适的做法是分两阶段处理向量召回 Top 20 → 去重与元数据过滤 → 重排得到 Top 5 → 交给大模型如果暂时没有重排模型也可以先做简单规则来源版本必须有效同一文档最多保留两段标题或正文命中关键实体时加分过短、只有页眉页脚的片段直接丢弃。即使暂时不引入复杂框架也要避免“召回多少就全部喂多少”。八、最后才检查生成阶段如果正确片段已经稳定排在前几名模型仍然答错再检查生成提示词。可以使用明确但不过长的约束只根据“参考资料”回答。资料中没有答案时直接说明“当前资料不足”不要使用常识补全。回答末尾列出使用到的来源文件。若资料之间存在冲突指出冲突并优先采用版本更新的资料。组装上下文时带上来源context_blocks[]fordocument,metadatainzip(documents,metadatas):context_blocks.append(f来源{metadata[source]}\nf版本{metadata[version]}\nf内容{document})context\n\n---\n\n.join(context_blocks)不要只把正文拼在一起。没有来源和版本模型也无法判断冲突资料应该信哪一份。九、建立一套最小验收题集只手工问一两个问题很容易出现“这次刚好对了”。可以先准备 10—20 个小题TEST_CASES[{question:局域网电脑为什么不能访问系统,expected_source:部署说明.md,expected_keyword:0.0.0.0,},{question:迁移项目时数据库文件在哪里,expected_source:数据备份.md,expected_keyword:instance/app.db,},{question:真实密码能不能放进仓库,expected_source:账号安全.md,expected_keyword:不得保存,},]先测“召回是否正确”不要把生成模型的不确定性混进来defevaluate_retrieval(test_cases:list[dict])-None:passed0forcaseintest_cases:query_vectorembed_texts([case[question]])[0]resultcollection.query(query_embeddings[query_vector],n_results3,include[documents,metadatas],)documentsresult[documents][0]sources[item[source]foriteminresult[metadatas][0]]ok(case[expected_source]insourcesandany(case[expected_keyword]intextfortextindocuments))print(通过ifokelse失败,case[question],sources)passedint(ok)print(f召回通过率{passed}/{len(test_cases)})每次修改切片规则、Embedding 模型或过滤逻辑都重新跑一遍。这样才能知道优化到底有效还是只把某一个问题碰巧修好了。验收记录至少要把“是否召回正确”和“是否回答正确”分开。一次修改是否有效应该由固定题集和可保存的中间结果证明而不是靠临时问对一个问题。十、一份更实用的排查顺序遇到“资料有答案但模型答错”时可以按下面顺序检查。10.1 原始资料原文是否真的包含答案解析后的文本是否丢字PDF 是否是扫描图片表格内容是否被打乱页眉页脚是否重复混入正文。10.2 切片条件和结论是否被拆开标题是否跟随正文一个片段是否包含太多主题重叠是否制造了大量重复片段。10.3 向量入库和查询是否使用同一个模型模型名称和维度是否记录更换模型后是否完整重建集合中英文、缩写和专业词是否适合当前模型。10.4 数据库是否重复上传同一文档是否还存在旧版本ID 是否稳定元数据过滤是否把正确结果排除。10.5 检索最终查询文本是什么正确片段排在第几距离值和其他候选相差多少Top K 是否太小或太大是否需要关键词混合检索或重排。10.6 生成上下文是否真的传给模型是否带来源和版本是否要求资料不足时拒答是否有旧对话内容覆盖了当前资料上下文是否太长导致重点被淹没。排查顺序整理先打印检索片段、来源、版本和距离再检查切片、Embedding、旧数据、过滤条件和查询改写。只有候选片段和最终上下文正确提示词优化才有可解释的基础。整个过程可以收敛成几个能直接验证的问题原文还在不在 片段切对没有 向量是不是同一个模型生成的 正确资料有没有被召回 最终上下文到底传了什么把这些答案连同固定题集一起保存后续调整top_k、切片、召回、重排或提示词时就能判断变化发生在哪一层无需编造命中率来说明效果。参考资料Ollama Embeddings 官方文档Ollama Embed APIChroma Query and GetChroma Metadata FilteringChroma Update and Upsert
返回列表