ARTICLE DETAIL

资讯详情

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

大模型笔记(四):向量库/检索召回的不同方式-Chroma/FAISS 配 TaoToken 统一 Key 的 config.toml 骨架

大模型笔记(四):向量库/检索召回的不同方式-Chroma/FAISS 配 TaoToken 统一 Key 的 config.toml 骨架 1. 从一次召回翻车说起Chroma 和 FAISS 到底差在哪向量库这个词听起来很玄其实它干的事很朴素把文本变成一串数字向量存起来然后你拿一个问题也变成一串数字去里面找“数字长得最像”的那几条文本。检索召回就是这一步“找最像”的过程。Chroma 和 FAISS 是本地跑 RAG 时最常被拿来对比的两个选择一个像自带收纳盒的储物柜一个像只给你索引图纸的纯引擎。我最初做本地知识库时用 Chroma 跑通了 demo换成 FAISS 后召回结果却对不上排查半天才发现是两者在“距离度量”和“持久化方式”上的默认行为不同。这篇就围绕这个差异展开先讲清楚 Chroma 与 FAISS 在检索召回链路里的定位区别再给出用 TaoToken 统一 Key 的config.toml骨架最后用同一批文档分别灌进两个库跑一次召回对比让你在本地就能复现。适合谁看已经会用 LangChain 做文档切分、想搞清楚向量库选型的人手里有多个模型 Key、想统一走一个 API 通道的人以及被as_retriever的search_type参数绕晕的人。全文命令和配置都可直接复制环境是 Python 3.10 LangChain 0.2 以上。2. TaoToken 前置统一 Key 与 API 通道在讲向量库之前得先把模型调用这条线理顺。因为检索召回链路里有两个地方要调模型一是 embedding 模型把文本转成向量二是召回后可能还要用 LLM 做答案生成。如果每个环节都单独配 Key配置文件会散得到处都是。TaoToken 在这里的角色是统一入口你拿一个 Key通过它的 API 通道去调不同的模型embedding 和 chat 都能走同一个base_url。这样config.toml里只需要维护一份凭证换模型时改model字段就行不用动 Key。你需要先拿到自己的 API Key入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite拿到 Key 后API 的基础地址是https://taotoken.net/api注意这个地址不带查询参数直接作为base_url使用。如果你用的是 OpenAI 兼容的 SDK通常需要在末尾补/v1具体以接入文档为准https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意Key 只存在本地配置文件或环境变量里不要写进会提交到 Git 的代码。下面config.toml里的 Key 字段建议用环境变量占位运行时再注入。3. 可复制配置config.toml 骨架与两个向量库的接入这一节是全文的核心。我先把config.toml的骨架给出来它把模型通道和向量库参数分开管理Chroma 和 FAISS 各自一个 section切换时只改active_store一个值。3.1 config.toml 完整骨架# config.toml # 模型通道统一走 TaoToken [llm] api_key ${TAOTOKEN_API_KEY} # 从环境变量读取 base_url https://taotoken.net/api/v1 chat_model deepseek-chat embedding_model bge-base-zh-v1.5 # 也可换成通道支持的其它 embedding # 文本切分参数 [splitter] chunk_size 500 chunk_overlap 80 separator \n\n # 向量库选择chroma 或 faiss [store] active_store chroma # Chroma 配置自带持久化目录 [store.chroma] persist_directory ./db/chroma collection_name kb_demo distance cosine # 余弦相似度 # FAISS 配置索引文件 映射文件 [store.faiss] index_path ./db/faiss/index.faiss docstore_path ./db/faiss/index.pkl distance euclidean # FAISS 默认欧氏距离 # 检索召回参数 [retriever] search_type similarity # similarity / mmr / similarity_score_threshold top_k 5 score_threshold 0.5 fetch_k 20 # mmr 时的候选池大小这个骨架的关键设计是[llm]只认一个base_urlembedding 和 chat 共用[store]用active_store做开关两个子 section 各自描述自己的持久化路径。下面分别看两个库怎么读这份配置。3.2 读取配置并构建 embedding# common.py import os import tomllib from langchain_openai import OpenAIEmbeddings, ChatOpenAI def load_config(path: str config.toml) - dict: with open(path, rb) as f: cfg tomllib.load(f) # 注入环境变量 cfg[llm][api_key] os.environ[TAOTOKEN_API_KEY] return cfg def build_embeddings(cfg: dict) - OpenAIEmbeddings: return OpenAIEmbeddings( openai_api_keycfg[llm][api_key], openai_api_basecfg[llm][base_url], modelcfg[llm][embedding_model], ) def build_llm(cfg: dict) - ChatOpenAI: return ChatOpenAI( openai_api_keycfg[llm][api_key], openai_api_basecfg[llm][base_url], modelcfg[llm][chat_model], temperature0, )tomllib是 Python 3.11 起内置的3.10 可以用tomli替代读法一样。embedding 和 chat 都指向同一个base_url这就是统一 Key 的意义换模型只改model字段。3.3 Chroma 接入自带持久化Chroma 的特点是“开箱即用”它自己管理存储目录你不需要关心索引文件长什么样。# store_chroma.py from langchain_chroma import Chroma from common import load_config, build_embeddings def build_chroma(splits): cfg load_config() embeddings build_embeddings(cfg) c cfg[store][chroma] vectorstore Chroma.from_documents( documentssplits, embeddingembeddings, persist_directoryc[persist_directory], collection_namec[collection_name], collection_metadata{hnsw:space: c[distance]}, ) return vectorstore def load_chroma(): cfg load_config() embeddings build_embeddings(cfg) c cfg[store][chroma] return Chroma( persist_directoryc[persist_directory], collection_namec[collection_name], embedding_functionembeddings, )注意collection_metadata{hnsw:space: cosine}这一行它决定了 Chroma 用余弦相似度。如果你不写默认是 L2 距离召回排序会和预期不一致——这是我踩过的坑之一。3.4 FAISS 接入索引与文档分开存FAISS 只负责向量索引文档原文和 ID 映射要另外存。LangChain 的封装帮你把这两件事绑在一起但落盘时是两个文件。# store_faiss.py import os from langchain_community.vectorstores import FAISS from common import load_config, build_embeddings def build_faiss(splits): cfg load_config() embeddings build_embeddings(cfg) f cfg[store][faiss] os.makedirs(os.path.dirname(f[index_path]), exist_okTrue) vectorstore FAISS.from_documents(splits, embeddingembeddings) vectorstore.save_local( folder_pathos.path.dirname(f[index_path]), index_nameindex, ) return vectorstore def load_faiss(): cfg load_config() embeddings build_embeddings(cfg) f cfg[store][faiss] return FAISS.load_local( folder_pathos.path.dirname(f[index_path]), embeddingsembeddings, index_nameindex, allow_dangerous_deserializationTrue, # 本地可信文件才开 )allow_dangerous_deserializationTrue是因为 FAISS 的 docstore 用 pickle 存加载时会反序列化。只在你确认文件来源可信时开启生产环境要谨慎。3.5 统一检索器构建两个库都通过as_retriever暴露检索接口参数名一致所以可以写一个工厂函数。# retriever_factory.py from common import load_config from store_chroma import load_chroma from store_faiss import load_faiss def get_retriever(): cfg load_config() r cfg[retriever] store cfg[store][active_store] if store chroma: vs load_chroma() elif store faiss: vs load_faiss() else: raise ValueError(funknown store: {store}) kwargs {k: r[top_k]} if r[search_type] mmr: kwargs[fetch_k] r[fetch_k] if r[search_type] similarity_score_threshold: kwargs[score_threshold] r[score_threshold] return vs.as_retriever(search_typer[search_type], search_kwargskwargs)到这里切换向量库只需要改config.toml里的active_store代码一行不动。4. 验证请求同一批文档跑两种召回配置写完了得验证它真的能召回。我准备了三段关于向量库的短文本分别灌进 Chroma 和 FAISS然后用同一个问题去查看返回结果和分数。4.1 准备文档并入库# ingest.py from langchain_core.documents import Document from store_chroma import build_chroma from store_faiss import build_faiss docs [ Document(page_contentChroma 是一个自带持久化的向量库适合快速搭建本地知识库。), Document(page_contentFAISS 是 Facebook 开源的相似度搜索库只负责索引文档要另外存。), Document(page_content检索召回的质量取决于 embedding 模型和切分策略而不只是向量库本身。), ] build_chroma(docs) build_faiss(docs) print(ingest done)运行export TAOTOKEN_API_KEY你的Key python ingest.py预期输出ingest done同时./db/chroma和./db/faiss目录下会出现文件。4.2 召回对比脚本# query_compare.py from retriever_factory import get_retriever question FAISS 和 Chroma 有什么区别 retriever get_retriever() results retriever.invoke(question) for i, doc in enumerate(results, 1): print(f[{i}] {doc.page_content})把config.toml的active_store改成chroma跑一次再改成faiss跑一次。两次都能返回三条文档但排序可能不同Chroma 配了 cosineFAISS 默认 euclidean对短文本来说差异不大但文档一多、向量维度一高距离度量的影响就会显现。4.3 换检索策略再验一次把search_type改成mmrfetch_k设成 3再跑一次。MMR 会在相关性和多样性之间做平衡返回的结果不会全是同一主题的重复内容。这一步能验证你的config.toml里fetch_k参数确实被读进去了。[retriever] search_type mmr top_k 2 fetch_k 3如果返回条数变成 2说明top_k生效如果结果之间差异变大说明 MMR 在起作用。5. 本篇常见错排查5.1 Chroma 召回结果和 FAISS 对不上最常见的原因是距离度量不一致。Chroma 默认 L2FAISS 默认也是 L2但如果你在 Chroma 里配了hnsw:spacecosine而 FAISS 没配两边排序就会不同。解决办法是在config.toml里显式声明各自的distance并确保 embedding 做了归一化余弦相似度要求向量归一化。5.2 FAISS load_local 报反序列化错误报错信息类似ValueError: The de-serialization relies on loading a pickle file。这是 LangChain 的安全限制需要在load_local里加allow_dangerous_deserializationTrue。但要注意这个参数只对你自己生成的索引文件开不要加载来源不明的 pkl。5.3 embedding 调用返回 401 或 404先检查base_url是否带了/v1。TaoToken 的 API 根地址是https://taotoken.net/apiOpenAI 兼容 SDK 通常需要https://taotoken.net/api/v1。如果 401检查环境变量TAOTOKEN_API_KEY是否真的注入到了进程里可以用echo $TAOTOKEN_API_KEY确认。5.4 as_retriever 的 k 参数不生效search_kwargs里的k会被search_type影响。比如similarity_score_threshold模式下如果所有文档分数都低于阈值返回可能是空的看起来像k没生效。先把score_threshold调低到 0.2 试试确认链路通了再往上调。5.5 切换 active_store 后仍读旧库Chroma 和 FAISS 的持久化路径不同但如果你改了persist_directory却没删旧目录Chroma 可能会读到旧 collection。排查方法是打印vectorstore._collection.count()看文档数是否和你灌入的一致。6. 继续往下走把召回接进对话链路检索召回跑通后下一步通常是把它接到 LLM 上做 RAG 问答。这时候你可以用同一个config.toml里的[llm]段构建 chat 模型把 retriever 返回的文档拼进 prompt。想先单独验证模型通道是否通可以直接在模型对话页面试一条https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite如果你打算长期跑编码类或 Agent 类任务反复调 embedding 和 chat可以考虑 Coding Plan它更适合高频调用的场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite接入过程中遇到 Key 或通道配置问题接入文档里有各语言的示例https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite最后留一个实用建议Chroma 和 FAISS 的选型不用纠结太久。本地小规模知识库、想少写持久化代码用 Chroma需要精细控制索引类型、或者向量规模上到百万级用 FAISS。真正影响召回质量的往往是切分策略和 embedding 模型而不是这两个库本身。把config.toml的active_store留成开关两边都跑一遍对比比看十篇评测都管用。
返回列表