ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

实战教程|用 LlamaIndex + Chroma 搭建多文档 RAG 引擎并封装为 MCP 服务,把 endpoint 改到 TaoToken

实战教程|用 LlamaIndex + Chroma 搭建多文档 RAG 引擎并封装为 MCP 服务,把 endpoint 改到 TaoToken 1. 多文档 RAG 落地时为什么总卡在“索引能跑、服务接不上”很多人第一次做多文档 RAG流程大概是这样的本地放几个 PDF、PPT、Markdown用 LlamaIndex 切块塞进 Chroma写个query_engine.query()跑通看到答案就以为完事了。结果一到“给别人用”这一步就卡住——同事想在自己的编辑器里调用Agent 想把它当成一个工具你只能让对方复制你的 Python 脚本或者临时开个 FastAPI 接口参数、返回格式、错误处理全得重新对齐。我试过最原始的方案把检索逻辑写成一个函数谁要用就 import。问题是每个客户端的调用方式都不一样有的要 JSON有的要流式有的要能列出“当前索引了哪些文档”。改到第三版的时候代码里已经全是if client xxx的分支。MCPModel Context Protocol解决的正是这个问题。它把“能力”抽象成 Tool客户端只要按协议调用不用关心你背后是 Chroma 还是 FAISS是 LlamaIndex 还是 LangChain。你要做的是把多文档 RAG 引擎包装成几个标准 Tool加文档、查状态、删文档、问答。这样无论是 Claude Code、Cline 还是自己写的 Agent接进来就能用。这篇要跑通的链路是本地多文档 → LlamaIndex 切分 → Chroma 向量库 → MCP Server 暴露检索接口 → 模型 endpoint 统一走 TaoToken。目标是一次跑通多文档问答闭环并且把模型调用通道固定下来避免今天换一个 Key、明天换一个 Base URL 的混乱。适合谁看已经写过基础 RAG demo但想把检索能力封装成服务、接进 Agent 工作流的开发者或者你正在用 Cline、Claude Code 这类工具想让自己本地的文档库变成可调用的工具。不需要你精通 MCP 协议细节跟着配置走就行。核心检索词先明确多文档 RAG、LlamaIndex、Chroma、MCP 服务、TaoToken endpoint。下面从目录结构开始一步步把这条链路搭起来。2. 前置准备TaoToken 统一 Key 与 API 通道配置在写 RAG 引擎之前先把模型调用通道固定下来。原因很简单RAG 的索引阶段要用嵌入模型查询阶段要用生成模型如果这两个模型分别走不同的供应商、不同的 Key后面排障会非常痛苦。TaoToken 提供的是统一的 API 通道你只需要一个 Key就能在同一个 Base URL 下调用不同模型。先明确三个东西Base URLhttps://taotoken.net/apiAPI Key在控制台创建格式通常是sk-开头Model ID嵌入模型和生成模型分别指定比如嵌入用text-embedding-3-small生成用gpt-4o-mini或你实际可用的模型控制台地址在这里https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite创建 Key 的页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite如果你还没决定用哪个模型可以先在模型对话页面测一下连通性https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite2.1 环境变量与依赖清单我习惯把配置放在.env里代码里用os.getenv读取。这样换 Key 的时候不用改代码。# .env TAOTOKEN_API_KEYsk-你的实际Key TAOTOKEN_BASE_URLhttps://taotoken.net/api EMBED_MODELtext-embedding-3-small LLM_MODELgpt-4o-mini CHROMA_PERSIST_DIR./chroma_db DOCS_DIR./docs依赖清单用requirements.txt固定版本避免不同机器上行为不一致llama-index0.11.0 llama-index-vector-stores-chroma0.2.0 llama-index-embeddings-openai0.2.0 llama-index-llms-openai0.2.0 chromadb0.5.0 mcp1.0.0 python-dotenv1.0.0 pypdf4.2.0 python-pptx0.6.23安装命令python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt2.2 目录结构目录结构直接影响后面 MCP Server 能不能顺利加载 RAG 引擎。我用的结构如下rag-mcp/ ├── .env ├── requirements.txt ├── docs/ # 放你的多文档 │ ├── report.pdf │ ├── slides.pptx │ └── notes.md ├── chroma_db/ # Chroma 持久化目录 ├── rag_engine.py # RAG 引擎封装 ├── mcp_server.py # MCP Server 入口 └── test_client.py # 本地验证脚本rag_engine.py负责索引和查询mcp_server.py只负责把引擎的方法暴露成 Tool。这样分层的好处是你可以单独测试 RAG 引擎不用每次都启动 MCP Server。2.3 验证 TaoToken 通道连通性在写 RAG 之前先确认 Key 和 Base URL 能通。写一个最小脚本# test_connection.py import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL) ) resp client.chat.completions.create( modelos.getenv(LLM_MODEL), messages[{role: user, content: 只回复两个字连通}] ) print(resp.choices[0].message.content)运行python test_connection.py如果输出“连通”说明通道没问题。如果报 401检查 Key 是否复制完整如果报Connection error检查 Base URL 是否写成了https://taotoken.net/api注意结尾没有斜杠。这一步看起来简单但后面 RAG 出问题时你至少能确定不是通道的问题。嵌入模型也建议单独测一次emb client.embeddings.create( modelos.getenv(EMBED_MODEL), input测试嵌入 ) print(len(emb.data[0].embedding))输出一个数字比如 1536说明嵌入通道也正常。3. 可复制配置LlamaIndex Chroma 索引与 MCP Server 封装这一节是核心把 RAG 引擎和 MCP Server 的代码完整写出来。你可以直接复制到对应文件里改一下.env就能跑。3.1 RAG 引擎切分、入库、查询rag_engine.py的职责加载docs/下的多文档用 LlamaIndex 切分成 Node嵌入后存入 Chroma提供add_document、list_documents、delete_document、query四个方法# rag_engine.py import os import hashlib from pathlib import Path from typing import Optional import chromadb from dotenv import load_dotenv from llama_index.core import ( VectorStoreIndex, SimpleDirectoryReader, StorageContext, Settings, ) from llama_index.core.node_parser import SentenceSplitter from llama_index.vector_stores.chroma import ChromaVectorStore from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.llms.openai import OpenAI load_dotenv() class RAGEngine: def __init__(self): self.persist_dir os.getenv(CHROMA_PERSIST_DIR, ./chroma_db) self.docs_dir os.getenv(DOCS_DIR, ./docs) # 统一走 TaoToken 通道 Settings.embed_model OpenAIEmbedding( modelos.getenv(EMBED_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), api_baseos.getenv(TAOTOKEN_BASE_URL), ) Settings.llm OpenAI( modelos.getenv(LLM_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), api_baseos.getenv(TAOTOKEN_BASE_URL), ) Settings.node_parser SentenceSplitter(chunk_size512, chunk_overlap50) self._init_index() def _init_index(self): client chromadb.PersistentClient(pathself.persist_dir) collection client.get_or_create_collection(rag_docs) vector_store ChromaVectorStore(chroma_collectioncollection) storage_context StorageContext.from_defaults(vector_storevector_store) self.index VectorStoreIndex.from_vector_store( vector_storevector_store, storage_contextstorage_context, ) def _file_id(self, path: str) - str: return hashlib.md5(Path(path).read_bytes()).hexdigest()[:12] def add_document(self, file_path: str) - dict: path Path(file_path) if not path.exists(): return {ok: False, error: f文件不存在: {file_path}} doc_id self._file_id(file_path) reader SimpleDirectoryReader(input_files[str(path)]) documents reader.load_data() for doc in documents: doc.metadata[doc_id] doc_id doc.metadata[file_name] path.name nodes Settings.node_parser.get_nodes_from_documents(documents) self.index.insert_nodes(nodes) return {ok: True, doc_id: doc_id, file_name: path.name, nodes: len(nodes)} def list_documents(self) - dict: client chromadb.PersistentClient(pathself.persist_dir) collection client.get_or_create_collection(rag_docs) data collection.get(include[metadatas]) seen {} for meta in data[metadatas]: if meta and doc_id in meta: seen[meta[doc_id]] meta.get(file_name, unknown) return {ok: True, documents: [{doc_id: k, file_name: v} for k, v in seen.items()]} def delete_document(self, doc_id: str) - dict: client chromadb.PersistentClient(pathself.persist_dir) collection client.get_or_create_collection(rag_docs) collection.delete(where{doc_id: doc_id}) return {ok: True, deleted: doc_id} def query(self, question: str, doc_id: Optional[str] None) - dict: from llama_index.core.vector_stores import MetadataFilters, MetadataFilter, FilterOperator filters None if doc_id: filters MetadataFilters(filters[ MetadataFilter(keydoc_id, valuedoc_id, operatorFilterOperator.EQ) ]) retriever self.index.as_retriever(similarity_top_k4, filtersfilters) nodes retriever.retrieve(question) context \n\n.join([n.get_content() for n in nodes]) prompt f根据以下文档内容回答问题如果内容中没有答案直接说不知道。 文档内容 {context} 问题{question} 答案 response Settings.llm.complete(prompt) sources list({n.metadata.get(file_name, unknown) for n in nodes}) return { ok: True, answer: str(response), sources: sources, node_count: len(nodes), }几个关键点说明Settings.embed_model和Settings.llm都显式传了api_base指向 TaoToken 的 Base URL。这样嵌入和生成走同一个通道Key 也统一。doc_id用文件内容的 MD5 前 12 位保证同一文件重复添加时 ID 一致方便删除。query支持按doc_id过滤这样你可以只查某一个文档而不是全库检索。3.2 MCP Server把引擎方法暴露成 Toolmcp_server.py用官方mcp包把上面四个方法注册成 Tool。这里用 stdio 模式方便本地客户端直接拉起。# mcp_server.py import json from mcp.server.fastmcp import FastMCP from rag_engine import RAGEngine mcp FastMCP(rag-mcp-server) engine RAGEngine() mcp.tool() def add_document(file_path: str) - str: 将指定路径的文档加入 RAG 索引。支持 pdf、pptx、md、txt。 result engine.add_document(file_path) return json.dumps(result, ensure_asciiFalse, indent2) mcp.tool() def list_documents() - str: 列出当前已索引的所有文档及其 doc_id。 result engine.list_documents() return json.dumps(result, ensure_asciiFalse, indent2) mcp.tool() def delete_document(doc_id: str) - str: 根据 doc_id 删除文档索引。 result engine.delete_document(doc_id) return json.dumps(result, ensure_asciiFalse, indent2) mcp.tool() def query_documents(question: str, doc_id: str ) - str: 基于已索引的多文档回答问题。可选 doc_id 限定只查某个文档。 result engine.query(question, doc_iddoc_id or None) return json.dumps(result, ensure_asciiFalse, indent2) if __name__ __main__: mcp.run()3.3 MCP 客户端配置片段如果你用 Cline 或 Claude Code需要在配置里加上这个 Server。以 Cline 的 MCP 配置为例cline_mcp_settings.json{ mcpServers: { rag-mcp: { command: python, args: [/绝对路径/rag-mcp/mcp_server.py], env: { TAOTOKEN_API_KEY: sk-你的实际Key, TAOTOKEN_BASE_URL: https://taotoken.net/api, EMBED_MODEL: text-embedding-3-small, LLM_MODEL: gpt-4o-mini, CHROMA_PERSIST_DIR: /绝对路径/rag-mcp/chroma_db, DOCS_DIR: /绝对路径/rag-mcp/docs } } } }注意三件套必须齐全Base URL、Key、Model ID。少一个都会在调用时报错。command用你虚拟环境里的 python 绝对路径避免客户端找不到依赖。Claude Code 的配置类似放在~/.claude/claude_code_config.json或项目级配置里结构一致。如果你用的是 Codex 的auth.json风格配置把env部分对应填进去即可。4. 验证请求从索引到检索命中的完整检查配置写完先别急着接客户端。用本地脚本把 RAG 引擎单独跑一遍确认索引和检索都正常。4.1 添加文档并查看状态# test_client.py from rag_engine import RAGEngine engine RAGEngine() # 添加两个文档 print(engine.add_document(./docs/report.pdf)) print(engine.add_document(./docs/slides.pptx)) # 查看索引状态 print(engine.list_documents())预期输出类似{ok: true, doc_id: a1b2c3d4e5f6, file_name: report.pdf, nodes: 42} {ok: true, doc_id: f6e5d4c3b2a1, file_name: slides.pptx, nodes: 18} {ok: true, documents: [{doc_id: a1b2c3d4e5f6, file_name: report.pdf}, {doc_id: f6e5d4c3b2a1, file_name: slides.pptx}]}如果nodes是 0说明文档没被正确解析。PDF 可能是扫描件需要 OCRPPTX 检查python-pptx是否安装成功。4.2 检索命中检查result engine.query(报告里提到的核心结论是什么) print(result[answer]) print(来源, result[sources]) print(命中节点数, result[node_count])重点看三个东西answer是否和文档内容相关而不是胡编sources是否包含你添加的文件名node_count是否大于 0如果node_count是 0说明检索没命中。可能原因嵌入模型没走通、Chroma 里没数据、或者问题太偏。先换一个和文档标题接近的问题试试。4.3 按文档过滤查询result engine.query(这个 PPT 讲了什么, doc_idf6e5d4c3b2a1) print(result[answer])这一步验证元数据过滤是否生效。如果返回的内容只来自 PPT说明过滤正常。4.4 通过 MCP 客户端调用启动 MCP Server 后在 Cline 里应该能看到rag-mcp提供的四个 Tool。调用list_documents返回的 JSON 和上面一致。再调用query_documents传入问题看返回的answer和sources。如果客户端里调用失败先看客户端的 MCP 日志。常见的是command路径不对或者env没传进去导致 Key 为空。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节列几个真实会遇到的报错以及对应的排查路径。5.1 401 Unauthorized报错原文通常是openai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}排查顺序检查.env里TAOTOKEN_API_KEY是否完整有没有多余空格检查TAOTOKEN_BASE_URL是否是https://taotoken.net/api不要写成https://taotoken.net/api/v1或带斜杠在 MCP 客户端配置里env是否真的传进去了。有些客户端不会自动读取.env必须在配置里显式写如果本地脚本能通、客户端不通基本就是env没传进去。5.2 local proxy failed报错原文APIConnectionError: Connection error. local proxy failed这个通常是网络层的问题。检查Base URL 是否写错比如写成了http://而不是https://本机是否有其他网络工具干扰防火墙是否拦截了出站请求先用curl测一下curl -X POST https://taotoken.net/api/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:hi}]}如果curl能通说明是代码里的配置问题如果curl也不通检查网络环境。5.3 reading choices 报错报错原文KeyError: choices 或 AttributeError: NoneType object has no attribute choices这个通常出现在模型返回体不符合预期时。排查模型 ID 是否写对比如gpt-4o-mini写成了gpt4o-mini是否把嵌入模型当成了生成模型调用返回体是否被中间层改过在query方法里加一行日志打印原始返回response Settings.llm.complete(prompt) print(RAW:, response)如果RAW是空或者报错信息就能定位到是模型调用层的问题。5.4 OAuth 相关报错如果你用的是 Claude Code 或某些需要 OAuth 的客户端可能会看到OAuth token expired or invalid注意MCP Server 本身不走 OAuth它用的是你在env里配的 API Key。OAuth 报错通常来自客户端自身的登录态和 RAG 引擎无关。解决办法是重新登录客户端或者检查客户端的配置文件是否被覆盖。5.5 检索命中但答案不对如果node_count大于 0但answer和文档无关检查chunk_size是否太小导致上下文被切碎。可以调到 1024 试试similarity_top_k是否太小调到 6 或 8嵌入模型和生成模型是否匹配。如果嵌入用了一个模型生成用了另一个语义空间可能不一致5.6 三件套检查清单无论遇到哪种报错先对照这个清单检查项正确值常见错误Base URLhttps://taotoken.net/api多写/v1或结尾斜杠API Keysk-开头完整字符串复制时漏字符、带空格Model ID与通道支持的模型一致拼写错误、用了不存在的模型环境变量在 MCP 配置的env里显式传入只放在.env里客户端读不到排障时如果拿不准先去 API Keys 页面重新生成一个 Key 试试https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档在这里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite6. 把 RAG 能力接进 Agent 工作流下一步怎么走到这里多文档 RAG 引擎和 MCP Server 已经跑通了。你可以在 Cline 里直接问“我索引的文档里关于 X 的结论是什么”它会自动调用query_documents返回带来源的答案。如果你想让这套东西长期跑在编码或 Agent 工作流里建议把模型调用固定到 Coding Plan 通道这样不用每次手动换 Key额度也更好管理https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite几个可以继续优化的方向增量索引现在每次add_document都会重新切分。可以在doc_id已存在时跳过只处理新文件。混合检索Chroma 只做向量检索可以再加一个 BM25 做关键词召回然后合并排序。多模态如果文档里有图表可以像 PPT 场景那样把页面转成图片用视觉模型解析后再入库。缓存嵌入结果可以缓存到本地避免重复调用嵌入模型。最后提醒一句MCP Server 的env里不要放生产数据库的直连信息RAG 引擎只读本地文档目录就够了。如果你要接内部系统单独走一个只读账号别把权限开太大。整套代码跑下来最花时间的其实是文档解析和切分参数调整模型通道反而是一次配好就不用动。把 Base URL、Key、Model ID 这三件套固定住后面换文档、换客户端都只是改配置的事。
返回列表