1. 从零搭一套本地 RAG:为什么我选 MCP + 通义千问 + FAISS
大语言模型实战里最容易卡住的不是模型本身,而是「模型怎么拿到我的私有资料」。RAG(检索增强生成)就是解决这个问题的标准答案:先从你的知识库里检索相关片段,再把片段塞进提示词让模型生成回答。这套流程能处理模型没见过的最新信息,回答基于真实数据,还能随时往知识库里加自定义文档。
但真正动手时你会发现两个麻烦:一是模型服务商太多,通义千问、Claude、GPT 各有一套 Key 和 Base URL,切换一次就要改一遍代码;二是检索逻辑和生成逻辑耦合在一起,换个客户端就得重写。MCP(Model Context Protocol)正好解决第二个问题——它把「检索」封装成一个标准工具,任何支持 MCP 的客户端都能调用,服务端只管维护 FAISS 索引。而 TaoToken 解决第一个问题:一个统一 Key 走通所有模型服务,Base URL 固定,模型 ID 按需切换。
这套组合适合谁?适合已经跑过 Hello World、想真正落地一个可复现 RAG 链路的开发者。你不需要 GPU,一台普通笔记本就能跑 FAISS-CPU;你也不需要多个平台账号,TaoToken 一个 Key 覆盖通义千问的生成和嵌入。下面我把整条链路拆成可复制的步骤,从环境准备到端到端问答验证,每一步都有命令和配置。
2. TaoToken 前置准备:统一 Key 与 API 通道配置
在写任何 RAG 代码之前,先把模型服务的入口统一掉。传统做法是去阿里云百炼申请 DashScope Key,再单独配通义千问的 Base URL,一旦想换模型就得改环境变量。TaoToken 的思路是提供一个 OpenAI 兼容的统一入口,你只需要记住一个 Base URL 和一个 Key,模型 ID 在请求里指定即可。
先拿到 Key。访问 TaoToken 控制台创建 API Key,路径是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,登录后在 API Keys 页面新建一个,复制保存。这个 Key 同时用于对话模型和嵌入模型,不需要分别申请。
接着确认 API 通道地址。TaoToken 的 API 根地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 OpenAI 客户端的 base_url 使用。如果你用的是 OpenAI SDK,它会自动拼接 /chat/completions 和 /embeddings 路径,所以 base_url 写到 /api 即可,不要多加 /v1。
模型 ID 方面,通义千问系列在 TaoToken 上的命名和官方一致:生成用 qwen-plus 或 qwen-max,嵌入用 text-embedding-v4。你可以在模型对话页面先手动试一次,确认 Key 和模型 ID 能通,再写进代码。模型对话入口: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
环境变量我习惯放在项目根目录的 .env 文件里,用 python-dotenv 加载。这样代码里不出现明文 Key,也方便切换。配置如下:
# .env 文件,放在项目根目录 TAOTOKEN_API_KEY=sk-你的TaoToken密钥 TAOTOKEN_BASE_URL=https://taotoken.net/api QWEN_CHAT_MODEL=qwen-plus QWEN_EMBED_MODEL=text-embedding-v4验证环境变量是否加载成功,跑一段最小脚本:
from dotenv import load_dotenv import os load_dotenv() print("API Key:", "OK" if os.getenv("TAOTOKEN_API_KEY") else "Missing") print("Base URL:", os.getenv("TAOTOKEN_BASE_URL")) print("Chat Model:", os.getenv("QWEN_CHAT_MODEL"))输出应该是 OK 和对应的地址、模型名。如果 Key 显示 Missing,检查 .env 是否在运行目录下,或者 load_dotenv 的路径参数是否指对。这一步过了,后面所有模型调用都走这个通道,不用再碰其他平台。
3. 可复制配置:MCP Server 与 FAISS 索引构建脚本
这一节是整篇的核心,给出可以直接复制运行的 MCP Server 代码和 FAISS 索引构建逻辑。项目结构建议这样组织:
mcp-rag-demo/ ├── rag-server/ │ └── server.py # MCP Server 主程序 ├── rag-client/ │ └── client.py # MCP Client 主程序 ├── docs/ │ └── knowledge.txt # 你的知识库文档 ├── .env └── requirements.txt依赖安装:
pip install faiss-cpu mcp openai python-dotenv numpy版本上,faiss-cpu 用 1.10 以上,mcp 用 1.6 以上,openai 用 1.75 以上即可。这些包在 Python 3.10 环境下实测稳定。
先写 MCP Server。它做三件事:初始化 TaoToken 客户端、提供 index_docs 工具把文档转成向量存进 FAISS、提供 retrieve_docs 工具按查询检索最相似的片段。
# rag-server/server.py import os import numpy as np import faiss from dotenv import load_dotenv from openai import OpenAI from mcp.server.fastmcp import FastMCP load_dotenv() mcp = FastMCP("rag-server") client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), ) _index = None _docs = [] def embed_texts(texts): resp = client.embeddings.create( model=os.getenv("QWEN_EMBED_MODEL"), input=texts, ) return np.array([d.embedding for d in resp.data], dtype="float32") @mcp.tool() def index_docs(docs: list[str]) -> str: """把文档列表索引到 FAISS 向量库""" global _index, _docs _docs = docs embeddings = embed_texts(docs) dim = embeddings.shape[1] _index = faiss.IndexFlatL2(dim) _index.add(embeddings) return f"已索引 {len(docs)} 篇文档,维度 {dim}" @mcp.tool() def retrieve_docs(query: str, top_k: int = 3) -> str: """检索与查询最相关的文档片段""" if _index is None: return "索引为空,请先调用 index_docs" q_vec = embed_texts([query]) distances, indices = _index.search(q_vec, top_k) results = [] for rank, idx in enumerate(indices[0]): if 0 <= idx < len(_docs): results.append(f"[{rank}] {_docs[idx]}") return "\n".join(results) if __name__ == "__main__": mcp.run()这里的关键点:embed_texts 走的是 TaoToken 的 embeddings 接口,模型 ID 是 text-embedding-v4,返回 1536 维向量。FAISS 用 IndexFlatL2 做精确检索,文档量在几万条以内性能足够。@mcp.tool() 装饰器把普通函数注册成 MCP 工具,客户端通过标准协议调用,不需要关心底层是 HTTP 还是 stdio。
再写 MCP Client。它负责连接 Server、加载知识库、调用检索工具,最后把检索结果拼进提示词交给通义千问生成回答。
# rag-client/client.py import os import sys import asyncio from dotenv import load_dotenv from openai import OpenAI from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client load_dotenv() client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), ) async def main(server_script: str): params = StdioServerParameters( command=sys.executable, args=[server_script], ) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # 加载知识库 with open("docs/knowledge.txt", "r", encoding="utf-8") as f: docs = [line.strip() for line in f if line.strip()] result = await session.call_tool("index_docs", {"docs": docs}) print("索引结果:", result.content[0].text) # 交互问答 while True: query = input("\n请输入问题(exit 退出): ") if query.lower() in ("exit", "quit"): break retrieved = await session.call_tool( "retrieve_docs", {"query": query, "top_k": 3} ) context = retrieved.content[0].text resp = client.chat.completions.create( model=os.getenv("QWEN_CHAT_MODEL"), messages=[ { "role": "system", "content": "你是知识库助手,只根据提供的文档片段回答,不要编造。", }, { "role": "user", "content": f"问题:{query}\n\n相关文档:\n{context}", }, ], ) print("\n回答:", resp.choices[0].message.content) if __name__ == "__main__": asyncio.run(main(sys.argv[1]))启动方式:先开一个终端跑 Server,再开另一个终端跑 Client 并传入 Server 脚本路径。
# 终端 1 python rag-server/server.py # 终端 2 python rag-client/client.py rag-server/server.py如果你用 Claude Code 或 Cline 这类支持 MCP 的编辑器,可以把 Server 注册进配置文件。以 Claude Code 的 settings 为例,在项目根目录的 .mcp.json 里写:
{ "mcpServers": { "rag-server": { "command": "python", "args": ["rag-server/server.py"], "env": { "TAOTOKEN_API_KEY": "sk-你的密钥", "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "QWEN_EMBED_MODEL": "text-embedding-v4" } } } }这样编辑器启动时会自动拉起 MCP Server,你在对话里就能直接调用 retrieve_docs 工具。注意 Base URL、Key、Model ID 三件套要写全,缺一个都会导致连接失败。
4. 验证请求:端到端问答与成功结果对照
配置写完,跑一遍完整链路。准备一个 docs/knowledge.txt,每行一条知识片段,比如:
FAISS 是 Facebook 开源的向量相似度检索库,支持十亿级向量。 RAG 的核心是先检索后生成,检索质量决定回答质量。 MCP 是模型上下文协议,用标准方式把工具暴露给 LLM 客户端。 通义千问的 text-embedding-v4 输出 1536 维向量。 TaoToken 提供 OpenAI 兼容接口,一个 Key 调用多种模型。启动 Server 后,终端 1 应该没有报错,安静等待连接。终端 2 运行 Client,预期输出:
索引结果: 已索引 5 篇文档,维度 1536 请输入问题(exit 退出): MCP 是什么 回答: MCP 是模型上下文协议,它用标准方式把工具暴露给 LLM 客户端, 让客户端可以调用服务端定义的工具,比如检索文档。再试一个需要跨文档综合的问题:
请输入问题(exit 退出): 这套 RAG 用了哪些组件 回答: 这套 RAG 使用了 FAISS 做向量检索,通义千问的 text-embedding-v4 做嵌入,qwen-plus 做生成,并通过 MCP 协议把检索工具暴露给客户端。如果回答准确引用了知识库内容,说明整条链路通了。你可以打开 TaoToken 控制台的用量页面,确认 embeddings 和 chat 两类请求都有记录。这一步的意义在于:检索走 MCP 工具、生成走 TaoToken 统一通道,两条路径都验证过,后面换模型或加文档都不用改架构。
想快速验证模型本身是否正常,可以单独跑一段最小对话请求:
from openai import OpenAI import os from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), ) resp = client.chat.completions.create( model="qwen-plus", messages=[{"role": "user", "content": "用一句话解释 RAG"}], ) print(resp.choices[0].message.content)这段能出结果,说明 Key 和通道没问题,问题就只可能在 MCP 或 FAISS 环节。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
实际跑的时候,报错集中在几个地方。我按真实遇到的顺序列出来,对照排查。
401 Unauthorized。最常见的原因是 Key 没加载或写错。先确认 .env 里的 TAOTOKEN_API_KEY 没有多余空格,再确认 load_dotenv() 在 OpenAI 客户端初始化之前调用。如果你把 Key 写进了 MCP 配置的 env 字段,检查 JSON 里有没有转义问题。还有一种情况是 base_url 写成了 https://taotoken.net/api/v1,多加了 /v1 导致路径拼接错误,改成 https://taotoken.net/api 即可。
local proxy failed 或 connection refused。这类报错通常出现在 MCP Client 启动 Server 子进程时。检查 server_script 路径是否正确,sys.executable 是否指向当前虚拟环境的 Python。如果你在 conda 环境里跑,确认 Client 和 Server 用的是同一个解释器。另外,Server 脚本里如果有语法错误,子进程会直接退出,Client 侧看到的就是连接失败。先在终端单独运行 python rag-server/server.py,确认能正常启动再走 Client。
reading choices 报错,比如 'NoneType' object has no attribute 'choices' 或 reading 'choices'。这通常是 API 返回结构不符合预期。先打印完整响应看看:
resp = client.chat.completions.create(...) print(resp)如果返回的是错误对象,检查模型 ID 是否拼错。qwen-plus 写成 qwen_plus 或 Qwen-Plus 都会失败。嵌入模型同理,text-embedding-v4 不能写成 text-embedding-v3。另一个原因是 messages 格式不对,role 必须是 system、user、assistant 之一,content 必须是字符串。
OAuth 相关报错。如果你在 Claude Code 或 Cline 里配置 MCP,可能会遇到 OAuth token 过期或未授权的提示。这类问题一般和 MCP Server 本身无关,而是编辑器侧的认证状态。先确认编辑器的模型通道配置正确,Base URL 指向 https://taotoken.net/api ,Key 用 TaoToken 的 Key。如果编辑器要求 OAuth 登录,按它的流程走一遍,再重启编辑器让 MCP 配置生效。
还有一个隐蔽的坑:FAISS 索引维度不匹配。如果你先索引用了一个嵌入模型,后来换了模型但没重建索引,search 时会报维度错误。解决办法是每次换嵌入模型都重新调用 index_docs。代码里 _index 是全局变量,重启 Server 会清空,所以每次启动都要重新索引,这也是为什么 Client 启动时先调 index_docs。
排查时建议打开日志。在 Server 里加一行 print 到 stderr,Client 侧能看到子进程输出。MCP 的 stdio 传输会把 Server 的标准错误透传出来,对定位问题很有帮助。
6. 语义一致 CTA:把这条链路用到你的真实项目
跑通 demo 只是开始。真正有价值的是把这条链路接到你自己的数据上。你可以把 docs/knowledge.txt 换成从 PDF、Markdown、数据库导出的文本,按段落切分后每行一条。切分粒度建议 200 到 500 字,太短检索不到上下文,太长会稀释相关性。
如果文档量大,IndexFlatL2 会变慢,可以换成 IndexIVFFlat 做近似检索,先聚类再搜索,速度提升明显。代码改动很小:
quantizer = faiss.IndexFlatL2(dim) index = faiss.IndexIVFFlat(quantizer, dim, 100) index.train(embeddings) index.add(embeddings) index.nlist = 100检索时设置 index.nprobe = 10,平衡速度和召回。
想把 RAG 接到长期编码或 Agent 工作流里,可以用 Coding Plan 统一管理模型调用和额度,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。它适合需要持续调用模型、又不想每次手动配 Key 的场景。
接入文档里有更完整的 MCP 配置示例和模型列表,遇到本文没覆盖的报错可以去查: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,需要新建或轮换 Key 时用得上。
最后说一个我踩过的坑:MCP Server 里不要直接连生产数据库。检索工具应该只读、只查索引,写操作走单独的通道。这样即使客户端被滥用,也不会污染你的数据源。把索引构建做成离线任务,定时重建,线上 Server 只负责查询,稳定性和安全性都好很多。