跳到正文

RAG PROJECT INTERVIEW · Q10 / 25

Chunk 为什么要带标题、页码和前后片段 ID?

解释 metadata 如何同时服务于硬过滤、排序增强、上下文扩展与答案引用,并给出不依赖具体向量库的字段设计思路。

作者与复核:吴师兄内容依据:公开 RAG 教程、GitHub 教学代码与工程推导查看关联教程 →查看吴师兄公开作品 →

25 道题目Q10
本页内容14 节

30 秒先说结论

Chunk 除正文外,至少应保存文档 ID、章节路径、页码或位置、内容类型和版本;需要上下文扩展时再保存前后片段 ID。检索前可用知识库、文档、权限或时间字段硬过滤,排序时利用标题和类型,回答后则用来源字段定位原文。字段必须跟业务动作对应,不能为了丰富而堆 metadata。

先看一个教学场景。假设检索系统找回了一段内容:

申请需要在规定时间内提交,超过期限将进入补充审核。

这段话看起来很相关,但系统还不知道几件事。

它来自哪份文档?属于哪个章节?“规定时间”到底是多少?上一段是不是刚好写了适用对象?这份文档是不是已经被新版本替换?当前用户又是否有权查看?

如果 Chunk 里只有正文和向量,这些问题都无法可靠回答。

于是,面试官会继续问:

RAG 的 Chunk 除了正文,还应该保存哪些 metadata?这些字段分别解决什么问题?

常见回答是列出一串字段:doc_idpagetitlecreated_atprev_idnext_id

字段名背得再多,也不等于设计合理。真正需要回答的是:哪个字段参与搜索前过滤,哪个字段补足语义,哪个字段帮助取回上下文,哪个字段让用户回到原文。

Metadata 不是备注,而是检索链路的一部分

只保存正文和向量,确实能做出一个演示版本。

用户问题被编码成向量,向量库找到相近文本,再交给大模型回答。只要知识库很小、文档没有版本、所有用户权限相同,系统看起来也能工作。

一旦进入多文档场景,问题马上出现。下面仍然用教学场景说明常见故障。

在这个纯教学例子里,假设用户只想查某个知识库,系统却从另一个知识库带回内容;旧制度与新制度文字相似,旧版本反而排得更靠前;检索命中“但以下情况除外”,模型却看不到上一段的适用条件;回答末尾只显示一个文件名,用户打开后还要自己在几十页里寻找原句。这些是假设出来的故障类型,只用来说明 Metadata 缺失可能影响哪些环节。

这些故障不是换一个 Embedding 模型就能解决的。它们需要明确的文档边界、状态、位置和关系。

正文、增强文本和 metadata 各自负责什么

正文用于展示与核验,增强文本帮助检索理解语境,metadata 则负责过滤、边界、关系和来源。

Metadata 的价值,可以归纳成六类动作:过滤、排序、去重、扩展、展示和审计。

如果某个字段不服务任何动作,也没有明确的消费者,它只会增加解析、入库和更新时的同步成本。

字段应该怎样从业务动作反推?

第一类是身份字段。

chunk_id 用来稳定定位当前片段,doc_id 标明所属文档,kb_id 标明所属知识库。多租户场景还需要租户或权限边界。它们让系统知道一段内容属于谁,也支持检索前限定范围。

第二类是结构字段。

章节路径、标题层级、内容类型、父内容 ID、前后片段 ID,描述当前 Chunk 在原文结构中的位置。它们支持命中小块后取回必要上下文,也能防止把不同章节、不同表格或不同代码函数拼到一起。

第三类是来源字段。

页码、幻灯片编号、时间戳、页面坐标和原始文件名,帮助前端把回答引用变成可点击来源。这里的目标不是让页面显示更多标签,而是让用户能回到正确位置核验。

第四类是状态与版本字段。

解析版本、文档版本、更新时间和可用状态,用来区分新旧内容。相关度排序只能判断“哪段更像问题”,不能单独判断“哪段现在仍然有效”。

为了避免把字段列表误当成生产 Schema,可以先分成两组。

第一组是当前链路确实需要的基础字段。例如当前系统需要限定知识库和文档,就保存 kb_iddoc_id;需要打开原页,就保存页码和位置;需要区分内容类型,就保存 content_type

第二组是只有对应能力启用后才增加的关系字段。只有做父子召回时才需要 parent_id,只有做相邻扩展时才需要 prev_idnext_id,只有同时维护多个版本时才需要完整的版本和可用状态流程。

这些名字只用于解释字段职责,不是一份可以直接复制到生产环境的 Schema。实际项目必须根据资料类型、权限模型、引用方式和上下文策略裁剪,并验证每个字段真的被链路消费。

字段设计完成后,还需要给每个字段写清三个问题:由谁产生,在哪一步被消费,原文更新时怎样失效。例如页码由解析器产生,引用组件消费,文档版本变化时随 Chunk 一起重建;权限范围由文档接入层产生,检索过滤和缓存共同消费,授权变化时必须立即影响可搜索范围。答不出这三个问题的字段,多半只是“以后也许有用”的装饰。

类型和缺失规则也要稳定。页码是整数还是字符串,章节路径是数组还是拼接文本,位置坐标使用哪套页面尺寸,都应在入库契约里固定。否则解析器写入一种格式,检索接口按另一种格式读取,字段虽然存在,运行时仍会悄悄失效。对于没有页码的网页或没有坐标的纯文本,应明确允许为空并提供替代定位方式,而不是填入伪造的默认值。

下面用教学伪代码表示一个最低限度的契约检查。它不是完整 Schema,只强调进入索引前先验证身份、范围和来源:

required = ["chunk_id", "doc_id", "kb_id", "version", "content_type"]
assert all(record.get(key) is not None for key in required)
assert record["source_ref"] or record["source_url"]
assert record["embedding_text"] and record["display_text"]

如果校验失败,记录应进入可观察的失败状态,不能用空字符串补齐后继续入库。空 kb_id 会破坏范围过滤,空来源会让引用失去核验入口,这类问题不是“以后再补”的展示缺陷。

从业务动作反推 Chunk 字段

先明确过滤、扩展、引用和版本处理,再决定保存哪些字段,比先堆一份大而全的 Schema 更可靠。

过滤、排序和生成后校验,为什么不能混成一件事?

这三个动作发生在不同位置。

过滤先决定哪些内容有资格参加本次搜索。用户选择的知识库、允许访问的文档、租户边界和内容状态,都应该尽量在这一层生效。被排除的内容不进入候选,也不会出现在模型上下文里。

排序是在合法候选中判断谁更相关。标题命中、内容类型和更新时间可以成为排序信号,但不能用“分数低一点”代替权限隔离。越权内容即使排在最后,也不应该让模型看到。

生成后校验则检查模型使用了哪些证据,引用编号是否存在,回答能否回到来源。它能发现错引和漏引,却无法挽回前面已经把错误资料送入模型的问题。

判断原则可以用一个纯教学场景说明。一份旧制度处于停用状态,如果系统只是降低它的排序分,它仍可能在新文档召回失败时进入上下文。更稳妥的处理是先通过状态和版本字段决定它是否有资格被检索。如果业务确实需要查询历史版本,再由明确的时间条件或独立入口放开,而不是让新旧制度默认混在一起竞争。

权限还要贯穿缓存。假设两位用户提出完全相同的问题,但可访问的知识库不同,如果答案缓存只使用 Query 作为键,第一个用户看到的证据可能被第二个用户复用。缓存键至少要包含与检索范围一致的租户、知识库、权限版本或其他隔离维度;权限发生变化时,也要有对应失效策略。否则搜索层过滤正确,缓存层仍可能绕过边界。

过滤后的日志不能直接记录敏感正文来证明“没有越权”。更合适的是记录请求范围、候选的文档身份与过滤原因,并限制谁能查看这些审计字段。Metadata 既服务安全,也可能本身包含敏感信息,设计时要同时考虑最小化保存和日志脱敏。

Metadata 设计因此要和检索接口一起看。字段存进数据库,却没有进入 get_filters 或其他过滤条件,等于链路只完成了一半。相反,接口接受了知识库和文档条件,入库记录却缺少对应字段,搜索层同样无法落实边界。

过滤条件入口和候选字段应该能够在同一条请求记录中核对。这是一种需要结合实际链路验证的观测设计,目的是检查候选是否满足知识库、文档状态等范围约束,而不是只看相关性排序。

哪些信息应该参与 Embedding?

Metadata 不等于全部拼进向量文本。

内部 ID、页码、时间戳和布尔状态通常用于过滤或定位,直接加入 Embedding 往往没有帮助。章节标题则可能需要进入增强文本,因为正文里的“上述条件”“该流程”“以下情况”离开标题后并不自包含。

比较清楚的做法,是区分三份数据。

第一份是原始正文。它用于展示、引用和人工核验,不应混入系统为了检索临时拼接的说明。

第二份是增强文本。它可以把章节路径、表题或必要的父级标题补到正文前面,帮助向量模型理解语境。

第三份是结构化 metadata。它保留 ID、状态、位置、权限和关系,用于精确过滤与后续动作。

如果把三者混成一段文本,会出现两个问题。检索时很难区分正文与系统补充内容,展示时又可能把内部字段和重复标题直接暴露给用户。

分开存储还能支持独立更新。调整增强文本的拼接方式时,可以重建向量,不需要假装原始文档也发生了变化。

前后片段 ID 为什么不能无条件使用?

用户问到一条例外条件,检索只命中“但以下情况除外”。系统需要上一段,才能知道例外针对哪条规则。

prev_idnext_id 可以帮助取回相邻内容,但“相邻”不等于“相关”。

当前 Chunk 可能位于章节末尾,下一个 Chunk 已经进入另一个主题。跨页表格的下一块应该继续带回,同一页面上的另一张表却不应该混进来。文档更新后,旧版本的前后关系也不能继续指向新版本。

扩展前至少要检查几个边界:是否属于同一文档版本,是否仍在同一章节或父内容下,内容类型是否兼容,分块顺序是否连续。

更复杂的父子切分也是同样道理。小块用于检索,命中后按 parent_id 返回更完整的语义单元。若父子关系没有在更新时一起维护,系统可能取回旧父块或错误章节。

相邻扩展最容易越过哪些边界

前后 ID 只是导航线索。扩展之前仍要核对版本、章节、父内容和类型,避免把另一个主题带进上下文。

来源字段怎样贯穿整条链路?

页码和坐标不能等到生成答案时再补。

解析阶段识别页面和版面区域,切分阶段把多个原子单元合并为 Chunk,同时合并它们的位置范围。入库阶段保存这些字段,检索返回时原样带出,生成阶段为证据分配稳定编号,前端最后把内部编号映射成文档、页码和高亮区域。

任何一层丢掉位置,后面都无法凭空恢复。

让模型根据正文猜页码,看起来能生成完整引用,实际上引用没有可核验依据。让模型自己编文件编号也是同样的问题。模型可以选择引用哪个证据,却不应该创造证据的身份和位置。

检索返回 Chunk 时,不应该只返回一段文字,还要把后续排序、排查和展示需要的信息一起传下去,例如 doc_idkb_idpositions 和候选分数。最终启用哪些字段,要根据实际链路核验。

回答错误时,也可以沿着来源字段反查。先定位最终引用的 Chunk,再看它属于哪份文档、哪个版本、哪次解析,以及通过哪条召回路线进入候选。

一条引用最好拥有稳定、可验证的生成路径。检索返回内部 chunk_id 和来源区域,生成阶段只为已有候选分配短编号,模型输出编号后由服务端映射成文档标题、页码与链接。模型不能自行填写文件名和页码,前端也不能只根据标题模糊搜索。这样引用错了时,可以区分“模型选错证据”和“证据定位字段丢失”,而不是把两类问题混成一次回答错误。

同一段证据经过 Rerank、去重或相邻扩展后,身份也不能丢。可以为最终上下文保存从原候选到展示证据的映射:它由哪条 Query 找回,是否经过父块扩展,最终使用了哪些原始区域。这个映射让团队能够重放一次回答,也能解释为什么前端打开的是某一页。

四种常见故障,应该先查什么?

第一种,答案内容正确,却无法打开原文位置。

先检查解析阶段有没有页码和坐标,切分时是否正确合并,入库字段是否保留,检索接口又是否返回。不要在前端临时猜位置。

第二种,搜索 A 知识库却返回 B 知识库内容。

检查 kb_id、租户和文档状态是否在检索前进入过滤条件。生成后再删除越权内容太晚,因为越权证据已经参与了排序和回答。

第三种,命中例外条款却缺少适用前提。

检查当前 Chunk 是否带章节路径、父 ID 和前后关系,再确认扩展没有跨过章节或版本边界。

第四种,文档更新后仍引用旧页码。

检查 Chunk 与来源位置是否和文档版本绑定,旧版本是否仍处于可用状态,以及缓存或引用映射是否继续指向旧记录。

版本变化还会让“同一页”失去稳定含义。新文档在前面插入一页后,旧页码可能整体后移;只保存文件名和页码,历史引用就可能指向另一段内容。作为版本化设计建议,可以同时保留稳定的文档版本、Chunk 身份和原始位置,展示时再映射到对应版本的文件。这项能力是否需要实现,要由产品是否保留历史回答和历史文档决定。

如果产品允许查看历史回答,还要明确历史回答引用的是当时版本,还是始终跳转到最新版本。前者强调可审计,后者强调当前有效,两种产品语义不同,不能只靠一个 page 字段默认决定。

Chunk metadata 上线前的检查清单

从一条 Chunk 出发,依次验证过滤、上下文扩展、来源定位和版本更新,才能证明字段真的被链路使用。

怎么验证 Metadata 设计不是纸上谈兵?

可以选一条具体 Chunk 做贯穿测试。

先从存储中读取它,核对所属知识库、文档、章节、页码、内容类型和版本。再发起限定知识库与文档的查询,确认过滤真正生效。

接着准备一个必须依赖上一段才能回答的问题,观察系统是否只扩展同一章节、同一版本的必要内容。然后点击回答引用,确认能回到同一文件、同一页或同一页面区域。

最后更新原文,重新入库并运行同一问题。检查旧 Chunk 的可用状态、引用映射和前后关系是否同步变化。

验证时不要只看字段“存在”。一个从未参与过滤、展示或审计的字段,即使数据库里有值,也不能证明设计有效。

更严格的验收可以准备一组“违反约束”的请求。用无权用户搜索一段已知存在的资料,确认它不进入候选;停用一个旧版本,确认普通查询无法命中但历史入口仍可按规则访问;删除一个来源定位字段,确认入库质检或引用组件明确报错,而不是生成一个看似可点击的假链接。

还可以做字段使用清单。把每个字段与解析、索引、过滤、重排、上下文扩展、引用和审计逐项对应,并从请求日志抽取一条完整链路核对。如果某字段长期没有消费者,考虑删除;如果某个关键动作依赖运行时临时猜测,应该把缺失信息前移到解析或入库阶段。

上线后的监控不能只统计 Metadata 非空率。更重要的是过滤拒绝是否符合预期、引用打开成功率、相邻扩展跨边界次数、当前版本与候选版本是否一致。非空字段可能填错,只有业务动作成功才证明数据可用。

面试官继续追问,怎么接?

Metadata 是不是越丰富越好?

不是。字段越多,解析、入库、更新和跨系统同步的成本越高。应该为每个字段找到明确消费者。没有业务动作、查询条件或展示需求的字段,可以先不保存。

标题到底放正文、Embedding 文本,还是 metadata?

可以三者分开考虑。原始标题用于展示,结构化标题路径放在 metadata,必要时再复制到增强文本帮助向量理解。这样检索获得语境,用户看到的原文又不会被重复拼接污染。

权限字段为什么要在检索前过滤?

因为检索后过滤会让无权内容参与候选排序,甚至已经进入模型上下文。权限和租户边界应尽量在搜索范围层生效,并在缓存键和日志中保持同一隔离维度。

前后 Chunk 扩展会不会增加很多噪声?

会,所以扩展应该由问题和结构共同触发。先检查命中内容是否真的缺少前提,再依据父内容、章节、类型和版本限制范围。扩展后的上下文仍要进入评测,观察完整性提升是否伴随噪声增加。

把 Metadata 讲成一条可追溯的数据链

如果要把字段、职责和边界一次说完整,可以这样回答:

Chunk 不能只有正文和向量。正文供模型阅读,metadata 则负责过滤、结构边界、上下文扩展、版本和来源追溯。基础字段通常有 kb_iddoc_id、章节路径、页码或坐标、内容类型和版本;需要扩展时再加父块与前后块关系。知识库、租户、文档状态等字段应在检索前做硬过滤,标题与章节可以进入增强文本补语境,页码和位置则贯穿解析、切分、入库、检索和前端引用。

我会把原始正文、用于 Embedding 的增强文本和结构化 metadata 分开保存。命中相邻块后也不会直接向两边扩展,而会检查同一版本、章节、父内容和类型。验证时选一条 Chunk 贯穿整条链路,检查过滤是否生效、缺失上下文能否正确补回、引用能否打开原文,以及文档更新后旧位置和关系是否同步失效。

一份字段清单本身还不能证明设计成立。面试官更关心每个字段服务哪个动作,字段怎样从解析一路传到回答,以及缺少它时系统具体会错在哪里。

INTERVIEW FOLLOW-UPS

面试官会怎样继续追问?

这些追问会继续检查方案取舍、验证方法和项目事实。
  1. Q01

    哪些字段参与向量化,哪些只用于过滤?

  2. Q02

    前后片段扩展怎样避免拉入另一个章节?

  3. Q03

    文档更新后,旧 Chunk 的版本怎样处理?

  4. Q04

    来源页码和坐标在解析、入库、返回中怎样传递?

PROJECT CHECK

换成你的项目,这几件事能否说清楚?

  • 展示一条真实 Chunk 数据结构
  • 逐字段说明它服务的业务动作
  • 演示一次 metadata 过滤
  • 确认回答引用可回到原文位置
用自己的项目经历练一次