第 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 的第一步。下一章会把文本片段转成向量,并存入向量库。