1. 为什么企业都在折腾 RAG 知识库
这两年但凡跟企业数字化沾边的团队,几乎都绕不开一个词:RAG。全称 Retrieval-Augmented Generation,检索增强生成。说人话就是——大模型本身不知道你公司内部的规章制度、产品手册、历史工单,那就别硬让它瞎编,先帮它把相关资料找出来,再让它照着资料回答。
我最早接触 RAG 是给一个做工业设备的客户做售后问答。他们有几万份 PDF 维修手册,客服每天被问“报警代码 E023 怎么处理”这种问题问到崩溃。直接上大模型?它连这个型号都没见过,张口就是胡编。后来用 RAG 把手册切片入库,问题一进来先检索相关段落,再喂给大模型组织语言,准确率从“基本没法用”直接拉到“客服敢直接复制粘贴回复”。
这就是 RAG 的核心价值:让大模型基于你的私有数据说话,而不是基于它训练时记住的互联网公开数据。企业知识库、智能客服、内部文档问答、合同审查辅助,全是这个套路。
但真动手搭的时候,坑就来了。最典型的就是检索质量差——明明库里有答案,就是搜不出来。这时候向量数据库的选型和用法就成了关键。我试过本地用 FAISS、Chroma,小数据量还行,一旦上到几十万条切片,再叠加多租户、权限过滤、高并发,本地方案就开始力不从心。所以这篇就围绕基于腾讯云向量数据库搭建企业级 RAG 知识库这条路线,把完整流程、参数选择、踩坑经验一次讲透。
适合谁看?如果你会用 Python,懂一点大模型 API 调用,想给公司搭一个能真正落地的知识库问答系统,那这篇就是给你写的。完全没接触过向量数据库也没关系,我会把每个环节为什么这么做讲清楚。
2. 整体架构设计与技术选型思路
2.1 一套 RAG 系统到底由哪几块拼成
先把骨架搭清楚,不然后面写代码就是盲人摸象。一个完整的 RAG 知识库,拆开来看是这么几个环节:
- 文档接入层:把 PDF、Word、Markdown、网页、数据库记录等各种来源的原始资料收进来。
- 解析与切片层:把文档解析成纯文本,再切成适合检索的小块(chunk)。
- 向量化层:用 Embedding 模型把每个 chunk 转成向量。
- 向量存储与检索层:把向量存进向量数据库,查询时做相似度检索。这一层就是腾讯云向量数据库的主场。
- 生成层:把检索到的内容拼进 Prompt,交给大模型生成最终回答。
- 应用层:对外提供 API 或界面,处理多轮对话、权限、引用溯源等。
这六层里,最容易出问题、也最影响最终效果的是切片层和检索层。很多人以为换个更强的模型就能解决答不准的问题,其实十有八九是检索没召回正确内容,模型再强也白搭。
2.2 为什么选腾讯云向量数据库而不是本地方案
我先把选型逻辑摆出来,这样你知道什么场景该用什么。
| 方案 | 适用场景 | 优势 | 局限 |
|---|---|---|---|
| FAISS / Chroma | 本地实验、数据量小 | 零成本、部署快 | 无持久化保障、无分布式、权限弱 |
| 自建 Milvus | 有运维团队、数据敏感 | 可控性强、功能全 | 运维成本高、扩容麻烦 |
| 腾讯云向量数据库 | 企业生产、要快速上线 | 托管免运维、弹性扩容、支持标量过滤 | 需要云环境、有成本 |
选腾讯云向量数据库的核心理由有三条。第一,托管服务省掉了运维,你不用半夜起来处理分片失衡、磁盘打满这些破事。第二,它支持向量检索加标量字段过滤的组合查询,这对企业知识库太重要了——不同部门、不同权限的人只能搜到自己能看的文档,这个过滤必须在检索层完成,不能等检索完再筛。第三,弹性扩容,企业文档是持续增长的,今天十万条明天可能一百万条,本地方案扩容要停机迁移,云上基本是平滑的。
提示:如果你的数据量在几万条以内,且只是内部小范围试用,本地 Chroma 完全够用,不必上云。上云的门槛在于数据量和并发要求。
2.3 切片策略:RAG 效果的地基
这块我要单独拎出来讲,因为它决定了后面所有环节的上限。切片(chunking)就是把长文档切成一段段可检索的文本块。切得太碎,语义不完整,检索出来断章取义;切得太大,一个 chunk 里混了好几个主题,向量表达就模糊了。
我的经验是:中文技术文档,chunk 大小控制在 300 到 500 字,重叠 50 到 80 字。为什么要有重叠?因为一句话可能正好被切在边界上,重叠能保证语义连续。为什么是 300 到 500 字?因为主流 Embedding 模型对这段长度的语义表达最集中,再长就开始稀释了。
切片还要考虑结构。比如 Markdown 有标题层级,最好按标题切,让每个 chunk 自带上下文。PDF 里的表格要单独处理,别硬切成文本,否则行列关系全乱。这些细节后面实操部分会展开。
3. 核心环节拆解与实操要点
3.1 环境准备与依赖安装
先把环境弄干净。我习惯用 Python 3.10,太新的版本有些库还没跟上,太老的又缺特性。
python -m venv rag_env source rag_env/bin/activate # Windows 用 rag_env\Scripts\activate pip install tcvectordb langchain openai pypdf tiktoken这里解释下几个关键依赖。tcvectordb是腾讯云向量数据库的官方 SDK,负责所有跟数据库的交互。langchain用来做文档加载和切片的编排,虽然它有点重,但省事。pypdf处理 PDF 解析。tiktoken用来精确计算 token 数,切片时按 token 切比按字符切更准。
注意:Embedding 模型和生成模型可以分开选。Embedding 建议用中文效果好的,比如 BGE 系列或者腾讯自家的。生成模型用你手头有的就行,DeepSeek、通义、混元都能接。
3.2 文档解析:别小看这一步
文档解析是脏活累活,但做不好后面全崩。我踩过最大的坑就是 PDF 解析。很多 PDF 是扫描件或者排版复杂,直接抽文本会得到一堆乱码和错位的表格。
我的处理原则是:
- 纯文本 PDF:用 pypdf 直接抽,简单可靠。
- 扫描件 PDF:必须上 OCR,否则抽出来是空的。
- Word 文档:用 python-docx,注意保留标题样式,方便后续按结构切。
- 网页:用 readability 类库抽正文,去掉导航和广告。
解析完还要做清洗:去掉多余空行、页眉页脚、乱码字符。这一步看着琐碎,但能显著提升检索质量。我一般会写个清洗函数,把连续空白压成一个空格,把全角半角统一。
3.3 向量化与入库:参数怎么定
向量化就是把文本转成一组浮点数。这里的关键参数是向量维度,它必须和 Embedding 模型输出一致。比如 BGE-large 是 1024 维,你就得建 1024 维的集合。维度对不上,数据根本存不进去。
入库时除了向量本身,还要存原始文本和元数据。元数据就是标量字段,比如文档来源、部门、创建时间、权限标签。这些字段是后面做过滤检索的依据。
from tcvectordb.model.collection import Collection from tcvectordb.model.index import Index, FilterIndex, VectorIndex from tcvectordb.model.enum import FieldType, IndexType, MetricType index = Index( filter_index=[ FilterIndex("doc_id", FieldType.String), FilterIndex("department", FieldType.String), ], vector_index=[ VectorIndex("vector", 1024, IndexType.HNSW, MetricType.COSINE) ] )这里MetricType.COSINE是余弦相似度,文本检索基本都用它。IndexType.HNSW是一种近似最近邻算法,检索快、召回率高,是向量检索的默认选择。
提示:批量入库时,建议每批 100 到 500 条。太小网络开销大,太大容易超时。入库前先做去重,相同内容的 chunk 没必要存两遍。
3.4 检索策略:召回质量的决定性一环
检索不是简单地把问题转成向量然后搜 top-k 就完事。企业场景下,我一般用混合检索加过滤的组合拳。
先说 top-k 怎么定。k 太小可能漏掉正确答案,k 太大又会引入噪声干扰生成。我的经验值是先召回 10 到 20 条,再用重排序模型精选出 3 到 5 条喂给大模型。这样既保证召回率,又控制住噪声。
再说过滤。比如用户是财务部的,那检索时就要加上department == "finance"的过滤条件,确保他搜不到研发部的机密文档。这个过滤在向量检索的同时完成,效率比检索完再筛高得多。
results = collection.search( data=[query_vector], limit=10, filter='department = "finance"', output_fields=["text", "doc_id"] )4. 完整实操流程与关键代码实现
4.1 从零到一:搭建流程全景
我把整个流程串一遍,你可以照着一步步来。
第一步,准备文档。把公司资料整理到一个目录,按类型分文件夹。
第二步,解析清洗。写个脚本批量处理,输出成统一的 JSON 格式,每条包含text和metadata。
第三步,切片。按前面说的 300 到 500 字、重叠 50 到 80 字来切。
第四步,向量化。调 Embedding 接口,把每个 chunk 转成向量。
第五步,建集合入库。先建好索引结构,再批量写入。
第六步,写检索和生成逻辑。查询进来先向量化,检索,拼 Prompt,调大模型。
第七步,封装成 API。用 FastAPI 或 Flask 对外提供服务。
4.2 切片与向量化的代码实现
先看切片。我用 langchain 的 RecursiveCharacterTextSplitter,它对中文支持还行,但要注意分隔符要加上中文标点。
from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=60, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) chunks = splitter.split_text(document_text)separators的顺序很重要,它会优先按段落切,段落太长再按句子切,保证语义尽量完整。
向量化部分,假设你用某个 Embedding 接口:
def embed_texts(texts): # 批量调用 Embedding 接口,返回向量列表 response = embedding_client.embeddings(input=texts) return [item.embedding for item in response.data]注意:Embedding 接口一般有批量上限,比如一次最多 16 条或 32 条。要分批调用,别一次塞几百条进去。
4.3 入库与检索的完整代码
建集合和入库:
from tcvectordb.model.document import Document # 建集合 db.create_collection( name="enterprise_kb", dimension=1024, index=index ) collection = db.collection("enterprise_kb") # 批量入库 docs = [] for i, chunk in enumerate(chunks): docs.append(Document( id=str(i), vector=vectors[i], text=chunk, doc_id=source_id, department=dept )) collection.upsert(documents=docs)检索加生成:
def rag_query(question, dept): q_vector = embed_texts([question])[0] results = collection.search( data=[q_vector], limit=10, filter=f'department = "{dept}"', output_fields=["text"] ) context = "\n\n".join([r["text"] for r in results[0]]) prompt = f"根据以下资料回答问题,资料没有的内容不要编造。\n\n资料:\n{context}\n\n问题:{question}" answer = llm_client.chat(prompt) return answer这段代码是整个系统的核心。filter保证权限隔离,context拼接检索结果,Prompt 里明确要求“资料没有的不要编造”,这是抑制幻觉的关键。
4.4 参数选择的计算过程
有人问 chunk_size 到底怎么算出来的。我给你个推导。假设你的 Embedding 模型最大输入是 512 token,中文大概 1 个字对应 1.5 个 token,那 512 token 约等于 340 个汉字。为了留余量,chunk 定在 300 到 400 字比较稳妥。重叠取 chunk 的 15% 到 20%,也就是 50 到 80 字,保证边界语义不丢。
top-k 的选择也有讲究。假设你的知识库有 10 万条 chunk,一次检索召回 10 条,那召回率取决于正确内容是否在这 10 条里。经验上,配合重排序,10 到 20 条的初筛能覆盖绝大多数正确答案。
5. 常见问题与排查技巧实录
5.1 检索答不准的排查思路
这是最高频的问题。用户问了个问题,系统答非所问。排查顺序我一般这么走:
先看检索结果本身对不对。把 top-10 的 chunk 打出来看,如果正确答案根本不在里面,那是检索问题;如果在里面但生成答错了,那是生成问题。
检索问题又分几种。一是切片不合理,答案被切散了。解决办法是调整 chunk_size 和重叠。二是Embedding 模型不适合中文,换个中文优化的模型。三是问题表述和文档表述差异太大,比如用户问“怎么报销”,文档写的是“费用申请流程”,这种语义鸿沟要靠查询改写或者混合检索来补。
生成问题通常是 Prompt 没写好。要么是没强调“基于资料回答”,要么是 context 太长把关键信息淹没了。这时候减少 top-k,或者上重排序。
5.2 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 检索结果完全不相关 | 向量维度不匹配 / 模型不对 | 检查维度,换 Embedding 模型 |
| 答案断章取义 | chunk 切太碎 | 增大 chunk_size,增加重叠 |
| 答案混入无关内容 | top-k 太大 | 减小 k,加重排序 |
| 权限过滤失效 | filter 字段没建索引 | 检查 FilterIndex 配置 |
| 入库报错 | 维度或字段类型不符 | 核对集合 schema |
| 响应慢 | 检索量大 / 无索引 | 确认用了 HNSW 索引 |
5.3 独家避坑经验
几个文档里不会写但实际会遇到的坑。
第一,元数据字段别用中文名。有些数据库对字段名有字符限制,用英文最稳。
第二,入库前一定要去重。同一份文档被重复上传是常事,不去重会导致检索结果里全是重复内容,浪费 top-k 名额。
第三,注意 Embedding 和检索的一致性。入库用的哪个模型,查询就必须用同一个模型。换了模型,老数据全部作废,得重新入库。这个坑我踩过,换了模型忘了重建库,检索结果全是乱的。
第四,Prompt 里要给出处。让模型在回答时标注引用了哪条资料,方便用户核实,也方便你排查问题。
第五,做好日志。每次查询记录问题、检索到的 chunk、最终回答。出问题时这些日志就是救命稻草。
6. 效果优化与进阶方向
6.1 重排序:让召回更精准
初筛召回 10 到 20 条之后,用一个重排序模型(Reranker)对这 10 到 20 条重新打分,选出最相关的 3 到 5 条。重排序模型比向量检索更精细,因为它能同时看问题和文档做交叉编码,而不是各自编码后算距离。
实测下来,加了重排序之后,答案准确率能提升 15% 到 30%,尤其是那种问题表述和文档表述差异大的场景。代价是多一次模型调用,延迟增加几百毫秒,但对企业知识库来说完全值得。
6.2 查询改写:缩小语义鸿沟
用户的问题往往很口语化,而文档是书面语。查询改写就是让大模型先把用户问题改写成几个更接近文档表述的查询,然后分别检索,合并结果。
比如用户问“报销要多久到账”,改写成“费用报销 到账时间 流程”,检索命中率立刻上去了。这个技巧在多轮对话里尤其有用,因为用户经常用“那这个呢”这种指代,需要结合上下文补全。
6.3 结构化知识库与 RAG 的配合
热词里提到的 kg 知识库、ontology rag,其实是在说一件事:纯向量检索处理不了强结构化的关系查询。比如“A 产品的负责人是谁”这种问题,答案在数据库里是一条明确的关系,用向量检索反而绕。
我的做法是混合架构:结构化的事实类查询走知识图谱或数据库,非结构化的文档问答走 RAG。两者用路由层分发。这样各取所长,效果比纯 RAG 好很多。
6.4 后续可以扩展的方向
搭好基础版之后,可以往这几个方向走。一是多模态,支持图片和表格的检索。二是增量更新,文档变了自动重新切片入库,不用全量重建。三是多租户隔离,一套系统服务多个部门或客户。四是评估体系,建一套测试集,每次改动都跑一遍,量化效果变化。
我个人在实际操作中的体会是,RAG 系统没有一步到位的,都是先跑通再优化。别一上来就追求完美,先把最小可用版本搭起来,用真实问题去测,根据 bad case 逐个优化。检索质量是磨出来的,不是设计出来的。