topic: ai
RAG 系统搭建全过程 2023 年上半年,公司决定上线一个内部知识库问答系统。
需求很简单:员工可以问系统问题,系统从文档库中找到答案,用自然语言回复。
听起来简单,但做起来全是坑。
架构设计 1 用户 → 问题 → 向量化 → 向量检索 → 文档拼接 → LLM → 回答
核心流程:
文档入库:PDF/Word/MD → 文本 → 分块 → 向量化 → 存入向量库
查询:用户问题 → 向量化 → 向量检索 Top-K → 拼接上下文 → LLM 生成
技术选型 向量数据库
数据库
优点
缺点
Qdrant
Rust 实现,性能高
社区相对较小
Milvus
功能完善,分布式
重,资源消耗大
Chroma
简单,Python 原生
生产环境不够
最终选了 Qdrant ,部署简单,性能也够用。
分块策略 这是 RAG 效果的关键。
试了三种:
固定长度(500字符):效果一般,上下文容易断裂
递归分块(按段落、句子):效果最好
语义分块(用 LLM 判断):成本太高
最终用 递归分块 :
1 2 3 4 5 6 7 from langchain.text_splitter import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter( chunk_size=500 , chunk_overlap=50 , separators=["\n\n" , "\n" , "。" , " " , "" ] )
Embedding 模型
OpenAI text-embedding-ada-002:效果好,但贵
本地模型(sentence-transformers):免费,效果稍差
前期用 OpenAI,后期考虑切换本地。
核心代码 文档处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from langchain_community.document_loaders import PyPDFLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterdef process_document (file_path ): if file_path.endswith('.pdf' ): loader = PyPDFLoader(file_path) elif file_path.endswith('.md' ): loader = TextLoader(file_path) docs = loader.load() splitter = RecursiveCharacterTextSplitter( chunk_size=500 , chunk_overlap=50 ) chunks = splitter.split_documents(docs) embeddings = OpenAIEmbeddings() vectors = embeddings.embed_documents([c.page_content for c in chunks]) qdrant_client.upsert( collection_name="knowledge_base" , points=[ { "id" : i, "vector" : vectors[i], "payload" : {"content" : chunks[i].page_content} } for i in range (len (chunks)) ] )
查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def query (question, top_k=3 ): embeddings = OpenAIEmbeddings() question_vector = embeddings.embed_query(question) results = qdrant_client.search( collection_name="knowledge_base" , query_vector=question_vector, limit=top_k ) context = "\n\n" .join([r.payload["content" ] for r in results]) prompt = f"""基于以下上下文回答问题。 上下文: {context} 问题:{question} 回答:""" response = openai.ChatCompletion.create( model="gpt-3.5-turbo" , messages=[{"role" : "user" , "content" : prompt}] ) return response.choices[0 ].message.content
效果优化 1. Rerank 直接检索的结果不够准确,加了 rerank 模型:
1 2 3 4 5 6 7 8 from langchain_community.cross_encoders import HuggingFaceCrossEncoderreranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base" ) def rerank (query, documents ): scores = reranker.predict([(query, doc) for doc in documents]) sorted_docs = sorted (zip (documents, scores), key=lambda x: x[1 ], reverse=True ) return [d for d, s in sorted_docs[:3 ]]
效果:从 60% 提升到 85%。
2. 混合检索 向量检索 + 关键词检索 结合:
1 2 3 4 5 6 7 8 vector_results = vector_db.similarity_search(query) keyword_results = bm25.retrieve(query) combined = fusion_rank(vector_results, keyword_results)
3. Prompt 优化 针对公司场景定制 prompt:
1 2 3 4 5 6 7 8 9 10 11 12 13 prompt = """你是一个内部知识库助手,只能基于提供的上下文回答。 如果上下文中没有相关信息,请回答"抱歉,我暂时没有找到相关信息"。 回答要求: 1. 简洁明了 2. 如有必要,可以列出参考文档 3. 禁止编造答案 上下文:{context} 问题:{question} 回答:"""
遇到的问题 问题1:文档格式混乱 PDF 有表格、图片、公式,处理起来很麻烦。
解决:针对不同格式用不同的解析器,表格转成 Markdown,图片忽略或 OCR。
问题2:检索召回低 有些问题明明文档里有,但就是搜不到。
解决:增加同义词扩展,比如”服务器”→”主机”、”网络”→”网卡”。
问题3:LLM 幻觉 LLM 会一本正经地编造答案。
解决:要求 LLM 引用原文,增加置信度检查。
总结 RAG 系统看似简单,实际要做好需要大量调优。
核心经验:
分块策略 是基础,分不好后面都白搭
检索优化 效果明显,rerank + 混合检索
持续迭代 ,没有一劳永逸的方案
目前系统运行稳定,准确率基本维持在 85% 以上。接下来计划接入更多数据源,优化响应速度。