1. 本地文件问答为什么总卡在模型接入这一步
很多人第一次用 LlamaIndex 搭本地文件问答,卡住的地方往往不是切分参数,也不是向量库选型,而是模型服务怎么接。LlamaIndex 默认走 OpenAI 的接口,环境变量里塞一个OPENAI_API_KEY就能跑,但真到落地阶段,你会遇到几个很现实的问题:密钥散落在不同脚本里、换模型要改一堆代码、团队里几个人共用一套额度不好管、想对比不同模型效果还得来回改配置。
我试过把 key 硬编码在 notebook 里,结果文件一多、脚本一多,自己都记不清哪个 key 对应哪个项目。后来改成统一走一个兼容 OpenAI 协议的通道,把 Base URL、Key、Model ID 三件套集中管理,LlamaIndex 侧只需要改Settings或OpenAI类的初始化参数,检索链路本身完全不用动。这篇就按这个思路,从文档加载、切分、向量索引一路写到查询引擎,把本地文件问答应用跑通,并核对引用来源。
先说清楚这套东西适合谁:手里有一堆 PDF、Markdown、TXT 想做成能问答的知识库;或者你在做企业内部文档助手,需要可控的模型接入方式;再或者你只是想快速验证 RAG 链路,不想在密钥管理上耗时间。核心检索词就三个:LlamaIndex、本地文件问答应用、统一 Key 接入。下面所有代码都可以直接复制,路径和参数按你自己的环境改一下即可。
LlamaIndex 的定位是「数据框架」,它把非结构化文档转成可检索的索引,再让 LLM 基于检索结果回答。整个链路拆开就是:加载器读文件 → 切分器切块 → 嵌入模型转向量 → 向量库存储 → 查询引擎检索 + 合成答案。这里面只有嵌入和合成两步需要调模型服务,也就是我们要接的那部分。把这两步的通道统一了,剩下的都是本地计算。
2. TaoToken 前置准备:统一 Key 与 API 通道怎么配
在写 LlamaIndex 代码之前,先把模型服务的接入信息准备好。TaoToken 提供的是兼容 OpenAI 协议的 API 通道,也就是说 LlamaIndex 里所有基于 OpenAI 接口的类都能直接用,不需要额外写适配层。你需要拿到三样东西:Base URL、API Key、Model ID。
Base URL 用https://taotoken.net/api,注意这个地址不带任何查询参数。API Key 在控制台的 API Keys 页面创建,建议一个项目建一个 key,方便后面按项目统计用量和吊销。Model ID 就是你实际要调的模型标识,比如对话模型和嵌入模型各选一个。这三件套后面会反复出现,建议先写进环境变量,别硬编码。
创建 key 的入口在这里:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。进去之后点新建,复制出来的 key 只显示一次,记得存好。如果你还没决定用哪个模型,可以先到模型对话页面看看有哪些可用模型:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
环境变量建议这样设,Linux/macOS 用 export,Windows 用 set 或写进系统环境变量:
export TAOTOKEN_API_KEY="你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_LLM_MODEL="你的对话模型ID" export TAOTOKEN_EMBED_MODEL="你的嵌入模型ID"注意:Base URL 结尾不要多加
/v1或斜杠,LlamaIndex 的 OpenAI 兼容类会自己拼接路径。多写一层路径是最常见的 404 来源。
为什么强调「统一 Key」?因为 LlamaIndex 里至少有两个地方要调模型:嵌入模型负责把文本块转成向量,对话模型负责基于检索结果生成答案。如果这两步走不同供应商、不同 key,配置会散成两处。统一走一个通道后,你只需要维护一份 Base URL 和一份 Key,换模型时改一个 Model ID 就行。对于本地文件问答这种需要反复调参的场景,这个收敛很关键。
另外提一句 Coding Plan,如果你后面要把这个问答应用长期跑起来、或者接成 Agent 持续调用,按量计费可能不如套餐划算,可以到 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 看看额度方案。前期验证阶段用按量就够了,别一上来就买套餐。
3. 可复制配置:LlamaIndex 索引构建与查询片段
这一节是核心,直接给能跑的代码。先装依赖:
pip install -U llama-index llama-index-vector-stores-chroma chromadbLlamaIndex 新版本把很多集成拆成了独立包,向量库、嵌入、LLM 都可能要单独装。上面这套组合覆盖了本地 Chroma 持久化 + OpenAI 兼容接入。装完先确认版本:
python -c "import llama_index.core; print(llama_index.core.__version__)"接下来是全局配置。LlamaIndex 用Settings统一管理 LLM 和嵌入模型,这是最省事的写法:
import os from llama_index.core import Settings from llama_index.llms.openai_like import OpenAILike from llama_index.embeddings.openai import OpenAIEmbedding BASE_URL = os.environ["TAOTOKEN_BASE_URL"] API_KEY = os.environ["TAOTOKEN_API_KEY"] Settings.llm = OpenAILike( model=os.environ["TAOTOKEN_LLM_MODEL"], api_base=BASE_URL, api_key=API_KEY, is_chat_model=True, temperature=0.1, ) Settings.embed_model = OpenAIEmbedding( model=os.environ["TAOTOKEN_EMBED_MODEL"], api_base=BASE_URL, api_key=API_KEY, )这里用OpenAILike而不是OpenAI,是因为前者对自定义 Base URL 的兼容性更稳,is_chat_model=True明确告诉它走 chat 接口。temperature=0.1是为了问答场景答案更贴文档,减少发挥。
然后是加载和切分。假设你的文件放在./docs目录,混合了 md 和 pdf:
from llama_index.core import SimpleDirectoryReader from llama_index.core.node_parser import SentenceSplitter documents = SimpleDirectoryReader( input_dir="./docs", recursive=True, required_exts=[".md", ".txt", ".pdf"], ).load_data() splitter = SentenceSplitter(chunk_size=512, chunk_overlap=64) nodes = splitter.get_nodes_from_documents(documents) print(f"文档数 {len(documents)},切分后节点数 {len(nodes)}")chunk_size=512是个稳妥起点,中文文档可以适当放大到 768。chunk_overlap=64保证跨块的句子不被切断。切完打印节点数,如果节点数是 0,说明文件没被读到,先查路径和扩展名。
接着建索引并持久化到 Chroma:
import chromadb from llama_index.core import VectorStoreIndex, StorageContext from llama_index.vector_stores.chroma import ChromaVectorStore chroma_client = chromadb.PersistentClient(path="./chroma_db") collection = chroma_client.get_or_create_collection("local_qa") vector_store = ChromaVectorStore(chroma_collection=collection) storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex( nodes, storage_context=storage_context, show_progress=True, ) index.storage_context.persist(persist_dir="./storage")PersistentClient会把向量落盘到./chroma_db,下次启动不用重新嵌入。persist额外存了 docstore 和 index 元数据,重建时能省一次嵌入开销。
查询引擎配置,重点是让它返回引用来源:
from llama_index.core.query_engine import RetrieverQueryEngine from llama_index.core.retrievers import VectorIndexRetriever from llama_index.core.response_synthesizers import get_response_synthesizer retriever = VectorIndexRetriever(index=index, similarity_top_k=4) synthesizer = get_response_synthesizer(response_mode="compact") query_engine = RetrieverQueryEngine( retriever=retriever, response_synthesizer=synthesizer, ) response = query_engine.query("这个项目的安装步骤是什么?") print(response) for node in response.source_nodes: print(node.metadata.get("file_name"), node.score)similarity_top_k=4是召回块数,文档多可以调到 6。response_mode="compact"会把召回的块压缩后再喂给模型,省 token。source_nodes就是引用来源,file_name来自加载器自动写入的元数据,score是相似度,用来判断召回质量。
如果你想把配置写成文件而不是散在代码里,可以用一个settings.json:
{ "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "llm_model": "你的对话模型ID", "embed_model": "你的嵌入模型ID", "chunk_size": 512, "chunk_overlap": 64, "top_k": 4 }代码里读这个 json 再初始化Settings,团队协作时改配置不用动代码。注意api_key_env存的是环境变量名而不是 key 本身,避免密钥进版本库。
4. 验证请求:跑通样例问答并核对引用来源
配置写完必须验证,不然你不知道是检索没召回还是模型没接上。准备一个样例文件./docs/readme.md,内容随便写几段,比如项目介绍、安装命令、常见问题。然后按顺序跑。
第一步,单独验证嵌入通道。这一步不涉及检索,只确认 Base URL 和 Key 能通:
from llama_index.embeddings.openai import OpenAIEmbedding import os emb = OpenAIEmbedding( model=os.environ["TAOTOKEN_EMBED_MODEL"], api_base=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], ) vec = emb.get_text_embedding("测试文本") print(len(vec))打印出向量维度(比如 1536 或 1024)就说明嵌入通道正常。如果这里报 401,直接跳到第 5 节排查。
第二步,单独验证对话通道:
from llama_index.llms.openai_like import OpenAILike import os llm = OpenAILike( model=os.environ["TAOTOKEN_LLM_MODEL"], api_base=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], is_chat_model=True, ) print(llm.complete("用一句话说明什么是向量检索"))能返回一句通顺的话,说明对话通道也通了。这两步分开验证的好处是,出问题时能立刻定位是嵌入挂了还是对话挂了,不用在完整链路里猜。
第三步,跑完整问答并核对来源。用第 3 节的query_engine,问一个只有样例文件里才有答案的问题:
response = query_engine.query("安装依赖的命令是什么?") print("答案:", response.response) print("引用来源:") for i, node in enumerate(response.source_nodes): print(f"[{i}] 文件={node.metadata.get('file_name')} 分数={node.score:.4f}") print(" 片段:", node.text[:120].replace("\n", " "))成功的结果长这样:答案里包含你写在样例文件里的安装命令,source_nodes里至少有一个节点的file_name是readme.md,score在 0.7 以上。如果答案对但来源为空,说明response_mode或检索器配置有问题;如果来源对但答案跑偏,多半是temperature太高或召回块数不够。
实测下来,similarity_top_k从 4 调到 6,召回覆盖率会明显提升,但 token 消耗也上去。建议先用 4 跑通,再根据答案质量微调。核对来源这个动作别省,它是判断 RAG 是否真的在工作、而不是模型在瞎编的唯一依据。你可以故意问一个文档里没有的问题,看它是否老实说「文档中没有相关信息」,如果它硬编一个答案,说明提示词或response_mode需要收紧。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来,遇到哪个查哪个。
401 Unauthorized / invalid api key。最常见。先确认环境变量真的被读到了,在 Python 里print(os.environ.get("TAOTOKEN_API_KEY")),如果打印 None,说明 export 没生效或写在了别的 shell。再确认 key 没有多余空格,复制时容易带上换行。最后确认 Base URL 是https://taotoken.net/api,没有多写/v1。三件套里 Base URL、Key、Model ID 任何一个错都会 401 或 404。
local proxy failed / connection error。这类报错通常是网络层没通,或者本地有残留的代理环境变量指向了不可用地址。检查HTTP_PROXY、HTTPS_PROXY是否被设成了奇怪的值,清掉再试。另外确认你的运行环境能正常访问外网 API,公司内网可能需要走特定出口。注意这里说的是正常的网络连通性排查,不涉及任何绕过网络管理的手段。
Error reading choices / KeyError 'choices'。这个报错说明请求发出去了、也返回了,但返回体结构不是预期的 OpenAI 格式。常见原因是 Model ID 填成了嵌入模型,或者填了一个不支持 chat 的模型。对话模型和嵌入模型要分开填,Settings.llm用对话模型,Settings.embed_model用嵌入模型。另外OpenAILike的is_chat_model要设 True,否则它可能按 completion 接口发请求,返回结构对不上。
OAuth / authentication 相关报错。如果你用的是某些需要 OAuth 的客户端或 CLI,报错里出现 OAuth 字样,通常是客户端自己的鉴权流程没走完,和 API Key 通道是两回事。LlamaIndex 这套代码走的是 API Key,不涉及 OAuth。如果你在 Codex 或 Claude Code 这类工具里配置,注意它们的配置文件格式不同:Codex 用auth.json,Claude Code 用settings.json,Cline 用 MCP 配置。不管哪个,核心都是 Base URL + Key + Model ID 三件套,缺一不可。
Chroma 相关报错。get_or_create_collection如果报维度不匹配,说明你换了嵌入模型但复用了旧 collection。嵌入模型一换,向量维度就变,旧库必须删掉重建。直接删./chroma_db目录重新跑索引即可。另外PersistentClient的 path 不要指向已有文件,要指向目录。
节点数为 0。文件没被读到。检查input_dir路径、required_exts是否包含你的文件后缀、文件编码是否是 UTF-8。PDF 扫描件没有文字层,加载器读出来是空的,需要先做 OCR。
排障时建议把Settings初始化、嵌入验证、对话验证、完整问答分成四步单独跑,哪步挂了一眼就能看出来。别一上来就跑完整链路,报错信息会混在一起。
6. 把链路固定下来:从验证到长期使用的接入建议
跑通之后,建议把配置和代码固化成一个可复用的小项目,而不是留在 notebook 里。目录结构可以这样:config/settings.json放非敏感配置,环境变量放密钥,ingest.py负责加载切分建索引,query.py负责查询,docs/放源文件,chroma_db/和storage/是生成物、加进.gitignore。
日常使用就两条命令:文档更新后跑python ingest.py重建索引,提问时跑python query.py "你的问题"。如果文档量大、重建慢,可以只对新增文件做增量嵌入,LlamaIndex 的IngestionPipeline配合 docstore 能跳过已处理的节点。
关于模型接入,再强调一次三件套的写法,不管你后面用 Cline、Codex 还是 Claude Code,配置逻辑都一样:Base URL 填https://taotoken.net/api,Key 填控制台创建的 key,Model ID 填实际模型。Claude Code 的settings.json里对应字段是base_url、api_key、model;Codex 的auth.json里是api_base、api_key、model;Cline 的 MCP 配置里是baseUrl、apiKey、model。字段名不同,含义一致。
如果你要把这个问答应用接成长期跑的 Agent,或者团队多人共用,建议看一下 Coding Plan 的额度方案,比按量计费更可控:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各客户端的完整配置示例。想先试模型效果,直接去模型对话页面发几条消息感受一下:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后给个实用技巧:把similarity_top_k和chunk_size做成命令行参数,每次调参不用改代码。再写一个小的评估脚本,准备 10 个已知答案的问题,跑一遍看命中率和来源准确率,这样换模型、换切分参数时能快速对比,而不是凭感觉。本地文件问答的价值在于答案可溯源,来源核对这一步永远别省。