一、引言KMS 知识管理平台是我负责了两年的项目服务公司内部 2000 员工沉淀了约 15 万篇文档。但它的搜索功能一直是个痛点。用户输入去年合规审查的通过标准是什么传统搜索引擎返回一堆包含合规“审查”标准关键词的文档列表。用户需要逐篇点开、阅读、筛选——很多时候翻了 10 分钟还找不到想要的答案。这不是搜索算法的问题而是关键字匹配的天花板。用户的真实意图是帮我找到答案而关键字搜索只能做到帮你找到可能包含答案的文档。2025 年初我开始探索 RAGRetrieval-Augmented Generation检索增强生成。三个月后第一个版本在 KMS 内部上线。这篇文章记录我从 0 到 1 的完整实践过程——从架构设计、技术选型、到踩坑与优化。二、RAG 是什么一张图讲清楚先用一句话概括 RAG 的核心思想不让 LLM 凭记忆回答而是先检索相关文档把文档内容和问题一起喂给 LLM让它基于参考资料来回答。这样做的好处显而易见知识可更新文档变了检索结果就变了不需要重新训练模型减少幻觉LLM 被约束在检索到的文档范围内回答不太会凭空编造来源可追溯每个答案都能指向具体的源文档方便核实RAG 流程分为两个阶段离线阶段文档入库文档解析将 Markdown、PDF、Word 等格式统一转为纯文本文本切分Chunking将长文档切成适当大小的文本块向量化Embedding将每个文本块转换为向量向量存储将向量存入向量数据库在线阶段用户提问用户输入问题问题向量化向量检索在向量数据库中查找最相似的文本块Prompt 组装将检索到的文本块 用户问题组装成 PromptLLM 生成大模型基于上下文生成答案三、系统架构设计KMS RAG 系统的整体架构分为四层文档层负责文档的接入与预处理。KMS 中 90% 的文档是 Markdown 格式Tiptap 编辑器产出剩下 10% 是以附件形式上传的 PDF 和 Word。# 文档解析器 —— 统一不同格式fromlangchain_community.document_loadersimport(UnstructuredMarkdownLoader,PyPDFLoader,Docx2txtLoader,)defload_document(file_path:str,file_type:str)-list[Document]:loader_map{md:UnstructuredMarkdownLoader,pdf:PyPDFLoader,docx:Docx2txtLoader,}loader_classloader_map.get(file_type)ifnotloader_class:raiseValueError(fUnsupported file type:{file_type})loaderloader_class(file_path)returnloader.load()向量化层负责文本块的 Embedding 生成。我选择了text-embedding-3-small模型在成本和效果之间取得了较好的平衡。fromopenaiimportOpenAI clientOpenAI()defcreate_embeddings(texts:list[str],model:strtext-embedding-3-small)-list[list[float]]:批量生成文本的向量表示responseclient.embeddings.create(modelmodel,inputtexts,)return[item.embeddingforiteminresponse.data]检索层负责根据用户问题召回最相关的文档片段。这里采用了混合检索策略——向量检索 关键字检索两者互补。fromlangchain_community.vectorstoresimportPGVectorfromlangchain.retrieversimportBM25RetrieverclassHybridRetriever:混合检索器向量检索 BM25 关键字检索def__init__(self,vector_store:PGVector,bm25_retriever:BM25Retriever):self.vector_storevector_store self.bm25_retrieverbm25_retrieverdefretrieve(self,query:str,top_k:int5)-list[Document]:# 向量检索语义相似vector_docsself.vector_store.similarity_search(query,ktop_k)# BM25 关键字检索keyword_docsself.bm25_retriever.get_relevant_documents(query)[:top_k]# 合并去重 RRF (Reciprocal Rank Fusion) 重排序returnself._rrf_fusion(vector_docs,keyword_docs)生成层负责组装 Prompt 并调用 LLM 生成最终答案。defbuild_rag_prompt(query:str,retrieved_docs:list[Document])-str:组装 RAG Promptcontext\n\n---\n\n.join(f【来源{doc.metadata.get(source,未知)}】\n{doc.page_content}fordocinretrieved_docs)returnf你是一个专业的 KMS 知识库助。请基于以下参考资料回答用户的问题。 ## 参考资料{context}## 回答要求 1. 如果参考资料中包含答案请直接引用并标注来源 2. 如果参考资料不足以回答问题请明确说明 3. 不要编造参考资料中没有的信息 ## 用户问题{query}## 回答四、Chunking 策略被低估的关键环节Chunking 决定了检索的颗粒度。切太大——检索精度下降噪声多切太小——上下文不足答案片段化。我在 KMS 上实验了三组参数策略Chunk 大小Overlap检索精度Recall5适用场景固定长度512 tokens10%78%通用基线固定长度1024 tokens10%81%长文档语义切分不固定句子级87%Markdown 文档最终选择语义切分——基于 Markdown 的标题层级##、###作为天然的分界点fromlangchain.text_splitterimportMarkdownHeaderTextSplitter headers_to_split_on[(##,h2_section),(###,h3_section),(####,h4_section),]splitterMarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on,strip_headersFalse,)# 按 Markdown 标题层级切分每个 section 自带层级元数据splitssplitter.split_text(markdown_content)这种做法让每个 Chunk 自带标题上下文元数据中的 h2_section、h3_section在检索时 LLM 能理解这个片段属于哪个章节生成的答案更有条理。五、向量数据库选型KMS 的技术栈是 Python PostgreSQL所以向量数据库的候选范围很明确方案优势劣势结论Milvus性能最强千万级向量不虚需要独立部署和维护太重小团队不合适Pinecone免运维开箱即用数据出境合规问题、成本随规模上升金融科技不适合PGVector复用 PostgreSQL零额外运维百万级后性能下降✅ 当前最优解PGVector 最大的优势是零额外运维成本——KMS 本来就用 PostgreSQL开启 PGVector 插件只需要一行 SQLCREATEEXTENSION vector;-- 创建向量存储表CREATETABLEdocument_embeddings(id UUIDPRIMARYKEYDEFAULTgen_random_uuid(),doc_id UUIDREFERENCESdocuments(id),chunk_indexINT,contentTEXT,embedding VECTOR(1536),-- text-embedding-3-small 的维度metadata JSONB,created_atTIMESTAMPDEFAULTNOW());-- 创建索引IVFFlat 适合 10 万级数据CREATEINDEXONdocument_embeddingsUSINGivfflat(embedding vector_cosine_ops)WITH(lists100);15 万文档切分后约 60 万个 ChunkPGVector 的 IVFFlat 索引在Recall5 85%的条件下查询延迟约 120ms完全够用。六、踩坑与优化清单坑一Overlap 设置过小导致答案断层现象用户问KMS 权限模型有哪三种角色检索召回了三段分别讲admin、editor、viewer的内容——但没召回讲权限模型概述的段落导致 LLM 不理解这三种角色之间的关系。根因Chunk 之间的 Overlap 只有 50 个字符而概述段落和详细描述之间隔了 200 字符没有被相邻 Chunk 覆盖。解决将 Overlap 提升到 Chunk 大小的 15%约 150 tokens同时在检索策略上增加父文档召回——检索到某个 Chunk 后连带召回它所属的整个 Section。坑二向量检索的语义漂移现象用户搜索财务报表模板结果中出现了大量季度报告模板。从向量角度看它们的语义确实很接近但对用户来说是两种不同的文档。根因纯向量检索对同义词和近义概念区分度不够。解决引入混合检索——BM25 关键字检索 向量检索用 RRF 算法融合排序defrrf_fusion(rankings:list[list[Document]],k:int60)-list[Document]: Reciprocal Rank Fusion: 多路检索结果融合 RRF_score(d) Σ 1 / (k rank_i(d)) scores{}forrankinginrankings:forrank,docinenumerate(ranking):doc_iddoc.metadata.get(chunk_id,doc.page_content[:50])scores[doc_id]scores.get(doc_id,0)1/(krank1)# 按融合分数排序sorted_scoressorted(scores.items(),keylambdax:x[1],reverseTrue)return[self._get_doc_by_id(doc_id)fordoc_id,_insorted_scores]坑三Prompt Template 设计不当LLM 放飞自我现象早期版本的 Prompt 没有明确约束如果不知道就说不知道导致 LLM 在检索不到相关资料时用自己训练数据中的知识填补——产生了幻觉答案。解决在 Prompt 中明确加入边界指令——“如果参考资料不足以回答问题请明确说明不要编造”。这一点在生成层代码中已经体现。坑四成本控制被忽略账单惊魂第一个月测试阶段Embedding API 和 GPT-4o 的调用费用接近 $200。排查发现两个问题每次用户搜索都重新 Embedding 查询文本实际上可以缓存热门查询检索召回了 20 个 Chunktop_k20实际 5 个就够优化后热门查询的 Embedding 结果用 Redis 缓存TTL 1 小时top_k 降到 5配合 Rerank 精排月成本降到约 $45七、总结三个月时间RAG 从概念到上线最深的体会是RAG 的工程难点不是模型而是文档处理、检索策略和 Prompt 工程。模型是别人的但你的文档结构、Chunking 策略、检索调优是别人替代不了的。当前版本还远不完美。下一步计划Rerank 模型引入在粗排后用 Cross-Encoder 做精排进一步提升检索精度多轮对话支持让用户能追问而不仅仅是单轮问答用户反馈闭环收集用户对答案的点赞/点踩用于持续优化检索策略如果你也在做类似的事情建议先从最小可行方案开始PGVector 语义切分 混合检索三个组件搭好基本能满足 80% 的企业知识库场景。剩下的 20% 留给持续迭代。这篇文章记录了我将 RAG 落地到 KMS 企业知识管理平台的完整过程。项目还在持续迭代中欢迎交流。