第 13 章:文档上传与文本切分

4 阅读2分钟

第 13 章:文档上传与文本切分

本章目标

这一章处理知识库的入口:用户上传文档后,如何把长文档切成适合检索的小片段。

本章效果

左侧知识库状态展示了入库文档和片段数量。示例资料被切分成 12 个片段,后续会用于向量检索。

文档上传与文本切分转存失败,建议直接上传图片文件

为什么要切分

文档通常很长,不能整篇塞进模型上下文。即使模型上下文足够长,也会带来成本高、检索差、注意力分散等问题。

切分的目标是把文档变成一组语义相对完整的小块:

原始文档
  -> chunk 1
  -> chunk 2
  -> chunk 3

每个 chunk 后续会生成 Embedding 并存入向量库。

文档数据结构

export interface SourceDocument {
  id: string;
  title: string;
  content: string;
  type: "markdown" | "txt" | "pdf";
  createdAt: number;
}

export interface DocumentChunk {
  id: string;
  documentId: string;
  title: string;
  content: string;
  index: number;
  metadata: {
    source: string;
  };
}

简单文本切分

第一版可以用段落切分:

export function splitByParagraph(document: SourceDocument): DocumentChunk[] {
  return document.content
    .split(/\n{2,}/)
    .map((content) => content.trim())
    .filter(Boolean)
    .map((content, index) => ({
      id: `${document.id}-${index}`,
      documentId: document.id,
      title: document.title,
      content,
      index,
      metadata: {
        source: document.title
      }
    }));
}

这对 Markdown 和纯文本比较友好。

带重叠的切分

有些信息跨段落,完全切开会丢上下文。可以增加 overlap:

export function splitText(text: string, chunkSize = 800, overlap = 120) {
  const chunks: string[] = [];
  let start = 0;

  while (start < text.length) {
    const end = Math.min(start + chunkSize, text.length);
    chunks.push(text.slice(start, end));
    start = end - overlap;
    if (start < 0) start = 0;
    if (end === text.length) break;
  }

  return chunks;
}

真实项目可以使用 LangChain 的文本切分器,但小册里先手写一版,能帮助读者理解原理。

上传接口

第一版可以先支持纯文本:

export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get("file");

  if (!(file instanceof File)) {
    return Response.json({ message: "缺少文件" }, { status: 400 });
  }

  const content = await file.text();

  const document = {
    id: crypto.randomUUID(),
    title: file.name,
    content,
    type: "txt" as const,
    createdAt: Date.now()
  };

  const chunks = splitByParagraph(document);

  return Response.json({
    document,
    chunks
  });
}

实战任务

完成:

  • 文档和 chunk 类型定义
  • 纯文本上传接口
  • 段落切分函数
  • chunk 预览页面

常见坑

切分太大会导致检索不准,切分太小会丢语义。

不要丢掉来源信息。后面引用来源全靠 metadata。

PDF 解析不要一开始就做复杂。先支持 Markdown/TXT,把主链路跑通。

本章小结

文档切分是 RAG 的第一步。下一章会把文本片段转成向量,并存入向量库。