登录
首页 >  科技周边 >  人工智能

AI 知识库分块实战:按标题层级切文档,减少回答跑偏

来源:17golang原创

时间:2026-06-13 05:35:43 101浏览 收藏

很多 AI 知识库刚上线时看起来能回答问题,但一遇到跨段落说明、表格解释、版本差异,就容易出现“答了一半”“引用不到原文”“把两个章节混在一起”的情况。问题常常不在模型本身,而在文档入库前的分块策略。

本文用一个内部产品手册知识库做例子,拆解一套更稳的分块流程:先按标题层级切文档,再用小窗口补足上下文,最后把来源、章节、页码等元数据一起写入索引。这样做不追求复杂,但能明显减少回答跑偏。

摘要

本篇文章会完成三个目标:判断分块粒度是否合适、实现一个标题优先的分块函数、用问题回放修正常见命中失败。适合正在做 RAG、企业知识库、客服问答、技术文档问答的开发者阅读。

适合人群

  • 已经能把文档写入向量库,但问答效果不稳定的同学。
  • 需要处理 Markdown、产品手册、接口文档、制度文档的后端或 AI 应用开发者。
  • 想把“能搜到”进一步优化成“搜得准、答得全”的工程团队。

目录

  • 为什么不能只按固定字数切分
  • 按标题层级切分:先保住语义边界
  • 加重叠窗口和元数据:让检索结果可追踪
  • 用问题回放修正分块策略
  • 常见坑和总结

为什么不能只按固定字数切分

固定字数切分最大的优点是简单,但它容易把一个完整解释切断。例如“退款规则”的第一段解释条件,第二段解释例外,第三段给出处理流程。如果刚好按长度切开,检索时可能只命中条件,没有命中例外,最后回答就会偏。

更推荐先把文档看成有层级的内容树:一级标题是主题,二级标题是具体问题,段落和表格是证据。分块时先尊重这棵树,再用长度上限控制单块大小。

AI 知识库按标题层级分块的入库流程:原始文档经过标题解析、语义块合并、重叠窗口补齐后写入向量索引

按标题层级切分:先保住语义边界

标题优先的分块逻辑可以概括为四步:

  1. 解析 Markdown 或 HTML 标题,记录当前章节路径。
  2. 把同一小节下的段落、列表、表格先合并成候选块。
  3. 候选块超过上限时,再按段落或句子拆成小块。
  4. 每个小块都带上标题路径,避免脱离上下文。
from dataclasses import dataclass
from typing import Iterable


@dataclass
class Chunk:
    text: str
    title_path: list[str]
    source: str
    page: int | None = None


def split_by_heading(lines: Iterable[str], source: str, max_chars: int = 900) -> list[Chunk]:
    title_path: list[str] = []
    buffer: list[str] = []
    chunks: list[Chunk] = []

    def flush() -> None:
        if not buffer:
            return
        text = "\n".join(buffer).strip()
        if not text:
            buffer.clear()
            return
        while len(text) > max_chars:
            head, text = text[:max_chars], text[max_chars:]
            chunks.append(Chunk(text=head.strip(), title_path=title_path.copy(), source=source))
        if text.strip():
            chunks.append(Chunk(text=text.strip(), title_path=title_path.copy(), source=source))
        buffer.clear()

    for raw in lines:
        line = raw.rstrip()
        if line.startswith("#"):
            flush()
            level = len(line) - len(line.lstrip("#"))
            title = line[level:].strip()
            title_path = title_path[: level - 1] + [title]
        else:
            buffer.append(line)

    flush()
    return chunks

这段代码不是最终的生产版本,但它说明了关键思路:标题变化时先落盘当前候选块,每个块保留一份标题路径。后续拼上下文时,可以把标题路径一起放进提示词,让模型知道这段内容属于哪个主题。

加重叠窗口和元数据:让检索结果可追踪

分块不能太大,也不能太碎。太大会让检索结果带入噪声;太碎会丢上下文。一个常用折中是:单块控制在 500 到 1000 个中文字符,块与块之间保留 80 到 150 个字符的重叠窗口。实际数值要跟文档类型和模型上下文长度一起调。

元数据同样重要。至少建议保存:

  • source:原始文件名或 URL,方便引用和排查。
  • title_path:标题层级,例如“订单 / 退款 / 特殊场景”。
  • page:PDF 页码或网页锚点,方便人工复核。
  • chunk_index:同一文档内的块序号,用来找相邻上下文。
{
  "text": "退款申请提交后,系统会先校验订单状态...",
  "metadata": {
    "source": "after-sale-guide.md",
    "title_path": ["售后", "退款规则"],
    "chunk_index": 12,
    "page": null
  }
}

有了这些元数据,回答里不仅能给出结论,还能返回“来自哪份文档、哪个章节”。当用户反馈答案不对时,开发者可以快速回看命中的原文,而不是只盯着模型输出猜原因。

用问题回放修正分块策略

分块策略是否合适,不能只看代码,要用真实问题回放。做法是准备 20 到 50 个高频问题,每个问题记录期望命中的章节。然后跑一遍检索,观察失败类型。

AI 知识库问题回放修正路径:用户问题命中错误块后,通过查看来源、合并相邻块、调整重叠窗口,最终得到可追踪答案

失败类型一:命中了同名章节,但不是同一产品

解决方式是把产品线、版本号、适用范围加入元数据,并在检索过滤条件里使用。例如客服知识库里可能有“企业版退款”和“个人版退款”,标题相似但规则不同。

失败类型二:命中了条件,没命中例外

这通常说明块太碎,或者重叠窗口太短。可以把相邻块一起回填给模型,或把分块上限从 500 提到 800,再观察命中结果。

失败类型三:命中了整页长文,答案被噪声干扰

这说明块太大,需要按二级或三级标题进一步拆分。对于表格、列表这类结构化内容,尽量不要拆断表头和关键行。

常见坑

  • 只看向量相似度,不看来源。 知识库不是搜索框,回答必须能回到原文。
  • 所有文档用同一粒度。 FAQ、接口文档、制度手册适合的分块大小并不一样。
  • 忽略标题路径。 没有标题路径时,一个短段落很容易失去语义位置。
  • 把图片文字直接丢掉。 如果重要流程藏在截图里,需要先做 OCR 或人工补录。

延伸阅读

如果要接入具体向量模型或分块库,建议以官方文档为准:OpenAI Embeddings 文档介绍了把文本转成向量表示的基础概念,LangChain Text Splitters 文档提供了常见文本切分器的设计参考。

总结

AI 知识库回答跑偏,很多时候不是“模型不够聪明”,而是入库时没有保住语义边界。更稳的做法是:按标题层级切分,控制块大小,保留重叠窗口和元数据,再用真实问题回放不断修正。这样构建出来的知识库,既更容易命中关键内容,也更容易解释答案来源。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>