LlamaIndex 接入大模型:LLM 与 Embedding 配置指南
LlamaIndex 专为构建 RAG(检索增强生成)应用而设计,核心抽象是”索引→检索→合成”三层管道。通过配置 OpenAI LLM 和 OpenAIEmbedding,可接入任意 OpenAI 兼容平台,无需修改索引和查询逻辑。
如果你是第一次上手 LlamaIndex,大概率会踩这个坑:文档跑通了官方 quickstart,一到自己的项目就报错,或者查询结果驴唇不对马嘴。多半不是代码写错了,而是没搞清楚这套管道里”谁在什么时候被调用”。构建索引时用的是 embed_model,把文档切块、转成向量、存进向量库;查询时先用同一个 embed_model 把问题转成向量去检索,再用 llm 把检索到的片段拼进 prompt 生成答案。两个模型各司其职,任何一个配错了 api_base 或 key,都不会在”配置”那一步报错,而是在你真正调用 .query() 的那一刻才炸——这是新手最容易懵的地方,因为报错栈离你写错的那行代码已经很远了。搞懂这个先后顺序,后面遇到问题排查起来就快得多。
安装
pip install llama-index llama-index-llms-openai llama-index-embeddings-openai
LlamaIndex v0.10+ 采用模块化包结构,LLM 和 Embedding 集成分包安装。
配置 LLM
import os
from llama_index.llms.openai import OpenAI
llm = OpenAI(
model="gpt-4o-mini",
api_key=os.environ["OPENAI_API_KEY"],
api_base=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
temperature=0.1,
)
# 快速测试
resp = llm.complete("Python 中的列表推导式是什么?")
print(resp.text)
这里几个参数值得多说两句。api_base 优先级高于环境变量里的 OPENAI_BASE_URL——如果你两边都设了还不一致,以显式传参为准,这是新人联调时最容易忽略的隐藏坑,明明改了 .env 却发现请求还是打到了老地址,回头一看是代码里写死了 api_base 参数覆盖了环境变量。temperature=0.1 这个值不是随便拍的:RAG 场景要的是”忠实于检索到的材料”而不是”天马行空地发挥”,温度调太高(比如 0.7 以上)容易让模型脱离检索片段自己编,业内俗称”检索增强生成变成检索无关生成”;如果你发现回答开始夹带检索内容里没有的细节,第一反应应该是把温度往下调,而不是急着换模型。
跑通这段代码,你应该看到模型老老实实地解释了列表推导式的语法和用法,是一段连贯的中文文字,而不是报错栈。如果这一步就报错,先分诊:AuthenticationError(通常带 401)说明 key 不对或者 key 和 api_base 对应的平台对不上——很多人从 OpenAI 官方 key 切换到国内中转平台时忘了同步换 key,这是最常见的低级错误;RateLimitError(429)说明触发了限流,要么请求太快要么账户额度用完;APIConnectionError 或者超时,多半是 api_base 域名写错、末尾多了斜杠、或者网络到不了那个地址,先用 curl 单独测一下这个 endpoint 能不能连通,比在 Python 里反复调试更省时间。
配置 Embedding
Embedding 模型负责把文本转成向量,用于相似度检索:
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = OpenAIEmbedding(
model="text-embedding-3-small",
api_key=os.environ["OPENAI_API_KEY"],
api_base=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
)
有个坑必须提前说清楚:向量维度一旦定了就别中途换 Embedding 模型。text-embedding-3-small 输出 1536 维向量,如果你的索引已经用这个模型建好了,之后又心血来潮换成另一个维度不同的模型(哪怕只是升级到 text-embedding-3-large 的 3072 维),旧索引里存的向量和新查询生成的向量维度对不上,轻则报 dimension mismatch 之类的错误,重则不报错但检索结果全是垃圾(因为向量库允许维度不匹配时做了静默截断或填充)。稳妥做法是:换模型前先想清楚要不要重建索引,测试阶段就把这个决定定下来,别等生产环境数据量上去了才发现要重新跑一遍 embedding,那时候的时间和 token 成本都不是闹着玩的。
全局设置(推荐)
用 Settings 一次性注入,后续所有索引和查询自动使用:
from llama_index.core import Settings
Settings.llm = llm
Settings.embed_model = embed_model
Settings.chunk_size = 512 # 分块大小
Settings.chunk_overlap = 50 # 块间重叠
Settings 本质上是个全局单例,你在任何地方赋值一次,后面所有 VectorStoreIndex.from_documents()、as_query_engine() 都会隐式读取它,不用每次传参——这既是它方便的地方,也是它容易埋雷的地方。如果你的项目里同时要跑两套不同的索引(比如一套中文文档用 512 分块,一套代码文档需要更大的分块保留完整函数),全局共用一个 Settings 就不够用了,这时候要么在构建索引时用 ServiceContext(旧版本)或者直接给 from_documents() 传局部的 embed_model/transformations 参数覆盖全局设置,别指望改一次 Settings.chunk_size 就能给不同索引分别生效。
chunk_size 和 chunk_overlap 这两个参数直接决定你的检索质量和 token 成本。分块太小(比如 128),一个知识点可能被切成两截,检索到其中一半会让 LLM 回答得残缺不全;分块太大(比如 2048),单次检索塞进 prompt 的无关内容变多,既拉高成本又稀释了有效信息的密度,模型也更容易”看走眼”。50 的 overlap 是给分块边界留了一点缓冲,防止关键句子刚好卡在两个块的分界线上被拦腰截断。中文文档因为没有天然的单词边界,实践中 512 左右是个比较稳的起点,但具体多大要看你的文档类型:FAQ 类短文档可以再小一点,长篇技术手册可以适当放大到 800-1024,没有放之四海皆准的数字,需要拿你自己的语料实测几组对比一下命中率。
构建索引与查询
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
# 1. 加载文档
documents = SimpleDirectoryReader("./docs").load_data()
# 2. 构建向量索引(自动调用 embed_model 生成向量)
index = VectorStoreIndex.from_documents(documents)
# 3. 创建查询引擎(自动调用 llm 合成答案)
query_engine = index.as_query_engine(similarity_top_k=3)
response = query_engine.query("如何配置大模型 API?")
print(response)
这段代码跑起来后,第 2 步(构建向量索引)是整个流程里最耗 token 也最耗时的一步——每个文档块都要单独调一次 embedding 接口,文档数量一大,光是这一步的 API 调用次数就可能是几百上千次。如果你的文档库有上万篇,别傻等它跑完再去做别的事,先用一个小样本(比如 20 篇)跑通全流程,确认查询结果符合预期,再把全量文档丢进去批量处理,中途失败了也好定位是哪批文档的问题。
索引建好之后千万别每次运行脚本都重新构建一遍,那是在把钱往火里扔。正确做法是构建完立刻持久化:
# 首次构建后持久化到本地
index.storage_context.persist(persist_dir="./storage")
下次启动脚本时改成从磁盘加载,不再重新调用 embedding 接口:
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)
query_engine = index.as_query_engine(similarity_top_k=3)
只有当源文档发生了增删改,才需要重新构建或者用 index.insert() 增量插入新文档,而不是无脑重建整个索引——增量插入只对新增的那部分文档调用 embedding,成本是重建的一个零头。
如果你的应用是面向用户的对话式产品,一次性等 query_engine.query() 跑完再显示结果,用户体验会很差(尤其检索加合成加起来要好几秒)。这时候换成流式查询引擎,答案生成的过程就能像打字机一样逐字吐出来:
query_engine = index.as_query_engine(streaming=True, similarity_top_k=3)
streaming_response = query_engine.query("如何配置大模型 API?")
streaming_response.print_response_stream()
生产环境里再加一层重试退避也是必修课,检索增强场景经常在批量导入文档时触发限流,简单粗暴的写法是配合 tenacity 库:
from tenacity import retry, wait_exponential, stop_after_attempt
@retry(wait=wait_exponential(multiplier=1, min=2, max=30), stop=stop_after_attempt(5))
def safe_query(engine, question):
return engine.query(question)
指数退避比固定间隔重试更聪明——第一次失败等 2 秒,再失败等 4 秒、8 秒,一路加倍到封顶 30 秒,既给平台喘息空间,也不会因为死等固定的短间隔而把限流触发得更频繁。
切换国内模型的注意事项
| 配置项 | 说明 |
|---|---|
api_base | 改为国内兼容平台端点,如 https://api.lidayun.com/v1 |
LLM model | 填平台实际模型名,如 deepseek-chat |
Embedding model | 部分平台不提供 embedding,可用 HuggingFaceEmbedding 替代 |
api_key | 对应平台的 key,非 OpenAI 原始 key |
Embedding 不提供是切换国内平台时最容易卡壳的地方,很多中转平台优先接入了对话模型,向量模型要么没有要么型号有限。这时候别硬凑,HuggingFaceEmbedding 是本地跑向量模型的靠谱退路,跑在你自己的机器或服务器上,不占用对话平台的 API 额度:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-zh-v1.5")
bge-small-zh 系列是专门针对中文语料训练的,中文检索效果通常比直接套用英文向量模型要好,代价是首次加载要下载模型权重(几百 MB 起步),而且推理放在本地跑,如果你的机器没有 GPU,大批量文档跑 embedding 会明显比调云端 API 慢,这个取舍要提前想清楚:小规模文档、本地部署、对隐私敏感,选本地 embedding;大批量文档、追求速度、不介意调用云端 API,还是老老实实找一个提供 embedding 接口的平台。
再多说一句选型的判断依据,不是每个场景都非本地不可:
| 场景 | 建议 |
|---|---|
| 文档量小(几百篇以内)、追求部署简单 | 直接用平台提供的 Embedding 接口 |
| 文档涉及敏感信息、不能出域 | 本地 HuggingFaceEmbedding,向量库也部署在内网 |
| 文档量极大(十万篇以上)、预算有限 | 本地部署 + 批量异步调用,摊薄单位成本 |
| 平台恰好提供 embedding 接口 | 优先用平台自带的,省去本地环境维护成本 |
成本这块也得心里有本账。对话模型的费用大头是每次查询时”检索片段+问题”一起塞进 prompt 产生的 input token,similarity_top_k=3 意味着每次查询平均要塞进 3 个分块的内容,chunk_size=512 的情况下大致是 1500 字符量级的上下文,这部分是每次查询都要重复付费的;而 embedding 的费用只发生在建索引和增量插入的时候,是一次性成本(除非文档频繁更新)。所以真正决定你长期账单的是查询频率和 similarity_top_k 的取值,不是文档库有多大——这也是为什么盲目调大 similarity_top_k 想”多检索点保险”,看着是提升召回率,实际是在按查询次数线性放大你的 token 账单。
常见问题
Q:索引构建时报 RateLimitError,文档量大怎么办?
可设置 Settings.num_output=256 减少单次输出,或在 SimpleDirectoryReader 时分批处理,并启用本地持久化:index.storage_context.persist("./storage"),避免重复 embedding。
Q:LlamaIndex 与 LangChain 如何选择? LlamaIndex 在复杂文档检索、多路由查询引擎方面更深;LangChain 在工具调用、agent、多模型编排更灵活。两者不互斥,也可混用 LLM 组件。
Q:llama-index 旧包与新包如何迁移?
v0.10 前统一在 llama_index 包下,v0.10 起拆分为 llama-index-core + 各集成包。迁移时需卸载旧包重新安装对应集成包,导入路径也需更新。
延伸阅读:大模型 API 接入完全指南 · 接入教程 Hub · LangChain 接入大模型 · RAG 应用构建指南