1. 项目概述:为什么一个“基础RAG应用”值得花一整天认真搭一遍

你有没有过这种体验:手头有一堆PDF、Word、网页文本,想快速从中找出某段话的出处,或者让AI基于这些材料回答专业问题,但每次提问都像在大海捞针?不是答非所问,就是胡编乱造。这时候,RAG(检索增强生成)就不是个时髦词,而是你真正能用上的生产力工具。而LlamaIndex,就是目前把这件事做得最“顺手”的框架——它不强迫你写几十行向量数据库操作代码,也不要求你手动拼接prompt模板,而是用几行Python,就把数据加载、切块、嵌入、索引、检索、生成这一整条链路串起来了。我第一次用它跑通一个本地知识库问答,从下载模型到得到第一句准确回答,只用了47分钟。这背后没有魔法,只有清晰的设计逻辑和对开发者真实痛点的深刻理解。本项目标题里说的“加载模型和构建检索器”,看似只是两个动作,实则涵盖了RAG系统最核心的初始化阶段: 模型是大脑,检索器是眼睛 。没有合适的模型,再好的检索结果也生成不出人话;没有高效的检索器,再强的模型也只能对着错误的上下文瞎猜。所以,这不是一个“Hello World”式的玩具项目,而是你后续所有RAG实战的基石。无论你是刚接触AI的业务分析师,还是想快速验证想法的工程师,只要你想把私有数据变成可对话的知识资产,这个基础结构就必须亲手搭一遍、调一遍、踩一遍坑。接下来的内容,我会完全按照一个资深从业者的真实工作流来展开:不讲虚的原理,只告诉你每一步为什么这么写、参数怎么选、哪里最容易出错、以及我踩过的那些坑,是怎么被一行日志或一个配置项救回来的。

2. 核心设计思路拆解:LlamaIndex为何能成为RAG开发的“瑞士军刀”

2.1 从LangChain到LlamaIndex:不是替代,而是聚焦

网上关于“LlamaIndex和LangChain区别”的讨论铺天盖地,但很多答案都停留在表面。作为一个同时用过两者搭建过生产级知识库的人,我的体会是:LangChain像一个功能齐全的“工具箱”,里面扳手、螺丝刀、电钻一应俱全,但你要自己决定先拧哪颗螺丝、再接哪根线;而LlamaIndex更像一把为“数据连接”这一个任务深度打磨的“万用钳”,它把90%的RAG通用流程都预设好了最优路径,你只需要提供数据和模型,剩下的“怎么切、怎么存、怎么找、怎么问”,它都替你规划得明明白白。比如,LangChain里你要分别引入DocumentLoader、TextSplitter、Embeddings、VectorStore、Retriever、LLM,再手动把它们串起来;而在LlamaIndex里, SimpleDirectoryReader 自动处理各种文件格式, SentenceSplitter 默认按语义切分, VectorStoreIndex 一键完成嵌入与索引, as_query_engine() 直接返回一个能回答问题的对象。这种设计不是偷懒,而是源于一个深刻认知: 在绝大多数RAG场景中,“如何高效地把我的数据喂给大模型”才是核心瓶颈,而不是“如何实现一个通用的Agent调度框架” 。所以LlamaIndex把全部精力放在了数据管道上,它的 Node Document Index QueryEngine 这几个核心概念,每一个都直指数据流动中的关键节点。当你看到 index.as_query_engine() 这行代码时,它背后封装的其实是:查询文本的嵌入计算、向量相似度搜索、相关文档的排序与重排、上下文的动态拼接、以及最终prompt的构造与发送。你不需要知道Milvus的 search_config nprobe 参数是什么意思,LlamaIndex已经为你选了一个在精度和速度间平衡的默认值。这种“默认即合理”的哲学,正是它能快速上手的根本原因。

2.2 RAG架构的三层抽象:数据层、索引层、查询层

一个健壮的RAG系统,绝不是把文档扔进数据库然后查一下那么简单。LlamaIndex的精妙之处,在于它用三层清晰的抽象,把复杂性层层隔离:

  • 数据层(Data Layer) :这是你的原始材料,PDF、TXT、Markdown、甚至数据库里的记录。LlamaIndex通过 Reader (如 SimpleDirectoryReader , PandasCSVReader )统一入口,把不同来源、不同格式的数据,都转换成内部标准的 Document 对象。每个 Document 不仅包含 text ,还自带 metadata (文件名、页码、作者等),这为后续的精准过滤打下了基础。我曾经处理过一份带大量表格的财务报告PDF,用默认的 PyMuPDFReader 读出来全是乱码,换成 UnstructuredReader 后,表格内容和文字结构都完美保留。这说明, 数据层的质量,直接决定了整个RAG系统的天花板

  • 索引层(Index Layer) :这是RAG的“记忆中枢”。 Document 经过 NodeParser (如 SentenceSplitter )被切成更小的语义单元 Node ,每个 Node 再通过 EmbeddingModel (如 OpenAIEmbedding )转换成高维向量,最后存入 VectorStore (如 MilvusVectorStore , ChromaVectorStore )。这里的关键在于,LlamaIndex把“向量化”和“存储”解耦了。你可以用OpenAI的API做嵌入,但把向量存在本地的Chroma里;也可以用开源的 BAAI/bge-small-en-v1.5 模型做嵌入,再存到云上的Zilliz Cloud。这种灵活性,让你能根据数据敏感性、预算、性能要求,自由组合技术栈。比如,客户明确要求数据不出内网,我就用 llama-cpp-python 加载一个量化后的 nomic-embed-text-v1.5 模型,配合 Qdrant 向量库,整个流程都在本地完成,连网络请求都不发一次。

  • 查询层(Query Layer) :这是用户直接交互的“前台”。 QueryEngine 是这一层的核心,它接收自然语言问题,执行完整的RAG流程:问题嵌入 → 向量检索 → 文档重排(Rerank)→ 上下文注入 → LLM生成。LlamaIndex提供了多种 QueryEngine RetrieverQueryEngine 适合简单问答, SubQuestionQueryEngine 能自动把复杂问题拆成子问题并行检索, CondenseQuestionQueryEngine 则会先把你模糊的问题“浓缩”成一个更精准的查询词,再进行检索。选择哪个,取决于你的场景。我给一个法律咨询公司做的知识库,就用了 CondenseQuestionQueryEngine ,因为律师提问往往很口语化(“那个去年签的合同,违约金怎么算?”),直接检索效果很差,先浓缩成“合同违约金计算条款”再搜,准确率提升了60%。

这三层抽象,让LlamaIndex既足够简单(新手几分钟就能跑通),又足够强大(专家可以深入每一层定制)。它不是一个黑盒,而是一个透明的、可插拔的流水线。

2.3 “加载模型”与“构建检索器”的本质:一次初始化,两次绑定

回到项目标题,“加载模型和构建检索器”听起来是两个独立动作,但在LlamaIndex的语境下,它们是一次初始化过程中的两个关键绑定环节。这里的“模型”,其实包含两类:

  • 嵌入模型(Embedding Model) :负责把文本(无论是文档还是问题)转换成向量。它决定了“什么内容是相关的”这个根本判断。LlamaIndex默认使用OpenAI的 text-embedding-ada-002 ,但你完全可以换成HuggingFace上的开源模型,比如 BAAI/bge-base-en-v1.5 。选择依据很简单:看你的数据语言和领域。处理中文法律文书, bge-zh-v1.5 的效果远超英文模型;处理代码, codegeex2 的嵌入空间更能捕捉函数签名和变量关系。

  • 大语言模型(LLM) :负责最终的文本生成。它决定了“如何把检索到的信息,组织成一句人话”。LlamaIndex支持几乎所有主流LLM接口,从OpenAI、Anthropic,到本地运行的 llama.cpp Ollama vLLM 。关键点在于, LLM的选择,必须与你的硬件和延迟要求匹配 。我在一台RTX 3090上部署 Qwen2-7B ,推理速度尚可;但如果换成 Qwen3.5-9B ,显存就会爆掉,必须用4-bit量化。而 Qwen2-1.5B 在CPU上都能跑,虽然生成质量稍逊,但胜在稳定和低成本。

“构建检索器”,则是将这两个模型,与你的数据索引绑定的过程。 VectorStoreIndex.from_documents(documents, storage_context=storage_context) 这行代码,就是把 documents (数据)、 storage_context (存储位置,含向量库配置)、以及隐式使用的 EmbeddingModel (由全局设置或 Settings 指定)三者关联起来。而 index.as_query_engine() ,则是把 Index (检索能力)和 LLM (生成能力)最终组装成一个可用的服务。所以,这不是简单的“加载”和“构建”,而是一次精密的“系统集成”。

3. 核心细节解析与实操要点:从零开始搭建你的第一个RAG

3.1 环境准备与依赖安装:避开那些“看似成功”的坑

别急着写代码,环境配置是第一步,也是最容易翻车的地方。我见过太多人卡在 pip install llama-index 这一步,报一堆 pydantic 版本冲突的错误。这是因为LlamaIndex的主干包( llama-index )和它的生态包( llama-index-vector-stores-milvus )对依赖版本要求非常严格。我的经验是, 永远不要用 pip install llama-index 来安装最新版 。官方文档里推荐的 pip install "llama-index[all]" ,在实际项目中往往是个陷阱,因为它会把所有可能用到的向量库、LLM适配器都装上,导致依赖地狱。

正确的做法是: 按需安装,精确到小版本 。以我们这个基础项目为例,目标是用Milvus做向量库,用OpenAI API做嵌入和LLM,那么只需安装三个包:

pip install "llama-index-core==0.10.45"
pip install "llama-index-vector-stores-milvus==0.2.8"
pip install "llama-index-llms-openai==0.1.32"

这三个版本号,是我反复测试后确认能完美协同的组合。 llama-index-core 是核心框架, llama-index-vector-stores-milvus 是Milvus的适配器, llama-index-llms-openai 是OpenAI的LLM适配器。它们各自的 pyproject.toml 文件里,都锁定了兼容的 llama-index-core 版本。如果你强行升级 llama-index-core 到0.11.x,那另外两个包的 import 就会失败,报 ModuleNotFoundError 。这个细节,官方文档不会写,但却是你能否顺利跑通的第一道门槛。

另一个常见坑是 pymilvus 的版本。 llama-index-vector-stores-milvus==0.2.8 要求 pymilvus>=2.4.2 ,但如果你装的是最新的 pymilvus==2.5.0 ,在某些Linux发行版上会因为 grpcio 版本冲突而启动失败。我的解决方案是,先装一个稳定的 pymilvus==2.4.10 ,再装 milvus-lite (一个轻量级的嵌入式Milvus,非常适合本地开发和测试):

pip install pymilvus==2.4.10 milvus-lite

milvus-lite 的好处是,它不需要你单独启动一个Milvus服务进程,所有数据都存在一个本地文件里(比如 ./milvus.db ),开箱即用。对于学习和原型验证,它比折腾Docker容器要省心一百倍。等你确定方案可行,再迁移到云上的Zilliz Cloud或自建的Milvus集群,这才是合理的演进路径。

提示:在虚拟环境中操作!用 python -m venv rag_env 创建一个干净的虚拟环境,然后激活它。这能彻底避免系统Python环境被污染,也是专业开发者的必备习惯。

3.2 数据加载与预处理:别让垃圾输入毁掉你的RAG

数据是RAG的燃料,但不是所有燃料都一样。 SimpleDirectoryReader 是LlamaIndex提供的最便捷的加载器,但它默认的行为,可能并不适合你的数据。比如,它默认会递归扫描子目录,如果你的 data/ 文件夹里混着一些 .git .DS_Store 之类的系统文件,它也会试图去读,然后报错。所以, 第一步永远是显式指定 input_files input_dir ,并排除无关文件

from llama_index.core import SimpleDirectoryReader

# 只加载指定的几个文件,绝对安全
documents = SimpleDirectoryReader(
    input_files=[
        "./data/paul_graham_essay.txt",
        "./data/uber_2021.pdf"
    ]
).load_data()

# 或者,加载整个目录,但排除特定后缀
documents = SimpleDirectoryReader(
    input_dir="./data/",
    required_exts=[".txt", ".pdf", ".md"],  # 只处理这三种格式
    filename_as_id=True  # 把文件名作为doc_id,方便后续追踪
).load_data()

filename_as_id=True 这个参数极其重要。它让每个 Document doc_id 字段等于文件名,这样当你在查询结果里看到一段回答,就能立刻知道它来自哪个文件,这对调试和审计至关重要。否则, doc_id 是一串随机UUID,你根本无法定位源头。

加载完之后,千万别直接拿去建索引。先打印几个 Document 看看内容:

print(f"Loaded {len(documents)} documents")
for i, doc in enumerate(documents[:2]):
    print(f"\n--- Document {i+1} (ID: {doc.doc_id}) ---")
    print(doc.text[:200] + "..." if len(doc.text) > 200 else doc.text)
    print(f"Metadata: {doc.metadata}")

你可能会发现,PDF里的页眉页脚、扫描件OCR出来的乱码、或者网页HTML里的 <script> 标签,都被原封不动地塞进了 text 里。这些噪声会严重污染嵌入向量,让检索结果偏离主题。这时候,就需要 NodeParser 来“净化”数据。 SentenceSplitter 是默认选择,但它有一个致命弱点:它按标点切分,对长段落效果很好,但对短标题、列表项就容易切碎。我处理技术文档时,发现 HierarchicalNodeParser 效果更好,它能识别标题层级(H1, H2),把一个章节下的所有内容保留在同一个 Node 里,保证了语义完整性。它的用法也很简单:

from llama_index.core.node_parser import HierarchicalNodeParser

node_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[2048, 512, 128],  # 大中小三级切分粒度
    include_metadata=True,
    include_prev_next_rel=True  # 记录前后节点关系,用于上下文扩展
)

nodes = node_parser.get_nodes_from_documents(documents)

chunk_sizes 参数是精髓。 [2048, 512, 128] 意味着:先按2048字符切一个粗粒度 Node ,再在这个 Node 里按512字符切更细的,最后再按128字符切最细的。这样,一个长技术文档的“概述”部分可能是一个2048字符的 Node ,而其中的某个具体API参数说明,则是一个128字符的 Node 。查询时,系统会优先返回最细粒度的相关 Node ,确保答案精准。这个技巧,是我在一个API文档知识库项目里,把平均响应时间从3.2秒降到1.1秒的关键。

3.3 模型加载与配置:让“大脑”和“眼睛”各司其职

“加载模型”在LlamaIndex里,其实是一个“声明”而非“执行”的过程。你不需要在代码里写 model = load_model(...) ,而是通过 Settings 对象,告诉整个框架:“当需要嵌入时,用这个;当需要生成时,用那个”。这是一种声明式编程思想,让代码更简洁,也更易维护。

首先,配置嵌入模型。我们以OpenAI为例:

from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding

Settings.embed_model = OpenAIEmbedding(
    model="text-embedding-3-small",  # 推荐!比ada-002便宜3倍,效果更好
    api_key="sk-...",  # 你的OpenAI Key
    embed_batch_size=100,  # 批处理大小,越大越快,但显存/内存占用越高
)

text-embedding-3-small 是OpenAI在2024年推出的新模型,它在成本、速度和效果上达到了一个极佳的平衡点。 embed_batch_size=100 是关键参数。如果你有1000个文档,每个文档被切分成10个 Node ,那就是10000个向量要计算。如果 batch_size=1 ,就要发10000次API请求,慢且贵;如果 batch_size=100 ,就只需要100次请求,效率提升百倍。但要注意, batch_size 不能无限制增大,它受限于你的网络带宽和OpenAI的API限流。我实测下来,100是一个在大多数网络环境下都稳如老狗的值。

接着,配置LLM。同样用OpenAI:

from llama_index.llms.openai import OpenAI

Settings.llm = OpenAI(
    model="gpt-4o-mini",  # 新一代小模型,性价比之王
    api_key="sk-...",
    temperature=0.1,  # 降低温度,让回答更确定、更少幻觉
    max_tokens=1024,  # 控制输出长度,避免无限生成
)

gpt-4o-mini 是2024年推出的明星小模型,它的综合能力接近旧版 gpt-3.5-turbo ,但价格只有其1/3,响应速度却快了一倍。 temperature=0.1 是RAG场景的黄金参数。温度太高(0.7+),LLM会为了“说得漂亮”而编造事实;温度太低(0.0),它又会过于死板,无法灵活组织语言。0.1是一个很好的折中点,它让LLM忠实于检索到的上下文,只做“转述”和“总结”,不做“创作”。

最后,别忘了设置全局 Settings 的其他选项,它们对RAG效果影响巨大:

from llama_index.core import Settings

Settings.chunk_size = 512  # Node的默认大小,与NodeParser的chunk_sizes配合
Settings.context_window = 4096  # LLM的上下文窗口,必须与你选的模型匹配
Settings.num_output = 512  # LLM单次生成的最大token数

context_window 尤其重要。如果你用的是 gpt-4o-mini (上下文4096),但 Settings.context_window 设成了8192,那么在拼接检索到的文档时,系统会试图塞入更多内容,结果超出模型限制,直接报错。反之,如果设得太小,又会浪费宝贵的上下文空间。所以, Settings 里的每一个参数,都必须与你实际选用的模型规格严格对齐 。这是我踩过最多次的坑,没有之一。

4. 实操过程与核心环节实现:从代码到可运行的RAG服务

4.1 构建向量索引:让数据“活”起来的三步走

构建检索器的核心,就是创建一个 VectorStoreIndex 。这个过程可以分解为三个清晰的步骤:准备向量库、准备存储上下文、创建索引。每一步都有其不可替代的作用。

第一步:准备向量库(VectorStore)

向量库是数据的“物理仓库”。我们选择 MilvusVectorStore ,因为它成熟、稳定、社区活跃。初始化时,最关键的参数是 uri dim

from llama_index.vector_stores.milvus import MilvusVectorStore

# 创建一个轻量级的Milvus实例,数据存在本地文件
vector_store = MilvusVectorStore(
    uri="./milvus_demo.db",  # 本地文件路径,milvus-lite会自动创建
    dim=1536,  # 嵌入向量的维度,必须与embed_model输出一致
    overwrite=True,  # 每次运行都清空旧数据,方便调试
    collection_name="rag_knowledge_base",  # 自定义集合名,便于管理
)

dim=1536 这个数字,是 text-embedding-3-small 模型的标准输出维度。如果你换成了 bge-base-en-v1.5 ,它的维度是768,这里就必须改成 dim=768 ,否则会报 Dimension mismatch 错误。 overwrite=True 是开发阶段的利器,它确保你每次运行脚本,都是在一个干净的“新世界”里,避免了旧数据干扰新实验。上线后,当然要改成 False ,并做好数据备份。

第二步:准备存储上下文(StorageContext)

StorageContext 是LlamaIndex的“胶水”,它把 VectorStore (物理存储)和 Index (逻辑索引)粘合在一起。它还可以容纳其他存储组件,比如 DocStore (存原始文档)、 IndexStore (存索引元数据),但在基础RAG里,我们只需要 VectorStore

from llama_index.core import StorageContext

storage_context = StorageContext.from_defaults(
    vector_store=vector_store  # 将向量库注入存储上下文
)

这行代码看起来平淡无奇,但它完成了最关键的绑定:从此,所有通过这个 storage_context 创建的 Index ,其向量数据都会被存进 ./milvus_demo.db 这个文件里。

第三步:创建索引(VectorStoreIndex)

这是最后一步,也是最激动人心的一步。它把 documents (数据)、 storage_context (存储)、以及隐式绑定的 Settings.embed_model (嵌入模型)三者,正式组装成一个可查询的 Index

from llama_index.core import VectorStoreIndex

# 开始构建索引,这一步会触发嵌入计算和向量入库
index = VectorStoreIndex.from_documents(
    documents=documents,
    storage_context=storage_context,
    show_progress=True  # 显示进度条,让你知道它没卡住
)

# 可选:持久化索引,以便下次直接加载,不用重建
index.storage_context.persist(persist_dir="./storage/")

show_progress=True 是良心设计。当你处理几百个PDF时,这个进度条能让你安心等待,而不是怀疑程序是不是挂了。 persist() 方法则是一个性能优化技巧。索引构建(尤其是嵌入计算)是耗时操作。第一次运行,它会花几分钟;但如果你把 index storage_context 一起保存到磁盘,下次启动时,就可以跳过整个构建过程,直接从磁盘加载:

# 下次启动时,直接加载已构建好的索引
from llama_index.core import StorageContext, load_index_from_storage

storage_context = StorageContext.from_defaults(persist_dir="./storage/")
index = load_index_from_storage(storage_context)

这能让你的RAG服务启动时间从几分钟缩短到几秒钟,用户体验天壤之别。这个技巧,很多初学者都不知道,但它却是生产环境的标配。

4.2 构建查询引擎:让RAG真正“开口说话”

有了 index ,下一步就是让它能回答问题。 as_query_engine() 是通往这个目标的捷径,但它背后有丰富的配置选项,决定了你的RAG是“能用”还是“好用”。

最简用法:

query_engine = index.as_query_engine()
response = query_engine.query("What did the author learn?")
print(response)

这行代码会返回一个 Response 对象,它的 response 属性就是生成的答案。但这种用法,用的是LlamaIndex的默认 RetrieverQueryEngine ,它只做最基础的检索+生成,没有重排,没有上下文优化。

进阶用法:添加重排器(Reranker)

默认的向量检索,是按余弦相似度排序的,但相似度高,不代表对当前问题最有用。比如,一个问题“Uber的2021年营收是多少?”,检索可能返回一篇讲Uber商业模式的长文(相似度高),而忽略了一张财报摘要的表格(相似度略低但信息更精准)。这时,就需要 Reranker 来二次打分。LlamaIndex集成了 CohereRerank BGERerank 等开源重排器:

from llama_index.postprocessor.cohere_rerank import CohereRerank

cohere_rerank = CohereRerank(
    api_key="your-cohere-key",
    top_n=3,  # 重排后只保留前3个最相关的结果
)

query_engine = index.as_query_engine(
    node_postprocessors=[cohere_rerank]  # 在检索后,对结果进行重排
)

top_n=3 是经验值。重排器本身也有计算开销, top_n 设得太大,反而拖慢整体速度。我测试过,对于大多数企业知识库, top_n=3 5 ,能在效果和速度之间取得最佳平衡。

高级用法:自定义提示词(Prompt)

有时候,LLM的默认回答风格不符合你的要求。比如,你希望它总是先给出结论,再解释原因;或者,你希望它在不确定时,明确说“我不知道”。这就需要修改 prompt_template

from llama_index.core.prompts import PromptTemplate

# 定义一个更严格的提示词
qa_prompt_tmpl_str = (
    "Context information is below.\n"
    "---------------------\n"
    "{context_str}\n"
    "---------------------\n"
    "Given the context information and not prior knowledge, "
    "answer the query. If the context does not contain the answer, "
    "respond with 'I cannot find the answer in the provided documents.'\n"
    "Query: {query_str}\n"
    "Answer: "
)
qa_prompt_tmpl = PromptTemplate(qa_prompt_tmpl_str)

query_engine = index.as_query_engine(
    text_qa_template=qa_prompt_tmpl  # 应用自定义提示词
)

这个提示词强制LLM“只基于上下文回答”,并给出了一个明确的“不知道”话术,极大减少了幻觉。我在一个医疗知识库项目里,用这个模板,把“编造答案”的比例从12%降到了0.3%。这证明, 一个好的提示词,有时比换一个更贵的LLM模型,效果还要显著

4.3 本地模型接入实战:用Ollama和llama.cpp打造离线RAG

依赖OpenAI API固然方便,但数据隐私、网络延迟、费用控制,都是绕不开的问题。接入本地模型,是每个RAG工程师的必修课。LlamaIndex对本地模型的支持堪称业界标杆,我们以 Ollama llama.cpp 为例,展示如何无缝切换。

Ollama接入:

Ollama是本地大模型的“App Store”,安装简单,模型丰富。首先,确保Ollama服务在本地运行( ollama serve ),然后拉取一个模型,比如 llama3:8b

ollama pull llama3:8b

接着,在Python中,用 Ollama 类代替 OpenAI

from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(
    model="llama3:8b",  # 模型名,必须与ollama list里的一致
    request_timeout=120.0,  # Ollama响应可能较慢,延长超时
    base_url="http://localhost:11434",  # Ollama默认地址
)

就这么简单。LlamaIndex会自动通过HTTP API与Ollama通信。 request_timeout=120.0 是关键,因为本地模型推理比API慢得多,不加这个,很容易超时报错。

llama.cpp接入:

llama.cpp 是极致的轻量化方案,它能让一个7B模型在MacBook Air的M1芯片上流畅运行。你需要先用 llama.cpp convert.py 脚本,把HuggingFace上的模型(如 Qwen2-1.5B )转换成GGUF格式,然后用 llama-server 启动一个本地API服务:

# 启动一个本地LLM服务,监听端口8080
llama-server -m ./models/qwen2-1.5b.Q4_K_M.gguf -c 2048 --port 8080

然后,在LlamaIndex中,把它当作一个普通的OpenAI兼容API来用:

from llama_index.llms.openai import OpenAI

Settings.llm = OpenAI(
    model="qwen2-1.5b",  # 这个名字可以任意,只是标识
    api_key="no-key-needed",  # llama.cpp不需要key
    base_url="http://localhost:8080/v1",  # 指向本地服务
    api_version="v1",  # llama.cpp的API版本
)

你看,除了 base_url api_version ,其他配置和用OpenAI一模一样。这就是LlamaIndex的优雅之处:它把底层LLM的差异,完全抽象掉了。你可以在开发时用OpenAI快速迭代,上线时无缝切换到本地 llama.cpp ,代码几乎不用改。这种“一次开发,多端部署”的能力,是它成为RAG首选框架的核心竞争力。

5. 常见问题与排查技巧实录:那些让我熬夜到凌晨三点的Bug

5.1 向量维度不匹配:一个数字引发的血案

这是RAG新手遇到的第一个,也是最经典的错误。报错信息通常是:

ValueError: Dimension mismatch: expected 768, got 1536

或者更隐蔽的:

pymilvus.exceptions.MilvusException: <MilvusException: (code=1, message=Invalid dimension: 1536, should be less than or equal to 128)>

前者是 embed_model 输出的维度(1536)和 MilvusVectorStore 期望的维度(768)不一致;后者是 Milvus 本身的硬性限制——老版本Milvus(2.3.x)最大只支持128维,而现代嵌入模型动辄768或1536维。这个问题的根源,往往不在你的代码,而在于你安装的包版本。

排查与解决:

  1. 确认 embed_model 的维度 :在代码里加一行,打印出来:

    from llama_index.embeddings.openai import OpenAIEmbedding
    embed_model = OpenAIEmbedding(model="text-embedding-3-small")
    print("Embedding dimension:", embed_model._get_text_embedding("test").shape[0])
    

    运行后,你会看到输出 1536

  2. 确认 MilvusVectorStore dim 参数 :检查你的 vector_store = MilvusVectorStore(...) 这行, dim 参数是否等于1536?如果不是,立刻修正。

  3. 确认Milvus版本 :运行 pip show pymilvus ,看版本号。如果是 2.3.x ,请务必升级到 2.4.2 或更高。 2.4.x 版本移除了128维的硬限制。

  4. 终极保险 :如果你用的是开源嵌入模型,比如 BAAI/bge-small-en-v1.5 ,它的维度是384。那么你的 MilvusVectorStore 就必须是 dim=384 记住: embed_model 的维度、 MilvusVectorStore dim 参数、以及 pymilvus 的版本,三者必须严格匹配,缺一不可 。我曾因为一个 dim=384 写成了 dim=385 ,调试了整整一个下午,最后发现是键盘多按了一个键。

5.2 查询结果为空或不相关:不是模型不行,是检索器没调好

你满怀期待地输入一个问题, query_engine.query() 返回的却是一句“我不知道”,或者答案完全驴唇不对马嘴。这通常不是模型的问题,而是检索环节出了故障。

排查思路:

  • 第一步:绕过LLM,直接看检索结果 QueryEngine 的底层是 Retriever ,你可以把它单独拿出来测试:

    retriever = index.as_retriever(similarity_top_k=5)  # 检索前5个
    nodes = retriever.retrieve("What did the author learn?")
    for i, node in enumerate(nodes):
        print(f"\n--- Result {i+1} (Score: {node.score:.3f}) ---")
        print(node.text[:200] + "...")
    

    如果这里返回的 node.text 和你的问题完全无关,那问题就出在数据或嵌入模型上;如果 node.text 是相关的,但最终LLM生成的答案不好,那问题就在LLM或提示词上。

  • 第二步:检查数据质量 。用 print(documents[0].text[:500]) 看原始数据。我遇到过最离谱的情况:PDF是扫描件,OCR识别把“Uber”识别成了“U6er”,导致所有关于Uber的检索都失败。解决方案是换一个OCR引擎,或者手动校对关键文档。

  • 第三步:调整检索参数 similarity_top_k 太小(如1),可能漏掉关键信息;太大(如20),又会把噪声引入上下文。我一般从 5 开始试。另外, vector_store search_config 里,`

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐