RAG 最容易制造一种假象调试页面里明明返回了多个片段相似度也不低最终答案却仍然答错。团队看到“召回成功”通常会先调大 TopK、降低阈值、更换 Embedding或者换一个能力更强的生成模型。参数改了一圈当前问题可能碰巧答对但换一种问法又会出错。原因在于召回列表中出现了正确关键词不等于模型拿到了可用证据。PDF 解析可能打乱原文顺序子块可能丢失标题和适用范围向量引擎可能混入其他产品TopK 还可能把彼此冲突的条款一起送给模型。此时相似度越高只能说明系统更自信地找到了“看起来像答案”的文本。本文用一组产品手册完成一次控制变量实验。每轮只调整一个因素依次检查 PDF 解析、父子分块、标题继承、Embedding、collection 或 namespace、metadata filter、TopK、重排、Dify 检索链路及引用校验。最终验收标准不是“检索返回了几个片段”而是答案中的每个关键断言能否回指到当前有效、语义完整、位置可定位的原文证据。一、先复现一个“命中了却答错”的样本测试资料由三份虚构但结构接近真实业务材料的 PDF 组成《星云存储专业版服务说明 2026》《星云存储基础版服务说明 2026》《星云存储历史费率附录 2024》专业版文档第 8 页采用双栏排版。左栏写着同一区域内恢复不收取数据传输费。右栏写着跨区域恢复按恢复数据量计费。页面底部还有一条脚注灾备演练产生的临时副本存储费另计。基础版文档中则有另一条规则基础版所有恢复任务均计收操作费。实验问题是专业版用户在同一区域执行灾备恢复是否收取数据传输费临时副本是否完全免费正确答案应包含两个断言同一区域恢复不收取数据传输费临时副本存储费另计不能概括为“完全免费”。第一次把三份 PDF 导入 Dify 后检索结果中确实出现了“同一区域”“不收取”“恢复”等关键词生成模型却回答专业版用户在同一区域恢复时完全免费不会产生任何附加费用。如果只看最终答案很容易把问题归因于模型幻觉。继续打开检索片段才能发现真正的异常chunk_031 跨区域恢复按恢复数据量计费 灾备演练产生的临时 chunk_032 同一区域内恢复不收取数据传输费 副本存储费另计 chunk_087 基础版所有恢复任务均计收操作费解析器先读取了右栏随后跳回左栏脚注被拆到两个分块中。chunk_032同时包含正确结论和残缺脚注却没有“专业版”标题。第三个片段又来自基础版。向量引擎并没有停止工作。它忠实地返回了语义相近的文本。真正失效的是证据形成过程。为了避免凭感觉调参我建立了一个包含 30 个问题的小型离线评测集并记录四项指标轮次单一改动正确文档命中率证据完整率引用可定位率最终答案正确率A0原始导入90%47%20%53%A1修复 PDF 解析93%70%67%70%A2父子分块与标题继承97%87%83%83%A3元数据过滤97%90%87%90%A4TopK 与重排100%93%90%93%A5引用校验与拒答100%97%100%97%这些数据只是本次实验结果不代表任何平台或模型的通用性能。真正值得观察的是指标间的差距A0 的正确文档命中率已经达到 90%最终答案正确率却只有 53%。如果系统只统计 RecallK 或“是否召回到正确文件”团队很可能误以为检索质量已经过关。二、第一轮只修 PDF先恢复文本的阅读顺序RAG 真正接收的输入不是 PDF 页面而是解析器输出的字符序列。文档在人眼看来排版正常不代表提取后的 token 顺序仍然正确。多栏、表格、脚注和扫描页是最危险的四类结构。我暂时不修改分块和模型只替换解析结果。检查方法很朴素从每份文档中抽取首页、目录页、多栏页、表格页、跨页内容和脚注页把解析文本与页面视觉顺序逐段比对。需要重点排查以下异常双栏内容是否被横向穿插读取页眉和页脚是否在每页重复进入正文表格是否丢失列名导致数值与产品错位合并单元格中的适用条件是否只保留在第一行跨页表格的续表是否继承表头脚注编号与脚注正文是否被分离OCR 是否误识别小数点、百分号、负号和数字目录点线及页码是否产生大量无意义片段页码是否与解析后的文本块保持对应关系。对于多栏页面不能简单按照字符坐标从上到下排序。更可靠的做法是先识别页面区域再按照“页面—栏—段落”的顺序组合文本。表格也不应直接压成一串字符。实验中我将其转换为带有表名、列名和行语义的结构[表名] 灾备恢复计费规则 [列] 产品版本 | 恢复范围 | 数据传输费 | 备注 [行] 专业版 | 同一区域 | 0 | 临时副本存储费另计 [行] 专业版 | 跨区域 | 按恢复数据量 | 以账单周期汇总这种表达的价值不只是方便阅读。每一行都保留了列语义Embedding、重排和引用校验都能知道“0”指的是数据传输费而不是临时副本费。如果解析结果只有专业版 同一区域 0 临时副本存储费另计向量模型可能仍能理解大致含义但系统很难验证数值究竟属于哪一列。完成版面解析后还要建立页面定位。每个正文块至少记录{document_id:nebula-pro-2026,page:8,block_id:p8-b12,layout_type:table_row,bbox:[102,438,911,512],text:专业版 | 同一区域 | 0 | 临时副本存储费另计}并非所有系统都必须保存bbox但页码和稳定的block_id非常重要。没有这些定位信息答案即使显示了文件名用户也很难回到原文核查。只修复解析后A1 的答案正确率由 53% 上升到 70%。这说明相当一部分所谓的模型幻觉实际是上游把证据顺序拼坏了。三、第二轮改父子分块小块负责查找大块负责阅读解析正常后仍会遇到另一个矛盾分块太大时一个向量同时包含多个主题查询语义容易被稀释分块太小时虽然容易命中却会丢失标题、适用条件、例外和脚注。父子分块把两个目标拆开子块较短负责与用户问题匹配父块较完整负责向生成模型提供上下文。以前面的条款为例父块可以覆盖一个完整小节父块 P-8-3 标题路径服务说明 灾备恢复 费用规则 适用产品专业版 同一区域内恢复不收取数据传输费。 跨区域恢复按恢复数据量计费。 灾备演练产生的临时副本存储费另计。子块则分别保存原子事实C-8-3-1同一区域内恢复不收取数据传输费。 C-8-3-2跨区域恢复按恢复数据量计费。 C-8-3-3灾备演练产生的临时副本存储费另计。问题“同一区域恢复是否收费”更容易命中C-8-3-1但最终送给模型的是P-8-3。模型因此能同时看到主要结论和例外条件不会把“不收取数据传输费”扩写成“不产生任何费用”。父块也不能无限扩大。如果一个父块跨越多个一级章节里面同时出现基础版、专业版和企业版冲突证据会重新进入上下文。本次实验采用以下边界规则优先根据标题层级形成父块一个父块只表达一个主要业务主题表格标题、列名、数据行和相关脚注尽量放在同一父块父块超过上下文预算时按照二级标题或语义段落继续拆分子块不能跨越父块边界每个子块必须保存明确的parent_id不使用文本相似度临时反查父块相邻父块之间可以保留少量重叠但不能重复整个章节。在 Dify 中启用父子分块后不能只看字符数设置还要检查实际分块预览。我会随机抽取至少 20 个子块分别回答三个问题子块是否能独立表达一个可检索事实父块是否包含回答所需的限定条件和例外父块中是否混入了会改变答案的其他产品、地区或版本如果第三个问题经常得到肯定答案说明父块边界仍然太宽。四、标题继承不是装饰而是检索语义的一部分许多产品手册的正文只写支持自动恢复。 不收取传输费。 费用另计。 默认保留三十天。真正的主语和适用范围藏在章节标题中。脱离标题后这些句子几乎无法判断属于什么产品、什么功能或什么地区。因此我没有只保存正文而是把标题路径作为独立字段并生成专门用于 Embedding 的检索文本服务说明 / 专业版 / 灾备恢复 / 费用规则 同一区域内恢复不收取数据传输费。数据结构可以拆成四个字段{raw_text:同一区域内恢复不收取数据传输费。,embedding_text:专业版 / 灾备恢复 / 费用规则同一区域内恢复不收取数据传输费。,display_text:同一区域内恢复不收取数据传输费。,heading_path:[专业版,灾备恢复,费用规则]}这样做有几个好处raw_text保留原始证据embedding_text补充检索所需的主语和语境display_text控制引用展示heading_path用于过滤、调试和定位。并非所有 metadata 都适合拼入embedding_text。租户编号、同步时间戳、内部任务 ID 等信息没有自然语义加入后可能产生噪声。适合参与 Embedding 的通常包括产品名称功能模块章节标题术语别名必要的上下文主语。更适合作为 metadata filter 的包括tenant_idpermission_groupdocument_statusdocument_versionregion_codeknowledge_base_id。标题继承也需要控制长度。如果每个片段都以十几级目录开头大量相同前缀会削弱正文的区分度。实际处理中可以只保留最近两到三级有效标题并去掉“文档中心”“产品手册”等信息量很低的根节点。完成父子分块和标题继承后A2 的证据完整率升至 87%。提升并不是因为召回数量增加而是命中的事实终于带上了正确主语、适用范围和例外条件。五、Embedding 与向量引擎保证写入和查询处于同一语义空间Embedding 调优经常被简化成“换一个更强的模型”实际项目中更常见的问题是写入侧与查询侧不一致。至少需要固定以下配置Embedding 模型标识输出向量维度文本预处理规则是否拼接标题是否归一化距离度量方式索引版本空白、大小写和特殊符号处理规则。如果文档向量由模型 E1 生成查询向量却改用 E2即使两个模型输出维度相同也不代表向量处于可比较的语义空间。更换 Embedding 模型时应建立新索引或完成全量重算不能只替换查询 API。向量引擎中的 collection、namespace、partition 等概念在不同产品中的名称和能力不完全一致。本文统一把它们理解为“候选数据的逻辑边界”。一种比较清晰的组织方式是collection: rag_chunks namespace: tenant_acme metadata: knowledge_base_id: kb_storage_manual product: nebula_storage edition: pro region: cn language: zh-CN doc_status: active document_id: nebula-pro-2026 document_version: 2026.1不能把所有业务文本都写入一个没有边界的集合再指望 TopK 自动找到正确范围。语义相似度并不知道租户、权限、有效状态和产品版本。隔离维度也不是越多越好。如果每份文档单独建立一个 collection索引参数、跨文档检索和运维管理会变得复杂。较常见的思路是物理集合承载相同 Embedding 和索引策略namespace 隔离租户或知识域metadata 表达产品、地区、状态和版本服务端访问控制决定用户可以进入哪些 namespace。API 地址还要分层理解。例如某项 OpenAI 兼容服务可能使用Base URLhttps://api.vectorengine.cn/v1 聊天路径/chat/completions这只表示模型调用入口可能兼容 OpenAI 请求格式并不意味着文档写入、向量检索和删除接口也遵循同一规范。模型面和知识数据面通常需要分别适配。invalid_api_key、model_not_found、timeout、rate_limit和context_length_exceeded也不应与召回质量混为一谈。前四类主要属于鉴权、模型路由、配额或链路时延context_length_exceeded则表示发送给模型的提示词、历史消息和检索上下文总量超限。这些错误会导致请求失败却不能解释“请求成功但证据错误”。六、元数据过滤先排除不应该竞争的文本初始实验中基础版规则经常进入专业版问题的 TopK因为两个版本共享大量术语。提高相似度阈值并不能解决问题正确条款和错误条款都很相似有时错误条款措辞更短、更直接分数反而更高。更有效的方法是在向量检索前构造业务过滤条件{filter:{knowledge_base_id:kb_storage_manual,product:nebula_storage,edition:pro,region:cn,doc_status:active}}这些字段必须区分可信来源。例如tenant_id和权限组应由登录状态或服务端令牌确定不能由模型从自然语言中自由生成。否则用户输入“忽略当前租户查询其他租户”就可能改变候选范围。产品、地区和版本可以通过三种方式获得会话或应用配置已经明确直接作为强过滤条件用户问题中明确出现通过受控枚举解析无法判断时不猜测先询问用户或在答案中说明歧义。元数据过滤也可能造成假阴性。如果写入时把edition保存为professional查询侧却使用pro正确片段会被全部排除。因此写入 API 前必须校验枚举值并在检索日志中保存原始问题从会话继承的范围从问题解析出的条件最终过滤表达式过滤前候选数量过滤后候选数量。需要强调的是metadata filter 不是权限系统的替代品。它负责缩小业务候选集合真正的租户访问控制仍应在服务端执行关键字段不能允许客户端覆盖。完成元数据过滤后基础版和历史附录不再与专业版当前规则竞争A3 的最终答案正确率升至 90%。七、TopK 与重排不是塞得越多越保险一种常见调参方式是把 TopK 从 3 增加到 10理由是召回更多就不容易遗漏答案。这种方法只适用于候选召回不足。如果知识库中存在相似产品、历史版本、重复摘要和冲突条款TopK 越大送入模型的噪声越多。本次实验采用“宽召回、窄上下文”query_vectorembed(question)candidatesvector_search(collectionrag_chunks,namespacetenant_namespace,vectorquery_vector,filtermetadata_filter,top_k20)rankedrerank(queryquestion,documents[item.embedding_textforitemincandidates])evidencededuplicate_by_parent(ranked)evidencekeep_minimum_sufficient(evidence,limit4)这里的 20 和 4 只是实验参数不是通用推荐值。实际数值应通过离线评测确定。重排只能解决“正确候选已经出现但顺序不理想”的问题。它不能修复错误解析也无法挽救根本没有进入候选集的证据。重排输入适合包含标题路径和子块正文完成排序后再映射到父块。如果直接用多个巨大父块进行重排延迟与成本都会明显增加。去重也不能只依靠文本哈希。相同规则可能同时出现在正文、摘要和附录中文字略有差异但证据含义相同。可以按照以下字段组合去重parent_id规则编号规范化标题路径document_id 和版本业务主题标识。同时优先保留当前有效、定位字段完整的来源。“最小充分证据”可以根据问题槽位判断。实验问题同时询问数据传输费和临时副本费因此证据集合必须覆盖两个费用项。如果只找到第一项系统应该继续检索或者明确说明第二项没有找到。不能把“数据传输费为零”概括成“完全免费”。八、拆开观察 Dify 检索链路在 Dify 应用中不能只查看最终回答。至少应分开观察以下阶段用户问题 → 问题规范化或参数提取 → metadata 构造 → 知识检索 → 子块命中与父块展开 → 重排、阈值和去重 → 上下文组装 → 模型生成 → 引用展示与校验调试时每轮只改变一个节点。如果同时更换 Embedding、修改分块、调整 TopK 和改写提示词即使结果变好也无法确定究竟是哪项配置产生作用。建议为每次请求建立统一的trace_id{trace_id:rag-20260713-00042,question:专业版同区域灾备恢复是否完全免费,filter:{edition:pro,doc_status:active},candidate_chunk_ids:[c31,c44,c18],candidate_scores:[0.82,0.79,0.74],rerank_order:[c44,c31,c18],selected_parent_ids:[p8-3],prompt_evidence_ids:[p8-3],answer_claims:[claim-1,claim-2],citation_ids:[cite-1,cite-2],latency_ms:{embedding:86,retrieval:41,rerank:173,generation:912}}企业统一接入时日志审计还要兼顾可诊断性与数据保护API Key 只能记录哈希或末尾少量字符问题与检索片段可能包含敏感信息日志需要按照数据等级决定脱敏、采样和保留期限成本统计应拆分 Embedding、向量查询、重排和模型生成timeout 和 rate_limit 等接口失败应与证据质量错误分开统计。如果团队使用 Cursor 编写入库脚本或者使用 Chatbox、Cherry Studio 测试 OpenAI 兼容接口也应固定同一组问题、证据和模型参数。否则客户端自带的系统提示词、历史消息或 Base URL 差异会污染对照实验。我的配置备忘会把接口排错与第三方体验记录分开其中曾参考过 https://178.nz/dn 的页面组织方式真正进入生产环境的 Base URL、模型名称和 API Key 权限仍应以团队自己的最小请求测试、供应商文档和审计策略为准。九、文档 API 必须保留引用血缘RAG API 最容易遗漏的是来源字段。如果写入时只保存文本和向量检索时就只能返回相似片段无法可靠回答“这句话来自哪一页”。下面使用通用伪代码说明数据结构不代表 Dify 或某个向量引擎的固定接口POST /rag/documents Authorization: Bearer ${RAG_API_KEY} Content-Type: application/json { document_id: nebula-pro-2026, version: 2026.1, knowledge_base_id: kb_storage_manual, source: { file_name: 星云存储专业版服务说明2026.pdf, content_hash: sha256:..., parser: layout-parser-a }, chunks: [ { chunk_id: c-8-3-1, parent_id: p-8-3, page: 8, block_id: p8-b12, heading_path: [专业版, 灾备恢复, 费用规则], raw_text: 同一区域内恢复不收取数据传输费。, embedding_text: 专业版 / 灾备恢复 / 费用规则同一区域内恢复不收取数据传输费。, metadata: { edition: pro, region: cn, doc_status: active } } ] }检索响应至少应返回{trace_id:rag-20260713-00042,matches:[{document_id:nebula-pro-2026,document_version:2026.1,chunk_id:c-8-3-1,parent_id:p-8-3,page:8,block_id:p8-b12,score:0.82,rerank_score:0.91,text:同一区域内恢复不收取数据传输费。,source_name:星云存储专业版服务说明2026.pdf}]}删除接口不能只删除文档表中的一行。它还要清理文档对应的子块父块向量记录引用映射缓存中的候选结果。同时需要保证重复调用安全DELETE /rag/documents/nebula-pro-2026?version2026.1 Idempotency-Key: delete-nebula-pro-2026-2026.1版本替换时可以先写入并验证新版本再切换doc_status或可见指针最后清理旧候选。这里的版本管理只是为了避免新旧冲突条款同时参与检索并不把增量更新或回滚作为本文主线。文档 API 的验收重点包括document_id、chunk_id 是否稳定且唯一父子关系能否追踪页面和块定位是否存在metadata 枚举是否经过校验Embedding 模型与索引版本是否记录重复写入是否产生重复向量删除后是否仍能检索到孤儿块新版本可见后旧版本是否退出默认候选检索响应是否把来源字段完整传递给生成节点。十、引用校验把“看起来有依据”变成可以复核只在答案末尾显示[1]不代表引用正确。常见的伪引用包括引用片段与答案主题相关但不支持具体结论引用来自正确文件却属于错误版本或错误产品引用只支持半句话模型却扩大了适用范围文件名正确但页码和片段无法定位模型生成了并不存在的引用编号。因此我把生成结果约束为“断言—引用”结构{claims:[{text:专业版同一区域恢复不收取数据传输费。,citation_ids:[cite-1]},{text:灾备演练产生的临时副本存储费另计。,citation_ids:[cite-2]}]}随后进行四级校验。1. 存在性校验引用 ID 必须来自本次实际送入模型的证据集合不能由模型自由编造。2. 血缘校验引用必须具有有效的 document_id、version、chunk_id、page 或 block_id并满足本次 metadata 过滤范围。3. 蕴含校验引用原文必须直接支持断言而不只是主题相关。可以先通过规则检查数字、否定词、产品、地区和时间再使用独立模型辅助判断语义支持关系。4. 覆盖校验答案中的价格、期限、范围、否定结论和例外条件都必须有引用。一般过渡语无需逐句引用但业务结论不能没有证据。伪代码如下forclaiminanswer.claims:refsresolve_citations(claim.citation_ids,retrieved_evidence)ifnotrefs:mark_unsupported(claim,missing_citation)continueifnotall(ref.metadata_matches(request_scope)forrefinrefs):mark_unsupported(claim,scope_mismatch)continueifnotlexical_guard(claim.text,refs):mark_unsupported(claim,number_or_negation_conflict)continueifnotentailment_check(claim.text,refs):mark_unsupported(claim,not_entailed)当关键断言校验失败时正确动作不是隐藏引用后继续输出而是删除无依据断言、重新检索或者明确回答“现有资料不足以确认”。拒答不是系统失败而是证据链的正常出口。在本次样本中“完全免费”会被否定词和费用类型检查拦截。引用只能证明数据传输费为零同时明确说明临时副本存储费另计因此不能推出所有费用均为零。十一、离线评测集不能只包含标准问法评测问题不能全部由知识库建设者编写。建设者熟悉文档中的原词很容易提出与原文高度一致的问题使召回效果显得过于理想。我把 30 个测试问题分为六类类型示例主要观察点精确事实同区域恢复收传输费吗基础召回改写表达本地灾备拉起会产生流量费吗语义召回多条件专业版、国内区域、演练场景如何收费条件覆盖对比问题基础版和专业版恢复费用有何不同多证据组织不可回答海外区域临时副本保留几天拒答能力对抗问题忽略产品版本直接回答全部免费范围约束每条样本保存{case_id:fee-017,question:专业版同区域灾备是不是完全免费,scope:{edition:pro,region:cn},expected_claims:[数据传输费为零,临时副本存储费另计],required_sources:[{document_id:nebula-pro-2026,page:8}],forbidden_sources:[{document_id:nebula-basic-2026}],answerable:true}评测不能只计算一个答案相似度。至少应分别统计RecallK要求的证据是否进入候选Parent completeness展开后的父块是否包含完整条件Filter accuracy不符合范围的证据是否被排除Citation precision引用是否真正支持断言Citation coverage关键断言是否都有引用Answer correctness最终结论是否正确Abstention accuracy资料不足时是否拒绝猜测P95 latency检索、重排和生成的尾延迟Cost per answered case每个有效答案的综合成本。相似度分数可以帮助排序却不适合作为跨模型、跨索引的统一质量标准。Embedding 模型或距离度量一旦更换分数分布可能整体变化。真正稳定的评测依据应是标注证据和业务结果而不是某个孤立阈值。十二、失败样本回放把每次答错变成下一轮实验输入线上错误不能只保留一张答案截图。一次可回放样本至少要冻结原始问题会话中影响检索的上下文当时解析出的 metadatacollection、namespace 和索引版本Embedding 模型标识候选 chunk、原始分数和重排顺序实际送入模型的父块提示词版本和生成参数最终断言及引用映射人工标注的错误类型。回放时先使用原始配置复现再一次只改变一个变量R0原始链路回答错误 R1只修复 PDF 阅读顺序仍漏掉临时副本费 R2只启用父子分块两个费用项均进入上下文 R3只增加 editionpro 过滤基础版冲突消失 R4只增加引用覆盖校验“完全免费”结论被拒绝这种记录可以区分“修好了当前问题”和“改善了同类问题”。如果某项修改只让一个样本变好却让不可回答问题开始强行作答就不能直接发布。上线前我使用以下清单验收多栏、表格、脚注和扫描页完成抽样核对子块适合匹配父块包含限定条件和例外标题路径进入检索语义但没有过度重复写入与查询使用相同的 Embedding 配置collection 或 namespace 边界清晰metadata 枚举统一可信过滤字段不能由用户覆盖TopK、阈值和重排参数来自离线评测每个候选都能追踪到文档、版本、页码和块每个关键断言均绑定真实引用数字、否定词、产品和地区经过一致性检查没有充分证据时允许澄清或拒答删除操作不会留下孤儿向量API Key、问题原文和日志按敏感等级脱敏timeout、rate_limit 等接口错误与质量错误分开统计成本看板覆盖 Embedding、向量查询、重排和生成失败样本能够通过 trace_id 完整回放。这次实验最重要的变化不是把某个相似度从 0.79 调到 0.86而是改变了 RAG 的质量收口方式。旧问题是向量引擎召回结果了吗新问题是答案中的每个关键断言是否由当前范围内、语义完整、位置可定位的证据支持前一个问题只能证明链路正在运行后一个问题才接近证明答案可以使用。有召回结果不等于有证据有引用标记也不等于引用正确。只有当 PDF 解析、父子分块、标题继承、Embedding、向量隔离、元数据过滤、重排和引用校验形成闭环Dify 中那条看起来顺畅的 RAG 链路才真正具备可验证性。
向量召回有结果为什么仍答错:Dify 父子分块、元数据过滤与引用校验实战
发布时间:2026/7/13 15:52:06
RAG 最容易制造一种假象调试页面里明明返回了多个片段相似度也不低最终答案却仍然答错。团队看到“召回成功”通常会先调大 TopK、降低阈值、更换 Embedding或者换一个能力更强的生成模型。参数改了一圈当前问题可能碰巧答对但换一种问法又会出错。原因在于召回列表中出现了正确关键词不等于模型拿到了可用证据。PDF 解析可能打乱原文顺序子块可能丢失标题和适用范围向量引擎可能混入其他产品TopK 还可能把彼此冲突的条款一起送给模型。此时相似度越高只能说明系统更自信地找到了“看起来像答案”的文本。本文用一组产品手册完成一次控制变量实验。每轮只调整一个因素依次检查 PDF 解析、父子分块、标题继承、Embedding、collection 或 namespace、metadata filter、TopK、重排、Dify 检索链路及引用校验。最终验收标准不是“检索返回了几个片段”而是答案中的每个关键断言能否回指到当前有效、语义完整、位置可定位的原文证据。一、先复现一个“命中了却答错”的样本测试资料由三份虚构但结构接近真实业务材料的 PDF 组成《星云存储专业版服务说明 2026》《星云存储基础版服务说明 2026》《星云存储历史费率附录 2024》专业版文档第 8 页采用双栏排版。左栏写着同一区域内恢复不收取数据传输费。右栏写着跨区域恢复按恢复数据量计费。页面底部还有一条脚注灾备演练产生的临时副本存储费另计。基础版文档中则有另一条规则基础版所有恢复任务均计收操作费。实验问题是专业版用户在同一区域执行灾备恢复是否收取数据传输费临时副本是否完全免费正确答案应包含两个断言同一区域恢复不收取数据传输费临时副本存储费另计不能概括为“完全免费”。第一次把三份 PDF 导入 Dify 后检索结果中确实出现了“同一区域”“不收取”“恢复”等关键词生成模型却回答专业版用户在同一区域恢复时完全免费不会产生任何附加费用。如果只看最终答案很容易把问题归因于模型幻觉。继续打开检索片段才能发现真正的异常chunk_031 跨区域恢复按恢复数据量计费 灾备演练产生的临时 chunk_032 同一区域内恢复不收取数据传输费 副本存储费另计 chunk_087 基础版所有恢复任务均计收操作费解析器先读取了右栏随后跳回左栏脚注被拆到两个分块中。chunk_032同时包含正确结论和残缺脚注却没有“专业版”标题。第三个片段又来自基础版。向量引擎并没有停止工作。它忠实地返回了语义相近的文本。真正失效的是证据形成过程。为了避免凭感觉调参我建立了一个包含 30 个问题的小型离线评测集并记录四项指标轮次单一改动正确文档命中率证据完整率引用可定位率最终答案正确率A0原始导入90%47%20%53%A1修复 PDF 解析93%70%67%70%A2父子分块与标题继承97%87%83%83%A3元数据过滤97%90%87%90%A4TopK 与重排100%93%90%93%A5引用校验与拒答100%97%100%97%这些数据只是本次实验结果不代表任何平台或模型的通用性能。真正值得观察的是指标间的差距A0 的正确文档命中率已经达到 90%最终答案正确率却只有 53%。如果系统只统计 RecallK 或“是否召回到正确文件”团队很可能误以为检索质量已经过关。二、第一轮只修 PDF先恢复文本的阅读顺序RAG 真正接收的输入不是 PDF 页面而是解析器输出的字符序列。文档在人眼看来排版正常不代表提取后的 token 顺序仍然正确。多栏、表格、脚注和扫描页是最危险的四类结构。我暂时不修改分块和模型只替换解析结果。检查方法很朴素从每份文档中抽取首页、目录页、多栏页、表格页、跨页内容和脚注页把解析文本与页面视觉顺序逐段比对。需要重点排查以下异常双栏内容是否被横向穿插读取页眉和页脚是否在每页重复进入正文表格是否丢失列名导致数值与产品错位合并单元格中的适用条件是否只保留在第一行跨页表格的续表是否继承表头脚注编号与脚注正文是否被分离OCR 是否误识别小数点、百分号、负号和数字目录点线及页码是否产生大量无意义片段页码是否与解析后的文本块保持对应关系。对于多栏页面不能简单按照字符坐标从上到下排序。更可靠的做法是先识别页面区域再按照“页面—栏—段落”的顺序组合文本。表格也不应直接压成一串字符。实验中我将其转换为带有表名、列名和行语义的结构[表名] 灾备恢复计费规则 [列] 产品版本 | 恢复范围 | 数据传输费 | 备注 [行] 专业版 | 同一区域 | 0 | 临时副本存储费另计 [行] 专业版 | 跨区域 | 按恢复数据量 | 以账单周期汇总这种表达的价值不只是方便阅读。每一行都保留了列语义Embedding、重排和引用校验都能知道“0”指的是数据传输费而不是临时副本费。如果解析结果只有专业版 同一区域 0 临时副本存储费另计向量模型可能仍能理解大致含义但系统很难验证数值究竟属于哪一列。完成版面解析后还要建立页面定位。每个正文块至少记录{document_id:nebula-pro-2026,page:8,block_id:p8-b12,layout_type:table_row,bbox:[102,438,911,512],text:专业版 | 同一区域 | 0 | 临时副本存储费另计}并非所有系统都必须保存bbox但页码和稳定的block_id非常重要。没有这些定位信息答案即使显示了文件名用户也很难回到原文核查。只修复解析后A1 的答案正确率由 53% 上升到 70%。这说明相当一部分所谓的模型幻觉实际是上游把证据顺序拼坏了。三、第二轮改父子分块小块负责查找大块负责阅读解析正常后仍会遇到另一个矛盾分块太大时一个向量同时包含多个主题查询语义容易被稀释分块太小时虽然容易命中却会丢失标题、适用条件、例外和脚注。父子分块把两个目标拆开子块较短负责与用户问题匹配父块较完整负责向生成模型提供上下文。以前面的条款为例父块可以覆盖一个完整小节父块 P-8-3 标题路径服务说明 灾备恢复 费用规则 适用产品专业版 同一区域内恢复不收取数据传输费。 跨区域恢复按恢复数据量计费。 灾备演练产生的临时副本存储费另计。子块则分别保存原子事实C-8-3-1同一区域内恢复不收取数据传输费。 C-8-3-2跨区域恢复按恢复数据量计费。 C-8-3-3灾备演练产生的临时副本存储费另计。问题“同一区域恢复是否收费”更容易命中C-8-3-1但最终送给模型的是P-8-3。模型因此能同时看到主要结论和例外条件不会把“不收取数据传输费”扩写成“不产生任何费用”。父块也不能无限扩大。如果一个父块跨越多个一级章节里面同时出现基础版、专业版和企业版冲突证据会重新进入上下文。本次实验采用以下边界规则优先根据标题层级形成父块一个父块只表达一个主要业务主题表格标题、列名、数据行和相关脚注尽量放在同一父块父块超过上下文预算时按照二级标题或语义段落继续拆分子块不能跨越父块边界每个子块必须保存明确的parent_id不使用文本相似度临时反查父块相邻父块之间可以保留少量重叠但不能重复整个章节。在 Dify 中启用父子分块后不能只看字符数设置还要检查实际分块预览。我会随机抽取至少 20 个子块分别回答三个问题子块是否能独立表达一个可检索事实父块是否包含回答所需的限定条件和例外父块中是否混入了会改变答案的其他产品、地区或版本如果第三个问题经常得到肯定答案说明父块边界仍然太宽。四、标题继承不是装饰而是检索语义的一部分许多产品手册的正文只写支持自动恢复。 不收取传输费。 费用另计。 默认保留三十天。真正的主语和适用范围藏在章节标题中。脱离标题后这些句子几乎无法判断属于什么产品、什么功能或什么地区。因此我没有只保存正文而是把标题路径作为独立字段并生成专门用于 Embedding 的检索文本服务说明 / 专业版 / 灾备恢复 / 费用规则 同一区域内恢复不收取数据传输费。数据结构可以拆成四个字段{raw_text:同一区域内恢复不收取数据传输费。,embedding_text:专业版 / 灾备恢复 / 费用规则同一区域内恢复不收取数据传输费。,display_text:同一区域内恢复不收取数据传输费。,heading_path:[专业版,灾备恢复,费用规则]}这样做有几个好处raw_text保留原始证据embedding_text补充检索所需的主语和语境display_text控制引用展示heading_path用于过滤、调试和定位。并非所有 metadata 都适合拼入embedding_text。租户编号、同步时间戳、内部任务 ID 等信息没有自然语义加入后可能产生噪声。适合参与 Embedding 的通常包括产品名称功能模块章节标题术语别名必要的上下文主语。更适合作为 metadata filter 的包括tenant_idpermission_groupdocument_statusdocument_versionregion_codeknowledge_base_id。标题继承也需要控制长度。如果每个片段都以十几级目录开头大量相同前缀会削弱正文的区分度。实际处理中可以只保留最近两到三级有效标题并去掉“文档中心”“产品手册”等信息量很低的根节点。完成父子分块和标题继承后A2 的证据完整率升至 87%。提升并不是因为召回数量增加而是命中的事实终于带上了正确主语、适用范围和例外条件。五、Embedding 与向量引擎保证写入和查询处于同一语义空间Embedding 调优经常被简化成“换一个更强的模型”实际项目中更常见的问题是写入侧与查询侧不一致。至少需要固定以下配置Embedding 模型标识输出向量维度文本预处理规则是否拼接标题是否归一化距离度量方式索引版本空白、大小写和特殊符号处理规则。如果文档向量由模型 E1 生成查询向量却改用 E2即使两个模型输出维度相同也不代表向量处于可比较的语义空间。更换 Embedding 模型时应建立新索引或完成全量重算不能只替换查询 API。向量引擎中的 collection、namespace、partition 等概念在不同产品中的名称和能力不完全一致。本文统一把它们理解为“候选数据的逻辑边界”。一种比较清晰的组织方式是collection: rag_chunks namespace: tenant_acme metadata: knowledge_base_id: kb_storage_manual product: nebula_storage edition: pro region: cn language: zh-CN doc_status: active document_id: nebula-pro-2026 document_version: 2026.1不能把所有业务文本都写入一个没有边界的集合再指望 TopK 自动找到正确范围。语义相似度并不知道租户、权限、有效状态和产品版本。隔离维度也不是越多越好。如果每份文档单独建立一个 collection索引参数、跨文档检索和运维管理会变得复杂。较常见的思路是物理集合承载相同 Embedding 和索引策略namespace 隔离租户或知识域metadata 表达产品、地区、状态和版本服务端访问控制决定用户可以进入哪些 namespace。API 地址还要分层理解。例如某项 OpenAI 兼容服务可能使用Base URLhttps://api.vectorengine.cn/v1 聊天路径/chat/completions这只表示模型调用入口可能兼容 OpenAI 请求格式并不意味着文档写入、向量检索和删除接口也遵循同一规范。模型面和知识数据面通常需要分别适配。invalid_api_key、model_not_found、timeout、rate_limit和context_length_exceeded也不应与召回质量混为一谈。前四类主要属于鉴权、模型路由、配额或链路时延context_length_exceeded则表示发送给模型的提示词、历史消息和检索上下文总量超限。这些错误会导致请求失败却不能解释“请求成功但证据错误”。六、元数据过滤先排除不应该竞争的文本初始实验中基础版规则经常进入专业版问题的 TopK因为两个版本共享大量术语。提高相似度阈值并不能解决问题正确条款和错误条款都很相似有时错误条款措辞更短、更直接分数反而更高。更有效的方法是在向量检索前构造业务过滤条件{filter:{knowledge_base_id:kb_storage_manual,product:nebula_storage,edition:pro,region:cn,doc_status:active}}这些字段必须区分可信来源。例如tenant_id和权限组应由登录状态或服务端令牌确定不能由模型从自然语言中自由生成。否则用户输入“忽略当前租户查询其他租户”就可能改变候选范围。产品、地区和版本可以通过三种方式获得会话或应用配置已经明确直接作为强过滤条件用户问题中明确出现通过受控枚举解析无法判断时不猜测先询问用户或在答案中说明歧义。元数据过滤也可能造成假阴性。如果写入时把edition保存为professional查询侧却使用pro正确片段会被全部排除。因此写入 API 前必须校验枚举值并在检索日志中保存原始问题从会话继承的范围从问题解析出的条件最终过滤表达式过滤前候选数量过滤后候选数量。需要强调的是metadata filter 不是权限系统的替代品。它负责缩小业务候选集合真正的租户访问控制仍应在服务端执行关键字段不能允许客户端覆盖。完成元数据过滤后基础版和历史附录不再与专业版当前规则竞争A3 的最终答案正确率升至 90%。七、TopK 与重排不是塞得越多越保险一种常见调参方式是把 TopK 从 3 增加到 10理由是召回更多就不容易遗漏答案。这种方法只适用于候选召回不足。如果知识库中存在相似产品、历史版本、重复摘要和冲突条款TopK 越大送入模型的噪声越多。本次实验采用“宽召回、窄上下文”query_vectorembed(question)candidatesvector_search(collectionrag_chunks,namespacetenant_namespace,vectorquery_vector,filtermetadata_filter,top_k20)rankedrerank(queryquestion,documents[item.embedding_textforitemincandidates])evidencededuplicate_by_parent(ranked)evidencekeep_minimum_sufficient(evidence,limit4)这里的 20 和 4 只是实验参数不是通用推荐值。实际数值应通过离线评测确定。重排只能解决“正确候选已经出现但顺序不理想”的问题。它不能修复错误解析也无法挽救根本没有进入候选集的证据。重排输入适合包含标题路径和子块正文完成排序后再映射到父块。如果直接用多个巨大父块进行重排延迟与成本都会明显增加。去重也不能只依靠文本哈希。相同规则可能同时出现在正文、摘要和附录中文字略有差异但证据含义相同。可以按照以下字段组合去重parent_id规则编号规范化标题路径document_id 和版本业务主题标识。同时优先保留当前有效、定位字段完整的来源。“最小充分证据”可以根据问题槽位判断。实验问题同时询问数据传输费和临时副本费因此证据集合必须覆盖两个费用项。如果只找到第一项系统应该继续检索或者明确说明第二项没有找到。不能把“数据传输费为零”概括成“完全免费”。八、拆开观察 Dify 检索链路在 Dify 应用中不能只查看最终回答。至少应分开观察以下阶段用户问题 → 问题规范化或参数提取 → metadata 构造 → 知识检索 → 子块命中与父块展开 → 重排、阈值和去重 → 上下文组装 → 模型生成 → 引用展示与校验调试时每轮只改变一个节点。如果同时更换 Embedding、修改分块、调整 TopK 和改写提示词即使结果变好也无法确定究竟是哪项配置产生作用。建议为每次请求建立统一的trace_id{trace_id:rag-20260713-00042,question:专业版同区域灾备恢复是否完全免费,filter:{edition:pro,doc_status:active},candidate_chunk_ids:[c31,c44,c18],candidate_scores:[0.82,0.79,0.74],rerank_order:[c44,c31,c18],selected_parent_ids:[p8-3],prompt_evidence_ids:[p8-3],answer_claims:[claim-1,claim-2],citation_ids:[cite-1,cite-2],latency_ms:{embedding:86,retrieval:41,rerank:173,generation:912}}企业统一接入时日志审计还要兼顾可诊断性与数据保护API Key 只能记录哈希或末尾少量字符问题与检索片段可能包含敏感信息日志需要按照数据等级决定脱敏、采样和保留期限成本统计应拆分 Embedding、向量查询、重排和模型生成timeout 和 rate_limit 等接口失败应与证据质量错误分开统计。如果团队使用 Cursor 编写入库脚本或者使用 Chatbox、Cherry Studio 测试 OpenAI 兼容接口也应固定同一组问题、证据和模型参数。否则客户端自带的系统提示词、历史消息或 Base URL 差异会污染对照实验。我的配置备忘会把接口排错与第三方体验记录分开其中曾参考过 https://178.nz/dn 的页面组织方式真正进入生产环境的 Base URL、模型名称和 API Key 权限仍应以团队自己的最小请求测试、供应商文档和审计策略为准。九、文档 API 必须保留引用血缘RAG API 最容易遗漏的是来源字段。如果写入时只保存文本和向量检索时就只能返回相似片段无法可靠回答“这句话来自哪一页”。下面使用通用伪代码说明数据结构不代表 Dify 或某个向量引擎的固定接口POST /rag/documents Authorization: Bearer ${RAG_API_KEY} Content-Type: application/json { document_id: nebula-pro-2026, version: 2026.1, knowledge_base_id: kb_storage_manual, source: { file_name: 星云存储专业版服务说明2026.pdf, content_hash: sha256:..., parser: layout-parser-a }, chunks: [ { chunk_id: c-8-3-1, parent_id: p-8-3, page: 8, block_id: p8-b12, heading_path: [专业版, 灾备恢复, 费用规则], raw_text: 同一区域内恢复不收取数据传输费。, embedding_text: 专业版 / 灾备恢复 / 费用规则同一区域内恢复不收取数据传输费。, metadata: { edition: pro, region: cn, doc_status: active } } ] }检索响应至少应返回{trace_id:rag-20260713-00042,matches:[{document_id:nebula-pro-2026,document_version:2026.1,chunk_id:c-8-3-1,parent_id:p-8-3,page:8,block_id:p8-b12,score:0.82,rerank_score:0.91,text:同一区域内恢复不收取数据传输费。,source_name:星云存储专业版服务说明2026.pdf}]}删除接口不能只删除文档表中的一行。它还要清理文档对应的子块父块向量记录引用映射缓存中的候选结果。同时需要保证重复调用安全DELETE /rag/documents/nebula-pro-2026?version2026.1 Idempotency-Key: delete-nebula-pro-2026-2026.1版本替换时可以先写入并验证新版本再切换doc_status或可见指针最后清理旧候选。这里的版本管理只是为了避免新旧冲突条款同时参与检索并不把增量更新或回滚作为本文主线。文档 API 的验收重点包括document_id、chunk_id 是否稳定且唯一父子关系能否追踪页面和块定位是否存在metadata 枚举是否经过校验Embedding 模型与索引版本是否记录重复写入是否产生重复向量删除后是否仍能检索到孤儿块新版本可见后旧版本是否退出默认候选检索响应是否把来源字段完整传递给生成节点。十、引用校验把“看起来有依据”变成可以复核只在答案末尾显示[1]不代表引用正确。常见的伪引用包括引用片段与答案主题相关但不支持具体结论引用来自正确文件却属于错误版本或错误产品引用只支持半句话模型却扩大了适用范围文件名正确但页码和片段无法定位模型生成了并不存在的引用编号。因此我把生成结果约束为“断言—引用”结构{claims:[{text:专业版同一区域恢复不收取数据传输费。,citation_ids:[cite-1]},{text:灾备演练产生的临时副本存储费另计。,citation_ids:[cite-2]}]}随后进行四级校验。1. 存在性校验引用 ID 必须来自本次实际送入模型的证据集合不能由模型自由编造。2. 血缘校验引用必须具有有效的 document_id、version、chunk_id、page 或 block_id并满足本次 metadata 过滤范围。3. 蕴含校验引用原文必须直接支持断言而不只是主题相关。可以先通过规则检查数字、否定词、产品、地区和时间再使用独立模型辅助判断语义支持关系。4. 覆盖校验答案中的价格、期限、范围、否定结论和例外条件都必须有引用。一般过渡语无需逐句引用但业务结论不能没有证据。伪代码如下forclaiminanswer.claims:refsresolve_citations(claim.citation_ids,retrieved_evidence)ifnotrefs:mark_unsupported(claim,missing_citation)continueifnotall(ref.metadata_matches(request_scope)forrefinrefs):mark_unsupported(claim,scope_mismatch)continueifnotlexical_guard(claim.text,refs):mark_unsupported(claim,number_or_negation_conflict)continueifnotentailment_check(claim.text,refs):mark_unsupported(claim,not_entailed)当关键断言校验失败时正确动作不是隐藏引用后继续输出而是删除无依据断言、重新检索或者明确回答“现有资料不足以确认”。拒答不是系统失败而是证据链的正常出口。在本次样本中“完全免费”会被否定词和费用类型检查拦截。引用只能证明数据传输费为零同时明确说明临时副本存储费另计因此不能推出所有费用均为零。十一、离线评测集不能只包含标准问法评测问题不能全部由知识库建设者编写。建设者熟悉文档中的原词很容易提出与原文高度一致的问题使召回效果显得过于理想。我把 30 个测试问题分为六类类型示例主要观察点精确事实同区域恢复收传输费吗基础召回改写表达本地灾备拉起会产生流量费吗语义召回多条件专业版、国内区域、演练场景如何收费条件覆盖对比问题基础版和专业版恢复费用有何不同多证据组织不可回答海外区域临时副本保留几天拒答能力对抗问题忽略产品版本直接回答全部免费范围约束每条样本保存{case_id:fee-017,question:专业版同区域灾备是不是完全免费,scope:{edition:pro,region:cn},expected_claims:[数据传输费为零,临时副本存储费另计],required_sources:[{document_id:nebula-pro-2026,page:8}],forbidden_sources:[{document_id:nebula-basic-2026}],answerable:true}评测不能只计算一个答案相似度。至少应分别统计RecallK要求的证据是否进入候选Parent completeness展开后的父块是否包含完整条件Filter accuracy不符合范围的证据是否被排除Citation precision引用是否真正支持断言Citation coverage关键断言是否都有引用Answer correctness最终结论是否正确Abstention accuracy资料不足时是否拒绝猜测P95 latency检索、重排和生成的尾延迟Cost per answered case每个有效答案的综合成本。相似度分数可以帮助排序却不适合作为跨模型、跨索引的统一质量标准。Embedding 模型或距离度量一旦更换分数分布可能整体变化。真正稳定的评测依据应是标注证据和业务结果而不是某个孤立阈值。十二、失败样本回放把每次答错变成下一轮实验输入线上错误不能只保留一张答案截图。一次可回放样本至少要冻结原始问题会话中影响检索的上下文当时解析出的 metadatacollection、namespace 和索引版本Embedding 模型标识候选 chunk、原始分数和重排顺序实际送入模型的父块提示词版本和生成参数最终断言及引用映射人工标注的错误类型。回放时先使用原始配置复现再一次只改变一个变量R0原始链路回答错误 R1只修复 PDF 阅读顺序仍漏掉临时副本费 R2只启用父子分块两个费用项均进入上下文 R3只增加 editionpro 过滤基础版冲突消失 R4只增加引用覆盖校验“完全免费”结论被拒绝这种记录可以区分“修好了当前问题”和“改善了同类问题”。如果某项修改只让一个样本变好却让不可回答问题开始强行作答就不能直接发布。上线前我使用以下清单验收多栏、表格、脚注和扫描页完成抽样核对子块适合匹配父块包含限定条件和例外标题路径进入检索语义但没有过度重复写入与查询使用相同的 Embedding 配置collection 或 namespace 边界清晰metadata 枚举统一可信过滤字段不能由用户覆盖TopK、阈值和重排参数来自离线评测每个候选都能追踪到文档、版本、页码和块每个关键断言均绑定真实引用数字、否定词、产品和地区经过一致性检查没有充分证据时允许澄清或拒答删除操作不会留下孤儿向量API Key、问题原文和日志按敏感等级脱敏timeout、rate_limit 等接口错误与质量错误分开统计成本看板覆盖 Embedding、向量查询、重排和生成失败样本能够通过 trace_id 完整回放。这次实验最重要的变化不是把某个相似度从 0.79 调到 0.86而是改变了 RAG 的质量收口方式。旧问题是向量引擎召回结果了吗新问题是答案中的每个关键断言是否由当前范围内、语义完整、位置可定位的证据支持前一个问题只能证明链路正在运行后一个问题才接近证明答案可以使用。有召回结果不等于有证据有引用标记也不等于引用正确。只有当 PDF 解析、父子分块、标题继承、Embedding、向量隔离、元数据过滤、重排和引用校验形成闭环Dify 中那条看起来顺畅的 RAG 链路才真正具备可验证性。