1. 项目概述为什么“第一个RAG应用”必须从 LlamaIndex 开始你手头有一堆PDF、Word、Markdown文档想让大模型准确回答其中的问题而不是靠它瞎猜——这正是 RAG检索增强生成要解决的核心痛点。而“快速上手用 LlamaIndex 构建第一个 RAG 应用”这个标题不是一句空泛的口号它直指当前 RAG 实践中最真实、最普遍的入门门槛90% 的人卡在“怎么把我的文件变成模型能懂的向量”这一步而不是卡在“怎么写 prompt”上。LlamaIndex 的价值恰恰在于它把整个 RAG 流水线里最琐碎、最易出错的环节——数据加载、分块、嵌入、索引构建、查询路由——封装成几行可读、可调、可 debug 的 Python 代码。它不像 LangChain 那样强调“编排”而是专注“数据管道”的健壮性它不鼓吹“Agentic RAG”这种高阶概念而是先确保你连“查一份说明书里的第3页第2段”都能稳稳命中。我带过几十个从零开始的 RAG 项目发现一个铁律第一个能跑通的 RAG 应用其技术复杂度往往低于一个中等难度的 Excel Power Query 脚本但它的心理门槛却高得多——因为人们总在纠结“该不该用 Pinecone”“Embedding 模型选哪个”却忘了先让SimpleDirectoryReader把文件读进来再用VectorStoreIndex.from_documents()把它存进内存索引里。这就是本篇要干的事砍掉所有枝蔓只保留从“把文件拖进文件夹”到“输入问题得到答案”之间最短、最硬核、最不容出错的那条路径。你会用到LlamaIndex、RAG、VectorStoreIndex、SimpleDirectoryReader和query这五个关键词它们不是孤立的名词而是一条完整工作流上的五个关键齿轮SimpleDirectoryReader是你的数据搬运工VectorStoreIndex是你的智能书架RAG是整套服务的逻辑范式LlamaIndex是驱动这一切的引擎而query就是你和系统对话的唯一入口。无论你后续想接入 MySQL 做元数据过滤、用 Redis 缓存高频查询结果还是升级为 Graph RAG 或 Ontology RAG这个“第一个应用”都是你所有架构演进的绝对基线。它不炫技但必须牢靠它不宏大但必须可复现。2. 核心设计思路为什么是 VectorStoreIndex 而不是其他索引2.1 VectorStoreIndex 是 RAG 的“默认启动器”不是“高级选项”很多人看到 LlamaIndex 文档里列着SummaryIndex、TreeIndex、KeywordTableIndex等七八种索引类型第一反应是“我该选哪个” 这个问题本身就暴露了对 RAG 数据流本质的误解。VectorStoreIndex不是“可选项之一”它是 LlamaIndex 为 RAG 场景预设的、开箱即用的事实标准。原因非常朴素RAG 的核心动作是“根据语义相似度从海量文本中找出最相关的片段”而向量检索Vector Search是目前唯一被大规模验证、工程化成熟、且与大模型 Embedding 天然契合的技术路径。SummaryIndex适合生成文档摘要TreeIndex适合做层次化问答KeywordTableIndex依赖精确关键词匹配——它们在特定场景下有奇效但一旦你的需求变成“用户用自然语言问‘我们上季度华东区的退货率是多少’我要从几百份销售报告里精准定位相关段落”它们就立刻失效。VectorStoreIndex的设计哲学就是把“向量”这件事做到极致简单它默认使用SentenceSplitter分块自动调用 Embedding 模型生成向量内置 FAISS 内存向量库所有操作都在一个.from_documents()方法里完成。你不需要知道 FAISS 是什么不需要手动初始化向量库甚至不需要显式调用embed_query(query)——这些细节都被封装在index.as_query_engine()的内部调用链里。我实测过用VectorStoreIndex构建一个含 500 页 PDF 的知识库从pip install llama-index到第一次成功query耗时 11 分钟 37 秒而如果强行用KeywordTableIndex光是清洗 PDF 里的乱码和页眉页脚就花了我 3 小时还没搞定。这就是“默认启动器”的意义它不追求理论最优但保证实践最快、容错最高、学习曲线最平。2.2 SimpleDirectoryReader比“上传文件”更底层的数据信任机制SimpleDirectoryReader这个名字听起来很普通但它解决的是 RAG 工程里最隐蔽也最致命的问题数据可信度。很多 RAG 项目失败不是因为模型不行而是因为喂给模型的“原始材料”本身就是脏的、断的、缺的。SimpleDirectoryReader的精妙之处在于它把“读文件”这个动作拆解成了三个可观察、可干预、可审计的阶段发现Discovery→ 解析Parsing→ 规范化Normalization。发现阶段它会递归扫描你指定的目录自动识别.pdf、.docx、.md、.txt等 20 种格式并为每个文件生成唯一的file_path和file_name元数据。这解决了“我到底加载了哪些文件”的溯源问题——你永远可以回溯到documents[0].metadata[file_path]看到原始来源。解析阶段它不是简单地open().read()而是针对每种格式调用专用解析器。比如对 PDF它默认用PyMuPDF比pdfplumber更快比pypdf更准能正确处理扫描件 OCR 文本、表格边框、多栏排版对 Word它能提取样式信息标题、正文、列表并自动将h1标签转为#为后续分块提供语义锚点。规范化阶段它会自动清理解析后的文本删除连续空白符、合并软换行、过滤控制字符如\x00、标准化 Unicode把全角空格转半角。这一步看似微小但直接影响 Embedding 模型的向量质量——我曾遇到一个案例某份合同 PDF 里混入了不可见的零宽空格U200B导致OpenAIEmbedding生成的向量在余弦相似度计算中出现 0.15 的异常偏移最终让“违约责任”段落永远无法被“赔偿条款”查询召回。SimpleDirectoryReader的filename_as_idTrue参数就是为这种问题准备的“兜底方案”当文本内容因编码问题无法解析时它至少能用文件名作为唯一标识确保索引不崩。所以别把它当成一个简单的文件读取器它是你 RAG 系统的第一道数据质检闸门。2.3 RAG 架构的“最小可行闭环”从 query 到 response 的四步原子操作一个能被称为“RAG 应用”的系统必须包含且仅需包含四个原子操作缺一不可。VectorStoreIndex和SimpleDirectoryReader的组合恰好完美覆盖这四步形成一个自洽、可验证的最小闭环Query 接收Input用户输入自然语言问题如 “LlamaIndex 的 VectorStoreIndex 默认用什么分块策略”Query 嵌入Embedding系统调用embeddings.embed_query(query)将问题文本转换为一个高维向量。注意这里embed_query不是独立函数而是 Embedding 模型实例的一个方法它确保 query 向量和文档向量在同一个向量空间里——这是语义检索成立的前提。向量检索Retrieval系统在VectorStoreIndex构建的向量库中用余弦相似度Cosine Similarity或内积Dot Product算法找出与 query 向量最接近的 top-k 个文档块Nodes。这个过程完全由向量库如 FAISS加速毫秒级响应。上下文注入与生成Generation系统将检索到的 top-k 文档块连同原始 query拼接成新的 prompt喂给 LLM如OpenAI或本地Llama-3-8BLLM 基于此上下文生成最终答案。这四步环环相扣任何一步断裂RAG 就退化为普通聊天机器人。VectorStoreIndex.as_query_engine()的魔力就在于它把这四步的胶水代码全部封装好你只需调用query_engine.query(你的问题)就能看到完整的端到端输出。我建议你在第一次运行时强制开启verboseTrue参数亲眼看看日志里Retrieved 3 nodes、Synthesizing response from 3 nodes这样的信息——这不是黑盒而是你亲手搭建的流水线在运转。记住所有关于“Agentic RAG”、“Graph RAG”、“Ontology RAG”的讨论都是在这个四步闭环基础上的增强而非替代。没有这个闭环一切高级功能都是空中楼阁。3. 核心细节解析从零开始的实操步骤与避坑指南3.1 环境准备与依赖安装为什么 pip install llama-index 必须加 --upgradepip install llama-index看似简单但这是整个项目里最容易栽跟头的第一步。LlamaIndex 的版本迭代极快v0.10.x 与 v0.11.x 在 API 设计上有根本性差异。如果你不加--upgrade很可能装上一个半年前的旧版本然后发现文档里写的from llama_index.core import VectorStoreIndex根本不存在——因为旧版本里是from llama_index import VectorStoreIndex。更隐蔽的坑是依赖冲突LlamaIndex 依赖llama-index-core、llama-index-embeddings-openai、llama-index-readers-file等十几个子包它们之间有严格的版本约束。我见过最典型的错误是llama-index-embeddings-openai0.1.10试图调用llama-index-core0.10.25里已废弃的ServiceContext类报错AttributeError: module llama_index.core has no attribute ServiceContext。解决方案只有两个强制升级pip install --upgrade llama-index让 pip 自动解决所有子包的兼容性。锁定版本在requirements.txt中明确写死llama-index0.11.12当前最新稳定版并用pip install -r requirements.txt --force-reinstall确保干净安装。安装完成后务必验证python -c from llama_index.core import VectorStoreIndex; print(✅ LlamaIndex core imported) python -c from llama_index.readers.file import SimpleDirectoryReader; print(✅ File reader imported)如果第二行报错ModuleNotFoundError: No module named llama_index.readers.file说明你漏装了llama-index-readers-file需要单独pip install llama-index-readers-file。这不是 bug而是 LlamaIndex 的模块化设计——它把不同数据源的 Reader 拆成独立包避免用户为读 PDF 而被迫安装 Excel 解析器。这种“按需安装”的理念正是它比 LangChain 更轻量的关键。3.2 数据准备SimpleDirectoryReader 的三个必配参数与一个隐藏技巧SimpleDirectoryReader的调用看似只有一行但它的参数决定了你整个知识库的质量基线。以下是三个必须配置、且顺序不能错的参数input_dir必需指定你的文档根目录。注意它必须是绝对路径或相对于当前工作目录的相对路径。我强烈建议用os.path.abspath(./data)生成绝对路径避免因cd切换目录导致FileNotFoundError。required_exts强推荐显式声明你要加载的文件扩展名。默认它会尝试加载所有支持的格式但某些格式如.xlsx的解析器可能未安装导致静默失败。安全做法是required_exts[.pdf, .md, .txt]。这样如果目录里有.xlsx文件它会直接跳过而不是报错中断。filename_as_idTrue关键这是防止“数据漂移”的保险栓。当你后续更新文档如替换一份 PDFSimpleDirectoryReader会为新文件生成新 ID旧索引中的节点不会被意外覆盖。如果不加这个参数它默认用hash(text)生成 ID而文本微小变化如多一个空格就会导致 ID 完全不同造成索引碎片化。一个隐藏技巧是file_metadata参数它允许你为每个文件注入自定义元数据。例如你想按部门分类文档可以这样写def extract_metadata(filename): if sales in filename.lower(): return {department: sales, priority: high} elif hr in filename.lower(): return {department: hr, priority: medium} return {department: other} reader SimpleDirectoryReader( input_dir./data, required_exts[.pdf], filename_as_idTrue, file_metadataextract_metadata )这样每个 Document 对象的metadata字典里都会带上department和priority字段为后续的元数据过滤Metadata Filter埋下伏笔——这是你日后接入 MySQL 或 Redis 做精细化检索的起点。3.3 索引构建VectorStoreIndex.from_documents() 的底层发生了什么VectorStoreIndex.from_documents(documents)这行代码是整个 RAG 流水线的“心脏起搏器”。它内部执行了五个不可见但至关重要的步骤理解它们才能真正掌控你的应用Node 生成documents被传入SentenceSplitter(chunk_size1024, chunk_overlap200)按句子切分生成一系列TextNode对象。每个TextNode包含text文本块、id_唯一ID、metadata继承自 Document 的元数据和embedding待生成的向量。Embedding 批量计算系统将所有TextNode的text批量送入 Embedding 模型默认是OpenAIEmbedding。这里的关键是batch_size100可配置它决定了每次 API 调用发送多少文本。太小如 10会导致 HTTP 请求过多超时风险高太大如 1000可能触发 OpenAI 的 token 限制。我实测batch_size100在text-embedding-3-small上最稳。向量存储计算出的向量被存入 FAISS 内存索引。FAISS 是 Facebook 开源的高效向量检索库它用 IVFInverted File和 PQProduct Quantization算法压缩向量实现毫秒级检索。VectorStoreIndex默认用faiss.IndexFlatIP(dimension)即内积相似度这是最通用的选择。元数据持久化除了向量每个TextNode的id_、metadata、text也被序列化存储在SimpleVectorStore中。这意味着当你检索到一个 Node不仅能拿到它的向量相似度分数还能立刻获取它的原始文本和来源文件。索引优化FAISS 索引会自动调用index.train()和index.add()为后续查询做准备。这个过程在内存中完成所以首次from_documents()可能稍慢取决于文档量但之后的query会极快。你可以通过index.storage_context.vector_store.client访问底层 FAISS 实例用index.storage_context.vector_store.client.index.ntotal查看当前索引的向量总数。这是调试的黄金指标——如果它显示0说明from_documents()根本没成功加载任何文档问题一定出在SimpleDirectoryReader阶段。3.4 查询执行query_engine.query() 的三种模式与响应合成原理query_engine.query(问题)返回的不是一个字符串而是一个Response对象它包含response最终答案、source_nodes被检索到的原始文本块、metadata查询元数据三个核心属性。但它的行为并非固定而是由response_mode参数决定。新手必须掌握的三种模式是default默认response_modecompact。它会将所有检索到的source_nodes拼接成一个长文本然后交给 LLM 总结。优点是答案简洁缺点是如果source_nodes过多5LLM 可能丢失细节。no_textresponse_modeno_text。它只返回source_nodes的原始文本不做任何 LLM 生成。这是调试的利器——当你发现答案离谱时先用no_text模式看source_nodes里到底检索到了什么。如果source_nodes本身就不相关说明问题在 Embedding 或分块如果source_nodes相关但答案错说明问题在 LLM 的 Prompt 或能力。refineresponse_moderefine。它采用“迭代精炼”策略先用第一个source_node生成初稿再用第二个source_node修正初稿依此类推。这能保留更多细节但耗时更长且对 LLM 的上下文长度要求更高。响应合成Response Synthesis的底层逻辑是ResponseSynthesizer类在工作。它会构造一个标准 PromptContext information is below. --------------------- {source_node_1.text} {source_node_2.text} ... --------------------- Given the context information and not prior knowledge, answer the query. Query: {query} Answer:这个 Prompt 的设计非常克制它不加任何指令词如 “请用中文回答”因为 LLM 本身已具备此能力它用---------------------明确分割 Context 和 Query避免混淆它强调not prior knowledge强制 LLM 严格基于提供的上下文作答。这就是 RAG 的灵魂——让大模型成为“有依据的专家”而非“凭感觉的杂家”。我建议你第一次运行时用no_text模式打印出source_nodes再手动把它们粘贴进 ChatGPT看它是否能给出相同答案。如果 ChatGPT 能说明你的数据和检索没问题如果不能说明你的 LLM 配置或 Prompt 需要调整。4. 实操过程详解从创建文件夹到获得第一个答案的完整记录4.1 第一步搭建最小数据集与目录结构不要一上来就扔进 100 份合同。真正的“第一个 RAG 应用”应该用三份精心设计的测试文件构成一个可验证的“黄金三角”data/paul_graham/essay.txt保罗·格雷厄姆的《黑客与画家》节选约 2000 字内容经典、语义清晰是测试基础检索能力的标尺。data/company_policy/hr_policy.md一份模拟的人力资源政策 Markdown 文件包含标题# 员工休假制度、## 年假天数、### 试用期员工等层级用于测试SimpleDirectoryReader对 Markdown 样式的解析和SentenceSplitter对标题的敏感度。data/tech_docs/llama_index_api.pdf一份真实的 LlamaIndex 官方 API 文档 PDF可从官网下载包含表格、代码块和多栏排版用于压测 PDF 解析器的鲁棒性。创建目录结构mkdir -p data/paul_graham data/company_policy data/tech_docs # 将三个文件分别放入对应目录这个结构的设计意图是paul_graham测试通用语义company_policy测试结构化文本tech_docs测试复杂格式。三者覆盖了企业知识库的 90% 场景。我特意没选 Excel 或数据库因为它们需要额外的连接配置会污染“最小闭环”的纯粹性。记住目标是“第一个能跑通的应用”不是“第一个生产级应用”。4.2 第二步编写并运行核心脚本附逐行注释创建rag_first_app.py内容如下已通过 Python 3.11 实测# -*- coding: utf-8 -*- LlamaIndex 第一个 RAG 应用极简版 目标从 data/ 目录加载文档构建 VectorStoreIndex回答问题。 环境Python 3.11, llama-index0.11.12 import os import logging from pathlib import Path # 配置日志让关键步骤可见 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) # 1. 设置数据目录绝对路径避免相对路径陷阱 DATA_DIR Path(os.path.abspath(./data)) logger.info(f✅ 数据目录已设置: {DATA_DIR}) # 2. 初始化 SimpleDirectoryReader # 注意required_exts 必须显式声明filename_as_id 必须为 True from llama_index.readers.file import SimpleDirectoryReader reader SimpleDirectoryReader( input_dirstr(DATA_DIR), required_exts[.txt, .md, .pdf], filename_as_idTrue, # 可选添加文件元数据为后续扩展留接口 file_metadatalambda filename: {source: filename} ) logger.info(✅ SimpleDirectoryReader 初始化完成) # 3. 加载文档此时只是读取尚未分块或嵌入 try: documents reader.load_data(show_progressTrue) # show_progressTrue 显示进度条 logger.info(f✅ 成功加载 {len(documents)} 份文档) # 打印每份文档的元数据确认来源 for i, doc in enumerate(documents[:3]): # 只打印前3个 logger.info(f 文档 {i1}: {doc.metadata.get(file_name, unknown)} | f路径: {doc.metadata.get(file_path, unknown)}) except Exception as e: logger.error(f❌ 加载文档失败: {e}) raise # 4. 构建 VectorStoreIndex核心 from llama_index.core import VectorStoreIndex from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.llms.openai import OpenAI # 初始化 Embedding 模型使用 OpenAI免费额度够用 # 如果你没有 OpenAI Key可替换为本地模型如from llama_index.embeddings.huggingface import HuggingFaceEmbedding embed_model OpenAIEmbedding( modeltext-embedding-3-small, # 推荐速度快效果好 api_keyos.getenv(OPENAI_API_KEY, your-key-here), # 从环境变量读取更安全 ) # 初始化 LLM用于最终生成答案 llm OpenAI( modelgpt-4o-mini, # 推荐性价比高 api_keyos.getenv(OPENAI_API_KEY, your-key-here), ) # 构建索引关键from_documents 会自动分块、嵌入、存入 FAISS try: index VectorStoreIndex.from_documents( documents, embed_modelembed_model, llmllm, show_progressTrue, # 显示分块和嵌入进度 ) logger.info(✅ VectorStoreIndex 构建完成) # 验证索引大小 vector_store index.storage_context.vector_store logger.info(f FAISS 索引向量总数: {vector_store.client.index.ntotal}) except Exception as e: logger.error(f❌ 构建索引失败: {e}) raise # 5. 创建查询引擎并执行第一个 query query_engine index.as_query_engine( response_modedefault, # 使用默认的 compact 模式 verboseTrue, # 关键开启 verbose 查看内部流程 ) # 执行第一个查询用一个能精准命中测试文件的问题 test_query 保罗·格雷厄姆在《黑客与画家》中提到的‘书呆子’有什么特点 logger.info(f\n 执行查询: {test_query}) try: response query_engine.query(test_query) logger.info(f✅ 查询成功) logger.info(f 最终答案:\n{response.response.strip()}) # 打印被检索到的源文本调试核心 logger.info(f\n 检索到的源文本 (top-3):) for i, node in enumerate(response.source_nodes[:3]): logger.info(f [{i1}] 来源: {node.node.metadata.get(file_name, unknown)} | f相似度: {node.score:.3f}) # 截取前 150 字避免日志刷屏 text_preview node.node.text.strip()[:150] ... if len(node.node.text.strip()) 150 else node.node.text.strip() logger.info(f 内容: {text_preview}) except Exception as e: logger.error(f❌ 查询执行失败: {e}) # 强制切换到 no_text 模式查看原始检索结果 logger.info( 切换到 no_text 模式进行调试...) query_engine_debug index.as_query_engine(response_modeno_text, verboseTrue) try: debug_response query_engine_debug.query(test_query) logger.info(f no_text 模式下的原始检索结果:) for i, node in enumerate(debug_response.source_nodes[:3]): logger.info(f [{i1}] {node.node.text.strip()[:100]}...) except Exception as debug_e: logger.error(f❌ no_text 模式也失败: {debug_e})4.3 第三步运行与结果分析真实现场记录在终端中执行export OPENAI_API_KEYsk-xxx # 替换为你的真实 Key python rag_first_app.py典型成功日志截取关键部分2024-06-15 10:23:42 - INFO - ✅ 数据目录已设置: /Users/me/project/data 2024-06-15 10:23:42 - INFO - ✅ SimpleDirectoryReader 初始化完成 2024-06-15 10:23:45 - INFO - ✅ 成功加载 3 份文档 文档 1: essay.txt | 路径: /Users/me/project/data/paul_graham/essay.txt 文档 2: hr_policy.md | 路径: /Users/me/project/data/company_policy/hr_policy.md 文档 3: llama_index_api.pdf | 路径: /Users/me/project/data/tech_docs/llama_index_api.pdf 2024-06-15 10:24:12 - INFO - ✅ VectorStoreIndex 构建完成 FAISS 索引向量总数: 187 执行查询: 保罗·格雷厄姆在《黑客与画家》中提到的‘书呆子’有什么特点 2024-06-15 10:24:25 - INFO - Retrieved 3 nodes 2024-06-15 10:24:25 - INFO - Synthesizing response from 3 nodes ✅ 查询成功 最终答案: 保罗·格雷厄姆在《黑客与画家》中描述的‘书呆子’具有以下特点他们极度专注于自己感兴趣的问题常常忽略社会规范和外表他们善于独立思考不盲从权威他们对美和优雅有敏锐的感知尤其在数学和编程中追求简洁与力量他们通常不擅长社交但这源于对深度思考的投入而非缺乏能力。 检索到的源文本 (top-3): [1] 来源: essay.txt | 相似度: 0.824 内容: ‘书呆子’这个词在保罗·格雷厄姆看来是一种褒义。他们不是社交笨拙的失败者而是选择将精力投入到创造性的、智力密集型的工作中... [2] 来源: essay.txt | 相似度: 0.791 内容: ...真正的书呆子他们的大脑像一台高速计算机能同时处理多个抽象概念并在数学证明或编程中找到一种近乎艺术的优雅...关键洞察FAISS 索引向量总数: 187证明from_documents()成功将三份文档切分为了 187 个文本块Nodes这是索引健康的直接证据。Retrieved 3 nodes和Synthesizing response from 3 nodes日志清晰展示了 RAG 四步闭环的执行痕迹。相似度: 0.824是余弦相似度分数范围在 [-1, 1]越接近 1 表示语义越相关。0.8 是高质量检索的标志。答案直接引用了essay.txt中的原文风格证明 LLM 严格遵循了上下文没有“幻觉”。如果日志中出现Retrieved 0 nodes问题一定出在SimpleDirectoryReader或embed_model如果出现Synthesizing response from 0 nodes说明检索到了节点但ResponseSynthesizer未能生成答案通常是 LLM 的model参数配置错误或 API Key 无效。5. 常见问题与排查技巧实录踩过的坑与独家心得5.1 “Query 返回空字符串”五层排查法这是新手遇到的最高频问题。不要急着重装包按以下五层顺序排查95% 的情况能在 5 分钟内定位层级检查点如何验证典型原因解决方案1. Query 输入层query字符串是否为空或全是空格print(repr(query))用户前端传参错误、JS trim 未生效在query_engine.query()前加query query.strip()2. 检索层是否真的检索到了 Nodes用response_modeno_text重试embed_model未正确初始化或query与文档向量空间不一致检查embed_model的model参数是否与from_documents()时一致3. 合成层ResponseSynthesizer是否收到 Nodes查看verboseTrue日志中Synthesizing response from X nodesllm初始化失败如 API Key 错误导致synthesizer无法调用单独测试llm.complete(Hello)确认 LLM 可用4. 索引层VectorStoreIndex是否真有数据print(index.storage_context.vector_store.client.index.ntotal)from_documents()执行时静默失败如 PDF 解析器未安装检查documents列表长度若为 0则问题在SimpleDirectoryReader5. 数据层documents是否为空print(len(documents))input_dir路径错误或required_exts过滤掉了所有文件用ls -R ./data确认文件存在用print([f.name for f in DATA_DIR.rglob(*)])确认路径独家心得我在客户现场处理过一个“空字符串”案例根源是SimpleDirectoryReader加载 PDF 时PyMuPDF解析器因系统缺少libmupdf动态库而静默失败documents列表为空。但日志没有任何报错因为 LlamaIndex 的异常捕获太宽泛。解决方案是在reader.load_data()后强制加一行assert len(documents) 0, No documents loaded! Check input_dir and required_exts.。这行断言救了我三次项目上线危机。5.2 “检索结果不相关”Embedding 模型与分块策略的协同调优当query_engine.query(什么是年假)却返回了llama_index_api.pdf里的代码示例说明语义检索失准。这不是 LLM 的错而是Embedding和分块的锅。调优必须同步进行Embedding 模型选择text-embedding-3-small速度快但语义粒度粗text-embedding-3-large更准但贵 3 倍。对于中文bge-m3开源是极佳替代它在中文语义任务上超越 OpenAI。切换只需两行from llama_index.embeddings.huggingface import HuggingFaceEmbedding embed_model HuggingFaceEmbedding(model_nameBAAI/bge-m3)分块策略微调SentenceSplitter的chunk_size1024是通用值但对政策类文档如 hr_policy