每次查询都重新建索引太慢,想改成保存后加载,判断标准不是 persist 没报错,而是两条链路能各自单独跑通,并且同一问题返回的段落与分数对得上。建议先把“建索引 + 保存 + 基准查询”固定成一条链路,再用另一个进程只加载持久化目录查询,最后对照两次结果。下面的骨架以通用 LlamaIndex 接入为例,具体 API 名、持久化文件名和目录结构需要结合你安装的版本与存储后端确认。
适用场景:已能建索引查询,想改为落盘后加载。操作动作:先跑建索引并保存,记录节点数与耗时;再另起进程只从持久化目录加载并查询;最后用同一问题比对返回段落和分数。验证方式:查看落盘文件、确认加载进程未读取原始文档、比较来源节点与分数排序。风险边界:不同版本、存储后端或嵌入模型不一致时,加载可能报错或结果偏移,需以自己环境的实际输出为准。
跑一遍完整建索引流程并记录输出
先拿到一条可信的基准链路。把建索引、保存和基准查询放在同一个脚本里执行,但保存后先不要急着用加载链路,确保这条链路本身可复现。建议显式指定嵌入模型和相似度 top_k,否则不同时间运行可能因为默认配置变化导致后续对照失去意义。
import time
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding # 按实际包替换
DATA_DIR = './docs'
PERSIST_DIR = './storage'
def main():
t0 = time.perf_counter()
docs = SimpleDirectoryReader(DATA_DIR).load_data()
Settings.embed_model = HuggingFaceEmbedding(model_name='BAAI/bge-small-zh-v1.5')
index = VectorStoreIndex.from_documents(docs)
elapsed = time.perf_counter() - t0
print('document count:', len(docs))
print('node count:', len(index.docstore.docs))
print('build elapsed seconds:', round(elapsed, 3))
qe = index.as_query_engine(similarity_top_k=3)
resp = qe.query('你稍后要复用的同一个问题')
print('base response:', resp.response)
for i, sn in enumerate(resp.source_nodes, 1):
print('base node', i, 'score=', sn.score, 'id=', sn.node.node_id)
print(sn.node.get_content()[:200])
index.storage_context.persist(persist_dir=PERSIST_DIR)
if __name__ == '__main__':
main()节点数可用 len(index.docstore.docs) 观察,索引对象也可直接打印 index 看类型和存储上下文;不同版本属性名可能不同,先以实际能打印出来的对象为准。建索引耗时建议用 time.perf_counter() 包住文档读取、节点解析和嵌入构建三段,不要只包 from_documents,否则可能漏掉读取文档的时间。把终端输出重定向到 build.log,或把节点数、文档数、嵌入模型名、top_k、耗时写进一个小 json,后面比对时直接看日志,不靠记忆。
保存到指定目录后检查落盘产物
保存调用就是上面最后一行 index.storage_context.persist(persist_dir=PERSIST_DIR)。persist_dir 可以是相对路径,也可以是绝对路径;建议用 Path(PERSIST_DIR).resolve() 打印一次,确认两个脚本指向同一个位置。
落盘后不要只看 persist 是否返回,要看目录里的文件。可以用:
ls -la ./storage
find ./storage -maxdepth 3 -type f -printf '%p %s bytes\n'通常会出现 docstore.json、index_store.json、vector_store.json、graph_store.json 这类文件;使用不同 vector store 或图像存储时,文件名和子目录会不同,可能看到 default__vector_store.json、image__vector_store.json 或额外子目录。关键不是某个固定文件名必须出现,而是持久化目录里要有文档存储、索引存储和向量存储对应的产物。缺少文件时,persist 不一定报错,但另起进程加载时可能 FileNotFoundError、KeyError 或反序列化失败;也可能加载出一个空壳索引,查询时没有 source_nodes。文件大小为 0 或只有少量配置文件,也要先怀疑落盘不完整。
另起一个进程只加载索引再查询
验证不依赖原始文档也能查询。另开一个终端或另写 load_query.py,确保工作目录与建索引时一致,或者干脆把 PERSIST_DIR 写成绝对路径。加载脚本里不要出现 SimpleDirectoryReader、load_data() 和 DATA_DIR。
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
PERSIST_DIR = './storage'
Settings.embed_model = HuggingFaceEmbedding(model_name='BAAI/bge-small-zh-v1.5')
storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
index = load_index_from_storage(storage_context)
print('loaded node count:', len(index.docstore.docs))
qe = index.as_query_engine(similarity_top_k=3)
resp = qe.query('你稍后要复用的同一个问题')
print('load response:', resp.response)
for i, sn in enumerate(resp.source_nodes, 1):
print('load node', i, 'score=', sn.score, 'id=', sn.node.node_id)
print(sn.node.get_content()[:200])确认这次没有再次读取原始文档,可以从三处查:代码上搜索加载脚本没有 SimpleDirectoryReader、load_data、DATA_DIR;运行上可以临时把原始文档目录改名,再执行加载脚本,若仍能查询,说明只依赖持久化目录,用完记得改回;日志上打印当前工作目录和 PERSIST_DIR 的绝对路径,确认没有退回重新建索引。另起终端执行 python load_query.py,不要复用建索引时的 Python 会话。
同一问题在两条链路下比对结果
确认加载出来的索引与刚建的一致,前提是固定同一个查询语句、同一个 similarity_top_k、同一个嵌入模型和同一批持久化文件。生成答案的文字可能因为生成模型参数或上下文拼接顺序不同而不同,所以主要对照来源节点,而不是只对照 resp.response。
def snapshot(resp):
return [
{'score': round(sn.score, 6), 'id': sn.node.node_id,
'text': sn.node.get_content()[:120]}
for sn in resp.source_nodes
]
print('base:', snapshot(base_resp))
print('load:', snapshot(load_resp))预期上,相同持久化文件、相同嵌入模型和相同查询参数下,两条链路的来源节点 id 和文本通常应一致,分数排序也应一致。分数的绝对值可能因为版本、浮点序列化或检索实现不同而出现小差异,没有通用阈值;更稳的判断是来源节点集合一致、排序一致、内容一致,分数只做辅助。若来源节点不同或排序变化,先检查嵌入模型是否同名同版本、top_k 是否一致、持久化目录是否被覆盖或写错。加载进程若打印出了 building index 或 reading documents 一类日志,优先怀疑它仍在读原始文档,而不是索引不一致。
把加载失败时能看到的信号列出来
下次报错能立刻定位,建议把下面几类信号和对应检查动作放在一起看。
- 目录不存在或路径写错:通常表现为 FileNotFoundError、NotADirectoryError,或在 StorageContext.from_defaults 阶段就报找不到文件。先打印 Path(PERSIST_DIR).resolve()、Path(PERSIST_DIR).exists() 和 os.getcwd()。
- 目录存在但文件缺失:加载时可能出现 FileNotFoundError、KeyError 或 ValueError,也可能加载出一个对象但查询返回空 source_nodes。用 find 看文件清单和大小,确认 docstore、index_store、向量存储产物是否都在。
- 嵌入模型不一致:建索引和加载时用的模型名或维度不同,查询时可能维度不匹配报错,也可能不报错但返回不相关段落、分数分布异常。建议把建索引时的模型名、维度和 top_k 写进 sidecar 文件或构建日志,加载时逐项核对。
- 向量存储后端不一致:建索引用默认内存存储落盘,加载时换成其他 vector store,或反过来,可能报类型不匹配、找不到默认 store,或检索结果为空。持久化目录和加载代码要成对更换。
- 版本不一致:同一批文件在不同大版本之间移动,可能反序列化失败或字段缺失。尽量让建索引和加载使用同一个虚拟环境,加载前打印 llama_index.core.__version__。
- 加载链路偷偷建索引:日志里出现 building index、reading documents、load_data 等字样,说明加载脚本可能误调用了 from_documents 或 SimpleDirectoryReader,不是真正的只加载。