公司动态

向量数据库与RAG实战:从Embedding到AI知识库的完整链路

📅 2026/9/1 6:48:52
向量数据库与RAG实战:从Embedding到AI知识库的完整链路
很多人第一次接触“AI 知识库”这个概念时都会有一个困惑我明明已经把文档喂给大模型了为什么它还是答不出来我自己在最初做客服问答机器人时也踩过同样的坑。把 PDF、Word、网页内容一股脑丢给大模型然后问它“如果手机屏幕碎了保修政策是什么”模型要么答非所问要么直接编一个不存在的售后条款。问题不在大模型而在我们少做了一层关键工作把文本转换成计算机能够进行“语义匹配”的形态也就是向量。而负责管理、检索这些向量的基础设施就是今天几乎所有 AI 知识库应用都绕不开的向量数据库。这篇文章会从传统搜索的痛点讲起把 Embedding、语义搜索、向量数据库、RAG 这四个概念串成一条完整的技术链路。最后会用手写 Python 代码的方式跑通一个“文档切块 → 向量化 → 入库 → 语义检索 → 大模型回答”的最小 RAG 示例。全文偏实战适合正在做知识库问答、AI 客服、智能助手或者想理解 RAG 底层原理的开发者阅读。1. 为什么要关注向量数据库先看看传统搜索的痛点很多人以为向量数据库是一种“更快的关系型数据库”或者是一个“支持模糊查询的搜索引擎”。这个理解不算全对。向量数据库真正解决的是传统的关键词匹配在语义理解上的无力和失效。举一个真实场景。假设客服系统里有一条知识“碎屏保服务自签收之日起一年内因意外跌落导致的屏幕碎裂可免费维修一次。”如果用户提问是“我的手机不小心摔了屏幕裂了还能保修吗”用传统关系数据库的LIKE %屏幕%能查到但如果你用“误触导致的内屏显示异常算不算外观损坏”去查关键词和知识条目几乎没有重合传统搜索基本束手无策。更麻烦的是用户的表达习惯。同一个问题有人会说“申请退款”有人会说“不想要了把钱退给我”还有人会说“七天无理由退货怎么操作”。三者表达不同语义一致。传统搜索比如 Elasticsearch 的分词 BM25能处理的是“关键词重合”和“词法变体”它不理解“不想要了”和“退货”是同一个意思。要让系统理解这一点就得让文本先变成一个“语义坐标”再通过数学计算比较两条文本的接近程度。这就是 Embedding 和向量检索存在的意义。所以我的判断是向量数据库不是来取代传统数据库或搜索引擎的它补上的是“语义召回”这一层能力。如果你正在做的项目需要处理非结构化文本并且用户的问题无法用固定关键词覆盖那么向量数据库几乎是必选项。2. 基础概念Embedding、向量与相似度计算2.1 Embedding 是什么Embedding 的中文叫“嵌入”在 NLP 里可以通俗地理解成把一段文字映射成一个固定长度的数字数组这个数组就是“向量”。比如“手机屏幕碎了”经过 Embedding 模型可能得到这样一个 768 维的向量[0.0234, -0.1189, 0.0067, ..., 0.0456] # 长度为768这个数组本身没有直观含义但它有一个重要特性语义相近的两句话转换出来的向量在空间中距离更近语义无关的文本向量距离更远。这就像把词汇放到一张多维地图上“苹果”和“香蕉”靠近“苹果”和“汽车”远离。Embedding 模型做的是把整个句子放在这样一张“语义地图”上。给文本做 Embedding 的工具叫 Embedding 模型。中文场景里目前比较常用的是 BGE 系列比如BAAI/bge-small-zh-v1.5、BAAI/bge-large-zh-v1.5以及更新的BAAI/bge-m3。BGE 系列在中文语义匹配上表现较好也是很多开源项目默认使用的模型之一。实际项目中也可以直接使用云厂商提供的 Embedding API比如阿里云百炼平台上的文本向量服务好处是部署成本低、无需本地 GPU适合快速验证想法。2.2 向量之间的距离怎么算有了向量之后判断“语义接近”就变成了数学问题。常用的有三种度量方式度量方式特点常用场景余弦相似度只关心方向不受向量长度影响取值范围 [-1, 1] 或 [0, 1]文本语义匹配最常用内积点积与向量长度相关适合归一化向量向量已归一化时效率更高欧氏距离度量空间中的绝对距离越小越相似图像、部分推荐场景绝大多数文本向量库默认使用余弦相似度。因为文本向量经过归一化后内积和余弦相似度在数值上是等价的所以很多向量数据库内部会做归一化处理来提高计算效率。2.3 向量数据库是什么向量数据库是一种专门针对“向量数据”设计存储和检索能力的数据库。它和普通数据库最大的区别在于它支持高效的近似最近邻搜索ANN能够在百万甚至亿级向量中快速找到与查询向量最接近的前 N 条结果。数据库内部通常使用 HNSW、IVF 等索引算法把“全量暴力对比”变成“多层路的近似搜索”。对开发者来说你不需要理解全部算法细节只需要知道向量库让你能够在海量文本中毫秒级找到语义上最相关的几条内容。3. 语义搜索与传统搜索的区别为了更清楚地说明问题我把传统搜索和语义搜索的差异整理成一张表。对比维度传统搜索关键词/BM25语义搜索向量检索匹配原理分词后做词项匹配通过 Embedding 计算语义距离能否处理同义词部分依赖分词和同义词扩展可以语义相近即能召回口语化表达较弱用户说法偏离关键词就失效较强更关注“意思”而不是“字词”错别字容忍度低错字可能导致匹配失败相对高因为向量语义仍可能相近索引结构倒排索引向量索引HNSW、IVF 等可解释性高能讲清楚命中了哪些词低只能看到排名结果适用场景日志检索、精确匹配、结构化过滤知识库问答、推荐、去重、意图识别需要承认的是语义搜索不是万能药。它的一个明显问题是精确词匹配能力弱。比如用户想搜商品编码“SKU-10086”向量检索可能因为语义太“抽象”而召回错误。因此生产环境里的做法通常不是“只用向量检索”而是“向量检索 关键词检索”混合再通过 Rerank 模型对结果排序。这个后面会讲。4. RAGAI 知识库的核心工作流RAG 的全称是 Retrieval-Augmented Generation检索增强生成。它的核心思想很简单大模型回答问题前先从你自己的知识库里检索出相关内容再把这些内容拼进提示词让模型基于给定的资料回答。这样模型就不再只依赖训练数据里的“记忆”而是能够使用你提供的实时、私有、垂直领域的内容。为什么必须用 RAG两个关键原因。第一大模型的知识截止时间是固定的。你问它“今年最新的售后政策”它不会知道。第二大模型会“一本正经地胡说八道”也就是幻觉。当它不知道答案时它倾向于编造。RAG 通过把答案限制在检索到的资料范围内显著降低了幻觉风险。RAG 的典型流程可以拆成这样文档加载读取用户提供的 PDF、Word、Markdown、网页等格式的文档。文本切块Chunking把长文本拆成多个合适大小的片段。这一步对检索效果影响巨大。向量化Embedding用 Embedding 模型把每个文本块转换成向量。向量入库Ingestion把向量和原始文本、元数据写入向量数据库。检索Retrieval用户提问时把问题也转成向量在库里做相似度搜索得到 top K 相关文本块。重排Reranking可选步骤。对初选结果做精细化排序提升相关性。生成Generation把检索结果和用户问题拼成 Prompt交给大模型生成答案。这里面有一个非常容易忽视的点RAG 的效果上限很大程度由“检索质量”决定而不是由大模型决定的。如果检索阶段没有把真正相关的段落找出来后面大模型再强也没用。另外最近业界开始讨论 Agentic RAG即让 AI Agent 自主决定“什么时候检索、检索几次、检索什么”。它可以拆解复杂问题做多轮检索甚至调用外部工具。这是 RAG 的发展方向但本质上还是建立在今天要讲的这套基础能力之上。新手先跑通基础 RAG再考虑 Agentic 不迟。5. 向量数据库选型Chroma、Milvus、pgvector、Qdrant 怎么选市面上的向量数据库不少很多新手第一步就卡在选型上。我的建议是不要一开始就追求分布式和高性能先看你的数据规模和部署环境。数据库定位优点适合场景Chroma轻量级嵌入式向量库安装简单、API 友好、适合学习与原型个人项目、快速验证、小规模知识库Milvus分布式向量数据库支持百亿级向量、功能全面、CNCF 项目生产环境、大规模数据、高并发检索QdrantRust 编写的向量数据库性能好、过滤功能强、部署灵活生产环境、推荐系统、需要复杂过滤Weaviate向量数据库支持图式对象内置多种模块、与 GraphQL 集成好知识图谱与向量结合的场景pgvectorPostgreSQL 扩展不引入新组件直接使用 SQL已有 PostgreSQL 的团队数据量百万内Elasticsearch搜索引擎 向量能力既有全文检索又有向量检索生态成熟需要混合搜索的团队从开源社区的普及度看Milvus 和 Chroma 的使用者最多。Chroma 的优势在于“零部署”一个pip install就能跑Milvus 的优势在于生产可用性和生态完整性适合规模化建设 AI 知识库。如果你所在团队已经有 PostgreSQL并且数据量在百万级以内直接用 pgvector 是最省事的方案不需要额外维护一套基础设施。选型的核心判断标准有三个数据规模、并发量、运维成本。原型阶段选 Chroma/LanceDB生产阶段选 Milvus/Qdrant/Weaviate已经有 SQL 体系就选 pgvector。云上部署也可以直接购买向量数据库托管服务省去运维负担。6. 环境准备与基础配置本文的示例代码基于 Python 3.9。核心依赖有两个chromadb和sentence-transformers。sentence-transformers用于加载 Embedding 模型chromadb用于向量存储和检索。安装命令pip install chromadb sentence-transformers如果你本机没有 GPU也可以运行。BGE small 模型在 CPU 上做 Embedding 的速度可以接受适合学习。如果你希望用云厂商的 Embedding API则可以跳过sentence-transformers改为直接调用 HTTP 接口或 SDK。为了让本地示例能跑通我下面以开源模型 BGE 为例。再准备一份示例文档我假设它是一段客服 FAQ 文本内容如下实际使用时替换成你自己的知识文档碎屏保服务自签收之日起一年内因意外跌落、碰撞、挤压导致屏幕碎裂 可享受一次免费维修服务。需要用户提供购买凭证和碎屏照片。 如果屏幕仅有划痕没有碎裂不在碎屏保服务范围内。 碎屏保服务不包含电池、主板等内部硬件损坏的维修。下面就用这段文本做大模型知识库的“原始数据”。7. 完整示例代码实现从文本切块到 RAG 问答7.1 第一步文本切块切块是 RAG 里最容易被忽略的环节。切得太大检索时容易把无关内容混进来切得太小单个块信息量不足。比较简单的策略是固定长度加重叠窗口。# 文件路径rag_demo/chunking.py def split_text(text: str, chunk_size: int 150, overlap: int 30) - list[str]: 按固定长度切分文本相邻切块之间保留部分重叠避免语义被拦腰截断。 chunk_size每个文本块的最大字符数 overlap两个相邻块之间重叠的字符数 chunks [] start 0 text_len len(text) while start text_len: end start chunk_size chunk text[start:end] chunks.append(chunk) if end text_len: break start end - overlap return chunks if __name__ __main__: sample 碎屏保服务自签收之日起一年内因意外跌落、碰撞、挤压导致屏幕碎裂 可享受一次免费维修服务。需要用户提供购买凭证和碎屏照片。 如果屏幕仅有划痕没有碎裂不在碎屏保服务范围内。 碎屏保服务不包含电池、主板等内部硬件损坏的维修。 result split_text(sample) print(f切块数量: {len(result)}) for i, chunk in enumerate(result): print(f--- chunk {i} ---) print(chunk)固定长度切块适合快速跑通流程但生产环境有更进阶的策略比如按 Markdown 标题切、按段落切、父子块切分等这些在后面的最佳实践里会进一步说明。7.2 第二步创建向量数据库并写入数据使用 Chroma 的PersistentClient可以把数据持久化到本地磁盘下次启动不需要重新 Embedding。# 文件路径rag_demo/ingest.py import chromadb from chromadb.utils import embedding_functions # 1. 选择 Embedding 模型 sentence_transformer_ef embedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-small-zh-v1.5 ) # 2. 创建持久化客户端数据保存在 ./chroma_data 目录 client chromadb.PersistentClient(path./chroma_data) # 3. 创建或获取集合Collection并指定向量距离算法为 cosine collection client.get_or_create_collection( namefaq_kb, embedding_functionsentence_transformer_ef, metadata{hnsw:space: cosine} ) # 4. 准备文档数据。这里先用一段完整文档实际项目中通常是切块后的列表。 documents [ 碎屏保服务自签收之日起一年内因意外跌落、碰撞、挤压导致屏幕碎裂 可享受一次免费维修服务。需要用户提供购买凭证和碎屏照片。 如果屏幕仅有划痕没有碎裂不在碎屏保服务范围内。 碎屏保服务不包含电池、主板等内部硬件损坏的维修。, 七天无理由退货自签收之日起七日内如果商品未经使用且包装配件齐全 可以申请无理由退货。定制类商品、已激活的数码产品不适用七天无理由退货。 退货产生的运费由买家承担商品质量问题由卖家承担。, ] metadatas [ {source: 售后政策.md, category: 售后}, {source: 购物指南.md, category: 退货}, ] ids [fchunk_{i} for i in range(len(documents))] # 5. 写入向量数据库。collection.add 会先调用 embedding_function 生成向量再存储。 collection.add( documentsdocuments, metadatasmetadatas, idsids ) print(f集合中已存储 {collection.count()} 条文档)这里有几个细节值得解释get_or_create_collection如果集合已经存在不会重复创建避免多次运行脚本产生重复数据。embedding_function写入和查询必须使用同一个 Embedding 模型否则向量空间不一致检索结果会完全失真。metadata{hnsw:space: cosine}指定使用余弦相似度算法。如果修改这个参数需要删除旧集合重新创建。7.3 第三步语义搜索数据写入完成后最核心的验证环节就是查询。我们可以用自然语言问一句库里没有出现完全相同关键词的问题看看能否正确召回对应片段。# 文件路径rag_demo/search.py import chromadb from chromadb.utils import embedding_functions sentence_transformer_ef embedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-small-zh-v1.5 ) client chromadb.PersistentClient(path./chroma_data) collection client.get_collection( namefaq_kb, embedding_functionsentence_transformer_ef ) query_text 手机不小心摔坏了屏幕裂了能免费修吗 results collection.query( query_texts[query_text], n_results2 ) print(问题, query_text) print(- * 50) for i in range(len(results[ids][0])): doc_id results[ids][0][i] distance results[distances][0][i] if distances in results else N/A document results[documents][0][i] metadata results[metadatas][0][i] print(f排名 {i 1}: 文档ID{doc_id}, 相似度距离{distance}) print(f元数据: {metadata}) print(f内容片段: {document}) print(- * 50)如果一切正常你会看到“屏幕碎了”这条问题成功命中“碎屏保服务”这段内容而不是“七天无理由退货”。这就是语义搜索和关键词搜索最大的差别它靠“意思”找人而不是靠“字眼”找人。7.4 第四步接入大模型完成 RAG 问答检索只是拿到了“参考资料”最后一步是把参考内容和用户问题组合成 Prompt交给大模型生成答案。下面的代码使用 OpenAI 兼容接口实际项目可以替换为通义千问、DeepSeek、智谱、Ollama 本地模型等任意支持相同协议的服务。# 文件路径rag_demo/rag_qa.py import chromadb from chromadb.utils import embedding_functions from openai import OpenAI # 这里替换为你的大模型服务配置 # 如果用 Ollama 本地模型可以设置 base_urlhttp://localhost:11434/v1 LLM_CLIENT OpenAI( api_keyYOUR_API_KEY, base_urlYOUR_LLM_ENDPOINT ) LLM_MODEL your-model-name sentence_transformer_ef embedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-small-zh-v1.5 ) client chromadb.PersistentClient(path./chroma_data) collection client.get_collection( namefaq_kb, embedding_functionsentence_transformer_ef ) def rag_answer(question: str) - str: # 1. 检索 results collection.query( query_texts[question], n_results2 ) context_blocks results[documents][0] # 2. 构造 Prompt context \n\n.join(f【资料{i1}】\n{block} for i, block in enumerate(context_blocks)) prompt f你是某电商平台的客服助手。请根据以下知识库资料回答用户问题。 如果资料中没有提到相关信息请直接说明“资料中未找到相关内容”不要编造。 知识库资料 {context} 用户问题 {question} 请你用简洁、友好的中文回答。 # 3. 调用大模型 response LLM_CLIENT.chat.completions.create( modelLLM_MODEL, messages[ {role: system, content: 你是严谨的客服问答助手。}, {role: user, content: prompt} ], temperature0.2 ) return response.choices[0].message.content if __name__ __main__: question 我的手机摔了屏幕裂了能不能免费维修 answer rag_answer(question) print(问题, question) print(回答, answer)这个示例虽然短却已经具备了一个生产级 RAG 系统的最小骨架检索 提示词约束 生成。你后续要做的优化基本都是在“检索”和“提示词”这两层继续深化。8. 运行结果与效果验证为了让整个过程更有底气我来描述一下验证方法。先运行切块脚本python rag_demo/chunking.py预期输出是切块后的文本片段列表。如果chunk_size和overlap设置得当最后一个切块不会为空字符串。再运行入库脚本python rag_demo/ingest.py看到控制台输出集合中已存储 2 条文档说明写库成功。如果重复运行该脚本collection.count()会变成 4、6、8……这是因为 Chroma 不检查内容是否重复只按id去重。这也是为什么要用带有业务含义的 ID比如docs_xxx_chunk_1而不是简单的chunk_0。然后运行检索脚本python rag_demo/search.py判断成功的关键信号是第一条命中的文档必须是“碎屏保服务”而不是“七天无理由退货”。如果结果反了或者相似度距离特别大建议按下面的排查顺序检查Embedding 模型是否一致写入和查询是否用了同一个模型。hnsw:space参数是否设置正确。文档内容是否过短或切块过碎导致语义信息不足。查询语句是否太模糊换个更直接的说法再试。最后运行 RAG 问答脚本python rag_demo/rag_qa.py如果最终回答能准确提到“一年内”“意外跌落导致屏幕碎裂可免费维修一次”这类关键信息说明整条链路已经跑通。9. 常见问题与排查思路我在实际项目里总结了一些高频问题列成表供你参考。问题现象可能原因排查方式解决方案入库后collection.count()不断增加使用了自动生成的递增 ID导致重复写入检查写入逻辑中 ID 的生成方式使用内容 Hash 或业务标识作为 ID实现幂等写入查询结果语义相关性差Embedding 模型不适合你的语言或领域把查询和文档分别打印成向量检查距离分布换成中文表现更好的模型如bge-large-zh-v1.5或bge-m3写入慢使用 CPU 对长文本做 Embedding观察 CPU 占用率和耗时日志批量写入、换 GPU、或使用云上 Embedding API检索到相似但片段不完整切块策略不合理语义被截断打印切块内容检查边界上下文增加重叠窗口或改用段落级切分大模型答案依然在编造检索到的资料里其实没有答案但模型强行回答检查 RAG 输出中实际传递了哪些上下文在 Prompt 中明确声明“未找到就回答不知道”或设置更低温度向量数据库文件损坏或版本升级后读不了数据Chroma 版本更新导致持久化格式不兼容查看启动日志中的兼容性报错保存原始文档必要时重新入库升级前先备份chroma_data目录删除或修改某条文档后旧数据仍在向量库更新时需要按 ID 执行 upsert/delete确认使用的是update、upsert还是add根据业务场景显式删除旧 ID再写入新向量其中最需要提醒的是第一项。很多新手把collection.add理解成“数据库 insert”会重复写入同一批数据。RAG 的入库链路应该设计成可重复执行的 ETL 任务先按文档来源和切块序号生成稳定的 ID再执行 upsert。这样才能保证知识库更新是安全的、可回滚的。10. 最佳实践与工程建议跑通最小示例之后如果你要把它接到真实业务里下面这些建议会很有用。10.1 切块策略要跟着文档结构走固定长度切块适合“快速跑通”但真实文档往往有结构标题、段落、列表、表格。粗暴截断可能会把一个问题拆到两个块里。更好的策略是先识别 Markdown/HTML 的标题层级按标题切出“语义完整”的段落如果段落还是太长再在段落内部按句子边界切小。Chroma、LlamaIndex 等生态里都有对应的文本切分器不必自己重复造轮子。10.2 用元数据过滤减少噪音向量检索适合做“语义召回”但很多时候你需要配合精确筛选。比如知识库里有多个业务线的文档回答问题前先确定用户属于哪个业务线然后用where条件把候选范围限定在对应分类里。Chroma 的collection.query支持where和where_document参数。实际项目里元数据过滤可以显著提高召回精度。10.3 混合搜索 Rerank 是提升效果的关键纯向量检索在“同义改写”上有优势在“精确匹配”上有短板。生产级 RAG 通常会把向量检索结果和传统关键词检索结果合并再用一个 Rerank 模型对合并结果重新打分。Rerank 模型直接对“查询-文档对”做相关性判断效果通常比单纯计算向量距离更准。代价是多一次模型推理延迟和成本都会增加。10.4 数据更新要设计成幂等任务知识库不是一成不变的。文档会更新、下线、过期。建议每次入库前用内容 Hash 判断文档是否变化如果 Hash 相同直接跳过。如果文档内容变化了先删除旧 ID 对应的向量再写入新向量。这样既避免重复数据也避免旧版本残留。10.5 权限与安全边界如果知识库里包含内部资料要特别注意两点一是向量数据库本身的访问权限不要暴露在公网二是检索结果中可能包含敏感信息RAG 服务对外输出前要做内容过滤。涉及删除或批量更新操作时提前备份数据和确认脚本效果坚持最小权限原则。10.6 重视日志与可观测性RAG 链路较长加载、切块、向量化、检索、重排、生成。任何一个环节出问题最终答案都会异常。建议在每个环节都输出结构化日志至少记录处理了哪些文档、生成了多少切块、检索到了哪些片段、最终 Prompt 是什么。这样一旦线上问答效果不佳你能快速定位是“没检索到”还是“大模型没理解”。11. 总结与后续学习方向这篇文章从传统搜索的局限出发把 Embedding、语义搜索、向量数据库和 RAG 这条链路完整梳理了一遍。你可以把“向量数据库”理解成 AI 知识库的“记忆仓库”Embedding 是让文字变成可计算向量的“翻译器”语义搜索是“找相关记忆”的过程RAG 则是让大模型“带着参考资料回答问题”的完整工作流。建议你接下来做两件事。第一把文章里的最小 RAG 示例跑通替换成自己的文档亲手感受一下“语义检索”和“关键词搜索”的结果差异。第二选择一个方向继续深入如果你对数据工程感兴趣可以研究切块、元数据、混合搜索如果你对算法感兴趣可以研究 HNSW 索引和向量量化如果你想做生产应用可以研究 Milvus 部署、Rerank 模型和 Agentic RAG。值得记住的是向量数据库和 RAG 并不是某一款具体产品而是一套解决“让 AI 使用私有知识”的通用架构。把基础链路理解透无论是换数据库、换 Embedding 模型还是换大模型你都能快速迁移。收藏这篇文章等真正搭建 AI 知识库时按着这个思路一步步来比到处拼凑资料要高效得多。