从 PDF 到向量检索:一个最简单的本地 RAG 入库案例

最近看了不少企业级 RAG 平台的源码,发现一个通病:一上来就是 Milvus + Neo4j + MinIO 好几个中间件一起怼,想学习"文档怎么变成向量"这个核心链路,反而被一堆基础设施绕晕了。

这篇文章把这条链路拆到最简:解析 → 分块 → 向量化 → 入库,四步走完,全程本地跑,不需要任何服务器、不需要 Docker,一个 Python 脚本搞定。

技术选型

环节 用什么 为什么选它
解析 pymupdf4llm 纯 Python 库,几秒装完,直接把 PDF 转成带标题结构的 Markdown
分块 按标题/段落切分 不用额外依赖,几行代码
向量化 sentence-transformers + text2vec-base-chinese 中文效果好,模型不大
入库 Zvec 阿里开源的嵌入式向量库,pip install 即用,不用起服务

四个环节全是"进程内"跑的库,没有一个是要单独部署的服务。

环境准备

kotlin 复制代码
pip install pymupdf4llm sentence-transformers zvec --break-system-packages

Step 1:解析 ------ PDF 转 Markdown

py 复制代码
import pymupdf4llm

def parse_pdf(pdf_path: str) -> str:
    """把 PDF 转成保留标题结构的 Markdown 文本"""
    md_text = pymupdf4llm.to_markdown(pdf_path)
    return md_text

markdown_content = parse_pdf("sample.pdf")
print(markdown_content[:500])

为什么不直接用 PyPDF2 之类的库硬提取文字?因为那样会把标题、段落、表格全部拍扁成一坨纯文本,后面分块时没法按结构切,容易把一句话从中间切断。pymupdf4llm 会保留 Markdown 的 #、## 这些标题标记,分块时可以按标题层级切,语义更完整。

如果文档更复杂(扫描件、复杂表格、公式),可以把这一步换成 MinerU,接口不用改,只是解析函数内部实现不同。

Step 2:分块 ------ 按段落/标题切分

py 复制代码
from typing import List
import re

def split_into_chunks(markdown_text: str, max_chars: int = 500) -> List[str]:
    """按标题和段落切分,超长段落再按字数二次切分"""
    # 先按标题切成大块
    sections = re.split(r'\n(?=#{1,3}\s)', markdown_text)

    chunks = []
    for section in sections:
        section = section.strip()
        if not section:
            continue
        # 段落内部再按空行细分,避免单块过长
        paragraphs = [p.strip() for p in section.split("\n\n") if p.strip()]
        buffer = ""
        for p in paragraphs:
            if len(buffer) + len(p) > max_chars and buffer:
                chunks.append(buffer)
                buffer = p
            else:
                buffer = f"{buffer}\n\n{p}" if buffer else p
        if buffer:
            chunks.append(buffer)

    return chunks

chunks = split_into_chunks(markdown_content)
print(f"共切分为 {len(chunks)} 个 chunk")
for i, chunk in enumerate(chunks[:3]):
    print(f"[{i}] {chunk[:80]}...\n")

Step 3:向量化 ------ 文本转 Embedding

py 复制代码
from sentence_transformers import SentenceTransformer

embedding_model = SentenceTransformer("shibing624/text2vec-base-chinese")

def embed_chunk(text: str) -> List[float]:
    embedding = embedding_model.encode(text, normalize_embeddings=True)
    return embedding.tolist()

embeddings = [embed_chunk(chunk) for chunk in chunks]
embedding_dim = len(embeddings[0])
print(f"向量维度: {embedding_dim}")

Step 4:入库 ------ 写入 Zvec

py 复制代码
import zvec

# 定义 schema:一个存原文的字段 + 一个存向量的字段
collection_schema = zvec.CollectionSchema(
    name="pdf_kb",
    fields=[
        zvec.FieldSchema(name="text", data_type=zvec.DataType.STRING),
    ],
    vectors=[
        zvec.VectorSchema(
            name="embedding",
            data_type=zvec.DataType.VECTOR_FP32,
            dimension=embedding_dim,
            index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.COSINE),
        ),
    ],
)

# 本地建库,数据落在 ./pdf_kb_data 目录下
collection = zvec.create_and_open(path="./pdf_kb_data", schema=collection_schema)

def save_to_zvec(chunks: List[str], embeddings: List[List[float]]) -> None:
    for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
        collection.insert(
            zvec.Doc(
                id=str(i),
                vectors={"embedding": embedding},
                fields={"text": chunk},
            )
        )
    collection.optimize()  # 插入完成后构建索引,加速检索

save_to_zvec(chunks, embeddings)
print("入库完成")

验证一下:检索测试

py 复制代码
def retrieve(query: str, top_k: int = 3) -> List[str]:
    query_embedding = embed_chunk(query)
    result = collection.query(
        queries=zvec.Query(field_name="embedding", vector=query_embedding),
        topk=top_k,
    )
    return [doc.fields["text"] for doc in result]

results = retrieve("文档里提到的核心观点是什么?")
for i, chunk in enumerate(results):
    print(f"[{i}] {chunk}\n")

完整流程串起来

py 复制代码
def build_kb_from_pdf(pdf_path: str):
    markdown_content = parse_pdf(pdf_path)
    chunks = split_into_chunks(markdown_content)
    embeddings = [embed_chunk(c) for c in chunks]
    save_to_zvec(chunks, embeddings)
    print(f"处理完成:{pdf_path} -> {len(chunks)} 个 chunk 已入库")

build_kb_from_pdf("sample.pdf")

小结

这套流程的核心思路就四步:

  1. 解析:把 PDF 转成保留结构的 Markdown,而不是拍扁的纯文本
  2. 分块:按标题+段落切,避免语义被硬切断
  3. 向量化:用中文效果好的开源 embedding 模型
  4. 入库:选一个不需要起服务的嵌入式向量库

整个脚本没有 Docker、没有独立服务、没有云依赖,python build_kb.py 直接跑完,数据全部落在本地目录里。如果以后需求变复杂了(海量文档、多租户、知识图谱),再往上叠 MinerU、Milvus 这些重组件也不迟------但对于个人项目或者快速验证一个想法,这四步已经够用。

相关推荐
杭州默安科技5 分钟前
从“被动响应”到“主动遏制”:AI Agent时代,网络防御体系如何重构?
人工智能·网络安全·ai智能体
Angeldream1238 分钟前
第 10 章 医学影像伦理与安全
图像处理·人工智能·python·安全·机器学习·医学影像
2601_9653053935 分钟前
森林防火卡口与气象监测联动
人工智能
云和数据.ChenGuang43 分钟前
光哥说Python 异常深度解析:从异常模型到工程化实践
java·前端·人工智能·python·langchain
爱签AI电子合同1 小时前
电子合同全维度实测与选型指南
大数据·人工智能
AI办公探索者1 小时前
从SaaS到本地部署:大模型推理成本优化与8888元私有化方案的技术解析
大数据·人工智能
吕布辕门1 小时前
DeepSeek 大模型落地应用与价值实现指南
人工智能·机器学习
武子康1 小时前
手机加语音配件,断开连接后谁来保住这段话?
人工智能·后端·agent
迪菲赫尔曼1 小时前
【基础篇 12】把环境与源码接起来:检查 GPU,再安装服务器上的项目
运维·服务器·开发语言·人工智能·python·yolo·目标检测
技灵AI1 小时前
MiniMax H3 能做哪些视频?商品广告、人物表演与运镜怎么选
人工智能·prompt·aigc·音视频