1. 从零跑通 RAG:文档切分、向量化到 FAISS 检索的最小闭环
RAG(检索增强生成)说白了就是给生成式 AI 配一个「随身资料库」:你问问题,它先去你的文档里翻出最相关的几段,再把这些段落塞给模型,让模型基于真实材料回答,而不是凭记忆瞎编。对刚接触 RAG 的开发者来说,最容易卡住的地方不是算法,而是链路太长——切分、向量化、建索引、检索、拼 prompt、调模型,每一步都要接一个服务,光 Key 就要管好几套。这篇就带你用 TaoToken 的统一 Key 把这条链路串起来,从一份本地文档出发,跑通「提问 → 检索 → 生成」的完整最小闭环。
适合谁看:会一点 Python、想亲手搭一个能跑起来的 RAG demo、但还没搞清各环节怎么衔接的开发者。全程本地可复现,向量库用 FAISS(Facebook AI Similarity Search),嵌入和生成都走 TaoToken 的兼容接口,你只需要配一次环境变量。
先说清楚整体数据流,心里有张图后面就不容易乱:
- 把文档切成小块(chunk),每块保留一点上下文;
- 用嵌入模型把每块转成向量,存进 FAISS 索引;
- 用户提问时,把问题也转成向量,在 FAISS 里找最相似的 top-k 块;
- 把这些块拼成上下文,连同问题一起发给生成模型;
- 模型输出基于检索内容的答案。
这套流程里,第 2 步和第 5 步都要调模型,如果分别去不同平台申请 Key,配置会散落在好几个地方。用 TaoToken 的好处是嵌入和对话共用一个 Key、一个 Base URL,环境变量只维护一份,换模型也只改一个 Model ID。下面从环境准备开始,一步步来。
2. TaoToken 前置准备:统一 Key 与 Base URL 配置
在动手写代码前,先把「钥匙」配好。TaoToken 提供 OpenAI 兼容的接口,意味着你原来用 openai 库写的代码,基本只需要改 Base URL 和 Key 两处。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台生成 API Key。
拿到 Key 之后,不要硬编码进脚本,用环境变量管理,这样脚本可以提交到 Git 而不泄露密钥。Linux/macOS 下在终端执行:
export TAOTOKEN_API_KEY="sk-你的实际Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Windows PowerShell 用:
$env:TAOTOKEN_API_KEY="sk-你的实际Key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"注意 Base URL 是https://taotoken.net/api,不要多加/v1之类的后缀,OpenAI SDK 会自己拼接路径。如果你用的是某些需要完整路径的客户端,再按它的文档补全,但用官方 openai 库时保持这个值即可。
接下来装依赖。RAG 最小闭环需要四类库:向量检索(faiss-cpu)、文本嵌入(sentence-transformers 可选,但我们直接用 API 嵌入更统一)、OpenAI 兼容客户端(openai)、以及文档读取(pypdf 用于 PDF,纯文本可省):
pip install faiss-cpu openai numpy pypdf这里有个选择点:嵌入模型是用本地 sentence-transformers 还是走 API?本地模型不花钱、离线可用,但首次下载模型体积大、不同机器环境容易出问题;走 API 则统一由 TaoToken 提供,和生成模型共用一套鉴权,代码更干净。本文选 API 方案,保持「一个 Key 打通全链路」的主题。
配置检查:写一个最小脚本确认 Key 和 Base URL 生效,能列出或调用模型即可。这一步别跳过,很多后续报错其实都是环境变量没生效导致的。
import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) resp = client.embeddings.create( model="text-embedding-3-small", input="连通性测试", ) print("向量维度:", len(resp.data[0].embedding))如果打印出向量维度(比如 1536),说明嵌入通道通了。生成通道可以顺手用一次 chat 调用验证,但放到第 4 节一起做也行。这里先记住两个 Model ID:嵌入用text-embedding-3-small,生成用gpt-4o-mini(或你账号下可用的对话模型)。具体可用模型以控制台模型列表为准,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
提示:环境变量在关闭终端后会失效。想持久化,Linux/macOS 写进
~/.bashrc或~/.zshrc,Windows 用系统环境变量面板设置。生产环境建议用 .env 文件配合 python-dotenv 加载,并加入 .gitignore。
3. 可复制配置:FAISS 索引构建脚本与 settings 片段
这一节是全文的核心,给你一份能直接跑的脚本。先准备一份测试文档,比如knowledge.txt,里面放几段你熟悉的内容,方便验证检索是否命中。文档质量直接决定 RAG 上限,这点后面排障还会提。
第一步是切分。切分策略很关键:切太碎,单块语义不完整;切太大,检索精度下降还浪费 token。最小闭环用「按段落 + 固定长度兜底」就够了。下面脚本把文档按空行分段,再对超长段落做滑窗切分,块之间保留重叠(overlap),避免答案正好被切断。
import os import numpy as np import faiss from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) EMBED_MODEL = "text-embedding-3-small" CHUNK_SIZE = 300 # 每块最大字符数 CHUNK_OVERLAP = 50 # 相邻块重叠字符数 def load_text(path): with open(path, "r", encoding="utf-8") as f: return f.read() def split_text(text, size=CHUNK_SIZE, overlap=CHUNK_OVERLAP): paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()] chunks = [] for para in paragraphs: if len(para) <= size: chunks.append(para) else: start = 0 while start < len(para): end = start + size chunks.append(para[start:end]) start = end - overlap return chunks def embed(texts): resp = client.embeddings.create(model=EMBED_MODEL, input=texts) vectors = [d.embedding for d in resp.data] return np.array(vectors, dtype="float32") def build_index(chunks): vectors = embed(chunks) faiss.normalize_L2(vectors) # 归一化后用内积等价余弦相似度 index = faiss.IndexFlatIP(vectors.shape[1]) index.add(vectors) return index if __name__ == "__main__": text = load_text("knowledge.txt") chunks = split_text(text) print(f"切分得到 {len(chunks)} 个块") index = build_index(chunks) faiss.write_index(index, "rag.index") with open("rag_chunks.txt", "w", encoding="utf-8") as f: f.write("\n===\n".join(chunks)) print("索引已保存到 rag.index")几个参数说明,方便你按自己文档调:
| 参数 | 作用 | 调优方向 |
|---|---|---|
| CHUNK_SIZE | 单块最大字符数 | 中文 200–500 较稳,太长检索变钝 |
| CHUNK_OVERLAP | 相邻块重叠 | 取块大小的 10%–20%,防答案被切断 |
| IndexFlatIP | 内积索引 | 向量归一化后等价余弦相似度 |
| top-k | 检索返回块数 | 一般 3–5,太多会稀释上下文 |
如果你更习惯用配置文件管理,可以放一个settings.json,把模型和路径集中起来,脚本读它即可:
{ "base_url": "https://taotoken.net/api", "embed_model": "text-embedding-3-small", "chat_model": "gpt-4o-mini", "index_path": "rag.index", "chunks_path": "rag_chunks.txt", "top_k": 3 }注意这里 base_url 和 Key 分开管理:Key 走环境变量,其余非敏感配置走 JSON。这样团队协作时,别人 clone 下来只要配自己的 Key 就能跑。索引文件rag.index和块文本rag_chunks.txt要成对保存,检索时用索引拿到下标,再去块文本里取原文,两者顺序必须一致——这是新手最容易踩的坑之一,后面会专门讲。
4. 验证请求:一次端到端问答与预期输出
索引建好后,写检索 + 生成的查询脚本。逻辑是:把问题向量化,在 FAISS 里找 top-k 相似块,拼成上下文,再调对话模型。这里把生成模型也走 TaoToken,和嵌入共用同一个 client。
import os import numpy as np import faiss from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) EMBED_MODEL = "text-embedding-3-small" CHAT_MODEL = "gpt-4o-mini" TOP_K = 3 def load_chunks(path): with open(path, "r", encoding="utf-8") as f: return f.read().split("\n===\n") def retrieve(question, index, chunks, k=TOP_K): q_vec = client.embeddings.create( model=EMBED_MODEL, input=[question] ).data[0].embedding q = np.array([q_vec], dtype="float32") faiss.normalize_L2(q) scores, idx = index.search(q, k) return [(chunks[i], float(s)) for i, s in zip(idx[0], scores[0])] def answer(question, contexts): context_text = "\n\n".join(c for c, _ in contexts) prompt = ( "请仅根据下面的资料回答问题,资料中没有的信息不要编造。\n\n" f"资料:\n{context_text}\n\n问题:{question}" ) resp = client.chat.completions.create( model=CHAT_MODEL, messages=[{"role": "user", "content": prompt}], temperature=0.2, ) return resp.choices[0].message.content if __name__ == "__main__": index = faiss.read_index("rag.index") chunks = load_chunks("rag_chunks.txt") question = "RAG 相比直接用大模型回答有什么好处?" contexts = retrieve(question, index, chunks) print("检索到的片段及相似度:") for c, s in contexts: print(f"[{s:.3f}] {c[:60]}...") print("\n模型回答:") print(answer(question, contexts))预期输出分两部分。第一部分是检索结果,你会看到 3 个片段和它们的相似度分数,分数越接近 1 越相关,通常命中的片段分数在 0.3–0.8 之间(取决于文档和问题措辞)。第二部分是模型回答,它应该只基于你文档里的内容作答,如果文档里没有相关信息,理想情况下它会说「资料中未提及」,而不是硬编一个答案——这正是 RAG 减少幻觉的价值所在。
验证时建议做两个对照实验:一是问一个文档里明确写了的问题,看答案是否准确引用;二是问一个文档里完全没有的问题,看模型是否老实说不知道。第二个实验能帮你判断 prompt 里的约束是否生效。如果模型开始编,就把「资料中没有的信息不要编造」这句加强,或者降低 temperature。
注意:检索质量差时,先别急着怪模型。八成问题出在切分和嵌入上——块太大导致语义混杂,或问题措辞和文档用词差异太大导致向量不匹配。可以先把 top-k 调大看能否召回正确片段,再回头优化切分。
5. 本篇常见报错排查:401、local proxy failed 与 reading choices
跑这条链路,报错基本集中在鉴权、网络和响应解析三类。下面按真实遇到的错误对照排查。
401 Unauthorized / invalid api key:最常见。先确认环境变量真的生效了,在脚本里print(os.environ.get("TAOTOKEN_API_KEY"))看是不是 None 或空串。如果是在 IDE 里跑,注意 IDE 可能没继承你终端里 export 的变量,需要在运行配置里单独设置。另一个原因是 Key 复制时带了空格或换行,strip 一下。还有种情况是 Base URL 写错,比如误写成带/v1的路径导致鉴权端点不对,回到https://taotoken.net/api这个值。
local proxy failed / connection error:这类报错通常是本机网络或代理配置干扰。检查是否有残留的 HTTP_PROXY / HTTPS_PROXY 环境变量指向了不可用的地址,用echo $HTTPS_PROXY确认,必要时 unset 掉。另外确认 Base URL 拼写无误、没有多余斜杠。如果公司网络有出口限制,联系网络管理员放行对应域名即可,不要自行配置来路不明的转发工具。
Error reading choices / 'NoneType' object is not subscriptable:这个报错说明resp.choices是空的或结构不对。常见原因是模型名写错,接口返回了错误对象而不是正常响应,但代码直接去取choices[0]。排查方法是在调用后先打印完整响应:print(resp),看返回体里是 error 还是正常结构。把 CHAT_MODEL 换成控制台里确认可用的模型 ID 通常能解决。另外流式和非流式响应结构不同,如果你开了 stream=True 却按非流式解析,也会出这个错。
OAuth / 鉴权相关报错:如果你用的是某些命令行编码工具(比如 Claude Code 类客户端),它可能走的是 OAuth 或特定的 auth.json 配置,而不是简单的 API Key。这类工具接入时要把三件套配全:Base URL 填https://taotoken.net/api、API Key 填你的 TaoToken Key、Model ID 填控制台确认的模型名。三者缺一或 Model ID 写错,都会报鉴权或模型不存在。用 Cline、CC Switch 这类插件时同理,MCP 配置里也要保证这三项一致。
检索结果全是无关片段:不是报错但很常见。检查rag_chunks.txt和rag.index是否同一次构建产生的——如果你改了文档重新切分,却忘了重建索引,下标就会错位,检索出来的片段和实际内容对不上。养成「改文档就重建索引」的习惯,或者给索引文件加时间戳。
嵌入维度不匹配:如果换了嵌入模型,向量维度会变,旧索引直接加载会报维度错误。换模型必须重建索引,不能复用。
排查顺序建议:先确认环境变量 → 再确认 Base URL 和 Model ID → 打印原始响应看错误体 → 最后才怀疑代码逻辑。大部分问题在前两步就能定位。
6. 把链路用起来:从 demo 到可用系统的下一步
跑通最小闭环后,你手里其实已经有了一个能用的 RAG 骨架。接下来往哪个方向加,取决于你的场景。如果文档量大,IndexFlatIP这种暴力检索会变慢,可以换成IndexIVFFlat做近似检索,用一点召回率换速度;如果检索总是不准,试试混合检索——向量检索搭配 BM25 关键词检索,两者结果融合,对专有名词和缩写特别有效。
生成侧可以做的优化:把 top-k 的片段按相似度排序后拼进 prompt,并在每段前标注来源编号,让模型回答时能引用「根据资料 2」。这样既提升可信度,也方便你回溯是哪段材料支撑了答案。temperature 保持低值(0.1–0.3),RAG 场景不需要模型发挥创意。
工程化方面,把嵌入和索引构建做成离线批处理,查询走在线服务,两者解耦。索引可以定期重建,或者用支持增量写入的向量库替代 FAISS。Key 管理上,TaoToken 的统一入口让你在切换嵌入或生成模型时只改配置不改代码,这对快速试不同模型组合很友好。需要长期跑编码或 Agent 类任务时,可以了解下 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ),按用量规划更省心;单纯想先对话验证模型效果,用模型对话页(https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite )快速试;Key 管理在控制台(https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ),生成和轮换都在 API Keys 页面(https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite )。
最后给个实用建议:先用你自己的真实文档跑一遍,别用示例文本。真实文档里的表格、代码块、页眉页脚会暴露切分策略的问题,早发现早调整。我试过拿一份带大量表格的技术文档直接按字符切,结果表格被拦腰截断,检索出来的片段完全没法用,后来改成按标题层级切分才正常。RAG 的效果上限由数据质量决定,模型和检索只是放大器——这句话在你调优卡壳时值得反复想。