上一章 知识建模:实体、事实、推断 把业务知识拆成三层——实体是可锚定的对象,事实是可溯源的关系,推断是带依据链的衍生结论。这一章讲清三层为什么必须分开,混在一起会出什么事。 IV · 知识引擎 · 第 01 章 · 进阶 · 约 30 分钟
Agent 工程学习站 做棵大树 出品 主站 beatree.cn 免费 · 无需登录 · 进度存本机
IV 版 · 知识引擎 ◆ 第 02 章 ◆ Retrieval Augmented Generation
IV · 知识引擎 02 / 22 进阶

RAG 的链路与失效模式

Retrieval Augmented Generation
预计阅读 32 分钟
难度 进阶
关键词 RAG · 切分
本机状态 未读

「RAG 就是检索加生成」是这句话最大的问题——它把一个六个环节的链路压成了两个词,于是每个环节的失效模式都被忽略了。这一章把链路拆开,逐段列出失效模式与定位手段,最后给出一份可执行的排查顺序。

编辑部注 · 本章的失效模式表是实操清单,建议直接拿它去排查你手上的检索系统。

先纠正一个说法。「检索 + 生成」描述的是结果,不是过程。真实的链路有六个环节,而每个环节都能独立地把最终结果做错——而且错得毫无痕迹,你只能看到「回答不对」,看不到是哪一环出了问题。

一、六个环节

RAG 链路:六个环节,六个失效点 ① 查询理解 把用户口语转成检索意图: 实体归一、指代消解、改写 ✗ 指代未消解(「它」指谁?) ✗ 别名未归一(华东/东区) ✗ 口语未改写(「卖得好吗」) ② 切分 Chunking 把文档切成可检索的单元 ✗ 语义切断:答案被切在两块 ✗ 块过大:噪音多、匹配模糊 ✗ 块过小:丢上下文与前提 ✗ 表格/图表被切成乱码 ③ 索引 Indexing 向量化 + 建索引 ✗ 嵌入模型换了但没重建 ✗ 元数据没入索引(无法过滤) ✗ 增量更新遗漏,索引与源不一致 ④ 召回 Retrieval 从索引里取回候选 ✗ top-k 太小:漏掉关键块 ✗ 无过滤:取回错误版本的文档 ✗ 纯向量:精确匹配的词反而漏 ✗ 无时间过滤:旧版本被召回 ⑤ 重排 Rerank 对候选做精排 ✗ 跳过了重排,直接按相似度排 ✗ 重排模型与语料语言不匹配 ✗ 截断到 N 条时丢了关键证据 ⑥ 组装与生成 拼上下文 → 生成回答 ✗ 缺来源标注:模型无从判断可信 ✗ 上下文超限被截断:证据丢失 ✗ 没给「无相关资料」的出口 ✗ 引用无法点开核对 排查顺序:从后往前,二分定位 Step 1 把标准答案所在的原文块,手工塞进上下文 → 模型能答对吗?    能 → 问题在 ①-⑤(检索链路);不能 → 问题在 ⑥(组装/生成)或模型能力 Step 2 检查召回结果里有没有那个块(把 top-k 调到 100 看)    有 → 问题在 ⑤(排序)或 top-k 太小;没有 → 问题在 ①②③(建模/切分/索引) Step 3 用原文里的关键词直接做全文检索,能不能找到那个块?    能找到 → 是语义检索的问题(换嵌入模型或加混合检索);找不到 → 切分或索引有问题
图 1 RAG 六环节与各自的失效模式,以及底部那段排查顺序。排查的关键是「从后往前二分」——先用「手工塞原文」把问题区间砍成两半,再逐段收敛。很多团队一上来就换嵌入模型,实际上问题往往出在切分(②)上。

「检索 + 生成」压成两个词,漏掉了中间四个环节。真实的链路(Retrieval-Augmented Generation)有六段——查询理解(Query Understanding)、切分(Chunking)、索引(Indexing)、召回(Retrieval)、重排(Rerank)、组装——每段都能独立把结果做错,且错得毫无痕迹。

二、切分:最被低估的环节

如果说有一个环节的投入产出比最高,那就是切分——尤其是父子块(Parent-Child Chunking)这种策略。原因很简单:切错了的内容,后面用再好的模型也救不回来。

四种切分策略与适用场景
策略做法适合风险
固定长度每 N 个字符切一刀无结构的长文本必然切断语义,只能作为最后手段
按结构按标题层级 / 段落 / 列表项切有明确结构的文档(规范、报告)依赖文档结构质量;层级过深时块会很小
按语义相邻句子语义相似度骤降处切开叙述性长文计算成本高;结果不稳定,难调试
父子块小块用于检索,命中后返回其所属的大块推荐默认方案实现复杂度略高;需维护父子映射
父子块为什么值得作为默认方案

它同时解决了「块小则上下文不足」与「块大则匹配模糊」这对矛盾:

  • 检索阶段用小块——句子级的块语义聚焦,与查询的语义匹配更准,不会被整段里的无关内容稀释;
  • 返回阶段给大块——命中后返回它所属的段落或整个小节,保证模型拿到完整的前提与上下文;
  • 顺带的收益:可以只对小块做嵌入(数量多但每个都短),索引成本可控。

三个切分的实操细节(都很容易被忽略):

  1. 重叠(Overlap)是必要的。 相邻块之间保留 10%-20% 的重叠,可以显著降低「答案被切断」的概率。代价是索引体积变大。
  2. 表格必须特殊处理。 表格按字符切会变成乱码。正确做法是把表格转为 Markdown 或自然语言描述(「表头为 地区/转化率,华东行取值为 1.1%」),作为整体一个块。
  3. 每个块都要带上层级路径。 块的开头附上「文档标题 > 一级标题 > 二级标题」,这既能提升检索准确率(路径参与嵌入),也让模型知道这段内容的语境。
父子块:检索用小块,返回用大块 同一份材料被切成两套粒度——嵌入与匹配走子块,进上下文的是子块所属的父块 父块(小节)约 800–1500 字 · 不参与嵌入、不进候选排序 子块① 背景与前提 子块② 命中 转化率 1.1% 子块③ 原因分析 子块④ 结论与建议 ↑ 只有子块(约 200–400 字)被嵌入、被比对 ② 检索粒度 = 子块 查询向量只与子块比,语义聚焦, 不会被整段里的无关内容稀释。 命中:子块②(不是父块) ③ 返回粒度 = 父块 按 parent_id 上溯,把整个小节放进 上下文:前提、数据、结论都在。 同一父块的多个子块命中只返回一次 两套粒度分开,才同时拿到「匹配准」与「上下文全」——这正是它适合当默认方案的原因。 代价:要维护子块到父块的映射;命中膨胀后总字符变多,必须按预算截断并显式标注。
图 2 把「检索单元」和「返回单元」拆成两套粒度:小块负责被找到,大块负责被读懂。「块要小还是大」这个争论之所以长期没有答案,是因为它把两个问题问成了一个——父子块让它们各自取值。

三、动手:可调参的检索链路

rag_pipeline.pypython
from dataclasses import dataclass, field
from typing import Callable
import re
@dataclass
class Chunk:
    id: str
    text: str
    parent_id: str | None = None     # 父子块:指向所属的大块
    path: str = ""                   # 层级路径:文档 > 章节 > 小节
    meta: dict = field(default_factory=dict)   # source, updated_at, version, lang
    def embed_text(self) -> str:
        """参与嵌入的文本:带上层级路径,提升语义匹配质量"""
        return f"{self.path}\n{self.text}" if self.path else self.text
@dataclass
class RagConfig:
    top_k: int = 20                  # 召回条数(先宽)
    top_n: int = 6                   # 精排后保留条数
    min_score: float = 0.0           # 分数下限,低于此值视为无关
    require_meta: dict | None = None # 必须满足的元数据过滤,如 {"lang": "zh"}
    max_chars_per_chunk: int = 3000  # 单块入上下文的字符上限
def retrieve(query: str, cfg: RagConfig, embed, search) -> list[Chunk]:
    """
    召回阶段。注意三点:
      1. 元数据过滤必须在召回时做,而不是后面过滤 —— 否则 top-k 会被无关内容占满
      2. 先宽召回,再精排(top_k > top_n)
      3. 分数下限很重要:宁可返回空,也不要用低相关内容凑数
    """
    vec = embed(query)
    raw = search(vec, k=cfg.top_k, filters=cfg.require_meta)
    picked = [c for c, score in raw if score >= cfg.min_score]
    return picked
def expand_parents(chunks: list[Chunk], get_parent: Callable[[str], Chunk]) -> list[Chunk]:
    """命中子块后膨胀为父块,但去重 —— 命中同一父块的多个子块只保留一个"""
    seen, out = set(), []
    for c in chunks:
        if c.parent_id and c.parent_id not in seen:
            seen.add(c.parent_id)
            out.append(get_parent(c.parent_id))
        elif not c.parent_id:
            out.append(c)
    return out
def build_rag_context(query: str, cfg: RagConfig, embed, search,
                      rerank, get_parent) -> tuple[str, dict]:
    """
    完整链路 + 诊断信息。
    诊断信息是必须的 —— 没有它,出了问题只能靠猜。
    """
    diag = {}
    # ④ 召回
    cand = retrieve(query, cfg, embed, search)
    diag["召回条数"] = len(cand)
    diag["召回来源分布"] = {}
    for c in cand:
        s = c.meta.get("source", "unknown")
        diag["召回来源分布"][s] = diag["召回来源分布"].get(s, 0) + 1
    if not cand:
        return ("(未检索到相关资料。请明确说明信息不足,不要凭推测作答。)", diag)
    # ⑤ 重排
    ranked = rerank(query, cand)[: cfg.top_n]
    diag["重排后条数"] = len(ranked)
    diag["最高分来源"] = ranked[0].meta.get("source") if ranked else None
    # 父子块膨胀
    expanded = expand_parents(ranked, get_parent)
    diag["膨胀后条数"] = len(expanded)
    # ⑥ 组装:必须带来源、时间、层级;超限要明确标注截断
    lines, used, dropped = [], 0, 0
    for c in expanded:
        body = c.text
        if len(body) > cfg.max_chars_per_chunk:
            body = body[: cfg.max_chars_per_chunk] + "……[内容过长已截断]"
        block = (f"【来源 {c.meta.get('source','未知')}|"
                 f"更新 {c.meta.get('updated_at','未知')}|"
                 f"层级 {c.path or '—'}】\n{body}")
        lines.append(block)
        used += len(block)
    if dropped:
        lines.append(f"(另有 {dropped} 条因长度限制未纳入)")
    head = ("以下为检索到的材料。请严格基于这些材料回答,"
            "并在结论处标注依据的来源。若材料不足以回答,请直接说明不足,不要推测。\n\n")
    diag["上下文总字符"] = used
    return head + "\n\n".join(lines), diag
def split_by_structure(doc: str, doc_title: str, target: int = 600,
                       overlap: int = 100) -> list[Chunk]:
    """
    结构化切分(默认推荐):按标题切,超长再按段落切,段落仍超长才按长度切。
    每块都带上层级路径。
    """
    chunks: list[Chunk] = []
    # 简易实现:以 Markdown 标题为界
    sections = re.split(r"\n(?=#{1,3} )", doc)
    idx = 0
    for sec in sections:
        title = ""
        m = re.match(r"(#{1,3}) (.+)", sec)
        if m:
            title = m.group(2).strip()
        path = f"{doc_title} > {title}" if title else doc_title
        # 超长段落再按长度切,保留重叠
        if len(sec) <= target:
            chunks.append(Chunk(id=f"c{idx}", text=sec.strip(), path=path)); idx += 1
        else:
            start = 0
            while start < len(sec):
                piece = sec[start: start + target]
                chunks.append(Chunk(id=f"c{idx}", text=piece.strip(), path=path))
                idx += 1
                start += target - overlap
    return chunks
def table_to_text(rows: list[list[str]]) -> str:
    """
    表格处理:不要按字符切表格。
    转成「每行一句」的自然语言,作为一个整体块。
    """
    if not rows:
        return ""
    header = rows[0]
    lines = []
    for r in rows[1:]:
        pairs = ", ".join(f"{h}为{v}" for h, v in zip(header, r))
        lines.append(f"该表一行记录:{pairs}")
    return f"表头:{', '.join(header)}\n" + "\n".join(lines)
过滤时机决定你能拿到几条有效结果 同一个查询「只看 v3 版本文档」、同样取 top-20,两种做法的区别只在于过滤发生在哪一步 A · 先召回、再过滤(错) 绿色 = 符合条件的 v3 文档 红色 = 旧版本,取完才被丢掉 top-20 里 17 条来自旧版本 → 过完滤只剩 3 条;真正相关的第 21–25 名从未被召回进来。 可用结果 3 条 · 额度被无关内容占满,答案往往就差在那 17 条挤掉的位置上 B · 把过滤条件下推到检索层(对) 向量库只在 v3 这个子集里做近邻搜索 top-20 全部有效,而且第 21–25 名这类「不靠前但相关」的块也有机会进候选池。 可用结果 20 条 · 同样的 top-k,有效额度差 6 倍以上 差别不在召回质量,而在额度:过滤器每回收一个额度,就多一条可能命中的证据。 判据:过滤后剩下的结果少于召回条数的一半,说明过滤该下推;向量库不支持带过滤的近邻搜索就要换库。
图 3 「先取后滤」和「边滤边取」看起来只差一行代码,代价却是可用额度。凡是有确定性的条件(版本、语言、时间、权限),都要在召回时就参与筛选,而不是等结果回来再挑。
实操任务
  • 挑 10 个「检索失败」的查询,按图 1 底部的三步排查顺序定位,统计各环节的失败占比;
  • 把 top_k 从 20 调到 100,看那个「本应命中却没命中」的块是否出现——如果出现,说明问题是排序而不是召回;
  • 实现父子块:target=300 的小块用于检索、800 的父块用于返回,对比上下文长度与答案准确率的变化。

四、常见误区与追问

这一章的六环节表看起来像常识,但下面五条是「知道结论却仍然会做错」的地方。

4.1 误区:检索不到答案,就换一个更强的嵌入模型

错在哪:把「检索失败」默认归因到模型能力。为什么自然:换模型是唯一看起来「一次性解决」的动作,而且动作清晰、不用排查。但六环节里只有 ③④ 与嵌入模型相关,而 ② 切分与过滤错误会让目标块根本不在候选里。 判据:先做两步二分——把标准答案所在的原文块手工塞进上下文,模型能答对吗(不能则问题在生成侧);再用原文里的关键词做一次全文检索,能找到那个块吗(能找到则问题在语义召回,找不到就是切分或索引)。量级上,换嵌入模型通常带来「召回率几个百分点」的改善,而切分切错带来的是「这条直接得 0 分」,两者不是一个量级。

4.2 误区:元数据过滤可以等拿到结果后再做

错在哪:把过滤当成一段后期清洗代码。为什么自然:向量库的检索 API 用起来简单,先取一批再在本地挑,写起来更快。但 top-k 是额度:被过滤掉的每一条都占掉了一个名额。 判据:统计过滤后被丢掉的比例——如果超过召回条数的一半(例如 top-20 里 17 条来自旧版本,只剩 3 条可用),就必须把 元数据过滤(Metadata Filtering) 下推到检索层,让向量库只在符合条件的子集里做近邻搜索。这也解释了为什么选向量库时要先确认它支持带过滤的近邻查询,而不是事后补。

4.3 误区:top-k 越大越好,干脆取 100 条

错在哪:用「召回宽度」冒充「答案质量」。为什么自然:多取一些总不会漏,直觉上更安全。但 top-k 决定的是候选池大小,不是进上下文的内容;池子越大,低相关内容越多,精排与生成都会被稀释。 判据:把两头分开设——召回取 20~50 条(宽),重排后截到 4~8 条(窄),并且给召回设分数下限,低于阈值宁可返回空。一个可操作的检查:把 top-k 从 20 提到 100,如果最终进上下文的条数也跟着涨到 100,那你优化的是噪声占比,不是召回率。

4.4 误区:块要么小、要么大,必须选一个

错在哪:把两个粒度的问题问成了一个。为什么自然:「块小匹配准、块大上下文全」这个矛盾太显眼,于是默认要做取舍。实际做法是把它们解耦:检索块 200~400 字符,返回块 800~1500 字符,用父子映射连起来。 判据:如果你现在的做法是「把块调到 500 字左右,两个问题各让一步」,那答案准确率通常会卡在一个不上不下的水平;换成父子块后,同一份语料可以做到检索更准、上下文更全。顺带一条纪律:单块进上下文超过 3000 字符必须截断,并在块里显式写出「已截断多少字符」。

4.5 误区:RAG 的上限由生成模型决定

错在哪:把「答得不好」归因到生成侧。为什么自然:回答是生成模型写的,看起来它才是那个「做判断的人」。但生成侧只能使用检索给它的材料——材料里没有答案时,模型无论多强都只能编或者拒答。 判据:手工把正确原文塞进上下文,如果模型能给出正确答案,那么这次失败与生成模型无关,瓶颈在检索;如果塞了原文仍然答错,才轮到提示词与生成模型。这条判据只需几分钟,却能把排查方向一次砍成两半——而多数团队是先换模型、再回头查检索,把最难的一步留到最后。

五、自测

本章自测第 2 题为高频考点
Q1检索不到答案时,正确的第一步排查是什么?
B。这是「二分定位」的第一步:如果给了正确原文模型也答不对,那问题就不在检索上,改嵌入模型纯属浪费——把问题区间先砍一半,后面的排查才有方向。C 是合理的第二步(用于区分召回与排序问题),但作为第一步会引入新的混淆因素。
Q2为什么元数据过滤(如只搜某个版本、某种语言)必须在召回阶段做,而不是拿到结果后再过滤?
C。这是个很实际的问题:你取 top-20,其中 17 条来自旧版本文档,后过滤之后只剩 3 条,而真正相关的第 21-25 名根本没被召回进来。正确做法是把过滤条件下推到检索层,让向量库只在符合条件的子集里做近邻搜索。
Q3表格内容为什么不能按字符长度切分?
D。表格的信息在于「表头与单元格的对应关系」,切分一旦破坏这个关系,剩下的数字就毫无意义。正确做法:把表格转为自然语言或 Markdown 后作为整体一个块,例如「表头为 地区/转化率,华东行取值为 1.1%」——这样既能被检索到,也能被模型正确理解。

六、小结

环节关键动作最易犯的错
① 查询理解别名归一、指代消解、口语改写没做归一,检索直接漏召回
② 切分结构化切分 + 父子块 + 重叠 + 表格特殊处理按字符切、切坏表格、丢层级路径
③ 索引嵌入模型变更后必须重建换了模型没重建,检索结果完全错乱
④ 召回先宽召回、元数据过滤下推、设分数下限后过滤导致可用结果被挤空
⑤ 重排精排 + 截断到 N跳过重排,直接用相似度排序
⑥ 组装带来源/时间/层级 + 超限显式标注 + 给「无资料」出口没有来源标注,模型无从判断可信度

检索系统的调试只有一条有效路径:先二分定位环节,再动手改。跳过分步排查直接换模型,是耗时最长的一条路。本刊编辑部

下一章单独讲检索的物理基础:向量到底是什么,为什么「语义相近」和「对回答有用」是两件事。

七、参考与延伸

这一章的每个环节都有各自的深挖材料,下面按「先看实证、再看代码、最后读论文」排好了。全站不做原文转载,这里只登记链接与「为什么值得读」。

先看实证(量化「每个环节值多少钱」)

  • Anthropic · Introducing Contextual Retrieval —— 用数据回答「切分加语境」值多少:top-20 检索失败率从 5.7% 降到 3.7%(只加语境),叠加关键词召回降到 2.9%,再加精排降到 1.9%。想说服同事「先修切分和融合,别急着换嵌入模型」,这篇的数字最好用。

再看代码(动手实现召回与精排)

  • Sentence-Transformers · Retrieve & Re-Rank —— 官方实现的双阶段检索范例:先双塔召回 top-100,再交叉编码器精排。本章第二节的三段式分工,这份代码里就是可运行的最小版本。

最后读论文(对齐一手定义)

◇ 面试官会怎么问

共 6 条 · 其中 2 条高频 · 先自己答一遍,再展开对照

RAG 的完整链路有哪几步? 高频
六个环节,每一步都能单独把结果做错:
① 采集与清洗——把文档、表格、日志接进来,去重、去噪、处理编码与格式。
② 切分(Chunking)——决定检索的最小单位。切得太大召回不准且占 token,切得太小语义不完整。
③ 索引——为每个 chunk 建关键词索引与向量索引,并保留元数据(来源、时间、权限、层级结构)。
④ 召回——根据查询取回候选集(关键词 + 向量混合)。
⑤ 排序(Rerank)——对候选做精排,选出真正该进上下文的那几条。
⑥ 生成——把选中片段与问题一起交给模型,要求带引用作答。
最常见的误区是把 RAG 理解成"④ + ⑥",然后所有问题都往提示词上改。实际上大部分 RAG 故障发生在 ②③⑤,而这三步都在模型之外。

追问链

  1. 每一环的典型失效表现是什么?
  2. 怎么定位故障出在哪一环?
RAG 的典型失效模式有哪些?怎么定位?
按链路顺序列四类最常见的:
① 切分破坏语义——把一张表格从中间切断、把一个流程分成两半,导致召回片段残缺。表现为"答案缺一半"或"细节对不上"。定位:直接看召回片段的原文。
② 召回失败——正确片段根本没进候选集。这是最隐蔽的,因为模型会基于错误材料自信作答。定位:把"正确片段是否在候选集里"单独作为指标统计(召回率),而不是只看最终答案正确率。
③ 排序失败——正确片段在候选集里但被挤到后面,最终没进上下文。定位:看 rerank 前后的排名变化。
④ 生成忽略——材料都在上下文里,模型却没用,或者用了却编造了细节。定位:要求输出引用并在评测里检查引用是否真实存在。
关键方法论是把链路分层评测:先保证召回,再保证排序,最后才谈生成——否则你会一直在错误的环节上优化。
加分点:强调"召回率要单独统计",因为最终答案正确率会把召回失败和生成失败混在一起。
切分(chunking)你会怎么定?
按内容结构切,不要按固定字数机械切。
原则:① 尊重结构边界——按章节、段落、表格、列表项切,别把标题和它的正文分开;② 保留上下文——每个 chunk 带上标题路径(文档 > 章节 > 小节),让它在脱离原文时仍可被理解;③ 允许重叠——相邻 chunk 之间留 10%–20% 重叠,缓解边界信息丢失;④ 长度适配——大小要兼顾语义完整与检索粒度,通常几百 token;表格和代码这类结构化内容要整体保留,宁可超长。
还有一个常被忽略的点:要为不同类型的资料用不同的切分策略(政策文档按条款、会议记录按发言段落、指标字典按字典项),一刀切是精度损失的常见来源。
判断切分好坏的实用方法:随机抽 20 个 chunk 单独读,能不能看懂它在说什么——看不懂就是切坏了。
你的 RAG 系统在 10% 的查询上答错。给出一个能在十分钟内把问题区间砍成两半的排查方案。 高频
第一步:把标准答案所在的原文块手工塞进上下文,看模型能不能答对。
能答对,问题就在检索链路(查询理解、切分、索引、召回、重排);不能答对,问题在组装与生成,或者模型能力本身。
第二步:检查召回结果里有没有那个块(把 top-k 临时调到 100 再看)。有,是排序问题或 top-k 太小;没有,是切分或索引问题。
第三步:用原文里的关键词做一次全文检索。能找到,是语义召回的问题;找不到,是切分把答案切断了。
纪律:先定位、再动手。换嵌入模型通常只动几个百分点,而切分错误带来的是「这条直接得 0 分」,两者不是一个量级。

追问链

  1. 为什么这一步要「从后往前」做,而不是从查询理解开始?
  2. 统计完 10 个失败样本之后,你会优先改哪个环节,依据是什么?
加分点:加分点:能主动说出「排查顺序本身就是一份可执行清单」,并给出每一步的判断依据。
top-20 的召回里 17 条来自旧版本文档,过滤之后只剩 3 条可用。问题出在哪,怎么改?
问题出在把元数据过滤放在了召回之后。
top-k 是额度:被过滤掉的 17 条占掉了名额,而真正相关的第 21 到 25 名从来没有进入候选池。
正确做法是把过滤条件下推到检索层,让向量库只在符合条件的子集里做近邻搜索——同样的查询、同样的 top-k,可用结果从 3 条变成 20 条。
判据:过滤后被丢掉的比例超过召回条数的一半,就必须下推。选型阶段先确认向量库支持带过滤的近邻查询。

追问链

  1. 两路混合检索时,为什么两路必须用同一套过滤条件?
  2. 如果向量库不支持带过滤的近邻搜索,有哪些替代方案?
加分点:加分点:把「后过滤」的代价说成「额度被无关内容占满」,而不是「代码写起来更麻烦」。
同一份语料:把块调到 200 字,检索更准但答案变差;调到 1200 字,答案更全但召回变差。怎么解?
这不是调参能解决的问题,而是把两个粒度的问题问成了一个:
检索粒度(块越小语义越聚焦、匹配越准)与返回粒度(块越大上下文越全)本来就该分别取值。
做法是父子块:切成 200 到 400 字的子块用于嵌入与检索,命中后按 parent_id 返回其所属的 800 到 1500 字的父块,命中同一父块的多个子块只返回一次。
代价是要维护父子映射,并且膨胀之后总字符变多,必须按预算截断、并显式标注截断了多少。
另外相邻块保留 10% 到 20% 的重叠,降低答案被切在边界上的概率。

追问链

  1. 表格为什么不能按字符长度切?你会怎么处理表格?
  2. 每个块为什么要带上「文档 > 章节 > 小节」的层级路径?
加分点:加分点:把「块大还是块小」翻译成「检索粒度与返回粒度解耦」——这是真做过的人才会用的说法。
答这类题的通用结构

结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。

本章收尾 · Wrap up
掌握度自评
点一下给自己打分;低于 3 分建议加入复习队列
个人笔记 · Notes
    下一章 Embedding 与向量检索 语义检索为什么会找错?因为"语义相近"和"对回答问题有用"是两件事。这一章讲清向量检索的原理、相似度度量的选择,以及它的能力边界。 IV · 知识引擎 · 第 03 章 · 进阶 · 约 28 分钟