1. 从零构建向量数据库检索器:为什么嵌入与检索要拆开管
向量数据库检索器,说白了就是一套“把文字变成向量、存进库、再按相似度捞回来”的流水线。它要解决的核心问题是:当你有几百上千份文档时,怎么在毫秒级找到和问题最相关的几段内容。适合谁?适合正在做 RAG 应用、知识库问答、文档语义搜索的开发者,尤其是那些已经跑通 demo、但被嵌入模型调用和向量库配置搅得头大的同学。
我见过太多人卡在同一个地方:嵌入模型走一个通道,向量库走另一个通道,两边的 Key、Base URL、模型名各管各的。一旦要换嵌入模型,或者想从 OpenAI 切到别的供应商,整条链路就得重配一遍。更麻烦的是,嵌入调用和检索调用分散在不同文件里,排查问题时根本不知道是嵌入没生成对,还是索引没写进去。
所以这篇教程的思路很明确:用 TaoToken 统一 Key 和 API 通道,把嵌入生成和向量检索这两件事的调用入口收敛到一处。向量库这边我用 Pinecone 做参照,编排层用 LlamaIndex,因为这两个组合在社区里最常被拿来搭检索器。你跟着走完,会得到一份可复制的环境变量配置、一段能直接跑的写入加查询代码,以及一套遇到报错时能自己定位的排查方法。
整条链路分四步:先配好统一通道,再生成嵌入并写入索引,然后做一次 top-k 相似度查询,最后把结果解析成节点集放进自定义检索器。每一步我都会给出实际命令和参数,你照着改就能用。重点不是背 API,而是理解“嵌入调用”和“向量查询”这两个动作怎么通过同一个 Base URL 串起来。
2. TaoToken 前置:统一 Key 与 Base URL 的接入准备
在动手写检索器之前,先把调用通道理顺。TaoToken 在这里扮演的角色是统一的 API 入口:嵌入模型调用和后续可能的对话模型调用,都走同一个 Base URL 和同一把 Key。这样做的好处是,你的代码里只需要维护一份凭证,换模型时改一个 Model ID 就行,不用满项目找散落的 api_key。
你需要准备两样东西:一把 TaoToken 的 API Key,以及确认要用的嵌入模型 ID。Key 在控制台的 API Keys 页面创建,地址是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建后复制出来,后面写进环境变量。模型 ID 方面,嵌入场景常用的是文本嵌入类模型,具体名称以你控制台里可用的为准,写代码时填进embed_model参数。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL 的完整写法和各语言示例。核心就一句:所有请求的 Base URL 统一用https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为api_base或base_url填进去。
环境变量我建议这样组织,放在项目根目录的.env里:
# TaoToken 统一通道 TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api # 嵌入模型 ID(以控制台实际可用为准) EMBED_MODEL_ID=你的嵌入模型ID # Pinecone 配置 PINECONE_API_KEY=你的pinecone key PINECONE_ENV=us-west1-gcp PINECONE_INDEX=quickstart这里有个容易踩的坑:Pinecone 的 Key 和 TaoToken 的 Key 是两套东西,别混。TaoToken 管的是模型调用通道,Pinecone 管的是向量存储。两者通过 LlamaIndex 的PineconeVectorStore桥接起来,各用各的凭证。
如果你还没装依赖,先把这几个包装上:
pip install llama-index-core llama-index-vector-stores-pinecone llama-index-embeddings-openai pinecone python-dotenv pymupdf装完之后,在代码开头加载环境变量:
import os from dotenv import load_dotenv load_dotenv() api_key = os.environ["TAOTOKEN_API_KEY"] base_url = os.environ["TAOTOKEN_BASE_URL"] embed_model_id = os.environ["EMBED_MODEL_ID"]到这一步,通道就准备好了。接下来所有模型调用都复用base_url和api_key,不再出现第二个入口。
3. 可复制配置:嵌入模型与 Pinecone 索引的对接片段
这一节给你可以直接复制的配置片段,分三块:嵌入模型初始化、Pinecone 索引创建、LlamaIndex 的 VectorStore 包装。三块拼起来,就是检索器的存储底座。
先看嵌入模型。LlamaIndex 里用OpenAIEmbedding类来调嵌入接口,关键是把它指向 TaoToken 的 Base URL:
from llama_index.embeddings.openai import OpenAIEmbedding embed_model = OpenAIEmbedding( model=embed_model_id, api_key=api_key, api_base=base_url, )注意参数名是api_base,不是base_url,这是 LlamaIndex 这个类的写法。填错会直接报连接错误。model填你在控制台确认的嵌入模型 ID。
接着创建 Pinecone 索引。维度要和嵌入模型输出维度一致,常见嵌入模型是 1536 维,如果你用的模型维度不同,把dimension改掉:
import pinecone pinecone.init( api_key=os.environ["PINECONE_API_KEY"], environment=os.environ["PINECONE_ENV"], ) index_name = os.environ["PINECONE_INDEX"] if index_name not in pinecone.list_indexes(): pinecone.create_index( name=index_name, dimension=1536, metric="euclidean", ) pinecone_index = pinecone.Index(index_name)这里metric用euclidean还是cosine,取决于你的嵌入模型训练时用的距离度量。大多数文本嵌入用 cosine 更稳,但如果你参照的示例是 euclidean,保持一致也能跑通。关键是写入和查询用同一个 metric,别中途换。
然后用 LlamaIndex 的包装器把 Pinecone 索引接进来:
from llama_index.vector_stores.pinecone import PineconeVectorStore vector_store = PineconeVectorStore(pinecone_index=pinecone_index)到这里,存储层就绪。接下来是写入。加载文档、切分、生成嵌入、写进 Pinecone,这一串用VectorStoreIndex.from_documents一次完成:
from llama_index.core import VectorStoreIndex, StorageContext from llama_index.core.node_parser import SentenceSplitter from llama_index.readers.file import PyMuPDFReader loader = PyMuPDFReader() documents = loader.load(file_path="./data/your_doc.pdf") splitter = SentenceSplitter(chunk_size=1024) storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex.from_documents( documents, transformations=[splitter], storage_context=storage_context, embed_model=embed_model, )这段代码执行时,LlamaIndex 会调用embed_model把每个 chunk 转成向量,再通过vector_store写进 Pinecone。嵌入调用走的是 TaoToken 的 Base URL,向量写入走的是 Pinecone 自己的通道,两者互不干扰。
如果你想把配置写成 JSON 或 TOML 方便复用,可以这样组织:
{ "taotoken": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY" }, "embedding": { "model_id": "你的嵌入模型ID", "dimension": 1536 }, "pinecone": { "index_name": "quickstart", "metric": "euclidean", "environment": "us-west1-gcp" } }这份配置的好处是,嵌入模型和向量库的参数集中在一处,换模型时只改model_id和dimension,代码逻辑不动。
4. 验证请求:一次写入加一次查询确认检索可复现
配置写完,必须做一次端到端验证:写入一批数据,然后查一次,看结果是否稳定复现。这一步不做,后面出问题你都不知道是嵌入没生成还是索引没写进去。
先确认写入成功。写入后查一下索引的统计信息:
stats = pinecone_index.describe_index_stats() print(stats)如果total_vector_count大于 0,说明向量已经进去了。如果还是 0,回去检查from_documents那步有没有报错,以及embed_model是否真的调通了。
接着做查询。查询分两步:先把问题转成查询嵌入,再用这个嵌入去 Pinecone 做 top-k 相似度检索。
query_str = "你的问题文本" query_embedding = embed_model.get_query_embedding(query_str) from llama_index.core.vector_stores import VectorStoreQuery vector_store_query = VectorStoreQuery( query_embedding=query_embedding, similarity_top_k=2, mode="default", ) query_result = vector_store.query(vector_store_query)similarity_top_k=2表示返回最相似的 2 个节点。mode="default"是密集向量检索,如果你后面想试稀疏或混合模式,改这个参数即可。
拿到结果后,把节点和分数解析出来:
from llama_index.core.schema import NodeWithScore from typing import Optional nodes_with_scores = [] for idx, node in enumerate(query_result.nodes): score: Optional[float] = ( query_result.similarities[idx] if query_result.similarities else None ) nodes_with_scores.append(NodeWithScore(node=node, score=score)) for node in nodes_with_scores: print(f"Score: {node.score:.4f}") print(node.node.get_content()[:300]) print("---")跑通后你会看到两段文本和对应的相似度分数。验证可复现的关键动作是:同一个query_str连续查两次,返回的节点顺序和分数应该一致。如果两次结果不同,检查是不是索引里混入了重复数据,或者similarity_top_k设得太大导致边界抖动。
把这段逻辑封装成自定义检索器,方便后面接查询引擎:
from llama_index.core import QueryBundle from llama_index.core.retrievers import BaseRetriever from typing import Any, List class PineconeRetriever(BaseRetriever): def __init__(self, vector_store, embed_model, query_mode="default", similarity_top_k=2): self._vector_store = vector_store self._embed_model = embed_model self._query_mode = query_mode self._similarity_top_k = similarity_top_k super().__init__() def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: query_embedding = self._embed_model.get_query_embedding(query_bundle.query_str) vector_store_query = VectorStoreQuery( query_embedding=query_embedding, similarity_top_k=self._similarity_top_k, mode=self._query_mode, ) query_result = self._vector_store.query(vector_store_query) nodes_with_scores = [] for idx, node in enumerate(query_result.nodes): score = query_result.similarities[idx] if query_result.similarities else None nodes_with_scores.append(NodeWithScore(node=node, score=score)) return nodes_with_scores retriever = PineconeRetriever(vector_store, embed_model) retrieved_nodes = retriever.retrieve(query_str)到这里,一次完整的写入加查询就验证完了。检索结果可复现,说明嵌入通道和向量库通道都通了。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
跑这条链路时,报错基本集中在几个地方。我按实际遇到的频率排一下,每个都给出定位方法。
401 错误:最常见的是 Key 没配对。如果你看到401 Unauthorized,先确认TAOTOKEN_API_KEY有没有正确加载。用print(api_key[:8])看前几位,确认不是空值或占位符。另一个可能是把 Pinecone 的 Key 填到了 TaoToken 的位置,或者反过来。两套 Key 分开检查,别混用。
local proxy failed:这个报错通常出现在请求发不出去的时候。检查api_base是不是写成了https://taotoken.net/api,有没有多写斜杠或漏写。另外确认你的运行环境能正常访问外网接口,本地防火墙或公司网络策略有时会拦截。如果是在容器里跑,检查容器的网络配置。
reading choices 相关报错:如果你在解析响应时看到类似reading 'choices'的报错,说明返回结构和你预期的不一致。这通常是因为嵌入接口和对话接口的返回格式不同,你把对话的解析逻辑用到了嵌入上。嵌入调用走get_query_embedding,返回的是向量列表,不要去读choices字段。
OAuth 或认证类报错:如果你看到 OAuth 相关提示,检查是不是误用了需要额外认证的端点。TaoToken 的 API 调用用 Key 认证即可,Base URL 填对、Key 填对,不需要额外的 OAuth 流程。
Pinecone 维度不匹配:报错信息里会提到 dimension mismatch。回去检查create_index时的dimension和嵌入模型实际输出维度是否一致。1536 是常见值,但如果你换的模型输出 768 或 1024,索引维度必须同步改。改维度需要重建索引,旧数据要重新写入。
查询结果为空:如果query_result.nodes是空的,先确认索引里确实有数据(describe_index_stats看 count),再确认查询嵌入和写入嵌入用的是同一个模型。用不同模型生成的向量不在同一空间,相似度计算没有意义。
排查时有个通用思路:把链路拆成“嵌入生成”和“向量查询”两段,分别验证。先单独调embed_model.get_query_embedding("test"),看能不能返回向量;再单独用这个向量查 Pinecone,看能不能返回节点。哪段断了,问题就在哪段。
6. 语义一致 CTA:把统一通道用进你的检索器项目
走到这里,你已经有了一个能跑通的向量数据库检索器:嵌入生成走 TaoToken 统一通道,向量存储走 Pinecone,编排用 LlamaIndex。整条链路的 Key 和 Base URL 收敛在一处,换模型时只改一个 Model ID。
如果你接下来要把它接进实际项目,几个方向可以继续:一是把PineconeRetriever接到RetrieverQueryEngine,加上对话模型做完整 RAG 问答;二是试不同的mode参数,对比密集、稀疏、混合检索的效果;三是把嵌入模型换成别的,验证统一通道下切换成本有多低。
需要继续拿 Key 或看接入细节的,API Keys 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先验证模型对话效果的,可以去 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 试一下。如果你打算长期做编码和 Agent 类项目,Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
最后留一个实用技巧:把embed_model和vector_store的初始化封装成一个工厂函数,传入配置字典就返回组装好的检索器。这样你在不同项目里复用同一套通道配置,不用每次重写初始化逻辑。检索器这东西,配置对了就稳,配置错了就到处报错,把入口收敛好,后面省很多事。