
1. Day7 的坑FAISS 重启后向量全丢RAG 检索层得换思路AI Agent 30天速成走到第 7 天前面几天我们一直在用 FAISS 做向量检索。FAISS 是什么一句话说清它是 Meta 开源的高性能向量相似度搜索库能做什么在百万级向量里毫秒级找最近邻。适合谁适合做本地 RAG 检索层、推荐召回、去重匹配的开发者。但 Day3 那套 FAISS 内存索引有个致命问题——程序一重启向量全没了而且它没有元数据过滤、没有内置去重检索出来的片段经常高度重复。今天要解决的就是这个给 RAG 搭一套可复现的向量检索层用 Chroma 做持久化本地库用 FAISS 做对照配合 SigLIP 生成图文统一嵌入最后对比两种索引在召回质量和速度上的差异。整个链路里嵌入模型怎么调、Key 怎么管我用 TaoToken 统一走一个 API 通道省得每个模型单独配一套环境变量。先说清楚今天要交付什么一份可复制的依赖清单、一个 Chroma 持久化建库脚本、一个 FAISS 对照脚本、一组检索验证命令以及一组测试问题来验证召回是否稳定。你跟着敲完本地会多出一个./chroma_mm_db目录重启 Python 进程后向量还在这是 FAISS 内存索引做不到的。为什么第 7 天才换 Chroma因为前 6 天我们在打基础Day1 环境、Day2 文本嵌入、Day3 FAISS 内存检索、Day4 分块、Day5 工具网关、Day6 ReAct 循环。到了今天检索层要能落地、能过滤、能去重才撑得起后面多模态 Agent 的完整链路。Chroma 就是为 LLM RAG 设计的开箱即用不用单独部署服务。我试过把 FAISS 和 Chroma 放在同一个测试集上跑最直观的差别不是速度而是「重启后还能不能查」。FAISS 每次启动都要重新add一遍向量Chroma 直接get_or_create_collection就能接着用。这个差别在学习和调试阶段特别省时间。2. TaoToken 前置统一 Key 与 API 通道嵌入模型不再各配一套在动手写建库脚本之前先把调用通道理顺。RAG 检索层要调嵌入模型多模态还要调 SigLIP 这类图文编码模型如果每个模型都去单独申请 Key、单独配 Base URL环境变量会乱成一团。TaoToken 在这里的作用是提供一个统一的 API 通道把 Key 和 Base URL 收敛成一套配置嵌入模型、对话模型都从这里走。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置的时候别把查询串带进去。你需要先拿到 Key。进入控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建完复制那串sk-开头的字符串后面所有脚本都读同一个环境变量。如果你还没决定用哪个模型可以先去模型对话页面试一下嵌入模型和对话模型的返回地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 确认通道通了再写代码。这里有个关键点Chroma 本身不绑定嵌入模型它允许你传入自定义的 embedding function。所以我们的策略是——文本嵌入走 TaoToken 的 APISigLIP 图文嵌入走本地模型因为 SigLIP 权重可以离线缓存不依赖网络。两条路并行互不干扰。配置环境变量Linux/macOS 用export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiWindows PowerShell 用$env:TAOTOKEN_API_KEYsk-你的Key $env:TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你用 Claude Code 或者 Cline 这类工具做辅助编码接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL、Key、Model ID 三件套的填法。Coding Plan 适合长期写 Agent 代码的场景入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 如果你打算把 30 天速成坚持到底可以了解一下。注意Key 只放在环境变量里不要硬编码进脚本更不要提交到 Git。后面所有代码都通过os.environ读取。3. 可复制配置依赖清单 Chroma 持久化建库脚本先把依赖装齐。新建一个requirements-day7.txtchromadb0.4.24 faiss-cpu1.7.4 torch2.2.0 transformers4.38.0 pillow10.2.0 numpy1.26.4 openai1.14.0 scikit-learn1.4.0安装命令pip install -r requirements-day7.txtopenai这个包是用来走 TaoToken 的 OpenAI 兼容接口调嵌入模型的faiss-cpu是 CPU 版够学习用。装完先验证一下 Chroma 能不能 importpython -c import chromadb; print(chromadb.__version__)接下来是核心脚本chroma_store.py。它做三件事用 TaoToken 的 API 生成文本嵌入、用 Chroma 持久化到本地磁盘、支持元数据过滤和 MMR 去重检索。import os import chromadb from openai import OpenAI from typing import List, Dict # 走 TaoToken 统一通道 client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) EMBED_MODEL text-embedding-3-small def text_to_vector(text: str) - List[float]: resp client.embeddings.create(modelEMBED_MODEL, inputtext) return resp.data[0].embedding class ChromaStore: def __init__(self, persist_path./chroma_mm_db, coll_namemm_kb): # 本地持久化客户端重启不丢 self.client chromadb.PersistentClient(pathpersist_path) self.collection self.client.get_or_create_collection( namecoll_name, metadata{hnsw:space: cosine}, ) def add_text(self, text_list: List[str], source: str 文档): ids [ftxt_{source}_{i} for i in range(len(text_list))] vecs [text_to_vector(t) for t in text_list] metas [{type: text, source: source} for _ in text_list] self.collection.add( embeddingsvecs, documentstext_list, metadatasmetas, idsids, ) def search(self, query: str, search_type: str all, top_k: int 3, use_mmr: bool False) - List[Dict]: query_vec text_to_vector(query) where_filter {} if search_type text: where_filter {type: text} elif search_type image: where_filter {type: image} if use_mmr: res self.collection.max_marginal_relevance_search( query_embeddings[query_vec], n_resultstop_k, wherewhere_filter or None, ) docs res[0] if isinstance(res, list) else res[documents][0] metas res[1] if isinstance(res, list) else res[metadatas][0] return [{content: d, meta: m} for d, m in zip(docs, metas)] res self.collection.query( query_embeddings[query_vec], n_resultstop_k, wherewhere_filter or None, ) output [] for doc, meta, dist in zip( res[documents][0], res[metadatas][0], res[distances][0] ): output.append({ content: doc, meta: meta, distance: round(float(dist), 4), }) return output注意where_filter or None这个写法Chroma 在where{}空字典时会报错传None才是「不过滤」。这是踩过的坑之一后面排障章节会再提。如果你用 Cline 的 MCP 模式或者 Codex 的auth.json来辅助写代码记得把三件套填全Base URL 填https://taotoken.net/apiKey 填你的sk-串Model ID 填你实际用的嵌入或对话模型名。缺一个都会连不上。4. 验证请求建库、检索、对比 FAISS 速度先跑一个最小验证确认 TaoToken 通道和 Chroma 持久化都正常。新建test_chroma.pyfrom chroma_store import ChromaStore store ChromaStore() store.add_text([ 多模态RAG使用SigLIP实现图文统一向量检索, Chroma支持本地持久化重启不丢失向量数据, ReAct Agent可以自主调用图文检索工具, FAISS是内存索引适合高性能最近邻搜索, ], sourceDay7学习文档) ret store.search(什么是多模态RAG, search_typetext, top_k2) for item in ret: print(item[distance], item[content])运行python test_chroma.py预期输出类似0.2134 多模态RAG使用SigLIP实现图文统一向量检索 0.3871 ReAct Agent可以自主调用图文检索工具距离越小越相关0.21说明第一条命中很准。然后关键一步——重启验证。再开一个 Python 进程直接查不重新 addpython -c from chroma_store import ChromaStore s ChromaStore() print(s.collection.count()) print(s.search(Chroma 持久化, top_k1)) 如果count()返回 4说明向量已经落在./chroma_mm_db目录里重启不丢。这就是 Chroma 相对 FAISS 内存索引最实在的优势。接着做 FAISS 对照。新建faiss_compare.pyimport time import faiss import numpy as np from chroma_store import text_to_vector texts [ 多模态RAG使用SigLIP实现图文统一向量检索, Chroma支持本地持久化重启不丢失向量数据, ReAct Agent可以自主调用图文检索工具, FAISS是内存索引适合高性能最近邻搜索, ] * 25 # 扩到100条做速度对比 vecs np.array([text_to_vector(t) for t in texts], dtypefloat32) dim vecs.shape[1] index faiss.IndexFlatIP(dim) faiss.normalize_L2(vecs) index.add(vecs) q np.array([text_to_vector(什么是多模态RAG)], dtypefloat32) faiss.normalize_L2(q) t0 time.time() D, I index.search(q, 3) t1 time.time() print(fFAISS 检索耗时: {(t1-t0)*1000:.2f} ms) print(命中索引:, I[0])跑下来你会发现100 条向量时 FAISS 检索在 0.1ms 级别Chroma 因为要读磁盘、走 HNSW 索引大概在几毫秒到十几毫秒。但注意——这个对比不公平的地方在于FAISS 的向量是刚add进去的内存态Chroma 是持久化后从磁盘加载的。真实场景里FAISS 每次启动都要重新算一遍嵌入那部分耗时才是大头。用一组测试问题验证召回稳定性我准备了 5 个questions [ Chroma 怎么持久化, FAISS 和 Chroma 区别, SigLIP 是做什么的, ReAct 怎么调用工具, 多模态检索怎么过滤图片, ] for q in questions: r store.search(q, top_k1) print(q, -, r[0][content][:20], r[0][distance])如果每个问题的 top1 距离都稳定在 0.4 以下说明召回是稳的。如果某个问题距离突然飙到 0.8 以上多半是分块或嵌入模型的问题不是 Chroma 的锅。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来对。你在跑上面脚本时大概率会撞上下面几个。报错一401 Unauthorized。完整信息通常是openai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}。原因就两个Key 没读到或者 Key 复制时带了空格。先确认环境变量echo $TAOTOKEN_API_KEY如果输出为空说明当前 shell 没加载。Windows 下用echo $env:TAOTOKEN_API_KEY。如果输出有值但还报 401检查是不是把https://taotoken.net/api写成了带 UTM 的完整链接——Base URL 只填到/api不要带查询串。报错二local proxy failed / connection error。完整信息类似openai.APIConnectionError: Connection error.或local proxy failed。这类多半是本地网络环境或代理配置干扰了请求。检查你的HTTP_PROXY/HTTPS_PROXY环境变量如果设了但指向一个不可用的地址请求会直接失败。清掉再试unset HTTP_PROXY HTTPS_PROXY报错三reading choices / KeyError choices。完整信息是KeyError: choices或reading choices。这通常发生在你把对话模型的返回结构套用到嵌入接口上。嵌入接口返回的是resp.data[0].embedding没有choices字段。检查你的text_to_vector是不是误用了chat.completions.create。嵌入必须用client.embeddings.create。报错四OAuth / 认证流程卡住。如果你用 Claude Code 或类似工具接入报OAuth相关错误说明工具在走它自己的登录流程而不是读你的 API Key。这时候要检查工具的配置文件把认证方式切成 API Key 模式Base URL 填https://taotoken.net/apiKey 填sk-串Model ID 填实际模型名。三件套缺一不可。Claude Code 的接入细节在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 有说明。报错五Chroma where 过滤报错。完整信息类似Expected where to have exactly one operator。原因就是前面说的空字典问题。把where{}改成whereNone或者只在有过滤条件时才传where参数。报错六SigLIP 模型下载慢。如果你加了本地 SigLIP 做图文嵌入AutoProcessor.from_pretrained会去拉权重网络不好会卡住。解决办法是提前把权重缓存到本地用cache_dir指定路径或者先在有网环境下载好再离线加载。学习阶段也可以先用纯文本嵌入跑通链路图文部分后面再补。排查顺序建议先echo环境变量确认 Key 和 Base URL再单独跑一个最小embeddings.create请求确认通道最后才跑完整建库脚本。这样能把问题定位在「通道」还是「代码」上。6. 语义一致 CTA把检索层接进你的 Agent 链路检索层跑通之后下一步就是把它接进 Day6 的 ReAct 工具网关。你可以在网关里注册一个multimodal_search工具参数包括query、search_typeall/text/image、top_k、use_mmr底层直接调ChromaStore.search。这样 Agent 在 Thought 阶段判断需要查知识库时Action 就调这个工具Observation 拿到带距离分数的片段再决定是否继续检索。如果你在接入过程中卡在 Key 或通道配置上直接去 API Keys 页面重新生成一个地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 配合接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 对照着填。想先验证模型返回是否符合预期去模型对话页面发一条测试请求最快地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。如果你打算把 30 天速成里的 Agent 代码长期维护下去Coding Plan 的入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。最后留一个可复现的检查点重启进程后collection.count()不变、5 个测试问题的 top1 距离稳定、FAISS 对照脚本能跑出毫秒级耗时。这三条都过了Day7 的检索层就算落地了。明天可以把 SigLIP 图文嵌入补上让图片也能进同一个 Chroma 集合用typeimage元数据过滤实现以文搜图。