LlamaIndex 接自己的文档目录 / 先把加载切分索引三段跑通

文章导读
手头已经有一批 PDF、Word、Markdown,想用 LlamaIndex 接进来,最省事的路径不是先搭完整问答应用,而是把「加载、切分、索引」拆成三段分别跑通。每一段都有独立的输入输出,能单独打印、单独检查。哪一段对不上,就回到那一段排查,不用在整条链路里盲猜。下面的目录结构和代码骨架都按这个思路组织,文件名、路径、切分参数按你的环境替换即可。
📋 目录
  1. 整理一份结构清晰的测试文档目录
  2. 只做加载,把读到的文档对象打印出来
  3. 只做切分,检查节点数量和边界文本
  4. 把节点交给索引并确认写入成功
  5. 用三个已知问题回测三段链路
A A

手头已经有一批 PDF、Word、Markdown,想用 LlamaIndex 接进来,最省事的路径不是先搭完整问答应用,而是把「加载、切分、索引」拆成三段分别跑通。每一段都有独立的输入输出,能单独打印、单独检查。哪一段对不上,就回到那一段排查,不用在整条链路里盲猜。下面的目录结构和代码骨架都按这个思路组织,文件名、路径、切分参数按你的环境替换即可。

先用一份结构清晰的小目录,把 LlamaIndex 的加载、切分、索引三步拆开单独跑:加载后打印文档对象数量和元数据,切分后打印节点总数与每个节点的首尾文本,索引后核对节点数与重复构建行为,最后用三个已知答案的问题回测。这样做的边界是:验证的是“文档能否被正确读入并检索到”,不涉及切分策略调优、召回排序质量或线上服务稳定性,这些需要另做评估。

整理一份结构清晰的测试文档目录

测试目录不用大,但要能让每类文件对应到具体问题。建议至少覆盖三种格式,并且每种格式放一个「内容已知、答案唯一」的文件。目录层级保持两层:一层按格式分,一层放文件。这样后面回测时,能立刻判断某条检索结果来自哪个文件。

docs_test/
  pdf/
    refund_policy.pdf        # 里面写明退款期限
    shipping_fee.pdf         # 里面写明运费规则
  word/
    onboarding.docx          # 里面写清入职流程步骤数
  markdown/
    faq.md                   # 里面写明客服工作时间
  mixed/
    readme.md                # 和上面文件放在同一层,用来验证递归加载

命名建议:文件名直接带主题词,不要用 1.pdf、新建文档.docx。
同一主题只保留一个版本,避免旧稿被抓进来干扰回测。

混放的文件类型是故意的:把 markdown 放进 pdf 目录旁边,或者把 docx 放一层深,用来确认加载器是只读一层还是递归读取。文件名带主题词,是为了后面看到元数据里的 file_name 时能一眼对应。空文件和乱码文件也建议各放一个,比如一个 0 字节的 txt、一个编码非 UTF-8 的 txt,用来看它们会被如何处理,而不是等上线才发现。

只做加载,把读到的文档对象打印出来

这一段只做一件事:把目录读进来,打印每个文档对象。目的是确认文件有没有被正确读取,而不是急着切分。加载器用法骨架如下,具体类名以你安装的 LlamaIndex 版本和对应的 reader 包为准,不确定时先看 pip show 出来的包版本再决定 import 路径。

from pathlib import Path
from llama_index.core import SimpleDirectoryReader

DATA_DIR = Path("docs_test")

docs = SimpleDirectoryReader(
    input_dir=str(DATA_DIR),
    recursive=True,          # 是否读子目录,按需改
    required_exts=None,      # 需要时限定 .pdf/.md/.docx
    filename_as_id=True,     # 便于回查来源文件
).load_data()

print("文档对象数量:", len(docs))
for d in docs:
    meta = d.metadata
    text_len = len(d.text or "")
    print(meta.get("file_name"), meta.get("file_path"), "字符数=", text_len)
    print("前120字符:", (d.text or "")[:120].replace("\n", " "))

要看的字段:file_namefile_path,有的 reader 还会给 page_labelcreation_date。文档对象数量和目录里的文件数对不上,通常是加载器不认某种扩展名,或者 recursive 没开导致漏读子目录。空文件的表现是字符数为 0,但仍然可能出现在列表里;乱码文件的表现是字符数不为 0,但打印出来是替换字符或问号。这两种情况都建议在加载阶段先过滤掉,比如丢掉 text 为空的文档,避免把无意义内容喂给切分器。

只做切分,检查节点数量和边界文本

加载拿到的文档对象是整篇文本,切分后才变成可以送进索引的节点。这一步单独跑,就是为了看清文本被切成了什么。切分器参数先给保守值,后面再按文档结构调。

LlamaIndex 接自己的文档目录 / 先把加载切分索引三段跑通
from llama_index.core.node_parser import SentenceSplitter

splitter = SentenceSplitter(
    chunk_size=512,       # 目标字符/近似 token 长度上限
    chunk_overlap=50,     # 相邻块重叠,帮助跨块语义衔接
)

nodes = splitter.get_nodes_from_documents(docs)
print("节点总数:", len(nodes))
for i, n in enumerate(nodes[:10]):
    print("--- node", i, "来源:", n.metadata.get("file_name"))
    t = n.get_content()
    print("[头]", t[:80].replace("\n", " "))
    print("[尾]", t[-80:].replace("\n", " "))

要重点看的几点:节点总数是否远超文档数,如果切得过密,块会碎、上下文变少;chunk_size 调大后节点数应下降,这是判断参数是否生效的直接方式。看每个节点的头尾文本,能发现断句位置是否合理。表格和代码块容易被拦腰切断,表现为某个节点前半是表头、后半没有对应行,或者代码块只剩一半括号。遇到这种情况,可以调大 chunk_overlap、用按 Markdown 标题切的分割器,或者把这类文件单独走一条解析路径。切分参数没有普适最优值,先按文档平均段落长度试,再对照头尾文本微调。

把节点交给索引并确认写入成功

索引这一步,输入就是上一步的节点列表。为了排查方便,建议显式从节点建索引,而不是让 from_documents 内部再走一遍切分,否则你没法确定索引里到底存的是哪一批节点。

from llama_index.core import VectorStoreIndex

index = VectorStoreIndex(nodes)   # 直接用切分后的节点

# 核对索引内节点数
print("索引节点数:", len(index.docstore.docs))

# 也可以从检索侧确认
retriever = index.as_retriever(similarity_top_k=3)
hits = retriever.retrieve("退款期限是多久")
for h in hits:
    print(round(h.score, 4), h.node.metadata.get("file_name"))
    print(h.node.get_content()[:100].replace("\n", " "))

索引内节点数和切分节点数不一致时,先怀疑有没有被去重或过滤。重复建索引的行为需要说清楚:VectorStoreIndex(nodes) 每次调用通常会在内存里新建一份索引,旧索引不会自动更新;如果接的是持久化向量库,重复写入会不会覆盖、会不会新增,取决于该存储的写入策略和是否带稳定 ID。用 filename_as_id=True 或给节点设置稳定 id_ 的相对稳妥一些,但具体行为还是要在你的存储后端上验证一次。想避免每次重跑,可以把索引 persist 到本地目录,下次直接加载。

用三个已知问题回测三段链路

回测的目的不是评检索质量,而是定位问题出在哪一段。准备三个答案明确的问题,分别对应不同文件、不同格式。

  • 问题一:「退款期限是多久」→ 预期来源 refund_policy.pdf
  • 问题二:「客服工作时间」→ 预期来源 faq.md
  • 问题三:「入职流程有哪些步骤」→ 预期来源 onboarding.docx

逐条跑检索,记录返回的段落和来源文件名,然后按下面的顺序排查:如果检索结果里根本没有目标文件,先回加载段,确认这个文件是否被读进来、字符数是否正常;如果文件读进来了但检索不到,回切分段,看答案所在的文本是否被切成了两个节点,或者关键词被切散;如果切分正常但返回顺序不对、命中的是别的文件,则问题在索引或检索侧,检查是否混入了旧版本文档、相似度阈值是否合适。docx 和 pdf 的解析结果容易出现换行碎、空格多,回测时留意返回段落是否需要先做一次文本清洗。三个问题都能命中预期来源文件,说明加载、切分、索引这条基本链路是通的,之后再去调整切分粒度和召回条数才有意义。