从零搭建RAG知识库:FastAPI+LangChain+pgvector全流程实战 1. 从零开始搭一套RAG知识库先搞清楚它到底解决什么问题聊RAG之前先讲个真实场景。我去年帮一家做企业服务的朋友搭内部知识库他们公司光是产品文档、售后工单、销售话术就堆了几千份Word和PDF散落在各个同事的网盘里。每次新人入职光熟悉业务就要俩月客服查一个政策条款得翻好几个群聊记录。他们最初的想法是“喂给大模型让它背下来”结果发现GPT这类模型压根记不住几千页文档每次问细节都开始一本正经地胡编。这就是RAGRetrieval-Augmented Generation检索增强生成要解决的痛点让大模型在回答问题时先从外部知识库检索相关内容再基于检索结果生成答案。简单说就是给大模型配一个“随时能翻的资料员”。你问什么资料员先把相关文档段落找出来大模型再照着这些资料组织语言回答而不是凭空发挥。这套系统适合谁学如果你手里有大量文档需要做成问答机器人、给企业搭内部知识库、或者想把个人笔记变成一个能对话的第二大脑那这篇教程就是给你准备的。我接下来讲的不是概念科普而是一套能落地的方案从环境搭建、文档解析、切片策略、向量化、检索排序到最后的API封装和效果调优全流程走一遍。技术栈选的是目前社区最成熟的组合FastAPI LangChain PostgreSQL的pgvector插件顺便聊聊LangGraph在复杂流程里的作用。先说结论RAG知识库的搭建门槛没有想象中高真正拉开差距的是细节——文档怎么切、向量怎么存、检索结果怎么排序每步都会直接影响问答质量。这篇文章我会把每个环节的“为什么”讲清楚再把踩过的坑列出来照着抄就能跑通。2. 整体架构与方案选型为什么是FastAPI、LangChain和pgvector2.1 RAG系统的四段式流水线任何RAG系统不管换什么框架底层都是同一条流水线文档加载 → 文本切分 → 向量化存储 → 检索生成。这四步缺一不可环环相扣。文档加载把Word、PDF、TXT、Markdown等格式的原始文件读成纯文本。难点在于格式兼容和版面解析比如PDF里的表格、Word里的图片注释。文本切分长文档不能整篇丢给模型。一是向量化有长度限制二是检索粒度太粗会导致命中不精准。需要按语义或结构切成几百字的小块。向量化存储把每个文本块通过Embedding模型转成向量一串几百维的浮点数存入向量数据库。查询时把用户问题也转成向量用余弦相似度找最相似的文本块。检索生成把检索到的文本块和用户问题拼成Prompt交给大模型生成最终答案。这一步还可以加重排序、多轮对话等增强逻辑。理解这条流水线后面所有配置都是在为这四步做优化。我在搭建时见过不少一上来就堆框架的项目结果检索效果差却找不到原因——其实就是某一步没做扎实。2.2 技术栈选择的实际考量先说LangChain。现在很多人喷LangChain“过度封装”但我的观点是对于快速验证和中小规模项目它仍然是最省事的方案。它把文档加载器、文本切分器、向量存储封装成统一接口你换一个Embedding模型或向量库时不用改业务代码。比如今天用OpenAI的Embedding明天想换国产的BGE模型只需要改一行配置。再看向量存储我推荐PostgreSQL pgvector而不是单独部署Milvus或Weaviate。原因很实际pgvector是PostgreSQL的插件你的结构化数据用户信息、文档元数据、问答记录本来就可以存在PostgreSQL里用pgvector等于少维护一套数据库。数据量在百万级向量以内pgvector的性能完全够用。只有到了千万级以上、对检索延迟有极其苛刻要求时才值得上Milvus这类专用向量库。备份、权限管理、SQL查询这些能力直接复用PostgreSQL的生态。比如按文档来源过滤、按时间删除旧文档一条SQL就搞定。当然如果你已经有Milvus在跑也没必要强行迁到pgvector。我在后面会给出两者的对比和选择建议。2.3 LangGraph在什么场景下值得引入如果你只是做一个“文档问答”的基础版LangGraph不是必须的。但当你需要多轮对话、意图判断、多路检索、人工审核这些复杂流程时LangGraph的价值就体现出来了。我用一个真实例子说明之前做一个企业IT服务台的智能问答Agent用户问“我的电脑连不上打印机了怎么办”。简单RAG的做法是检索运维手册直接回答。但实际业务里系统需要先判断用户意图是咨询还是报障、检索知识库的同时还要查工单系统、如果知识库置信度低就转人工。这些分支逻辑用LangGraph的状态机模型来编排特别清晰——每个节点是一个处理步骤节点之间通过状态传递数据支持条件分支和循环。所以我的建议是第一版先用最简架构跑通等确有必要再加LangGraph。别一上来就上重型编排否则调试成本会拖垮你。2.4 向量数据库选型对比pgvector与Milvus怎么选这里整理一个选型参考表是我在多个项目里的实际体验维度pgvectorMilvus说明部署成本低PostgreSQL插件高独立集群个人和小团队优先pgvector性能百万级OK千万级以上优势明显数据量是核心判断标准功能只支持基础的近邻检索支持索引加速、分片、混合检索pgvector满足大多数业务生态复用PostgreSQL工具链有WebUI、监控组件运维视角pgvector更省心适用场景中小型业务系统、个人知识库大规模日志分析、推荐系统按需选择我的实际经验是项目早期别花太多时间在向量库选型上pgvector足以支撑你跑完整个流程。真到了性能瓶颈再迁移也不迟——LangChain的向量存储接口统一迁移成本没那么可怕。3. 环境搭建与数据处理文档解析和切片的每一步3.1 基础环境准备我假设你有一台能联网的Linux服务器或本地开发机Python版本3.10以上。下面是我常用的依赖清单直接用pip安装pip install fastapi uvicorn langchain langchain-openai langchain-community pip install pgvector psycopg2-binary sqlalchemy pip install pypdf python-docx python-pptx openpyxl pip install sentence-transformers解释下几个选择pypdf负责PDF解析python-docx处理Wordopenpyxl读Excelsentence-transformers用来加载本地Embedding模型。如果你所在环境访问OpenAI接口不方便可以用本地的BGE模型作为Embedding后面我会具体讲。PostgreSQL这边需要安装扩展CREATE EXTENSION IF NOT EXISTS vector;建一张存储文档向量的表字段设计如下CREATE TABLE document_chunks ( id SERIAL PRIMARY KEY, doc_id VARCHAR(100), chunk_text TEXT, chunk_metadata JSONB, embedding vector(768) ); CREATE INDEX ON document_chunks USING ivfflat (embedding vector_cosine_ops);这里有两个关键点。第一embedding的维度需要和你选的Embedding模型输出维度一致BGE-base是768维OpenAI的text-embedding-3-small是1536维建表前先确认。第二ivfflat索引是近似最近邻搜索检索速度快但会牺牲一点精度数据量少时也可以不建索引直接暴力搜索结果更准。3.2 文档解析Word、PDF、Excel各有各的坑这一步是整个RAG系统里最容易被低估的环节。很多人以为“把文件读出来就行”实际上格式解析的好坏直接决定后面检索质量的上限。PDF解析如果是纯文本PDFpypdf直接提取即可。但扫描件PDF必须先做OCR否则提取出来全是空白。我推荐paddleocr中文识别效果好不过需要额外装paddlepaddle。还有个技巧优先用带文本层的PDF比如WPS导出的结合pdfplumber处理表格比纯OCR要强得多。Word解析python-docx能读取段落但图片和表格里的内容会被忽略。对企业文档来说表格往往是核心信息。我的处理方式是段落用python-docx读表格单独提取组件把每一行转换成“表头: 值”的文本描述再拼接到原文里。比如一个配置参数表转成“timeout: 30秒; retry_count: 3次”这样的格式检索效果比直接读原表要好。Excel解析openpyxl读单元格不难难在语义组织。直接按行拼文本检索时会把表头和值拆散。我习惯把每个Sheet按“Sheet名列名每行数据”的格式组织成一段语义完整的描述。比如一个员工信息表每一行转成“张三部门技术部入职日期2023-05-01”。Unstructured库的补充如果你的文档来源特别杂可以试试unstructured这个库它对多格式解析支持很好而且LangChain有对应的Loader直接调用。缺点是依赖较重安装可能耗时较长。我的建议是先用手动方案处理常见格式等遇到特殊格式再用Unstructured兜底。3.3 切片策略别纠结固定字数按语义结构切最靠谱文本切分是RAG里影响效果最大、也最容易被忽视的一步。最笨的方法是按固定字符数切比如每500字一刀但这会切碎语义完整的段落——一个讲“退款政策”的章节可能被拆成两半检索时只能召回其中一半。我常用的策略是分层切分优先按文档结构切Markdown的标题、Word的标题样式就是天然的边界。每个大节内部再按语义段落空行或句号切。设置一个最大长度上限比如800字超过上限的段落再按句子边界切。相邻块之间保留一部分重叠一般50字避免关键信息正好落在切分边界上。LangChain里对应的是RecursiveCharacterTextSplitter但默认的\n\n和。分隔符在中文场景下不够智能。我一般会自定义分隔符列表优先用“。”这些句末标点再退回到换行符。代码大概是from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ] )这里separators的顺序很关键代表切分时的优先级。另外如果你的文档里有代码块、JSON配置这类内容固定字数切分会把格式切坏这种内容建议单独处理保持完整块。3.4 Embedding模型选型开源BGE还是OpenAI接口Embedding是RAG的“翻译官”把文本变成向量。选型主要看三个要素语义理解能力、中文支持、部署成本。目前中文场景最稳的开源选择是智源研究院的BGE系列bge-large-zh-v1.5或bge-m3语义理解强而且专门针对中文优化。用sentence-transformers加载非常方便from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5) embedding model.encode(你的文本内容)OpenAI的text-embedding-3-small质量也不错但有两个问题一是需要稳定访问API二是数据要送到外部企业场景可能涉及合规问题。我建议优先本地部署BGE效果不输在线接口而且零调用成本。有个细节必须提醒检索时用户问题的向量也必须用同一个Embedding模型生成。如果文档向量化用了BGE查询时却换了别的模型向量空间不一致检索结果会一塌糊涂。这个错误我见过不止一次。4. 核心模块实现从检索到生成的完整代码流程4.1 文档入库构建向量化写入管线环境就绪、解析和切片方案定好了接下来就是把整条管线串起来。建议先写一个独立的入库脚本把“解析-切片-向量化-写库”四步固化方便后续反复执行。import os from sqlalchemy import create_engine, text from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer # 初始化模型和数据库 model SentenceTransformer(BAAI/bge-large-zh-v1.5) engine create_engine(postgresqlpsycopg2://user:passwordlocalhost/rag_db) def process_file(file_path, doc_id): # 1. 加载文档这里以txt为例其他格式用对应Loader loader TextLoader(file_path, encodingutf-8) documents loader.load() # 2. 切片 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ] ) chunks splitter.split_documents(documents) # 3. 向量化并写入数据库 with engine.begin() as conn: for idx, chunk in enumerate(chunks): embedding model.encode(chunk.page_content).tolist() conn.execute( text( INSERT INTO document_chunks (doc_id, chunk_text, chunk_metadata, embedding) VALUES (:doc_id, :chunk_text, :metadata, :embedding) ), { doc_id: doc_id, chunk_text: chunk.page_content, metadata: chunk.metadata, embedding: embedding } ) print(f完成: {doc_id}, 共 {len(chunks)} 个切片) if __name__ __main__: process_file(./docs/产品手册.txt, product-manual-v1)这个脚本有几个设计决定要说明doc_id字段用来标记来源这样未来更新文档时可以按doc_id先删旧再插新实现增量更新。事务用with engine.begin()包住写入中途报错会整体回滚不会留下半截脏数据。每条chunk的chunk_metadata我建议至少存source来源文件名、page页码如果有、title所属章节这些信息在后面溯源和过滤时会用到。4.2 检索核心相似度计算与重排序检索这一步核心是“把用户问题转成向量去库里找最相似的文本块”。最朴素的实现def retrieve(query, top_k5): query_embedding model.encode(query).tolist() with engine.connect() as conn: result conn.execute( text( SELECT chunk_text, 1 - (embedding :query_embedding) AS similarity FROM document_chunks ORDER BY embedding :query_embedding LIMIT :top_k ), {query_embedding: query_embedding, top_k: top_k} ).fetchall() return result是pgvector的余弦距离运算符值越小越相似。用1 - distance把距离转成相似度方便展示。但这里有个核心问题向量相似度不等于语义相关性。用户提问“怎么退款”可能匹配到一段讲“退货政策”的文字向量距离很近但答案不够精准。如何提升检索质量我的经验是三步混合检索把向量检索和BM25关键词检索结合。比如用户问带有型号代码“XG-200”的问题关键词检索能精准命中型号向量检索则擅长处理同义表达。两者按加权合并结果。重排序Rerank先用向量检索召回Top 50再用一个交叉编码器模型如bge-reranker-base对召回结果逐条精排取Top 5。交叉编码器把查询和文档拼接起来做深度语义匹配精度高但因为要逐条算所以速度慢只适合做重排。元数据过滤很多业务场景需要限定范围。比如用户问的是“售后退款”就只检索来源为“售后政策”的文档。用SQL的WHERE chunk_metadata-category 售后先把候选集缩小再算相似度效率和精度都能提升。4.3 生成回答Prompt模板设计与多轮对话检索到的文本块不能直接丢给大模型要设计一个清晰的Prompt模板。我常用的模板大概是你是一个企业知识库问答助手。请严格基于参考资料中的内容回答用户问题。 参考资料 {context} 用户问题{question} 要求 1. 如果参考资料中没有答案明确回答资料库中未找到相关信息不要编造。 2. 回答中引用具体条款或数据时标注来源文件名。 3. 保持回答简洁专业。这里的关键是**“不要编造”和“标注来源”**。前者防止大模型在资料缺失时胡编后者让用户能回溯验证这在实际企业使用中非常重要。多轮对话的RAG设计和单轮问答不太一样。简单做法是把历史对话记录也拼进Prompt让模型理解上下文。但有个坑如果历史对话过长超出上下文窗口需要考虑截断或摘要。另一个更实际的问题是用户在多轮对话里说的“这个功能怎么开启”其中“这个功能”指代上一轮提到的名词如果检索时只用当前这一轮的问题很可能会检索不到正确文档。我的解法是把最近两轮的对话拼接起来作为检索query同时对用户的指代表达做一次改写让检索词更完整。4.4 FastAPI封装把RAG能力变成API服务所有逻辑跑通后用FastAPI包一层HTTP接口方便前端、企业微信、钉钉机器人等外部系统调用。核心代码from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QueryRequest(BaseModel): question: str history: list [] class QueryResponse(BaseModel): answer: str sources: list app.post(/rag/query, response_modelQueryResponse) def query_rag(req: QueryRequest): # 1. 检索 chunks retrieve(req.question, top_k5) # 2. 重排序 ranked rerank(req.question, chunks) # 3. 构造上下文 context \n\n.join([c[text] for c in ranked]) # 4. 调用大模型生成这里以OpenAI兼容接口为例 answer generate_answer(context, req.question, req.history) # 5. 返回答案和引用来源 return QueryResponse( answeranswer, sources[{text: c[text], source: c[source]} for c in ranked] ) if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)实际部署时有几个细节不能忽略流式输出大模型生成通常要几秒到几十秒如果等全部生成完再返回用户体验很差。FastAPI支持StreamingResponse可以一边生成一边推送给前端首字延迟能压到1秒内。限流与鉴权知识库API如果暴露在内网至少加一个API Key校验如果放公网还得上更严格的身份验证和限流策略。异步处理入库和检索建议用异步方式设计避免用户问A问题时入库B文档的耗时阻塞了接口响应。5. 进阶流程与常见坑Agentic RAG和效果调优的实战记录5.1 从RAG到Agentic RAG什么时候需要“能思考的检索”基础的RAG是“检索一次、生成一次”。但有些用户问题不是一次检索能解决的比如“我买的设备保修期到什么时候”系统需要先识别用户身份再查找订单和对应设备的保修条款可能需要多步检索和推理。这就是Agentic RAG的场景。典型的分层问题单轮检索Basic RAG → 带重排的检索Advanced RAG → 带查询改写和多步推理的检索Agentic RAG。Agentic RAG用LangGraph编排时我会定义几个节点意图识别、工具选择、检索、生成、转人工。每个节点是一个Python函数节点间的跳转由条件边决定。比如意图识别节点判断用户是想“查资料”还是“报故障”前者走检索节点后者走工单系统调用节点并在知识库回答后附加“是否需要创建工单”的提示。值得强调的是大部分知识库场景其实不需要Agentic RAG。我见过不少团队在基础RAG都没调好的情况下就上Agent结果流程复杂了效果反而更差。先把检索准确率拉上去再考虑让“Agent聪明起来”才靠谱。5.2 检索效果差的排查清单这是RAG项目里被问得最多的问题“为什么我的知识库回答得不准”我总结了一份排查顺序按“从数据到模型”逐层查排查项典型症状解决方案文档解析漏内容答案总是缺某类信息检查PDF是否OCR、表格是否被忽略切片粒度不合理检索到的块要么太碎要么太全调整chunk_size和overlap按结构切分Embedding模型不合适语义相似的查不出来换BGE类中文优化模型测试检索Top K太小正确答案没进候选集先召回Top 50再重排别只看Top 5缺少混合检索专有名词、型号匹配差叠加BM25关键词检索Prompt约束不够回答偏离资料、编造内容强化Prompt的“严格基于资料”约束我的经验是八成以上的“检索效果差”问题出在前两步解析和切片。很多人把精力花在模型调优上其实先去检查文档有没有被正确读出来、切片有没有切碎语义往往立竿见影。5.3 元数据过滤的坑Dify和自建方案的区别近期Dify很火很多人用它搭知识库省去了写代码的功夫。Dify的好处是图形化配置上传文档后自动完成解析、切片、向量化。但有个痛点Dify的知识库元数据过滤能力有限。比如你想实现“只检索某个特定类别的文档”在Dify里配置比较受限它的过滤器主要基于文档集Dataset级别的划分精细到单条元数据过滤的灵活性不够。我的建议是如果你的需求只是“上传几份文档做个问答”Dify完全够用且开发效率高但如果需要精细的权限控制、复杂的元数据过滤、或者要跟现有业务系统深度集成自建一套基于FastAPILangChainpgvector的方案反而更省心。两种路线我都跑过各自的适用边界很清楚不存在谁取代谁。5.4 知识库维护文档更新和增量入库搭建只是开始知识库最难的是长期维护。文档每天都在变知识库不更新就会逐渐失效。我用的维护策略每次导入新文档时给doc_id加上版本号或日期。更新时先按来源文档删除旧切块再执行入库脚本。定期对高热度文档做一次重新向量化因为Embedding模型升级后新旧向量可能不兼容。用定时任务比如cron扫描指定目录文件有变化就自动触发增量入库。举个例子我们现在的做法是开发了一个简单的文件监控脚本往“待入库”文件夹里丢入新的PDF或Word脚本检测到后自动完成解析、切片、入库并在完成后发一条通知。知识库负责人只需要负责审核源文档质量不用关心技术细节。5.5 成本控制与性能优化最后聊两个偏工程的话题成本和性能。成本方面最大的开销通常是大模型API调用。优化方向有三个能用小模型解决就不用大模型。检索阶段的Embedding用本地BGE零成本生成阶段根据任务复杂度选模型简单的条款问答用轻量模型就够。做答案缓存。用户在短时间内问相同或高度相似的问题直接返回上次结果能省下大量调用费用。压缩上下文。检索到的Top K块如果篇幅很大可以先用摘要模型压缩再拼进Prompt减少Token消耗。性能方面pgvector的索引优化是关键。数据量上到几十万条后ivfflat的召回率会下降。可以改用HNSW索引它是目前公认的向量检索效果和速度平衡最好的算法。建索引的SQL示例CREATE INDEX ON document_chunks USING hnsw (embedding vector_cosine_ops);HNSW的缺点是构建时间和内存占用更高所以更适合写入少、查询多的场景。另外查询时加上LIMIT、提前用元数据过滤缩小候选集都可以显著降低延迟。6. 实操过程中的经验总结与未来扩展先把最值得说的经验放前面RAG项目的成功与否七分在数据质量三分在模型选择。如果你发现问答效果不理想不要急着换大模型先回到文档解析和切片这一步重新检查。我在前面已经反复强调过这一点但这里还想再补一个真实案例某次我们处理一批扫描版PDF没有先做OCR结果所有内容向量化后全是乱码用户问什么都答非所问。排查了两天才定位到这个原因从那之后我们建了一个强制规则所有PDF入库前必须检测是否有文本层没有就自动触发OCR流水线。第二个经验是一定要做“答案溯源”。企业知识库的场景里用户对答案的信任度很重要。如果大模型只给结论不给来源用户很难判断内容是否可靠。实现很简单检索时把命中的文档名和原文片段一起返回在界面上显示引用来源。这不仅提高了可信度也让知识库管理员发现错误答案时能快速定位到具体文档是哪份。第三个经验是从小规模跑通再扩大。不要一开始就想着把几千份文档一次性导入。我建议先选一个业务模块比如“售后政策”相关文档几十份就够把整个流程跑通、效果调到满意再逐步扩大到其他模块。这样做的好处是问题范围小调优时能快速定位原因而且项目早期就能给业务方看到成果后续推广阻力小很多。关于未来扩展的方向我觉得有三个值得关注一是GraphRAG。传统RAG把文档切碎存入向量库但文档之间的关联关系比如“产品A的保修政策关联售后条款”没有被利用。GraphRAG在RAG基础上引入了知识图谱用实体和关系把信息组织起来适合处理需要跨文档推理的复杂问题。最近社区里讨论热度很高微软开源的GraphRAG已经提供了一套完整方案值得关注。二是多模态RAG。现在的知识库不只有文字还有产品图片、结构图、流程图。如何让模型同时理解图片和文字是多模态RAG要解决的问题。目前主流做法是用视觉语言模型把图片内容转成文字描述后再入库或者向量化图片本身。三是知识库的自学习闭环。用户提问后系统可以记录哪些问题在知识库里找不到答案自动生成“知识缺口清单”推给管理员。管理员补上新文档后这些问题再重新测试一遍确认答案是否得到改善。这个闭环跑起来后知识库的质量会持续提升而不是搭完就荒废。我个人在实际操作中的体会是RAG知识库的搭建本身并不神秘把基础流程做扎实、把数据质量把控住就已经能超过市面上大多数半成品方案。框架和工具迭代很快但核心逻辑不会变让模型在正确的资料约束下回答问题。把这句原则刻在脑子里无论未来工具怎么换你都不会迷失方向。