AI工具教程

搭建本地知识库问答:文档切分与检索效果怎么调

AI智能摘要

本地知识库问答答非所问,未必是模型不够强:如果召回片段本身没有答案,问题多半出在文档切分或检索;若片段相关,才该检查提示词与生成。文章从最小 RAG 管线出发,讲解按结构切分、调整块长与重叠、排查 top-k、引入 BM25 混合检索及选择中文嵌入模型,并建议用小型测试集量化每次改动。怎样判断该调哪一环,才能让答案更有依据?

— 此摘要由AI分析文章内容生成,仅供参考。

很多人第一次搭本地知识库问答都会卡在同一个地方:明明把文档都喂进去了,一问具体问题,模型要么答得牵强、找不到依据,要么直接把不相关的段落拼成一段看似合理实则离题的回答。更让人头疼的是,你改了一个参数,感觉"好像好了一点",但说不清到底好在哪,第二天换个问题又翻车。这篇教程就从"答案找不到依据、检索内容不相关"这个具体毛病出发,带你一步步调文档切分和向量检索,并用一个小小的测试集把每次改动的效果量化出来,而不是全凭感觉。

整个流程聚焦最基础的 RAG(检索增强生成,Retrieval-Augmented Generation)管线:整理文档、切分文本、建立向量索引、检索相关片段、拼进提示词交给大语言模型生成答案。我们不比较具体云服务,也不会宣称调完之后答案就绝对可靠——RAG 的本质是"让模型基于你给的材料回答",材料找得准不准,直接决定了答案的上限。

本地知识库 RAG 问答的基础流程示意图

先搞清楚:答案不靠谱,到底是哪一环出了问题

在动手调参之前,得先分清症状。RAG 回答不好,通常是两类原因,排查方向完全不同:

  • 检索阶段就没找对:发给模型的那几段文本本身就不包含答案,或者包含但被切碎了。这种情况下,模型再强也是巧妇难为无米之炊。
  • 检索对了但生成跑偏:相关片段其实被召回了,但模型没好好用,或者提示词写得让它自由发挥。

区分这两类的方法很简单:把每次检索实际召回的文本片段打印出来看一眼。如果你肉眼都能从召回的片段里找到答案,那问题在生成和提示词;如果召回的片段压根不沾边,那问题在切分和检索。这篇文章重点解决后者,因为它是绝大多数"找不到依据"的根源。

准备条件:一个能跑起来的最小管线

调参的前提是你得先有个能跑的东西。最小配置包括:一个嵌入模型(embedding model,把文本转成向量)、一个向量索引、一份整理好的文档。本地跑的话,用 Ollama 加 LangChain 是常见组合,向量索引用 FAISS 这类轻量方案即可。

文档整理这一步容易被忽略,但它决定了后面所有环节的质量。建议做三件事:

  1. 统一格式。把 PDF、DOCX 转成纯文本或 Markdown。Markdown 的好处是标题层级、列表这些结构能保留下来,后面按结构切分时很有用。
  2. 去掉噪音。页眉页脚、页码、重复的版权声明、导航菜单,这些内容进了索引只会稀释检索质量,让不相关的片段更容易被召回。
  3. 保留来源信息。给每个片段打上 source 之类的元数据,标明它来自哪个文件、哪一节。排查问题和给答案附引用时都靠它。

第一刀:切分长度和重叠怎么选

文档为什么要切块?因为大语言模型的上下文窗口有限,而且语义连贯的小段落比一整篇长文更容易被精准检索到。但切多大、重叠多少,是最影响效果又最容易拍脑袋的两个参数。

先看一段最基础的切分代码,用的是 LangChain 的字符切分器:

纯文本
from langchain_text_splitters import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
    separator="nn",     # 优先按空行(段落)切
    chunk_size=500,        # 每块最多 500 字符
    chunk_overlap=50,      # 相邻块重叠 50 字符
    length_function=len,
)
chunks = text_splitter.split_text(content)

这里三个关键字段:

  • separator 决定在哪里下刀。"nn" 表示优先在段落边界切,尽量不破坏语义。对中文文档,如果段落里很少空行,可以考虑用递归切分器(RecursiveCharacterTextSplitter),它会按一组分隔符逐级尝试,先段落、再句子、最后才硬切。
  • chunk_size 是每块的最大长度。注意这里按字符计数,中文一个字就是一个字符,和英文的 token 不完全对应。
  • chunk_overlap 是相邻块之间重叠的部分,作用是防止一句话正好被切在两块之间,导致关键信息被割裂。

不同长度的取舍

切块长度没有万能值,它本质是"检索精度"和"上下文完整性"之间的权衡。块越小,向量越聚焦,检索越容易命中具体的点,但一个块可能装不下完整的因果或步骤;块越大,上下文越完整,但一个块里混了好几个主题,向量会被"平均"得很模糊,反而不容易被精确命中。

下面这张表给出常见文档类型的起步参考值,注意是起步值,最终还得靠测试集验证:

文档类型chunk_size(字符)chunk_overlap说明
问答对、术语表、短条目200–40020–50每条本身独立,切小更精准
技术文档、操作手册400–80050–100保留完整步骤,重叠防止步骤被割裂
长篇文章、论文600–100080–150上下文更重要,适当加大

重叠一般取 chunk_size 的 10%–20% 比较稳妥。重叠太小,边界信息容易丢;重叠太大,会产生大量近乎重复的块,既占空间又让检索结果里塞满雷同内容。

一个实用的经验:如果你的文档结构性很强(比如 Markdown 带清晰标题),优先按标题结构切分,而不是死守固定字符数。按标题切能保证每块语义自洽,这往往比调字符数更有效。

第二刀:检索结果不相关时怎么排查

切好之后建索引、检索。如果打印出来发现召回的片段驴唇不对马嘴,按下面的顺序排查,基本能定位八成问题。

1. 先确认 top-k 够不够

检索时的 k 值(返回最相似的前几个片段)如果设得太小,比如只取 1,稍微有点偏差就全错。先把 k 调大一点,比如 5 或 8,看看正确的片段是不是其实排在第二第三位。如果是,说明召回没问题,是排序问题,可以考虑加重排(rerank)或混合检索。

2. 检查是不是切分破坏了语义

如果召回的片段都是半句话、从中间断开的内容,那是切分出了问题。回到上一步,换成按段落或标题的递归切分,或者适当加大重叠。被切碎的块向量语义不清,检索自然对不上。

3. 考虑纯向量检索的盲区

向量检索(稠密检索)擅长语义相近,但对精确关键词、专有名词、型号编号这类反而不敏感。比如你文档里有个产品代号"X-200",用户也问"X-200",纯向量未必能把它排到前面。这时候引入关键词检索(BM25 这类稀疏检索)做补充就很有用。

把两路结果融合的常见做法是 RRF(Reciprocal Rank Fusion,倒数排名融合)。它不看具体分数,只看每个片段在两路结果里的排名,按 1/(k+rank) 累加打分,k 是经验常数(常用 60,值越大两路结果越均衡)。这样既享受向量的语义泛化,又补上关键词的精确匹配。对"检索内容不相关"这个老毛病,混合检索往往是性价比最高的一招。

4. 别忘了嵌入模型本身

如果你的文档是中文,却用了一个主要在英文语料上训练的嵌入模型,检索效果会明显打折。换一个支持中文、维度合适的嵌入模型,有时候比反复调切分参数见效更快。嵌入模型就像检索的"眼睛",眼睛不行,切得再整齐也白搭。

向量检索与关键词检索融合的混合检索示意图

关键一步:用小型测试集量化每次改动

前面说的所有调整,如果只靠"感觉变好了"来判断,很快就会陷入反复横跳。真正专业的做法是:建一个小测试集,每改一次参数就跑一遍,用数字说话。

测试集不需要很大,20 到 50 个问题就足够看出趋势。每个问题你要事先知道答案应该来自文档的哪个片段(或哪一节)。格式可以简单到这样:

纯文本
test_set = [
    {
        "question": "产品 X-200 的最大工作温度是多少?",
        "expected_source": "spec.md#温度参数",   # 答案应来自的来源
    },
    {
        "question": "如何重置管理员密码?",
        "expected_source": "manual.md#账户管理",
    },
    # ... 更多问题
]

然后对每个问题跑检索,看返回的 top-k 片段里,是否包含了那个"应该命中"的来源。两个最实用的指标:

  • 命中率 / 召回率@k:期望来源出现在前 k 个结果里的问题占比。这个指标直接回答"检索有没有把对的材料找回来"。
  • 命中位置:对的片段平均排在第几位。位置越靠前,说明排序越好,生成时也越不容易被噪音干扰。

跑评估的核心逻辑可以简化成这样:

纯文本
def evaluate(retriever, test_set, k=5):
    hit = 0
    for case in test_set:
        docs = retriever.search(case["question"], k=k)
        sources = [d.metadata["source"] for d in docs]
        if case["expected_source"] in sources:
            hit += 1
    return hit / len(test_set)   # 命中率@k

有了这个数字,调参就从玄学变成了实验。比如你把 chunk_size 从 500 改到 300,重跑一遍,命中率从 0.68 升到 0.81,这就是实实在在的进步;如果反而掉了,说明切太碎丢了上下文,果断退回去。每次只改一个变量,否则你分不清是切分起作用还是检索起作用。

下面是一个典型的调优记录,可以照着这个思路做自己的实验表:

实验chunk_sizeoverlap检索方式命中率@5
基线50050纯向量0.68
调小块30040纯向量0.74
加混合检索30040向量+BM250.86
按标题切结构切—向量+BM250.90

(表中数字仅为示范调优过程的记录格式,不代表你的文档会得到相同结果。)

常见报错与坑

调试过程中几类高频问题,提前知道能省不少时间:

  • 召回片段全是重复内容:十有八九是 chunk_overlap 设得太大,或者文档本身有大量重复的页眉页脚没清理。先回去清噪音。
  • 中文检索明显比英文差:检查嵌入模型是否支持中文,以及文本编码是否统一为 UTF-8,乱码的文本向量化后完全不可用。
  • 改了文档却没生效:向量索引是构建时的快照,文档更新后必须重新切分并重建(或增量更新)索引,否则检索的还是旧内容。
  • 精确词查不到:纯向量的典型短板,引入 BM25 混合检索,或在检索前对查询做关键词提取。

验证结果与下一步

走完这一轮,你应该能做到:遇到"答案找不到依据"时,先打印召回片段定位是检索还是生成的问题;针对检索问题,从切分长度、重叠、混合检索、嵌入模型四个方向逐一调整;每一次调整都用小测试集的命中率来验证,而不是凭感觉。

当命中率稳定到你满意的水平后,可以再往后走几步:给召回结果加一层重排模型,进一步提升命中位置;在提示词里要求模型"只根据给定材料回答,找不到就说不知道",减少编造;给答案附上来源片段,让每个回答都可追溯、可核对。基础 RAG 流程本身不复杂,难点从来不在于搭起来,而在于用测试集持续地、可复现地把检索效果磨准——这也是它和"看起来能用"之间最大的区别。

热门话题

52okp 是一名关注人工智能、开源软件与效率工具的技术内容创作者,长期实践 Stable Diffusion、ComfyUI、AI 智能体、MCP、Codex 和各类开源项目。通过实际安装、配置与测试,整理可复现的操作教程、问题排查方法和工具使用经验。

登录用户才能发表评论! 登录账户

取消回复

评论列表 (1条):

加载更多评论 Loading...

延伸阅读:

暂无内容!

    返回顶部