跳到正文
返回文章列表

RAG 落地实践:那些文档里不会写的坑

把 RAG 从 demo 做到生产,真正的难点几乎都不在向量检索本身,而在数据、切分与评估。

RAG 落地实践:那些文档里不会写的坑

几乎每个做 AI 应用的团队都会经历这样一个阶段:用一个下午跑通 RAG 的 demo,然后花三个月把它做成能上线的产品。

这篇文章记录我在实践中踩过的坑,以及最后沉淀下来的一些判断。

Demo 与生产的距离

Demo 的隐含假设是:知识库是干净的、问题是清晰的、答案是存在的。

而生产环境的真实情况通常是:

  • 文档格式混乱,PDF 里有双栏、表格、页眉页脚
  • 同一件事在五个地方有五种说法,且互相矛盾
  • 用户问的问题根本不在知识库里
  • 有些问题需要跨多个文档推理才能回答

每一条都会让”检索准确率”这个指标失去意义。

切分策略比模型选择更重要

我见过太多团队在纠结用哪个 Embedding 模型,却把 chunk_size 设成 512 就再也没动过。

实际上,切分决定了检索的基本盘。一份被拦腰截断的文档,换什么模型都救不回来。

几个我验证过有效的做法:

  1. 按语义边界切,而不是按字符数切。优先在标题、段落、列表项之间断开。
  2. 保留上下文头。给每个 chunk 加上它所属的章节标题,检索时会带来明显提升。
  3. 重叠是必要的。10%–15% 的重叠能显著降低”答案正好卡在边界上”的概率。
  4. 表格单独处理。不要指望通用切分器能正确理解表格。
def split_by_heading(text: str, max_tokens: int = 700) -> list[dict]:
    """按标题切分,并把标题作为上下文头附加到每个片段。"""
    chunks, current, heading = [], [], None

    for line in text.splitlines():
        if line.startswith('#'):
            if current:
                chunks.append({"heading": heading, "text": "\n".join(current)})
                current = []
            heading = line.lstrip('# ').strip()
        current.append(line)

    if current:
        chunks.append({"heading": heading, "text": "\n".join(current)})

    return chunks

检索之后还有一道关

很多人把 RAG 理解成”检索 → 塞进 Prompt → 生成”,但中间其实少了一步:筛选。

召回 20 个片段,不代表这 20 个都有用。把它们全部塞进去,除了浪费 token,还会引入噪声干扰模型判断。

我的做法是两段式:

  • 粗排:向量检索召回 top-20,追求召回率
  • 精排:用 cross-encoder 或 LLM 打分,选出 top-3~5,追求准确率

好的 RAG 系统不是”检索得更多”,而是”检索得更准”。

评估:最难也最容易被跳过的一环

没有评估,你所有的优化都是玄学。

我建议从第一天就建立一个小而具体的评估集:

类型数量作用
事实型问题30验证基础检索能力
多跳问题15验证跨文档推理
无答案问题15验证系统会不会胡编
边界问题10验证异常输入的处理

“无答案问题”这一列尤其重要。一个不会说”我不知道”的 RAG 系统,比一个承认自己不知道的系统危险得多。

我的结论

如果只能记住一句话,那就是:

RAG 的质量上限由数据质量决定,而不是由模型决定。

把 80% 的精力花在数据清洗、切分和评估上,剩下的 20% 再考虑换模型。这个比例听起来很反直觉,但实践下来几乎总是对的。