切块

检索的单位是 chunk(块),不是整份文件。块太大:一次召回塞进很多无关句子,LLM 被噪声带偏。块太碎:答案需要的前后句被切开,向量「不像」完整语义。

经验起点(再按你的语料调):

语料chunk_size(字符,约)overlap
中文 FAQ / 制度300–80050–120
英文技术文档400–100080–150
表格 / 代码按逻辑单元(一个函数、一张表),不要按固定字数横切

chunk_size字符数对中文够用;若你用 tokenizer 计 token,以嵌入模型的上限为准(常见 8k,但检索块应远小于上限)。


重叠(overlap)

相邻块共享尾巴,避免「答案正好卡在切口」。重叠大约是块长的 10%–20%。重叠过大:重复存储、检索重复;过小:跨段事实丢失。

def window_split(text: str, size: int = 500, overlap: int = 80) -> list[str]:
    if size <= overlap:
        raise ValueError("size must be greater than overlap")
    chunks: list[str] = []
    i = 0
    n = len(text)
    while i < n:
        chunks.append(text[i : i + size].strip())
        if i + size >= n:
            break
        i += size - overlap
    return [c for c in chunks if c]

这是滑动窗口,不考虑语义。下面用分隔符改进。


按结构切,再按长度切

Markdown 和制度文档应按标题切开,再对过长节做窗口切。伪代码:

import re

def split_markdown(md: str, size: int = 500, overlap: int = 80) -> list[dict]:
    parts = re.split(r"(?m)^(#{1,3} .+)$", md)
    sections: list[tuple[str, str]] = []
    title = "(root)"
    buf = []
    for part in parts:
        if re.match(r"^#{1,3} ", part):
            if buf:
                sections.append((title, "".join(buf)))
            title, buf = part.strip(), []
        else:
            buf.append(part)
    if buf:
        sections.append((title, "".join(buf)))

    out: list[dict] = []
    for heading, body in sections:
        text = f"{heading}\n{body}".strip()
        for i, chunk in enumerate(window_split(text, size, overlap)):
            out.append({"heading": heading, "i": i, "text": chunk})
    return out

元数据里保留 heading 和文件名,检索过滤和引用都用得上。

不要把「第 3 条」和它的款项拆到两个不相邻的块里还只索引其中一半。列表、步骤、问答对应尽量同块。


其它策略(知道何时用)

策略做法适合
Recursive先按 \n## \n\n、空格依次切,超长再切通用默认
按标题上面的 Markdown 切法手册、本教程这种文档
父子块小块检索、返回父段(整节)需要完整条款、又要精准召回
问答对一块 = 一个 Q + A标准 FAQ
语义切嵌入后在相似度下跌处切开散文;更重、要评估才上

LangChain 的 RecursiveCharacterTextSplitter、LlamaIndex 的 node parser、Dify 的 General / Parent-child / Q&A 都是这些策略的产品名。原则相同,见 LangChain RAGLlamaIndexDify 知识库


中文注意

  • 不要按空格切中文(常常没有空格)。优先句号、换行、标题。
  • 「300 字符」大约是一段制度;英文 300 字符往往偏短。
  • 专有名词(产品代号、条款号)不要从中间切开。

切完后人工抽查 20 块:有没有半截表格、半个函数、问在一块答在另一块。这比调向量库参数更值钱。


最小检查

chunks = window_split(open("policy.md", encoding="utf-8").read())
print(len(chunks), min(map(len, chunks)), max(map(len, chunks)))
print(chunks[0][:200])

块数突然上万:可能按字符切了整本小说且 overlap 太大。块数只有 1:可能忘了切,整文件当一块(只适合极短 FAQ)。


下一步

评论